fix(bundling): add faux-ESM files so "import" in Node works with both named and default exports (#18916)

This commit is contained in:
Jack Hsu 2023-08-30 15:41:21 -04:00 committed by GitHub
parent c9aad2dc8c
commit 99c44f9e88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 11 deletions

View File

@ -35,10 +35,14 @@ describe('packaging libs', () => {
runCLI( runCLI(
`generate @nx/js:lib ${rollupLib} --bundler=rollup --no-interactive` `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 ${esbuildLib}`);
runCLI(`build ${viteLib}`); runCLI(`build ${viteLib}`);
runCLI(`build ${rollupLib}`); runCLI(`build ${rollupLib} --generateExportsField`);
const pmc = getPackageManagerCommand(); const pmc = getPackageManagerCommand();
let output: string; let output: string;
@ -66,10 +70,11 @@ describe('packaging libs', () => {
` `
const { ${esbuildLib} } = require('@proj/${esbuildLib}'); const { ${esbuildLib} } = require('@proj/${esbuildLib}');
const { ${viteLib} } = require('@proj/${viteLib}'); const { ${viteLib} } = require('@proj/${viteLib}');
const { ${rollupLib} } = require('@proj/${rollupLib}'); const { default: rollupDefault, ${rollupLib} } = require('@proj/${rollupLib}');
console.log(${esbuildLib}()); console.log(${esbuildLib}());
console.log(${viteLib}()); console.log(${viteLib}());
console.log(${rollupLib}()); console.log(${rollupLib}());
console.log(rollupDefault());
` `
); );
runCommand(pmc.install, { runCommand(pmc.install, {
@ -81,6 +86,7 @@ describe('packaging libs', () => {
expect(output).toContain(esbuildLib); expect(output).toContain(esbuildLib);
expect(output).toContain(viteLib); expect(output).toContain(viteLib);
expect(output).toContain(rollupLib); expect(output).toContain(rollupLib);
expect(output).toContain('rollup default');
// Make sure outputs in esm project // Make sure outputs in esm project
createFile( createFile(
@ -105,10 +111,11 @@ describe('packaging libs', () => {
` `
import { ${esbuildLib} } from '@proj/${esbuildLib}'; import { ${esbuildLib} } from '@proj/${esbuildLib}';
import { ${viteLib} } from '@proj/${viteLib}'; import { ${viteLib} } from '@proj/${viteLib}';
import { ${rollupLib} } from '@proj/${rollupLib}'; import rollupDefault, { ${rollupLib} } from '@proj/${rollupLib}';
console.log(${esbuildLib}()); console.log(${esbuildLib}());
console.log(${viteLib}()); console.log(${viteLib}());
console.log(${rollupLib}()); console.log(${rollupLib}());
console.log(rollupDefault());
` `
); );
runCommand(pmc.install, { runCommand(pmc.install, {
@ -120,6 +127,7 @@ describe('packaging libs', () => {
expect(output).toContain(esbuildLib); expect(output).toContain(esbuildLib);
expect(output).toContain(viteLib); expect(output).toContain(viteLib);
expect(output).toContain(rollupLib); expect(output).toContain(rollupLib);
expect(output).toContain('rollup default');
}, 500_000); }, 500_000);
it('should build with tsc, swc and be used in CJS/ESM projects', async () => { it('should build with tsc, swc and be used in CJS/ESM projects', async () => {

View File

@ -29,7 +29,8 @@ describe('Rollup Plugin', () => {
checkFilesExist(`dist/libs/${myPkg}/index.cjs.d.ts`); checkFilesExist(`dist/libs/${myPkg}/index.cjs.d.ts`);
expect(readJson(`dist/libs/${myPkg}/package.json`).exports).toEqual({ 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', default: './index.cjs.js',
}, },
'./package.json': './package.json', './package.json': './package.json',
@ -95,15 +96,18 @@ describe('Rollup Plugin', () => {
expect(readJson(`dist/libs/${myPkg}/package.json`).exports).toEqual({ expect(readJson(`dist/libs/${myPkg}/package.json`).exports).toEqual({
'./package.json': './package.json', './package.json': './package.json',
'.': { '.': {
import: './index.esm.js', module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js', default: './index.cjs.js',
}, },
'./bar': { './bar': {
import: './bar.esm.js', module: './bar.esm.js',
import: './bar.cjs.mjs',
default: './bar.cjs.js', default: './bar.cjs.js',
}, },
'./foo': { './foo': {
import: './foo.esm.js', module: './foo.esm.js',
import: './foo.cjs.mjs',
default: './foo.cjs.js', default: './foo.cjs.js',
}, },
}); });

View File

@ -98,7 +98,8 @@ describe('updatePackageJson', () => {
exports: { exports: {
'./package.json': './package.json', './package.json': './package.json',
'.': { '.': {
import: './index.esm.js', module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js', default: './index.cjs.js',
}, },
}, },

View File

@ -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 { ExecutorContext } from 'nx/src/config/misc-interfaces';
import { ProjectGraphProjectNode } from 'nx/src/config/project-graph'; import { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
import { import {
@ -6,9 +6,10 @@ import {
updateBuildableProjectPackageJsonDependencies, updateBuildableProjectPackageJsonDependencies,
} from '@nx/js/src/utils/buildable-libs-utils'; } from '@nx/js/src/utils/buildable-libs-utils';
import { writeJsonFile } from 'nx/src/utils/fileutils'; import { writeJsonFile } from 'nx/src/utils/fileutils';
import { writeFileSync } from 'fs';
import { PackageJson } from 'nx/src/utils/package-json'; import { PackageJson } from 'nx/src/utils/package-json';
import { NormalizedRollupExecutorOptions } from './normalize'; import { NormalizedRollupExecutorOptions } from './normalize';
import { normalizePath } from '@nx/devkit'; import { stripIndents } from '@nx/devkit';
// TODO(jack): Use updatePackageJson from @nx/js instead. // TODO(jack): Use updatePackageJson from @nx/js instead.
export function updatePackageJson( export function updatePackageJson(
@ -43,7 +44,10 @@ export function updatePackageJson(
if (options.generateExportsField) { if (options.generateExportsField) {
for (const [exportEntry, filePath] of Object.entries(esmExports)) { for (const [exportEntry, filePath] of Object.entries(esmExports)) {
packageJson.exports[exportEntry] = hasCjsFormat 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; : filePath;
} }
} }
@ -64,7 +68,35 @@ export function updatePackageJson(
if (options.generateExportsField) { if (options.generateExportsField) {
for (const [exportEntry, filePath] of Object.entries(cjsExports)) { for (const [exportEntry, filePath] of Object.entries(cjsExports)) {
if (hasEsmFormat) { 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; 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 { } else {
packageJson.exports[exportEntry] = filePath; packageJson.exports[exportEntry] = filePath;
} }