diff --git a/docs/generated/packages/esbuild/executors/esbuild.json b/docs/generated/packages/esbuild/executors/esbuild.json index 09a7622172461..fefe398783996 100644 --- a/docs/generated/packages/esbuild/executors/esbuild.json +++ b/docs/generated/packages/esbuild/executors/esbuild.json @@ -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).", diff --git a/e2e/esbuild/src/esbuild.test.ts b/e2e/esbuild/src/esbuild.test.ts index 0acbafb0292ec..e33a792fabf4d 100644 --- a/e2e/esbuild/src/esbuild.test.ts +++ b/e2e/esbuild/src/esbuild.test.ts @@ -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/); diff --git a/packages/esbuild/migrations.json b/packages/esbuild/migrations.json index 74bebae89a3ce..85b363d1938f6 100644 --- a/packages/esbuild/migrations.json +++ b/packages/esbuild/migrations.json @@ -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", diff --git a/packages/esbuild/src/executors/esbuild/esbuild.impl.ts b/packages/esbuild/src/executors/esbuild/esbuild.impl.ts index 717e881561bf7..26b07e0c68826 100644 --- a/packages/esbuild/src/executors/esbuild/esbuild.impl.ts +++ b/packages/esbuild/src/executors/esbuild/esbuild.impl.ts @@ -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,24 +70,27 @@ export async function* esbuildExecutor( } } - const cpjOptions: CopyPackageJsonOptions = { - ...options, - // TODO(jack): make types generate with esbuild - skipTypings: true, - outputFileExtensionForCjs: getOutExtension('cjs', options), - excludeLibsInPackageJson: !options.thirdParty, - updateBuildableProjectDepsInPackageJson: externalDependencies.length > 0, - }; + let packageJsonResult; + if (options.generatePackageJson) { + const cpjOptions: CopyPackageJsonOptions = { + ...options, + // TODO(jack): make types generate with esbuild + skipTypings: true, + outputFileExtensionForCjs: getOutExtension('cjs', options), + excludeLibsInPackageJson: !options.thirdParty, + updateBuildableProjectDepsInPackageJson: externalDependencies.length > 0, + }; + + // If we're bundling third-party packages, then any extra deps from external should be the only deps in package.json + if (options.thirdParty && externalDependencies.length > 0) { + cpjOptions.overrideDependencies = externalDependencies; + } else { + cpjOptions.extraDependencies = externalDependencies; + } - // If we're bundling third-party packages, then any extra deps from external should be the only deps in package.json - if (options.thirdParty && externalDependencies.length > 0) { - cpjOptions.overrideDependencies = externalDependencies; - } else { - cpjOptions.extraDependencies = externalDependencies; + packageJsonResult = await copyPackageJson(cpjOptions, context); } - const packageJsonResult = await copyPackageJson(cpjOptions, context); - if ('context' in esbuild) { // 0.17.0+ adds esbuild.context and context.watch() if (options.watch) { diff --git a/packages/esbuild/src/executors/esbuild/lib/build-esbuild-options.spec.ts b/packages/esbuild/src/executors/esbuild/lib/build-esbuild-options.spec.ts index 27af26c0c2134..b7fe2105a3deb 100644 --- a/packages/esbuild/src/executors/esbuild/lib/build-esbuild-options.spec.ts +++ b/packages/esbuild/src/executors/esbuild/lib/build-esbuild-options.spec.ts @@ -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, diff --git a/packages/esbuild/src/executors/esbuild/lib/normalize.spec.ts b/packages/esbuild/src/executors/esbuild/lib/normalize.spec.ts index 5b1451bfd23b2..4f9fed0b307fa 100644 --- a/packages/esbuild/src/executors/esbuild/lib/normalize.spec.ts +++ b/packages/esbuild/src/executors/esbuild/lib/normalize.spec.ts @@ -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({ - main: 'apps/myapp/src/index.ts', - outputPath: 'dist/apps/myapp', - tsConfig: 'apps/myapp/tsconfig.app.json', - project: 'apps/myapp/package.json', - assets: [], - }) + normalizeOptions( + { + main: 'apps/myapp/src/index.ts', + outputPath: 'dist/apps/myapp', + tsConfig: 'apps/myapp/tsconfig.app.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({ - main: 'apps/myapp/src/index.ts', - outputPath: 'dist/apps/myapp', - tsConfig: 'apps/myapp/tsconfig.app.json', - project: 'apps/myapp/package.json', - assets: [], - additionalEntryPoints: ['apps/myapp/src/extra-entry.ts'], - }) + normalizeOptions( + { + main: 'apps/myapp/src/index.ts', + outputPath: 'dist/apps/myapp', + tsConfig: 'apps/myapp/tsconfig.app.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({ - main: 'apps/myapp/src/index.ts', - outputPath: 'dist/apps/myapp', - tsConfig: 'apps/myapp/tsconfig.app.json', - project: 'apps/myapp/package.json', - assets: [], - outputFileName: 'test.js', - }) + normalizeOptions( + { + main: 'apps/myapp/src/index.ts', + outputPath: 'dist/apps/myapp', + tsConfig: 'apps/myapp/tsconfig.app.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({ - main: 'apps/myapp/src/index.ts', - outputPath: 'dist/apps/myapp', - tsConfig: 'apps/myapp/tsconfig.app.json', - project: 'apps/myapp/package.json', - assets: [], - additionalEntryPoints: ['apps/myapp/src/extra-entry.ts'], - outputFileName: 'test.js', - }) + normalizeOptions( + { + main: 'apps/myapp/src/index.ts', + outputPath: 'dist/apps/myapp', + tsConfig: 'apps/myapp/tsconfig.app.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: [], + }); + }); }); diff --git a/packages/esbuild/src/executors/esbuild/lib/normalize.ts b/packages/esbuild/src/executors/esbuild/lib/normalize.ts index a52a408cd3992..e1114e9ef7b43 100644 --- a/packages/esbuild/src/executors/esbuild/lib/normalize.ts +++ b/packages/esbuild/src/executors/esbuild/lib/normalize.ts @@ -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: diff --git a/packages/esbuild/src/executors/esbuild/schema.d.ts b/packages/esbuild/src/executors/esbuild/schema.d.ts index 8ff607c23cbbb..ee23cfba2747c 100644 --- a/packages/esbuild/src/executors/esbuild/schema.d.ts +++ b/packages/esbuild/src/executors/esbuild/schema.d.ts @@ -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; diff --git a/packages/esbuild/src/executors/esbuild/schema.json b/packages/esbuild/src/executors/esbuild/schema.json index 371c012cc7f99..5675e1764d943 100644 --- a/packages/esbuild/src/executors/esbuild/schema.json +++ b/packages/esbuild/src/executors/esbuild/schema.json @@ -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).", diff --git a/packages/esbuild/src/generators/esbuild-project/esbuild-project.spec.ts b/packages/esbuild/src/generators/esbuild-project/esbuild-project.spec.ts index 583c97d8e4dca..808c51bf3ce27 100644 --- a/packages/esbuild/src/generators/esbuild-project/esbuild-project.spec.ts +++ b/packages/esbuild/src/generators/esbuild-project/esbuild-project.spec.ts @@ -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}`, }); }); diff --git a/packages/esbuild/src/generators/esbuild-project/esbuild-project.ts b/packages/esbuild/src/generators/esbuild-project/esbuild-project.ts index d5fbf0a3814eb..eafafb804926d 100644 --- a/packages/esbuild/src/generators/esbuild-project/esbuild-project.ts +++ b/packages/esbuild/src/generators/esbuild-project/esbuild-project.ts @@ -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, }; diff --git a/packages/esbuild/src/migrations/update-15-8-7/set-generate-package-json.spec.ts b/packages/esbuild/src/migrations/update-15-8-7/set-generate-package-json.spec.ts new file mode 100644 index 0000000000000..c23eef22b457c --- /dev/null +++ b/packages/esbuild/src/migrations/update-15-8-7/set-generate-package-json.spec.ts @@ -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(); + }); +}); diff --git a/packages/esbuild/src/migrations/update-15-8-7/set-generate-package-json.ts b/packages/esbuild/src/migrations/update-15-8-7/set-generate-package-json.ts new file mode 100644 index 0000000000000..d2d8535167935 --- /dev/null +++ b/packages/esbuild/src/migrations/update-15-8-7/set-generate-package-json.ts @@ -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 { + 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); +} diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index ef5a750449db4..6fe60afcb6e63 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -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'; diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 1e22ed224b59c..26eac2dbf7d59 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -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'.