Skip to content

Commit

Permalink
fix(bundling): add faux-ESM files so "import" in Node works with both…
Browse files Browse the repository at this point in the history
… named and default exports (#18916)
  • Loading branch information
jaysoo authored Aug 30, 2023
1 parent c9aad2d commit 99c44f9
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 11 deletions.
14 changes: 11 additions & 3 deletions e2e/js/src/js-packaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, {
Expand All @@ -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(
Expand All @@ -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, {
Expand All @@ -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 () => {
Expand Down
12 changes: 8 additions & 4 deletions e2e/rollup/src/rollup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
Expand Down
38 changes: 35 additions & 3 deletions packages/rollup/src/executors/rollup/lib/update-package-json.ts
Original file line number Diff line number Diff line change
@@ -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,
} 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(
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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;
}
Expand Down

0 comments on commit 99c44f9

Please sign in to comment.