diff --git a/README.md b/README.md index 92f5880..2fde839 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,10 @@ Tool for building a Node.js [dual package](https://nodejs.org/api/packages.html# - Bidirectional ESM ↔️ CJS dual builds inferred from the package.json `type`. - Correctly preserves module systems for `.mts` and `.cts` file extensions. -- Resolves the [differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs). -- Use only one package.json and tsconfig.json. +- No extra configuration files needed, uses `package.json` and `tsconfig.json` files. +- Transforms the [differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs). +- Works with monorepos. + ## Requirements @@ -68,12 +70,27 @@ If you prefer to have both builds in directories inside of your defined `outDir` Assuming an `outDir` of `dist`, running the above will create `dist/esm` and `dist/cjs` directories. +### Module transforms + +TypeScript will throw compiler errors when using `import.meta` globals while targeting a CommonJS dual build, but _will not_ throw compiler errors when the inverse is true, i.e. using CommonJS globals (`__filename`, `__dirname`, etc.) while targeting an ES module dual build. There is an [open issue](https://github.com/microsoft/TypeScript/issues/58658) regarding this unexpected behavior. You can use the `--modules` option to have the [differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs) transformed by `duel` prior to running compilation with `tsc` so that there are no compilation or runtime errors. + +Note, there is a slight performance penalty since your project needs to be copied first to run the transforms before compiling with `tsc`. + +```json +"scripts": { + "build": "duel --modules" +} +``` + +This feature is still a work in progress regarding transforming `exports` when targeting an ES module build (relies on [`@knighted/module`](https://github.com/knightedcodemonkey/module)). + ## Options The available options are limited, because you should define most of them inside your project's `tsconfig.json` file. - `--project, -p` The path to the project's configuration file. Defaults to `tsconfig.json`. - `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to the cwd. +- `--modules, -m` Transform module globals for dual build target. Defaults to false. - `--dirs, -d` Outputs both builds to directories inside of `outDir`. Defaults to `false`. You can run `duel --help` to get the same info. Below is the output of that: @@ -84,6 +101,7 @@ Usage: duel [options] Options: --project, -p [path] Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'. --pkg-dir, -k [path] The directory to start looking for a package.json file. Defaults to cwd. +--modules, -m Transform module globals for dual build target. Defaults to false. --dirs, -d Output both builds to directories inside of outDir. [esm, cjs]. --help, -h Print this message. ``` @@ -94,7 +112,7 @@ These are definitely edge cases, and would only really come up if your project m - This is going to work best if your CJS-first project uses file extensions in _relative_ specifiers. This is completely acceptable in CJS projects, and [required in ESM projects](https://nodejs.org/api/esm.html#import-specifiers). This package makes no attempt to rewrite bare specifiers, or remap any relative specifiers to a directory index. -- Unfortunately, TypeScript doesn't really build [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) very well in regards to preserving module system by file extension. For instance, there doesn't appear to be a way to convert an arbitrary `.ts` file into another module system, _while also preserving the module system of `.mts` and `.cts` files_, without requiring **multiple** package.json files. In my opinion, the `tsc` compiler is fundamentally broken in this regard, and at best is enforcing usage patterns it shouldn't. This is only mentioned for transparency, `duel` will correct for this and produce files with the module system you would expect based on the file's extension, so that it works with [how Node.js determines module systems](https://nodejs.org/api/packages.html#determining-module-system). +- Unfortunately, TypeScript doesn't really build [dual packages](https://nodejs.org/api/packages.html#dual-commonjses-module-packages) very well. One instance of unexpected behavior is when the compiler throws errors for ES module globals when running a dual CJS build, but not for the inverse case, despite both causing runtime errors in Node.js. See the [open issue](https://github.com/microsoft/TypeScript/issues/58658). You can circumvent this with `duel` by using the `--modules` option if your project uses module globals such as `import.meta` properties or `__dirname`, `__filename`, etc. in a CommonJS project. - If doing an `import type` across module systems, i.e. from `.mts` into `.cts`, or vice versa, you might encounter the compilation error ``error TS1452: 'resolution-mode' assertions are only supported when `moduleResolution` is `node16` or `nodenext`.``. This is a [known issue](https://github.com/microsoft/TypeScript/issues/49055) and TypeScript currently suggests installing the nightly build, i.e. `npm i typescript@next`. @@ -102,4 +120,6 @@ These are definitely edge cases, and would only really come up if your project m ## Notes -As far as I can tell, `duel` is one (if not the only) way to get a correct dual package build using `tsc` with only **one package.json and tsconfig.json file**, _while also preserving module system by file extension_. Basically, how you expect things to work. The Microsoft backed TypeScript team [keep](https://github.com/microsoft/TypeScript/pull/54546) [talking](https://github.com/microsoft/TypeScript/issues/54593) about dual build support, but they continue to [refuse to rewrite specifiers](https://github.com/microsoft/TypeScript/issues/16577). +As far as I can tell, `duel` is one (if not the only) way to get a correct dual package build using `tsc` without requiring multiple `tsconfig.json` files or extra configuration. The Microsoft backed TypeScript team [keep](https://github.com/microsoft/TypeScript/pull/54546) [talking](https://github.com/microsoft/TypeScript/issues/54593) about dual build support, but they continue to [refuse to rewrite specifiers](https://github.com/microsoft/TypeScript/issues/16577). + +Fortunately, Node.js has added `--experimental-require-module` so that you can [`require()` ES modules](https://nodejs.org/api/esm.html#require) if they don't use top level await, which sets the stage for possibly no longer requiring dual builds. diff --git a/package-lock.json b/package-lock.json index 0229efd..fef0ebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@knighted/duel", - "version": "2.0.0-rc.0", + "version": "2.0.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@knighted/duel", - "version": "2.0.0-rc.0", + "version": "2.0.0-rc.1", "license": "MIT", "dependencies": { - "@knighted/module": "^1.0.0-alpha.3", + "@knighted/module": "^1.0.0-alpha.4", "@knighted/specifier": "^2.0.0-rc.1", "find-up": "^6.3.0", "glob": "^10.3.3", @@ -25,6 +25,7 @@ "eslint": "^8.45.0", "eslint-plugin-n": "^16.0.1", "prettier": "^3.2.4", + "tsx": "^4.11.2", "typescript": "^5.5.0-dev.20240525", "vite": "^5.2.8" }, @@ -795,9 +796,9 @@ } }, "node_modules/@knighted/module": { - "version": "1.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@knighted/module/-/module-1.0.0-alpha.3.tgz", - "integrity": "sha512-C9GXBdbho53HvhcTEuMBXm1vC7HW0FSZnn3et3f62yHgfAUYOru6g7S3fStJRHJlw4W1q0upNvrrQ0iBN3kpqw==", + "version": "1.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@knighted/module/-/module-1.0.0-alpha.4.tgz", + "integrity": "sha512-nRkfGyukGVo+dTfn8OYRLpF1SLrjQF9ZqKjpAOOPTJy75YX+RFFAWMgzutc9PqoXeIeD3UFy/euWOFW40OGWsg==", "dependencies": { "@babel/parser": "^7.24.6", "@babel/traverse": "^7.24.6", @@ -2030,6 +2031,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-tsconfig": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", @@ -2805,6 +2818,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3148,6 +3170,25 @@ "node": ">=4" } }, + "node_modules/tsx": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.11.2.tgz", + "integrity": "sha512-V5DL5v1BuItjsQ2FN9+4OjR7n5cr8hSgN+VGmm/fd2/0cgQdBIWHcQ3bFYm/5ZTmyxkTDBUIaRuW2divgfPe0A==", + "dev": true, + "dependencies": { + "esbuild": "~0.20.2", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index f6508cb..0cf2d7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/duel", - "version": "2.0.0-rc.0", + "version": "2.0.0-rc.1", "description": "TypeScript dual packages.", "type": "module", "main": "dist/esm/duel.js", @@ -20,6 +20,8 @@ "scripts": { "prettier": "prettier -w src/*.js test/*.js", "lint": "eslint src/*.js test/*.js", + "test:integration": "node --test --test-reporter=spec test/integration.js", + "test:monorepos": "node --test --test-reporter=spec test/monorepos.js", "test": "c8 --reporter=text --reporter=text-summary --reporter=lcov node --test --test-reporter=spec test/*.js", "build": "node src/duel.js --dirs", "prepack": "npm run build" @@ -59,11 +61,12 @@ "eslint": "^8.45.0", "eslint-plugin-n": "^16.0.1", "prettier": "^3.2.4", + "tsx": "^4.11.2", "typescript": "^5.5.0-dev.20240525", "vite": "^5.2.8" }, "dependencies": { - "@knighted/module": "^1.0.0-alpha.3", + "@knighted/module": "^1.0.0-alpha.4", "@knighted/specifier": "^2.0.0-rc.1", "find-up": "^6.3.0", "glob": "^10.3.3", diff --git a/src/duel.js b/src/duel.js index 8027b7a..4fbe33d 100755 --- a/src/duel.js +++ b/src/duel.js @@ -30,7 +30,7 @@ const duel = async args => { const ctx = await init(args) if (ctx) { - const { projectDir, tsconfig, configPath, dirs, pkg } = ctx + const { projectDir, tsconfig, configPath, modules, dirs, pkg } = ctx const tsc = await findUp( async dir => { const tscBin = join(dir, 'node_modules', '.bin', 'tsc') @@ -130,22 +130,47 @@ const duel = async args => { } if (success) { - const compileFiles = getCompileFiles(tsc, projectDir) const subDir = join(projectDir, `_${hex}_`) - const dualConfigPath = join(subDir, `tsconfig.${hex}.json`) const absoluteDualOutDir = join( projectDir, isCjsBuild ? join(outDir, 'cjs') : join(outDir, 'esm'), ) const tsconfigDual = getOverrideTsConfig() const pkgRename = 'package.json.bak' + let dualConfigPath = join(projectDir, `tsconfig.${hex}.json`) let errorMsg = '' - // Copy project directory as a subdirectory - await mkdir(subDir) - await Promise.all( - compileFiles.map(file => cp(file, resolve(subDir, relative(projectDir, file)))), - ) + if (modules) { + const compileFiles = getCompileFiles(tsc, projectDir) + + dualConfigPath = join(subDir, `tsconfig.${hex}.json`) + await mkdir(subDir) + await Promise.all( + compileFiles.map(file => + cp(file, join(subDir, relative(projectDir, file).replace(/^(\.\.\/)*/, ''))), + ), + ) + + /** + * Transform ambiguous modules for the target dual build. + * @see https://github.com/microsoft/TypeScript/issues/58658 + */ + const toTransform = await glob(`${subDir}/**/*{.js,.jsx,.ts,.tsx}`, { + ignore: 'node_modules/**', + }) + + for (const file of toTransform) { + /** + * Maybe include the option to transform modules implicitly + * (modules: true) so that `exports` are correctly converted + * when targeting a CJS dual build. Depends on @knighted/module + * supporting he `modules` option. + * + * @see https://github.com/microsoft/TypeScript/issues/58658 + */ + await transform(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' }) + } + } /** * Create a new package.json with updated `type` field. @@ -160,18 +185,6 @@ const duel = async args => { ) await writeFile(dualConfigPath, JSON.stringify(tsconfigDual)) - /** - * Transform ambiguous modules for the target dual build. - * @see https://github.com/microsoft/TypeScript/issues/58658 - */ - const toTransform = await glob(`${subDir}/**/*{.js,.jsx,.ts,.tsx}`, { - ignore: 'node_modules/**', - }) - - for (const file of toTransform) { - await transform(file, { out: file, type: isCjsBuild ? 'commonjs' : 'module' }) - } - // Build dual log('Starting dual build...') try { diff --git a/src/init.js b/src/init.js index d674172..d641e47 100644 --- a/src/init.js +++ b/src/init.js @@ -30,6 +30,11 @@ const init = async args => { short: 'k', default: cwd(), }, + modules: { + type: 'boolean', + short: 'm', + default: false, + }, dirs: { type: 'boolean', short: 'd', @@ -59,10 +64,19 @@ const init = async args => { log( '--pkg-dir, -k [path] \t The directory to start looking for a package.json file. Defaults to cwd.', ) + log( + '--modules, -m \t\t Transform module globals for dual build target. Defaults to false.', + ) log('--dirs, -d \t\t Output both builds to directories inside of outDir. [esm, cjs].') log('--help, -h \t\t Print this message.') } else { - const { project, 'target-extension': targetExt, 'pkg-dir': pkgDir, dirs } = parsed + const { + project, + 'target-extension': targetExt, + 'pkg-dir': pkgDir, + modules, + dirs, + } = parsed let configPath = resolve(project) let stats = null let pkg = null @@ -134,6 +148,7 @@ const init = async args => { return { pkg, dirs, + modules, tsconfig, projectDir, configPath, diff --git a/test/__fixtures__/modulesCjs/package.json b/test/__fixtures__/modulesCjs/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/test/__fixtures__/modulesCjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/__fixtures__/modulesCjs/src/file.ts b/test/__fixtures__/modulesCjs/src/file.ts new file mode 100644 index 0000000..c917c70 --- /dev/null +++ b/test/__fixtures__/modulesCjs/src/file.ts @@ -0,0 +1,15 @@ +import { argv, stdout } from 'node:process' +import { pathToFileURL } from 'node:url' +import { realpath } from 'node:fs/promises' + +const detectCalledFromCli = async (path: string) => { + const realPath = await realpath(path) + + if (__filename === pathToFileURL(realPath).href) { + stdout.write('invoked as cli') + } +} + +detectCalledFromCli(argv[1]) + +require.resolve(`${__dirname}/other.js`) diff --git a/test/__fixtures__/modulesCjs/src/other.ts b/test/__fixtures__/modulesCjs/src/other.ts new file mode 100644 index 0000000..5480210 --- /dev/null +++ b/test/__fixtures__/modulesCjs/src/other.ts @@ -0,0 +1 @@ +exports.other = true diff --git a/test/__fixtures__/modulesCjs/tsconfig.json b/test/__fixtures__/modulesCjs/tsconfig.json new file mode 100644 index 0000000..d5f58b2 --- /dev/null +++ b/test/__fixtures__/modulesCjs/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "strict": true, + }, + "include": ["src"], +} diff --git a/test/__fixtures__/modulesEsm/package.json b/test/__fixtures__/modulesEsm/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/test/__fixtures__/modulesEsm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/__fixtures__/modulesEsm/src/file.ts b/test/__fixtures__/modulesEsm/src/file.ts new file mode 100644 index 0000000..2d1ea8a --- /dev/null +++ b/test/__fixtures__/modulesEsm/src/file.ts @@ -0,0 +1,15 @@ +import { argv, stdout } from 'node:process' +import { pathToFileURL } from 'node:url' +import { realpath } from 'node:fs/promises' + +const detectCalledFromCli = async (path: string) => { + const realPath = await realpath(path) + + if (import.meta.url === pathToFileURL(realPath).href) { + stdout.write('invoked as cli') + } +} + +detectCalledFromCli(argv[1]) + +import.meta.resolve(`${import.meta.dirname}/other.js`) diff --git a/test/__fixtures__/modulesEsm/src/other.ts b/test/__fixtures__/modulesEsm/src/other.ts new file mode 100644 index 0000000..8fc166d --- /dev/null +++ b/test/__fixtures__/modulesEsm/src/other.ts @@ -0,0 +1 @@ +export const other = true diff --git a/test/__fixtures__/modulesEsm/tsconfig.json b/test/__fixtures__/modulesEsm/tsconfig.json new file mode 100644 index 0000000..d5f58b2 --- /dev/null +++ b/test/__fixtures__/modulesEsm/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "strict": true, + }, + "include": ["src"], +} diff --git a/test/__fixtures__/mononpm/one/package.json b/test/__fixtures__/mononpm/one/package.json new file mode 100644 index 0000000..f6f537d --- /dev/null +++ b/test/__fixtures__/mononpm/one/package.json @@ -0,0 +1,23 @@ +{ + "name": "one", + "version": "1.0.0", + "type": "module", + "main": "dist/main.js", + "exports": { + ".": { + "import": { + "types": "./dist/main.d.ts", + "default": "./dist/main.js" + }, + "require": { + "types": "./dist/cjs/main.d.cts", + "default": "./dist/cjs/main.cjs" + }, + "default": "./dist/main.js" + }, + "./package.json": "./package.json" + }, + "dependencies": { + "two": "^1.0.0" + } +} diff --git a/test/__fixtures__/mononpm/one/src/file.ts b/test/__fixtures__/mononpm/one/src/file.ts new file mode 100644 index 0000000..487e853 --- /dev/null +++ b/test/__fixtures__/mononpm/one/src/file.ts @@ -0,0 +1,6 @@ +import { say } from 'two/file' + +export const talk = (greet: string) => { + return say(greet) +} +export const oneFile = true diff --git a/test/__fixtures__/mononpm/one/src/main.ts b/test/__fixtures__/mononpm/one/src/main.ts new file mode 100644 index 0000000..22bd56e --- /dev/null +++ b/test/__fixtures__/mononpm/one/src/main.ts @@ -0,0 +1,11 @@ +import { say } from 'two' + +import { talk } from './file.js' +import { oneOther } from './other.js' + +const main = () => { + talk(`Welcome ${oneOther}`) + say(`Hello from ${import.meta.url}`) +} + +main() diff --git a/test/__fixtures__/mononpm/one/src/other.ts b/test/__fixtures__/mononpm/one/src/other.ts new file mode 100644 index 0000000..51682d5 --- /dev/null +++ b/test/__fixtures__/mononpm/one/src/other.ts @@ -0,0 +1,3 @@ +import { other } from 'two/other' + +export { other as oneOther } diff --git a/test/__fixtures__/mononpm/one/tsconfig.json b/test/__fixtures__/mononpm/one/tsconfig.json new file mode 100644 index 0000000..676aa8a --- /dev/null +++ b/test/__fixtures__/mononpm/one/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + }, + "include": ["src"], +} diff --git a/test/__fixtures__/mononpm/package-lock.json b/test/__fixtures__/mononpm/package-lock.json new file mode 100644 index 0000000..0799577 --- /dev/null +++ b/test/__fixtures__/mononpm/package-lock.json @@ -0,0 +1,55 @@ +{ + "name": "mononpm", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mononpm", + "version": "1.0.0", + "workspaces": [ + "one", + "two" + ], + "devDependencies": { + "typescript": "^5.4.5" + } + }, + "node_modules/one": { + "resolved": "one", + "link": true + }, + "node_modules/two": { + "resolved": "two", + "link": true + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "one": { + "version": "1.0.0", + "dependencies": { + "two": "^1.0.0" + } + }, + "true": { + "name": "mono-npm", + "version": "1.0.0", + "extraneous": true, + "license": "ISC" + }, + "two": { + "version": "1.0.0" + } + } +} diff --git a/test/__fixtures__/mononpm/package.json b/test/__fixtures__/mononpm/package.json new file mode 100644 index 0000000..ee47fbe --- /dev/null +++ b/test/__fixtures__/mononpm/package.json @@ -0,0 +1,12 @@ +{ + "name": "mononpm", + "version": "1.0.0", + "type": "module", + "workspaces": [ + "one", + "two" + ], + "devDependencies": { + "typescript": "^5.4.5" + } +} diff --git a/test/__fixtures__/mononpm/two/package.json b/test/__fixtures__/mononpm/two/package.json new file mode 100644 index 0000000..e073ff6 --- /dev/null +++ b/test/__fixtures__/mononpm/two/package.json @@ -0,0 +1,42 @@ +{ + "name": "two", + "version": "1.0.0", + "type": "module", + "main": "dist/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + }, + "default": "./dist/index.js" + }, + "./file": { + "import": { + "types": "./dist/file.d.ts", + "default": "./dist/file.js" + }, + "require": { + "types": "./dist/cjs/file.d.cts", + "default": "./dist/cjs/file.cjs" + }, + "default": "./dist/file.js" + }, + "./other": { + "import": { + "types": "./dist/other.d.ts", + "default": "./dist/other.js" + }, + "require": { + "types": "./dist/cjs/other.d.cts", + "default": "./dist/cjs/other.cjs" + }, + "default": "./dist/other.js" + }, + "./package.json": "./package.json" + } +} diff --git a/test/__fixtures__/mononpm/two/src/file.ts b/test/__fixtures__/mononpm/two/src/file.ts new file mode 100644 index 0000000..e9d1653 --- /dev/null +++ b/test/__fixtures__/mononpm/two/src/file.ts @@ -0,0 +1,3 @@ +export function say(msg: string): string { + return `${import.meta.url} ${msg}` +} diff --git a/test/__fixtures__/mononpm/two/src/index.ts b/test/__fixtures__/mononpm/two/src/index.ts new file mode 100644 index 0000000..1a34808 --- /dev/null +++ b/test/__fixtures__/mononpm/two/src/index.ts @@ -0,0 +1,2 @@ +export * from './file.js' +export * from './other.js' diff --git a/test/__fixtures__/mononpm/two/src/other.ts b/test/__fixtures__/mononpm/two/src/other.ts new file mode 100644 index 0000000..fac1c61 --- /dev/null +++ b/test/__fixtures__/mononpm/two/src/other.ts @@ -0,0 +1 @@ +export const other: string = 'other' diff --git a/test/__fixtures__/mononpm/two/tsconfig.json b/test/__fixtures__/mononpm/two/tsconfig.json new file mode 100644 index 0000000..676aa8a --- /dev/null +++ b/test/__fixtures__/mononpm/two/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + }, + "include": ["src"], +} diff --git a/test/__fixtures__/monopnpm/package.json b/test/__fixtures__/monopnpm/package.json new file mode 100644 index 0000000..314e13e --- /dev/null +++ b/test/__fixtures__/monopnpm/package.json @@ -0,0 +1,6 @@ +{ + "name": "monopnpm", + "version": "1.0.0", + "type": "module", + "packageManager": "pnpm@9.1.4+sha512.9df9cf27c91715646c7d675d1c9c8e41f6fce88246f1318c1aa6a1ed1aeb3c4f032fcdf4ba63cc69c4fe6d634279176b5358727d8f2cc1e65b65f43ce2f8bfb0" +} diff --git a/test/integration.js b/test/integration.js index 3870c6e..4c47e6a 100644 --- a/test/integration.js +++ b/test/integration.js @@ -80,13 +80,19 @@ describe('duel', () => { ) }) - it('creates a dual CJS build', async t => { + it('creates a dual CJS build while transforming module globals', async t => { const spy = t.mock.method(global.console, 'log') t.after(async () => { await rmDist(esmDist) }) - await duel(['--project', 'test/__fixtures__/esmProject', '--pkg-dir', esmProject]) + await duel([ + '--project', + 'test/__fixtures__/esmProject', + '--pkg-dir', + esmProject, + '-m', + ]) // Third call because of logging for starting each build. assert.ok( @@ -124,13 +130,19 @@ describe('duel', () => { assert.equal(statusCjs, 0) }) - it('creates a dual ESM build', async t => { + it('creates a dual ESM build while transforming module globals', async t => { const spy = t.mock.method(global.console, 'log') t.after(async () => { await rmDist(cjsDist) }) - await duel(['-p', 'test/__fixtures__/cjsProject/tsconfig.json', '-k', cjsProject]) + await duel([ + '-p', + 'test/__fixtures__/cjsProject/tsconfig.json', + '-k', + cjsProject, + '-m', + ]) assert.ok( spy.mock.calls[2].arguments[0].startsWith('Successfully created a dual ESM build'), diff --git a/test/monorepos.js b/test/monorepos.js new file mode 100644 index 0000000..a3b29ac --- /dev/null +++ b/test/monorepos.js @@ -0,0 +1,57 @@ +import { describe, it, before } from 'node:test' +import assert from 'node:assert/strict' +import { resolve, join } from 'node:path' +import { rm } from 'node:fs/promises' +import { spawnSync } from 'node:child_process' + +import { duel } from '../src/duel.js' + +const fixtures = resolve(import.meta.dirname, '__fixtures__') +const npm = join(fixtures, 'mononpm') +const npmOne = join(npm, 'one') +const npmTwo = join(npm, 'two') +const rmDist = async distPath => { + await rm(distPath, { recursive: true, force: true }) +} + +describe('duel monorepos', () => { + before(async () => { + await rmDist(join(npmOne, 'dist')) + await rmDist(join(npmTwo, 'dist')) + }) + + it('works with npm monorepos (workspaces)', async t => { + t.after(async () => { + await rmDist(join(npmOne, 'dist')) + await rmDist(join(npmTwo, 'dist')) + }) + + spawnSync('npm', ['install'], { cwd: npm }) + + // Build the packages (dependency first) + await duel(['-p', npmTwo, '-k', npmTwo, '-m']) + await duel(['-p', npmOne, '-k', npmOne, '-m']) + + // Check for runtime errors against Node.js + const { status: twoEsm } = spawnSync('node', [join(npmTwo, 'dist', 'file.js')], { + stdio: 'inherit', + }) + assert.equal(twoEsm, 0) + const { status: twoCjs } = spawnSync( + 'node', + [join(npmTwo, 'dist', 'cjs', 'file.cjs')], + { stdio: 'inherit' }, + ) + assert.equal(twoCjs, 0) + const { status: oneEsm } = spawnSync('node', [join(npmOne, 'dist', 'main.js')], { + stdio: 'inherit', + }) + assert.equal(oneEsm, 0) + const { status: oneCjs } = spawnSync( + 'node', + [join(npmOne, 'dist', 'cjs', 'main.cjs')], + { stdio: 'inherit' }, + ) + assert.equal(oneCjs, 0) + }) +})