feat(bundling): add for esbuild to enable/disable package.json generation (#15777)

This commit is contained in:
Jack Hsu 2023-03-21 08:48:16 -04:00 committed by GitHub
parent f10ecc1c8e
commit 7ebca5107e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 305 additions and 69 deletions

View File

@ -147,6 +147,11 @@
"default": false,
"x-priority": "internal"
},
"generatePackageJson": {
"type": "boolean",
"description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated.",
"default": false
},
"thirdParty": {
"type": "boolean",
"description": "Includes third-party packages in the bundle (i.e. npm packages).",

View File

@ -34,6 +34,17 @@ describe('EsBuild Plugin', () => {
updateFile(`libs/${myPkg}/assets/a.md`, 'file a');
updateFile(`libs/${myPkg}/assets/b.md`, 'file b');
// Copy package.json as asset rather than generate with Nx-detected fields.
runCLI(`build ${myPkg} --generatePackageJson=false`);
const packageJson = readJson(`libs/${myPkg}/package.json`);
// This is the file that is generated by lib generator (no deps, no main, etc.).
expect(packageJson).toEqual({
name: `@proj/${myPkg}`,
version: '0.0.1',
type: 'commonjs',
});
// Build normally with package.json generation.
runCLI(`build ${myPkg}`);
expect(runCommand(`node dist/libs/${myPkg}/index.js`)).toMatch(/Hello/);
@ -41,6 +52,9 @@ describe('EsBuild Plugin', () => {
checkFilesExist(`dist/libs/${myPkg}/package.json`);
expect(runCommand(`node dist/libs/${myPkg}`)).toMatch(/Hello/);
expect(runCommand(`node dist/libs/${myPkg}/index.js`)).toMatch(/Hello/);
// main field should be set correctly in package.json
expect(readFile(`dist/libs/${myPkg}/assets/a.md`)).toMatch(/file a/);
expect(readFile(`dist/libs/${myPkg}/assets/b.md`)).toMatch(/file b/);

View File

@ -1,5 +1,12 @@
{
"generators": {},
"generators": {
"set-generate-package-json": {
"cli": "nx",
"version": "15.8.7-beta.0",
"description": "Set generatePackageJson to true to maintain existing behavior of generating package.json in output path.",
"factory": "./src/migrations/update-15-8-7/set-generate-package-json"
}
},
"packageJsonUpdates": {
"15.7.0": {
"version": "15.7.0-beta.0",

View File

@ -41,7 +41,7 @@ export async function* esbuildExecutor(
_options: EsBuildExecutorOptions,
context: ExecutorContext
) {
const options = normalizeOptions(_options);
const options = normalizeOptions(_options, context);
if (options.deleteOutputPath) removeSync(options.outputPath);
const assetsResult = await copyAssets(options, context);
@ -70,6 +70,8 @@ export async function* esbuildExecutor(
}
}
let packageJsonResult;
if (options.generatePackageJson) {
const cpjOptions: CopyPackageJsonOptions = {
...options,
// TODO(jack): make types generate with esbuild
@ -86,7 +88,8 @@ export async function* esbuildExecutor(
cpjOptions.extraDependencies = externalDependencies;
}
const packageJsonResult = await copyPackageJson(cpjOptions, context);
packageJsonResult = await copyPackageJson(cpjOptions, context);
}
if ('context' in esbuild) {
// 0.17.0+ adds esbuild.context and context.watch()

View File

@ -45,7 +45,6 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
outputFileName: 'index.js',
singleEntry: true,
@ -82,7 +81,6 @@ describe('buildEsbuildOptions', () => {
additionalEntryPoints: ['apps/myapp/src/extra-entry.ts'],
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
outputFileName: 'index.js',
singleEntry: false,
@ -118,7 +116,6 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
outputFileName: 'index.js',
singleEntry: true,
@ -154,7 +151,6 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
outputFileName: 'index.js',
singleEntry: true,
@ -187,7 +183,6 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
outputFileName: 'index.js',
assets: [],
singleEntry: true,
@ -223,7 +218,6 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
outputFileName: 'index.js',
assets: [],
singleEntry: true,
@ -260,7 +254,6 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
outputFileName: 'index.js',
assets: [],
singleEntry: true,
@ -298,10 +291,9 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
outputFileName: 'index.js',
assets: [],
singleEntry: true,
outputFileName: 'index.js',
external: ['foo'],
esbuildOptions: {
external: ['bar'],
@ -334,8 +326,6 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
outputFileName: 'index.js',
assets: [],
singleEntry: true,
external: ['foo'],
@ -367,7 +357,40 @@ describe('buildEsbuildOptions', () => {
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
outputFileName: 'index.js',
assets: [],
singleEntry: true,
sourcemap: true,
external: [],
},
context
)
).toEqual({
bundle: false,
entryNames: '[dir]/[name]',
entryPoints: ['apps/myapp/src/index.ts'],
format: 'esm',
platform: 'node',
outdir: 'dist/apps/myapp',
tsconfig: 'apps/myapp/tsconfig.app.json',
external: undefined,
sourcemap: true,
outExtension: {
'.js': '.js',
},
});
});
it('should set sourcemap', () => {
expect(
buildEsbuildOptions(
'esm',
{
bundle: false,
platform: 'node',
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
outputFileName: 'index.js',
assets: [],
singleEntry: true,

View File

@ -1,21 +1,45 @@
import { normalizeOptions } from './normalize';
import { ExecutorContext } from '@nrwl/devkit';
describe('normalizeOptions', () => {
const context: ExecutorContext = {
root: '/',
cwd: '/',
isVerbose: false,
projectName: 'myapp',
projectGraph: {
nodes: {
myapp: {
type: 'app',
name: 'myapp',
data: {
root: 'apps/myapp',
files: [],
},
},
},
dependencies: {},
},
};
it('should handle single entry point options', () => {
expect(
normalizeOptions({
normalizeOptions(
{
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
generatePackageJson: true,
assets: [],
})
},
context
)
).toEqual({
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
generatePackageJson: true,
outputFileName: 'index.js',
singleEntry: true,
external: [],
@ -24,20 +48,23 @@ describe('normalizeOptions', () => {
it('should handle multiple entry point options', () => {
expect(
normalizeOptions({
normalizeOptions(
{
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
generatePackageJson: true,
additionalEntryPoints: ['apps/myapp/src/extra-entry.ts'],
})
},
context
)
).toEqual({
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
generatePackageJson: true,
outputFileName: 'index.js',
additionalEntryPoints: ['apps/myapp/src/extra-entry.ts'],
singleEntry: false,
@ -47,20 +74,23 @@ describe('normalizeOptions', () => {
it('should support custom output file name', () => {
expect(
normalizeOptions({
normalizeOptions(
{
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
generatePackageJson: true,
outputFileName: 'test.js',
})
},
context
)
).toEqual({
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
generatePackageJson: true,
outputFileName: 'test.js',
singleEntry: true,
external: [],
@ -69,15 +99,42 @@ describe('normalizeOptions', () => {
it('should validate against multiple entry points + outputFileName', () => {
expect(() =>
normalizeOptions({
normalizeOptions(
{
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
project: 'apps/myapp/package.json',
assets: [],
generatePackageJson: true,
additionalEntryPoints: ['apps/myapp/src/extra-entry.ts'],
outputFileName: 'test.js',
})
},
context
)
).toThrow(/Cannot use/);
});
it('should add package.json to assets array if generatePackageJson is false', () => {
expect(
normalizeOptions(
{
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
generatePackageJson: false,
assets: [],
},
context
)
).toEqual({
main: 'apps/myapp/src/index.ts',
outputPath: 'dist/apps/myapp',
tsConfig: 'apps/myapp/tsconfig.app.json',
assets: ['apps/myapp/package.json'],
generatePackageJson: false,
outputFileName: 'index.js',
singleEntry: true,
external: [],
});
});
});

View File

@ -3,10 +3,23 @@ import {
EsBuildExecutorOptions,
NormalizedEsBuildExecutorOptions,
} from '../schema';
import { ExecutorContext, joinPathFragments } from '@nrwl/devkit';
export function normalizeOptions(
options: EsBuildExecutorOptions
options: EsBuildExecutorOptions,
context: ExecutorContext
): NormalizedEsBuildExecutorOptions {
// If we're not generating package.json file, then copy it as-is as an asset.
const assets = options.generatePackageJson
? options.assets
: [
...options.assets,
joinPathFragments(
context.projectGraph.nodes[context.projectName].data.root,
'package.json'
),
];
if (options.additionalEntryPoints?.length > 0) {
const { outputFileName, ...rest } = options;
if (outputFileName) {
@ -16,6 +29,7 @@ export function normalizeOptions(
}
return {
...rest,
assets,
external: options.external ?? [],
singleEntry: false,
// Use the `main` file name as the output file name.
@ -26,6 +40,7 @@ export function normalizeOptions(
} else {
return {
...options,
assets,
external: options.external ?? [],
singleEntry: true,
outputFileName:

View File

@ -4,7 +4,7 @@ type Compiler = 'babel' | 'swc';
export interface EsBuildExecutorOptions {
additionalEntryPoints?: string[];
assets: AssetGlob[];
assets: (AssetGlob | string)[];
buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies';
bundle?: boolean;
deleteOutputPath?: boolean;
@ -20,7 +20,6 @@ export interface EsBuildExecutorOptions {
outputHashing?: 'none' | 'all';
outputPath: string;
platform?: 'node' | 'browser' | 'neutral';
project: string;
sourcemap?: boolean | 'linked' | 'inline' | 'external' | 'both';
skipTypeCheck?: boolean;
target?: string;

View File

@ -122,6 +122,11 @@
"default": false,
"x-priority": "internal"
},
"generatePackageJson": {
"type": "boolean",
"description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated.",
"default": false
},
"thirdParty": {
"type": "boolean",
"description": "Includes third-party packages in the bundle (i.e. npm packages).",

View File

@ -36,7 +36,6 @@ describe('esbuildProjectGenerator', () => {
main: `libs/mypkg/src/${main}`,
outputFileName: 'main.js',
outputPath: 'dist/libs/mypkg',
project: 'libs/mypkg/package.json',
tsConfig: `libs/mypkg/${tsConfig}`,
});
});

View File

@ -55,7 +55,6 @@ function addBuildTarget(tree: Tree, options: EsBuildProjectSchema) {
outputPath: joinPathFragments('dist', project.root),
outputFileName: 'main.js',
tsConfig,
project: `${project.root}/package.json`,
assets: [],
platform: options.platform,
};

View File

@ -0,0 +1,73 @@
import {
addProjectConfiguration,
readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import update from './set-generate-package-json';
describe('Migration: Set generatePackageJson', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should keep existing generatePackageJson option if it exists', async () => {
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/esbuild:esbuild',
options: {
generatePackageJson: false,
},
},
},
});
await update(tree);
const config = readProjectConfiguration(tree, 'myapp');
expect(config.targets.build.options).toEqual({
generatePackageJson: false,
});
});
it('should set generatePackageJson to true for esbuild targets', async () => {
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/esbuild:esbuild',
},
},
});
await update(tree);
const config = readProjectConfiguration(tree, 'myapp');
expect(config.targets.build.options).toEqual({
generatePackageJson: true,
});
});
it('should ignore targets not using esbuild', async () => {
addProjectConfiguration(tree, 'myapp', {
root: 'myapp',
targets: {
build: {
executor: '@nrwl/webpack:webpack',
},
},
});
await update(tree);
const config = readProjectConfiguration(tree, 'myapp');
expect(config.targets.build.options).toBeUndefined();
});
});

View File

@ -0,0 +1,32 @@
import type { Tree } from '@nrwl/devkit';
import {
formatFiles,
getProjects,
updateProjectConfiguration,
} from '@nrwl/devkit';
export default async function update(tree: Tree): Promise<void> {
const projects = getProjects(tree);
projects.forEach((projectConfig, projectName) => {
let shouldUpdate = false;
Object.entries(projectConfig.targets).forEach(
([targetName, targetConfig]) => {
if (targetConfig.executor === '@nrwl/esbuild:esbuild') {
shouldUpdate = true;
projectConfig.targets[targetName].options ??= {};
projectConfig.targets[targetName].options.generatePackageJson ??=
true;
}
}
);
if (shouldUpdate) {
updateProjectConfiguration(tree, projectName, projectConfig);
}
});
await formatFiles(tree);
}

View File

@ -163,6 +163,10 @@ function addProject(
},
};
if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.generatePackageJson = true;
}
if (options.bundler === 'rollup') {
projectConfiguration.targets.build.options.project = `${options.projectRoot}/package.json`;
projectConfiguration.targets.build.options.compiler = 'swc';

View File

@ -110,6 +110,7 @@ function getEsBuildConfig(
),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
assets: [joinPathFragments(project.sourceRoot, 'assets')],
generatePackageJson: true,
esbuildOptions: {
sourcemap: true,
// Generate CJS files as .js so imports can be './foo' rather than './foo.cjs'.