UNPKG

nx-esbuild-decorators

Version:

The Nx Plugin for esbuild contains executors and generators that support building applications using esbuild and also supports decorators

266 lines (235 loc) 8.64 kB
import * as chalk from 'chalk'; import type {ExecutorContext} from '@nx/devkit'; import {cacheDir, joinPathFragments, logger, stripIndents} from '@nx/devkit'; import {esbuildDecorators} from '@anatine/esbuild-decorators'; import { copyAssets, copyPackageJson, CopyPackageJsonOptions, printDiagnostics, runTypeCheck as _runTypeCheck, TypeCheckOptions, } from '@nx/js'; import * as esbuild from 'esbuild'; import {normalizeOptions} from './lib/normalize'; import {EsBuildExecutorOptions} from './schema'; import {removeSync, writeJsonSync} from 'fs-extra'; import {createAsyncIterable} from '@nx/devkit/src/utils/async-iterable'; import { buildEsbuildOptions, getOutExtension, getOutfile, } from './lib/build-esbuild-options'; import {getExtraDependencies} from './lib/get-extra-dependencies'; import {DependentBuildableProjectNode} from '@nx/js/src/utils/buildable-libs-utils'; import {join} from 'path'; const BUILD_WATCH_FAILED = `[ ${chalk.red( 'watch' )} ] build finished with errors (see above), watching for changes...`; const BUILD_WATCH_SUCCEEDED = `[ ${chalk.green( 'watch' )} ] build succeeded, watching for changes...`; // since the workspace has esbuild 0.17+ installed, there's no definition // of esbuild without 'context', therefore, the esbuild import in the else // branch below has type never, getting the type to cast later type EsBuild = typeof esbuild; export async function* esbuildExecutor( _options: EsBuildExecutorOptions, context: ExecutorContext ) { process.env.NODE_ENV ??= context.configurationName ?? 'production'; const options = { ...normalizeOptions(_options, context), plugins: [esbuildDecorators( { tsconfig: _options.tsConfig, cwd: process.cwd(), })], }; if (options.deleteOutputPath) removeSync(options.outputPath); const assetsResult = await copyAssets(options, context); const externalDependencies: DependentBuildableProjectNode[] = options.external.reduce((acc, name) => { const externalNode = context.projectGraph.externalNodes[`npm:${name}`]; if (externalNode) { acc.push({ name, outputs: [], node: externalNode, }); } return acc; }, []); if (!options.thirdParty) { const thirdPartyDependencies = getExtraDependencies( context.projectName, context.projectGraph ); for (const tpd of thirdPartyDependencies) { options.external.push((tpd.node.data as any).packageName); externalDependencies.push(tpd); } } let packageJsonResult; if (options.generatePackageJson) { if (context.projectGraph.nodes[context.projectName].type !== 'app') { logger.warn( stripIndents`The project ${context.projectName} is using the 'generatePackageJson' option which is deprecated for library projects. It should only be used for applications. For libraries, configure the project to use the '@nx/dependency-checks' ESLint rule instead (https://nx.dev/packages/eslint-plugin/documents/dependency-checks).` ); } const cpjOptions: CopyPackageJsonOptions = { ...options, // TODO(jack): make types generate with esbuild skipTypings: true, generateLockfile: true, outputFileExtensionForCjs: getOutExtension('cjs', options), excludeLibsInPackageJson: !options.thirdParty, }; // 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); } if (options.watch) { return yield* createAsyncIterable<{ success: boolean; outfile?: string }>( async ({next, done}) => { let hasTypeErrors = false; const disposeFns = await Promise.all( options.format.map(async (format, idx) => { const esbuildOptions = buildEsbuildOptions( format, options, context ); const ctx = await esbuild.context({ ...esbuildOptions, plugins: [ // Only emit info on one of the watch processes. idx === 0 ? { name: 'nx-watch-plugin', setup(build: esbuild.PluginBuild) { build.onEnd(async (result: esbuild.BuildResult) => { if (!options.skipTypeCheck) { const {errors} = await runTypeCheck( options, context ); hasTypeErrors = errors.length > 0; } const success = result.errors.length === 0 && !hasTypeErrors; if (!success) { logger.info(BUILD_WATCH_FAILED); } else { logger.info(BUILD_WATCH_SUCCEEDED); } next({ success, // Need to call getOutfile directly in the case of bundle=false and outfile is not set for esbuild. outfile: join( context.root, getOutfile(format, options, context) ), }); }); }, } : null, ...(esbuildOptions?.plugins || []), ].filter(Boolean), }); await ctx.watch(); return () => ctx.dispose(); }) ); registerCleanupCallback(() => { assetsResult?.stop(); packageJsonResult?.stop(); disposeFns.forEach((fn) => fn()); done(); // return from async iterable }); } ); } else { // Run type-checks first and bail if they don't pass. if (!options.skipTypeCheck) { const {errors} = await runTypeCheck(options, context); if (errors.length > 0) { yield {success: false}; return; } } // Emit a build event for each file format. for (let i = 0; i < options.format.length; i++) { const format = options.format[i]; const esbuildOptions = buildEsbuildOptions(format, options, context); const buildResult = await esbuild.build(esbuildOptions); if (options.metafile) { const filename = options.format.length === 1 ? 'meta.json' : `meta.${options.format[i]}.json`; writeJsonSync( joinPathFragments(options.outputPath, filename), buildResult.metafile ); } yield { success: buildResult.errors.length === 0, // Need to call getOutfile directly in the case of bundle=false and outfile is not set for esbuild. // This field is needed for `@nx/js:node` executor to work. outfile: join(context.root, getOutfile(format, options, context)), }; } } } function getTypeCheckOptions( options: EsBuildExecutorOptions, context: ExecutorContext ) { const {watch, tsConfig, outputPath} = options; const typeCheckOptions: TypeCheckOptions = { // TODO(jack): Add support for d.ts declaration files -- once the `@nx/js:tsc` changes are in we can use the same logic. mode: 'noEmit', tsConfigPath: tsConfig, // outDir: outputPath, workspaceRoot: context.root, rootDir: context.root, }; if (watch) { typeCheckOptions.incremental = true; typeCheckOptions.cacheDir = cacheDir; } return typeCheckOptions; } async function runTypeCheck( options: EsBuildExecutorOptions, context: ExecutorContext ) { const {errors, warnings} = await _runTypeCheck( getTypeCheckOptions(options, context) ); const hasErrors = errors.length > 0; const hasWarnings = warnings.length > 0; if (hasErrors || hasWarnings) { await printDiagnostics(errors, warnings); } return {errors, warnings}; } function registerCleanupCallback(callback: () => void) { const wrapped = () => { callback(); process.off('SIGINT', wrapped); process.off('SIGTERM', wrapped); process.off('exit', wrapped); }; process.on('SIGINT', wrapped); process.on('SIGTERM', wrapped); process.on('exit', wrapped); } export default esbuildExecutor;