diff --git a/docs/generated/packages/esbuild/executors/esbuild.json b/docs/generated/packages/esbuild/executors/esbuild.json index 1d9d3780d299e..9e52f09c4b0a7 100644 --- a/docs/generated/packages/esbuild/executors/esbuild.json +++ b/docs/generated/packages/esbuild/executors/esbuild.json @@ -163,9 +163,14 @@ }, "esbuildOptions": { "type": "object", - "description": "Additional options to pass to esbuild. See https://esbuild.github.io/api/.", + "description": "Additional options to pass to esbuild. See https://esbuild.github.io/api/. Cannot be used with 'esbuildConfig' option.", "additionalProperties": true, "x-priority": "important" + }, + "esbuildConfig": { + "type": "string", + "description": "Path to a esbuild configuration file. See https://esbuild.github.io/api/. Cannot be used with 'esbuildOptions' option.", + "x-priority": "important" } }, "required": ["tsConfig", "main", "outputPath"], diff --git a/e2e/esbuild/src/esbuild.test.ts b/e2e/esbuild/src/esbuild.test.ts index e33a792fabf4d..512b3638a347a 100644 --- a/e2e/esbuild/src/esbuild.test.ts +++ b/e2e/esbuild/src/esbuild.test.ts @@ -3,16 +3,16 @@ import { checkFilesExist, cleanupProject, newProject, + packageInstall, readFile, readJson, + rmDist, runCLI, runCommand, + runCommandUntil, uniq, updateFile, updateProjectConfig, - packageInstall, - rmDist, - runCommandUntil, waitUntil, } from '@nrwl/e2e/utils'; @@ -229,5 +229,22 @@ describe('EsBuild Plugin', () => { expect(runCommand(`node dist/libs/${myPkg}/main.js`)).toMatch(/main/); expect(runCommand(`node dist/libs/${myPkg}/extra.js`)).toMatch(/extra/); - }); + }, 120_000); + + it('should support external esbuild.config.js file', async () => { + const myPkg = uniq('my-pkg'); + runCLI(`generate @nrwl/js:lib ${myPkg} --bundler=esbuild`); + updateFile( + `libs/${myPkg}/esbuild.config.js`, + `console.log('custom config loaded');\nmodule.exports = {};\n` + ); + updateProjectConfig(myPkg, (json) => { + delete json.targets.build.options.esbuildOptions; + json.targets.build.options.esbuildConfig = `libs/${myPkg}/esbuild.config.js`; + return json; + }); + + const output = runCLI(`build ${myPkg}`); + expect(output).toContain('custom config loaded'); + }, 120_000); }); 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 72c6d1e30f294..c3c2c77ef5b48 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 @@ -49,6 +49,7 @@ describe('buildEsbuildOptions', () => { outputFileName: 'index.js', singleEntry: true, external: [], + userDefinedBuildOptions: {}, }, context ) @@ -89,6 +90,7 @@ describe('buildEsbuildOptions', () => { outputFileName: 'index.js', singleEntry: false, external: [], + userDefinedBuildOptions: {}, }, context ) @@ -128,6 +130,7 @@ describe('buildEsbuildOptions', () => { outputFileName: 'index.js', singleEntry: true, external: [], + userDefinedBuildOptions: {}, }, context ) @@ -167,6 +170,7 @@ describe('buildEsbuildOptions', () => { outputFileName: 'index.js', singleEntry: true, external: [], + userDefinedBuildOptions: {}, }, context ) @@ -203,7 +207,7 @@ describe('buildEsbuildOptions', () => { assets: [], singleEntry: true, external: [], - esbuildOptions: { + userDefinedBuildOptions: { outExtension: { '.js': '.mjs', }, @@ -242,7 +246,7 @@ describe('buildEsbuildOptions', () => { assets: [], singleEntry: true, external: [], - esbuildOptions: { + userDefinedBuildOptions: { outExtension: { '.js': '.js', }, @@ -282,7 +286,7 @@ describe('buildEsbuildOptions', () => { assets: [], singleEntry: true, external: [], - esbuildOptions: { + userDefinedBuildOptions: { outExtension: { '.js': '.cjs', }, @@ -323,7 +327,7 @@ describe('buildEsbuildOptions', () => { singleEntry: true, outputFileName: 'index.js', external: ['foo'], - esbuildOptions: { + userDefinedBuildOptions: { external: ['bar'], }, }, @@ -361,6 +365,7 @@ describe('buildEsbuildOptions', () => { assets: [], singleEntry: true, external: ['foo'], + userDefinedBuildOptions: {}, }, context ) @@ -398,6 +403,7 @@ describe('buildEsbuildOptions', () => { singleEntry: true, sourcemap: true, external: [], + userDefinedBuildOptions: {}, }, context ) @@ -434,6 +440,7 @@ describe('buildEsbuildOptions', () => { assets: [], singleEntry: true, external: [], + userDefinedBuildOptions: {}, }, context ) @@ -469,7 +476,7 @@ describe('buildEsbuildOptions', () => { outputFileName: 'index.js', assets: [], singleEntry: true, - esbuildOptions: { + userDefinedBuildOptions: { sourcemap: true, }, external: [], @@ -508,7 +515,7 @@ describe('buildEsbuildOptions', () => { outputFileName: 'index.js', assets: [], singleEntry: true, - esbuildOptions: { + userDefinedBuildOptions: { sourcemap: false, }, sourcemap: true, diff --git a/packages/esbuild/src/executors/esbuild/lib/build-esbuild-options.ts b/packages/esbuild/src/executors/esbuild/lib/build-esbuild-options.ts index 3c8563dd10eff..270ca17c5b104 100644 --- a/packages/esbuild/src/executors/esbuild/lib/build-esbuild-options.ts +++ b/packages/esbuild/src/executors/esbuild/lib/build-esbuild-options.ts @@ -1,19 +1,15 @@ import * as esbuild from 'esbuild'; import * as path from 'path'; -import { join, parse } from 'path'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { - normalizePath, ExecutorContext, joinPathFragments, + normalizePath, ProjectGraphProjectNode, } from '@nrwl/devkit'; -import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { getClientEnvironment } from '../../../utils/environment-variables'; -import { - EsBuildExecutorOptions, - NormalizedEsBuildExecutorOptions, -} from '../schema'; +import { NormalizedEsBuildExecutorOptions } from '../schema'; import { getEntryPoints } from '../../../utils/get-entry-points'; const ESM_FILE_EXTENSION = '.js'; @@ -25,14 +21,18 @@ export function buildEsbuildOptions( context: ExecutorContext ): esbuild.BuildOptions { const outExtension = getOutExtension(format, options); + const esbuildOptions: esbuild.BuildOptions = { - ...options.esbuildOptions, + ...options.userDefinedBuildOptions, entryNames: options.outputHashing === 'all' ? '[dir]/[name].[hash]' : '[dir]/[name]', bundle: options.bundle, // Cannot use external with bundle option external: options.bundle - ? [...(options.esbuildOptions?.external ?? []), ...options.external] + ? [ + ...(options.userDefinedBuildOptions?.external ?? []), + ...options.external, + ] : undefined, minify: options.minify, platform: options.platform, @@ -40,7 +40,8 @@ export function buildEsbuildOptions( metafile: options.metafile, tsconfig: options.tsConfig, sourcemap: - (options.sourcemap ?? options.esbuildOptions?.sourcemap) || false, + (options.sourcemap ?? options.userDefinedBuildOptions?.sourcemap) || + false, format, outExtension: { '.js': outExtension, @@ -119,9 +120,9 @@ export function buildEsbuildOptions( export function getOutExtension( format: 'cjs' | 'esm', - options: EsBuildExecutorOptions + options: NormalizedEsBuildExecutorOptions ): '.cjs' | '.mjs' | '.js' { - const userDefinedExt = options.esbuildOptions?.outExtension?.['.js']; + const userDefinedExt = options.userDefinedBuildOptions?.outExtension?.['.js']; // Allow users to change the output extensions from default CJS and ESM extensions. // CJS -> .js // ESM -> .mjs @@ -136,7 +137,7 @@ export function getOutExtension( export function getOutfile( format: 'cjs' | 'esm', - options: EsBuildExecutorOptions, + options: NormalizedEsBuildExecutorOptions, context: ExecutorContext ) { const ext = getOutExtension(format, options); @@ -144,7 +145,7 @@ export function getOutfile( context.target.options.outputPath, options.outputFileName ); - const { dir, name } = parse(candidate); + const { dir, name } = path.parse(candidate); return `${dir}/${name}${ext}`; } @@ -305,7 +306,7 @@ function getTsConfigCompilerPaths(context: ExecutorContext): { function getRootTsConfigPath(context: ExecutorContext): string | null { for (const tsConfigName of ['tsconfig.base.json', 'tsconfig.json']) { - const tsConfigPath = join(context.root, tsConfigName); + const tsConfigPath = path.join(context.root, tsConfigName); if (existsSync(tsConfigPath)) { return tsConfigPath; } diff --git a/packages/esbuild/src/executors/esbuild/lib/normalize.ts b/packages/esbuild/src/executors/esbuild/lib/normalize.ts index 67de72e6f39af..c38a6ee1b9284 100644 --- a/packages/esbuild/src/executors/esbuild/lib/normalize.ts +++ b/packages/esbuild/src/executors/esbuild/lib/normalize.ts @@ -1,10 +1,12 @@ -import { parse } from 'path'; +import * as fs from 'fs'; +import * as path from 'path'; import { EsBuildExecutorOptions, NormalizedEsBuildExecutorOptions, } from '../schema'; import { ExecutorContext, joinPathFragments, logger } from '@nrwl/devkit'; import chalk = require('chalk'); +import * as esbuild from 'esbuild'; export function normalizeOptions( options: EsBuildExecutorOptions, @@ -36,6 +38,24 @@ export function normalizeOptions( } const thirdParty = !options.bundle ? false : options.thirdParty; + + let userDefinedBuildOptions: esbuild.BuildOptions; + if (options.esbuildConfig) { + const userDefinedConfig = path.resolve(context.root, options.esbuildConfig); + + if (options.esbuildOptions) + throw new Error( + `Cannot use both esbuildOptions and esbuildConfig options. Remove one of them and try again.` + ); + if (!fs.existsSync(userDefinedConfig)) + throw new Error( + `Path of esbuildConfig does not exist: ${userDefinedConfig}` + ); + userDefinedBuildOptions = require(userDefinedConfig); + } else if (options.esbuildOptions) { + userDefinedBuildOptions = options.esbuildOptions; + } + if (options.additionalEntryPoints?.length > 0) { const { outputFileName, ...rest } = options; if (outputFileName) { @@ -47,23 +67,25 @@ export function normalizeOptions( ...rest, thirdParty, assets, + userDefinedBuildOptions, external: options.external ?? [], singleEntry: false, // Use the `main` file name as the output file name. // This is needed for `@nrwl/js:node` to know the main file to execute. // NOTE: The .js default extension may be replaced later in getOutfile() call. - outputFileName: `${parse(options.main).name}.js`, + outputFileName: `${path.parse(options.main).name}.js`, }; } else { return { ...options, thirdParty, assets, + userDefinedBuildOptions, external: options.external ?? [], singleEntry: true, outputFileName: // NOTE: The .js default extension may be replaced later in getOutfile() call. - options.outputFileName ?? `${parse(options.main).name}.js`, + options.outputFileName ?? `${path.parse(options.main).name}.js`, }; } } diff --git a/packages/esbuild/src/executors/esbuild/schema.d.ts b/packages/esbuild/src/executors/esbuild/schema.d.ts index c4e1188dd731d..1367c9da551ac 100644 --- a/packages/esbuild/src/executors/esbuild/schema.d.ts +++ b/packages/esbuild/src/executors/esbuild/schema.d.ts @@ -1,4 +1,5 @@ import { AssetGlob } from '@nrwl/js/src/utils/assets/assets'; +import * as esbuild from 'esbuild'; type Compiler = 'babel' | 'swc'; @@ -10,6 +11,7 @@ export interface EsBuildExecutorOptions { deleteOutputPath?: boolean; dependenciesFieldType?: boolean; esbuildOptions?: Record; + esbuildConfig?: string; external?: string[]; format?: Array<'esm' | 'cjs'>; generatePackageJson?: boolean; @@ -29,7 +31,8 @@ export interface EsBuildExecutorOptions { } export interface NormalizedEsBuildExecutorOptions - extends EsBuildExecutorOptions { + extends Omit { singleEntry: boolean; external: string[]; + userDefinedBuildOptions: esbuild.BuildOptions; } diff --git a/packages/esbuild/src/executors/esbuild/schema.json b/packages/esbuild/src/executors/esbuild/schema.json index 7b02214d7094e..d7ecf482b1595 100644 --- a/packages/esbuild/src/executors/esbuild/schema.json +++ b/packages/esbuild/src/executors/esbuild/schema.json @@ -138,9 +138,14 @@ }, "esbuildOptions": { "type": "object", - "description": "Additional options to pass to esbuild. See https://esbuild.github.io/api/.", + "description": "Additional options to pass to esbuild. See https://esbuild.github.io/api/. Cannot be used with 'esbuildConfig' option.", "additionalProperties": true, "x-priority": "important" + }, + "esbuildConfig": { + "type": "string", + "description": "Path to a esbuild configuration file. See https://esbuild.github.io/api/. Cannot be used with 'esbuildOptions' option.", + "x-priority": "important" } }, "required": ["tsConfig", "main", "outputPath"],