From 99c44f9e884acf27772505609d4d6cbf6a75fcfb Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Wed, 30 Aug 2023 15:41:21 -0400 Subject: [PATCH] fix(bundling): add faux-ESM files so "import" in Node works with both named and default exports (#18916) --- e2e/js/src/js-packaging.test.ts | 14 +++++-- e2e/rollup/src/rollup.test.ts | 12 ++++-- .../rollup/lib/update-package-json.spec.ts | 3 +- .../rollup/lib/update-package-json.ts | 38 +++++++++++++++++-- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/e2e/js/src/js-packaging.test.ts b/e2e/js/src/js-packaging.test.ts index 55352869a1a48..208133772405c 100644 --- a/e2e/js/src/js-packaging.test.ts +++ b/e2e/js/src/js-packaging.test.ts @@ -35,10 +35,14 @@ describe('packaging libs', () => { runCLI( `generate @nx/js:lib ${rollupLib} --bundler=rollup --no-interactive` ); + updateFile(`libs/${rollupLib}/src/index.ts`, (content) => { + // Test that default functions work in ESM (Node). + return `${content}\nexport default function f() { return 'rollup default' }`; + }); runCLI(`build ${esbuildLib}`); runCLI(`build ${viteLib}`); - runCLI(`build ${rollupLib}`); + runCLI(`build ${rollupLib} --generateExportsField`); const pmc = getPackageManagerCommand(); let output: string; @@ -66,10 +70,11 @@ describe('packaging libs', () => { ` const { ${esbuildLib} } = require('@proj/${esbuildLib}'); const { ${viteLib} } = require('@proj/${viteLib}'); - const { ${rollupLib} } = require('@proj/${rollupLib}'); + const { default: rollupDefault, ${rollupLib} } = require('@proj/${rollupLib}'); console.log(${esbuildLib}()); console.log(${viteLib}()); console.log(${rollupLib}()); + console.log(rollupDefault()); ` ); runCommand(pmc.install, { @@ -81,6 +86,7 @@ describe('packaging libs', () => { expect(output).toContain(esbuildLib); expect(output).toContain(viteLib); expect(output).toContain(rollupLib); + expect(output).toContain('rollup default'); // Make sure outputs in esm project createFile( @@ -105,10 +111,11 @@ describe('packaging libs', () => { ` import { ${esbuildLib} } from '@proj/${esbuildLib}'; import { ${viteLib} } from '@proj/${viteLib}'; - import { ${rollupLib} } from '@proj/${rollupLib}'; + import rollupDefault, { ${rollupLib} } from '@proj/${rollupLib}'; console.log(${esbuildLib}()); console.log(${viteLib}()); console.log(${rollupLib}()); + console.log(rollupDefault()); ` ); runCommand(pmc.install, { @@ -120,6 +127,7 @@ describe('packaging libs', () => { expect(output).toContain(esbuildLib); expect(output).toContain(viteLib); expect(output).toContain(rollupLib); + expect(output).toContain('rollup default'); }, 500_000); it('should build with tsc, swc and be used in CJS/ESM projects', async () => { diff --git a/e2e/rollup/src/rollup.test.ts b/e2e/rollup/src/rollup.test.ts index b7253ebc63c06..f27c1c0cfd7a0 100644 --- a/e2e/rollup/src/rollup.test.ts +++ b/e2e/rollup/src/rollup.test.ts @@ -29,7 +29,8 @@ describe('Rollup Plugin', () => { checkFilesExist(`dist/libs/${myPkg}/index.cjs.d.ts`); expect(readJson(`dist/libs/${myPkg}/package.json`).exports).toEqual({ '.': { - import: './index.esm.js', + module: './index.esm.js', + import: './index.cjs.mjs', default: './index.cjs.js', }, './package.json': './package.json', @@ -95,15 +96,18 @@ describe('Rollup Plugin', () => { expect(readJson(`dist/libs/${myPkg}/package.json`).exports).toEqual({ './package.json': './package.json', '.': { - import: './index.esm.js', + module: './index.esm.js', + import: './index.cjs.mjs', default: './index.cjs.js', }, './bar': { - import: './bar.esm.js', + module: './bar.esm.js', + import: './bar.cjs.mjs', default: './bar.cjs.js', }, './foo': { - import: './foo.esm.js', + module: './foo.esm.js', + import: './foo.cjs.mjs', default: './foo.cjs.js', }, }); diff --git a/packages/rollup/src/executors/rollup/lib/update-package-json.spec.ts b/packages/rollup/src/executors/rollup/lib/update-package-json.spec.ts index b5b1e28ec3527..f36c7b6e3c0dd 100644 --- a/packages/rollup/src/executors/rollup/lib/update-package-json.spec.ts +++ b/packages/rollup/src/executors/rollup/lib/update-package-json.spec.ts @@ -98,7 +98,8 @@ describe('updatePackageJson', () => { exports: { './package.json': './package.json', '.': { - import: './index.esm.js', + module: './index.esm.js', + import: './index.cjs.mjs', default: './index.cjs.js', }, }, diff --git a/packages/rollup/src/executors/rollup/lib/update-package-json.ts b/packages/rollup/src/executors/rollup/lib/update-package-json.ts index c1c0f7b7f0380..dc6c5c084af53 100644 --- a/packages/rollup/src/executors/rollup/lib/update-package-json.ts +++ b/packages/rollup/src/executors/rollup/lib/update-package-json.ts @@ -1,4 +1,4 @@ -import { basename, dirname, parse, relative } from 'path'; +import { basename, join, parse } from 'path'; import { ExecutorContext } from 'nx/src/config/misc-interfaces'; import { ProjectGraphProjectNode } from 'nx/src/config/project-graph'; import { @@ -6,9 +6,10 @@ import { updateBuildableProjectPackageJsonDependencies, } from '@nx/js/src/utils/buildable-libs-utils'; import { writeJsonFile } from 'nx/src/utils/fileutils'; +import { writeFileSync } from 'fs'; import { PackageJson } from 'nx/src/utils/package-json'; import { NormalizedRollupExecutorOptions } from './normalize'; -import { normalizePath } from '@nx/devkit'; +import { stripIndents } from '@nx/devkit'; // TODO(jack): Use updatePackageJson from @nx/js instead. export function updatePackageJson( @@ -43,7 +44,10 @@ export function updatePackageJson( if (options.generateExportsField) { for (const [exportEntry, filePath] of Object.entries(esmExports)) { packageJson.exports[exportEntry] = hasCjsFormat - ? { import: filePath } + ? // If CJS format is used, make sure `import` (from Node) points to same instance of the package. + // Otherwise, packages that are required to be singletons (like React, RxJS, etc.) will break. + // Reserve `module` entry for bundlers to accommodate tree-shaking. + { [hasCjsFormat ? 'module' : 'import']: filePath } : filePath; } } @@ -64,7 +68,35 @@ export function updatePackageJson( if (options.generateExportsField) { for (const [exportEntry, filePath] of Object.entries(cjsExports)) { if (hasEsmFormat) { + // If ESM format used, make sure `import` (from Node) points to a wrapped + // version of CJS file to ensure the package remains a singleton. + // TODO(jack): This can be made into a rollup plugin to re-use in Vite. + const relativeFile = parse(filePath).base; + const fauxEsmFilePath = filePath.replace(/\.cjs\.js$/, '.cjs.mjs'); + packageJson.exports[exportEntry]['import'] ??= fauxEsmFilePath; packageJson.exports[exportEntry]['default'] ??= filePath; + // Re-export from relative CJS file, and Node will synthetically export it as ESM. + // Make sure both ESM and CJS point to same instance of the package because libs like React, RxJS, etc. requires it. + // Also need a special .cjs.default.js file that re-exports the `default` from CJS, or else + // default import in Node will not work. + writeFileSync( + join( + options.outputPath, + filePath.replace(/\.cjs\.js$/, '.cjs.default.js') + ), + `exports._default = require('./${parse(filePath).base}').default;` + ); + writeFileSync( + join(options.outputPath, fauxEsmFilePath), + // Re-export from relative CJS file, and Node will synthetically export it as ESM. + stripIndents` + export * from './${relativeFile}'; + export { _default as default } from './${relativeFile.replace( + /\.cjs\.js$/, + '.cjs.default.js' + )}'; + ` + ); } else { packageJson.exports[exportEntry] = filePath; }