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
  • Loading branch information
jaysoo authored and Jack Hsu committed May 17, 2022
1 parent 8198b78 commit b3f4f65
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 65 deletions.
2 changes: 1 addition & 1 deletion docs/generated/packages/web.json
Original file line number Diff line number Diff line change
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
34 changes: 13 additions & 21 deletions e2e/react/src/react-package.test.ts
Original file line number Diff line number Diff line change
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`)
);
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
5 changes: 5 additions & 0 deletions packages/nx/src/utils/package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export interface PackageJson {
name: string;
version: string;
scripts?: Record<string, string>;
type?: 'module' | 'commonjs';
main?: string;
typings?: string;
module?: string;
exports?: Record<string, { require?: string; import?: string }>;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
Expand Down
113 changes: 113 additions & 0 deletions packages/web/src/executors/rollup/lib/update-package-json.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { updatePackageJson } from './update-package-json';
import * as utils from 'nx/src/utils/fileutils';

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: {} as any,
root: '',
cwd: '',
};

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

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

expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
exports: {
'.': {
import: './index.js',
},
},
main: './index.js',
module: './index.js',
type: 'module',
typings: './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 any
);

expect(utils.writeJsonFile).toHaveBeenCalledWith(expect.anything(), {
exports: {
'.': {
require: './index.cjs',
},
},
main: './index.cjs',
type: 'commonjs',
typings: './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 any
);

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

spy.mockRestore();
});
});
63 changes: 63 additions & 0 deletions packages/web/src/executors/rollup/lib/update-package-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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 type = options.format.includes('esm') ? 'module' : 'commonjs';
const exports = {
'.': {},
};

if (type === 'module') {
// `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';
packageJson.main = './index.js';
} else {
// Only if ESM is not used at all.
packageJson.main = './index.cjs';
}

if (options.format.includes('esm')) {
exports['.']['import'] = './index.js';
}
if (options.format.includes('umd') || options.format.includes('cjs')) {
exports['.']['require'] = './index.cjs';
}

packageJson.typings = `./${relative(
options.entryRoot,
options.entryFile
).replace(/\.[jt]sx?$/, '.d.ts')}`;
packageJson.type = type;
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
);
}
}
51 changes: 9 additions & 42 deletions packages/web/src/executors/rollup/rollup.impl.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import * as rollup from 'rollup';
import * as peerDepsExternal from 'rollup-plugin-peer-deps-external';
import { getBabelInputPlugin } from '@rollup/plugin-babel';
import { join, relative } from 'path';
import { join } from 'path';
import { from, Observable, of } from 'rxjs';
import { catchError, concatMap, last, scan, tap } from 'rxjs/operators';
import { eachValueFrom } from 'rxjs-for-await';
import * as autoprefixer from 'autoprefixer';
import type { ExecutorContext, ProjectGraphProjectNode } from '@nrwl/devkit';
import { logger, names, readJsonFile, writeJsonFile } from '@nrwl/devkit';
import { readCachedProjectGraph } from '@nrwl/devkit';
import type { ExecutorContext } from '@nrwl/devkit';
import {
logger,
names,
readCachedProjectGraph,
readJsonFile,
} from '@nrwl/devkit';
import {
calculateProjectDependencies,
computeCompilerOptionsPaths,
DependentBuildableProjectNode,
updateBuildableProjectPackageJsonDependencies,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import resolve from '@rollup/plugin-node-resolve';

Expand All @@ -28,6 +31,7 @@ import { analyze } from './lib/analyze-plugin';
import { deleteOutputDir } from '../../utils/fs';
import { swc } from './lib/swc-plugin';
import { validateTypes } from './lib/validate-types';
import { updatePackageJson } from './lib/update-package-json';

// These use require because the ES import isn't correct.
const commonjs = require('@rollup/plugin-commonjs');
Expand Down Expand Up @@ -293,43 +297,6 @@ function createCompilerOptions(options, dependencies) {
};
}

function updatePackageJson(
options: NormalizedWebRollupOptions,
context: ExecutorContext,
target: ProjectGraphProjectNode,
dependencies: DependentBuildableProjectNode[],
packageJson: any
) {
const entryFileTmpl = `./index.<%= extension %>.js`;
const typingsFile = relative(options.entryRoot, options.entryFile).replace(
/\.[jt]sx?$/,
'.d.ts'
);
if (options.format.includes('umd')) {
packageJson.main = entryFileTmpl.replace('<%= extension %>', 'umd');
} else if (options.format.includes('cjs')) {
packageJson.main = entryFileTmpl.replace('<%= extension %>', 'cjs');
}
packageJson.module = entryFileTmpl.replace('<%= extension %>', 'esm');
packageJson.typings = `./${typingsFile}`;
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
);
}
}

interface RollupCopyAssetOption {
src: string;
dest: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/executors/rollup/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"type": "string",
"enum": ["esm", "umd", "cjs"]
},
"default": ["esm", "umd"]
"default": ["esm"]
},
"external": {
"type": "array",
Expand Down

0 comments on commit b3f4f65

Please sign in to comment.