Skip to content

Commit

Permalink
feat(web): add support for Node ESM when used to package SSR-ready li…
Browse files Browse the repository at this point in the history
…brary (#10349)
  • Loading branch information
jaysoo committed May 18, 2022
1 parent 7b9b0cd commit 70f1a6b
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 78 deletions.
2 changes: 1 addition & 1 deletion docs/generated/packages/web.json
Expand Up @@ -659,7 +659,7 @@
"description": "Only build the specified comma-separated formats (`esm,umd,cjs`)",
"alias": "f",
"items": { "type": "string", "enum": ["esm", "umd", "cjs"] },
"default": ["esm", "umd"]
"default": ["esm"]
},
"external": {
"type": "array",
Expand Down
36 changes: 14 additions & 22 deletions e2e/react/src/react-package.test.ts
Expand Up @@ -104,11 +104,9 @@ describe('Build React libraries and apps', () => {
runCLI(`build ${childLib}`);
runCLI(`build ${childLib2}`);

checkFilesExist(`dist/libs/${childLib}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib}/index.umd.js`);
checkFilesExist(`dist/libs/${childLib}/index.js`);

checkFilesExist(`dist/libs/${childLib2}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib2}/index.umd.js`);
checkFilesExist(`dist/libs/${childLib2}/index.js`);

checkFilesExist(`dist/libs/${childLib}/assets/hello.txt`);
checkFilesExist(`dist/libs/${childLib2}/README.md`);
Expand All @@ -118,8 +116,7 @@ describe('Build React libraries and apps', () => {
*/
runCLI(`build ${parentLib}`);

checkFilesExist(`dist/libs/${parentLib}/index.esm.js`);
checkFilesExist(`dist/libs/${parentLib}/index.umd.js`);
checkFilesExist(`dist/libs/${parentLib}/index.js`);

const jsonFile = readJson(`dist/libs/${parentLib}/package.json`);
expect(jsonFile.peerDependencies).toEqual(
Expand All @@ -136,9 +133,9 @@ describe('Build React libraries and apps', () => {

runCLI(`build ${parentLib} --skip-nx-cache`);

checkFilesExist(`dist/libs/${parentLib}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib2}/index.esm.js`);
checkFilesExist(`dist/libs/${parentLib}/index.js`);
checkFilesExist(`dist/libs/${childLib}/index.js`);
checkFilesExist(`dist/libs/${childLib2}/index.js`);
});

it('should support --format option', () => {
Expand All @@ -151,25 +148,20 @@ export async function h() { return 'c'; }
`
);

runCLI(`build ${childLib} --format cjs,esm,umd`);
runCLI(`build ${childLib} --format cjs,esm`);

checkFilesExist(`dist/libs/${childLib}/index.cjs.js`);
checkFilesExist(`dist/libs/${childLib}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib}/index.umd.js`);
checkFilesExist(`dist/libs/${childLib}/index.cjs`);
checkFilesExist(`dist/libs/${childLib}/index.js`);

const cjsPackageSize = getSize(
tmpProjPath(`dist/libs/${childLib}/index.cjs.js`)
tmpProjPath(`dist/libs/${childLib}/index.cjs`)
);
const esmPackageSize = getSize(
tmpProjPath(`dist/libs/${childLib}/index.esm.js`)
);
const umdPackageSize = getSize(
tmpProjPath(`dist/libs/${childLib}/index.umd.js`)
tmpProjPath(`dist/libs/${childLib}/index.js`)
);

// This is a loose requirement that ESM and CJS packages should be less than the UMD counterpart.
expect(esmPackageSize).toBeLessThanOrEqual(umdPackageSize);
expect(cjsPackageSize).toBeLessThanOrEqual(umdPackageSize);
// This is a loose requirement that ESM should be smaller than CJS output.
expect(esmPackageSize).toBeLessThanOrEqual(cjsPackageSize);
});

it('should preserve the tsconfig target set by user', () => {
Expand Down Expand Up @@ -214,7 +206,7 @@ export async function h() { return 'c'; }
// What we're testing
runCLI(`build ${myLib}`);
// Assertion
const content = readFile(`dist/libs/${myLib}/index.esm.js`);
const content = readFile(`dist/libs/${myLib}/index.js`);

/**
* Then check if the result contains this "promise" polyfill?
Expand Down
2 changes: 1 addition & 1 deletion e2e/web/src/web.test.ts
Expand Up @@ -95,7 +95,7 @@ describe('Web Components Applications', () => {
checkFilesExist(`dist/apps/_should_not_remove.txt`);

// Asset that React runtime is imported
expect(readFile(`dist/libs/${libName}/index.esm.js`)).toMatch(
expect(readFile(`dist/libs/${libName}/index.js`)).toMatch(
/react\/jsx-runtime/
);

Expand Down
4 changes: 2 additions & 2 deletions e2e/workspace-integrations/src/cache.test.ts
Expand Up @@ -168,7 +168,7 @@ describe('cache', () => {
};
config.targets.build = {
executor: '@nrwl/workspace:run-commands',
outputs: [`dist/libs/${mylib1}/index.esm.js`],
outputs: [`dist/libs/${mylib1}/index.js`],
options: {
commands: [
{
Expand All @@ -193,7 +193,7 @@ describe('cache', () => {
expect(outputWithBuildTasksCached).toContain('cache');
expectCached(outputWithBuildTasksCached, [mylib1]);
// Ensure that only the specific file in outputs was copied to cache
expect(listFiles(`dist/libs/${mylib1}`)).toEqual([`index.esm.js`]);
expect(listFiles(`dist/libs/${mylib1}`)).toEqual([`index.js`]);
}, 120000);

function expectCached(
Expand Down
8 changes: 8 additions & 0 deletions packages/nx/src/utils/package-json.ts
Expand Up @@ -25,6 +25,14 @@ export interface PackageJson {
name: string;
version: string;
scripts?: Record<string, string>;
type?: 'module' | 'commonjs';
main?: string;
types?: string;
module?: string;
exports?: Record<
string,
{ types?: string; require?: string; import?: string }
>;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
Expand Down
156 changes: 156 additions & 0 deletions packages/web/src/executors/rollup/lib/update-package-json.spec.ts
@@ -0,0 +1,156 @@
import { updatePackageJson } from './update-package-json';
import * as utils from 'nx/src/utils/fileutils';
import { PackageJson } from 'nx/src/utils/package-json';

jest.mock('nx/src/utils/fileutils', () => ({
writeJsonFile: () => {},
}));

describe('updatePackageJson', () => {
const commonOptions = {
outputPath: 'dist/index.js',
tsConfig: './tsconfig.json',
project: './package.json',
entryFile: './index.js',
entryRoot: '.',
projectRoot: '.',
assets: [],
rollupConfig: [],
};

const sharedContext = {
isVerbose: false,
workspace: { version: 2, projects: {} },
root: '',
cwd: '',
};

it('should support ESM', () => {
const spy = jest.spyOn(utils, 'writeJsonFile');

updatePackageJson(
{
...commonOptions,
format: ['esm'],
},
sharedContext,
{ type: 'app', name: 'test', data: {} },
[],
{} as unknown as PackageJson
);

expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
exports: {
'.': {
types: './index.d.ts',
import: './index.js',
},
},
main: './index.js',
module: './index.js',
type: 'module',
types: './index.d.ts',
});

spy.mockRestore();
});

it('should support CJS', () => {
const spy = jest.spyOn(utils, 'writeJsonFile');

updatePackageJson(
{
...commonOptions,
format: ['cjs'],
},
sharedContext,
{ type: 'app', name: 'test', data: {} },
[],
{} as unknown as PackageJson
);

expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
exports: {
'.': {
types: './index.d.ts',
require: './index.cjs',
},
},
main: './index.cjs',
type: 'commonjs',
types: './index.d.ts',
});

spy.mockRestore();
});

it('should support ESM + CJS', () => {
const spy = jest.spyOn(utils, 'writeJsonFile');

updatePackageJson(
{
...commonOptions,
format: ['esm', 'cjs'],
},
sharedContext,
{ type: 'app', name: 'test', data: {} },
[],
{} as unknown as PackageJson
);

expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
exports: {
'.': {
types: './index.d.ts',
import: './index.js',
require: './index.cjs',
},
},
main: './index.cjs',
module: './index.js',
type: 'module',
types: './index.d.ts',
});

spy.mockRestore();
});

it('should support custom exports field', () => {
const spy = jest.spyOn(utils, 'writeJsonFile');

updatePackageJson(
{
...commonOptions,
format: ['esm'],
},
sharedContext,
{ type: 'app', name: 'test', data: {} },
[],
{
exports: {
foo: {
import: './foo.js',
},
},
} as unknown as PackageJson
);

expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
exports: {
'.': {
types: './index.d.ts',
import: './index.js',
},
foo: {
import: './foo.js',
},
},
main: './index.js',
module: './index.js',
type: 'module',
types: './index.d.ts',
});

spy.mockRestore();
});
});
76 changes: 76 additions & 0 deletions packages/web/src/executors/rollup/lib/update-package-json.ts
@@ -0,0 +1,76 @@
import { relative } from 'path';
import { ExecutorContext } from 'nx/src/config/misc-interfaces';
import { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
import {
DependentBuildableProjectNode,
updateBuildableProjectPackageJsonDependencies,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { writeJsonFile } from 'nx/src/utils/fileutils';
import { PackageJson } from 'nx/src/utils/package-json';
import { NormalizedWebRollupOptions } from './normalize';

export function updatePackageJson(
options: NormalizedWebRollupOptions,
context: ExecutorContext,
target: ProjectGraphProjectNode,
dependencies: DependentBuildableProjectNode[],
packageJson: PackageJson
) {
const hasEsmFormat = options.format.includes('esm');
const hasCjsFormat =
options.format.includes('umd') || options.format.includes('cjs');

const types = `./${relative(options.entryRoot, options.entryFile).replace(
/\.[jt]sx?$/,
'.d.ts'
)}`;
const exports = {
// TS 4.5+
'.': {
types,
},
};

if (hasEsmFormat) {
// `module` field is used by bundlers like rollup and webpack to detect ESM.
// May not be required in the future if type is already "module".
packageJson.module = './index.js';
exports['.']['import'] = './index.js';

if (!hasCjsFormat) {
packageJson.main = './index.js';
}
}

if (hasCjsFormat) {
packageJson.main = './index.cjs';
exports['.']['require'] = './index.cjs';
}

packageJson.type = options.format.includes('esm') ? 'module' : 'commonjs';

// Support for older TS versions < 4.5
packageJson.types = types;

packageJson.exports = {
...packageJson.exports,
...exports,
};

writeJsonFile(`${options.outputPath}/package.json`, packageJson);

if (
dependencies.length > 0 &&
options.updateBuildableProjectDepsInPackageJson
) {
updateBuildableProjectPackageJsonDependencies(
context.root,
context.projectName,
context.targetName,
context.configurationName,
target,
dependencies,
options.buildableProjectDepsInPackageJsonType
);
}
}
14 changes: 7 additions & 7 deletions packages/web/src/executors/rollup/rollup.impl.spec.ts
Expand Up @@ -30,7 +30,7 @@ describe('rollupExecutor', () => {
project: 'libs/ui/package.json',
tsConfig: 'libs/ui/tsconfig.json',
watch: false,
format: ['esm', 'umd'],
format: ['esm', 'cjs'],
};
});

Expand All @@ -52,17 +52,17 @@ describe('rollupExecutor', () => {
globals: { 'react/jsx-runtime': 'jsxRuntime' },
name: 'Example',
inlineDynamicImports: false,
chunkFileNames: '[name].esm.js',
entryFileNames: '[name].esm.js',
chunkFileNames: '[name].js',
entryFileNames: '[name].js',
},
{
dir: '/root/dist/ui',
format: 'umd',
format: 'cjs',
globals: { 'react/jsx-runtime': 'jsxRuntime' },
name: 'Example',
inlineDynamicImports: true,
chunkFileNames: '[name].umd.js',
entryFileNames: '[name].umd.js',
inlineDynamicImports: false,
chunkFileNames: '[name].cjs',
entryFileNames: '[name].cjs',
},
]);
});
Expand Down

1 comment on commit 70f1a6b

@vercel
Copy link

@vercel vercel bot commented on 70f1a6b May 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-nrwl.vercel.app
nx-dev-git-master-nrwl.vercel.app
nx-five.vercel.app
nx.dev

Please sign in to comment.