From 7cc2f8d46c26cbe7e6ed5a5ea9735fec8e445c5f Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Wed, 30 Aug 2023 12:29:04 -0400 Subject: [PATCH] fix(bundling): add faux-ESM files so "import" in Node works with both named and default exports --- e2e/js/src/js-packaging.test.ts | 14 +++++-- .../rollup/lib/update-package-json.ts | 40 +++++++++++++++++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/e2e/js/src/js-packaging.test.ts b/e2e/js/src/js-packaging.test.ts index 55352869a1a484..208133772405cd 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/packages/rollup/src/executors/rollup/lib/update-package-json.ts b/packages/rollup/src/executors/rollup/lib/update-package-json.ts index c1c0f7b7f03804..58c30adaac2ba8 100644 --- a/packages/rollup/src/executors/rollup/lib/update-package-json.ts +++ b/packages/rollup/src/executors/rollup/lib/update-package-json.ts @@ -1,14 +1,15 @@ -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 { DependentBuildableProjectNode, - updateBuildableProjectPackageJsonDependencies, + 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; }