UNPKG

@anatine/esbuildnx

Version:
440 lines (406 loc) 12.9 kB
import { BuildExecutorSchema } from './schema'; import { ExecutorContext } from '@nrwl/devkit'; import { normalizeBuildOptions } from '../../utils/normalize-options'; import { pathExistsSync } from 'fs-extra'; import { readJsonFile } from '@nrwl/workspace'; import { build, BuildFailure, BuildOptions, BuildResult } from 'esbuild'; import { spawn } from 'child_process'; import { esbuildDecorators } from '@anatine/esbuild-decorators'; import { gray, green, red, yellow } from 'chalk'; import watch from 'node-watch'; import { Observable, OperatorFunction, Subject, zip } from 'rxjs'; import { buffer, delay, filter, map, share } from 'rxjs/operators'; import { eachValueFrom } from 'rxjs-for-await'; import { format } from 'date-fns'; // import { exportDiagnostics } from '../../utils/print-diagnostics'; import { inspect } from 'util'; import { copyPackages, getPackagesToCopy } from '../../utils/walk-packages'; import { copyAssets } from '../../utils/assets'; import { OUTFILE_NAME } from '../../utils/constants'; import { NodeBuildEvent } from '@nrwl/node/src/executors/build/build.impl'; export function buildExecutor( rawOptions: BuildExecutorSchema, context: ExecutorContext ): AsyncIterableIterator<NodeBuildEvent> { const { sourceRoot, root } = context.workspace.projects[context.projectName]; if (!sourceRoot) { throw new Error(`${context.projectName} does not have a sourceRoot.`); } if (!root) { throw new Error(`${context.projectName} does not have a root.`); } // Eventually, it would be great to expose more esbuild settings on command line. // For now, the app root directory can utilize an esbuild.json file for build API settings // https://esbuild.github.io/api/#build-api const esBuildExists = pathExistsSync(`${root}/esbuild.json`); const packageExists = pathExistsSync(`${root}/package.json`); const esbuildConfig: BuildOptions = esBuildExists ? readJsonFile<BuildOptions>(`${root}/esbuild.json`) : { external: [] }; const projectPackage = packageExists ? readJsonFile(`${root}/package.json`) : {}; const options = normalizeBuildOptions( rawOptions, esbuildConfig, context.root, sourceRoot, root ); const outdir = `${options.outputPath}`; const outfile = `${outdir}/${OUTFILE_NAME}`; const watchDir = `${options.root}/${options.sourceRoot}`; const packages = packageExists ? Object.keys(projectPackage.dependencies) : []; esbuildConfig.external = [...packages, ...(esbuildConfig.external || [])]; const esbuildOptions: BuildOptions = { logLevel: 'silent', platform: 'node', bundle: options.bundle || true, sourcemap: 'external', charset: 'utf8', color: true, conditions: options.watch ? ['development'] : ['production'], watch: options.watch || false, absWorkingDir: options.root, plugins: [ esbuildDecorators({ cwd: options.root, }), ], // banner: { // js: '// Compiled by esbuildnx ', // }, tsconfig: options.tsConfig, entryPoints: [options.main], outdir, // outfile, ...esbuildConfig, incremental: options.watch || false, }; let buildCounter = 1; const buildSubscriber = runBuild(esbuildOptions, watchDir).pipe( map(({ buildResult, buildFailure }) => { let message = ''; const timeString = format(new Date(), 'h:mm:ss a'); const count = gray(`[${buildCounter}]`); const prefix = `esbuild ${count} ${timeString}`; // const warnings: string[] = []; if (buildResult?.warnings.length > 0) { let warningMessage = yellow(`${prefix} - Warnings:`); buildResult?.warnings.forEach((warning) => { warningMessage += `\n ${yellow(warning.location.file)}(${ warning.location.line },${warning.location.column}):`; warningMessage += ` ${warning.location.lineText.trim()}`; warningMessage += gray(`\n ${warning.text}\n`); }); // console.log(warningMessage); message += warningMessage; } if (buildFailure) { // console.log(red(`\nEsbuild Error ${count}`)); // console.error(stats.buildFailure); message += red(`Esbuild Error ${count}`); message += buildFailure; } else if (buildResult?.warnings.length > 0) { message += green( `${prefix} - Build finished with ${yellow( buildResult?.warnings.length )} warnings. \n` ); } else { message += green(`${prefix} - Build finished \n`); } buildCounter++; return { success: !buildFailure, message, }; }) ); let typeCounter = 1; const tscBufferTrigger = new Subject<boolean>(); const tscSubscriber = runTsc({ tsconfigPath: options.tsConfig, watch: options.watch || !!esbuildOptions.watch, root: options.root, useGlobal: false, }).pipe( map(({ info, error, end }) => { let message = ''; let hasErrors = Boolean(error); const count = gray(`[${typeCounter}]`); const prefix = `tsc ${count}`; if (error) { message += red(`${prefix} ${error.replace(/\n/g, '')} \n`); } else if (info) { if (info.match(/Found\s\d*\serror/)) { if (info.includes('Found 0 errors')) { message += green(`${prefix} ${info.replace(/\n/g, '')} \n`); } else { hasErrors = true; message += yellow(`${prefix} ${info.replace(/\n/g, '')} \n`); } tscBufferTrigger.next(true); } else { message += green(`${prefix} ${info.replace(/\n/g, '')} \n`); } } return { info, error, end, message, hasErrors }; }), bufferUntil(({ info }) => !!info?.match(/Found\s\d*\serror/)), // bufferUntil(({ info }) => true), map((values) => { typeCounter++; let message = ''; values.forEach((value) => (message += value.message)); // console.log(message); return { success: !values.find((value) => value.hasErrors), message, }; }) ); const packageCopySubscriber = runCopyPackages( process.cwd(), options.outputPath, esbuildOptions.external ).pipe( map((result) => { const message = result.error ?? result.copyResult; return { success: result.success, message, }; }) ); const assetCopySubscriber = runCopyAssets( options.assets, '', options.outputPath ).pipe(map((result) => result)); // exportDiagnostics( // `OUTPUT_LOG.ts`, // `const output = ${inspect( // { // cwd: process.cwd(), // options, // rawOptions, // context, // projGraph, // workspace, // }, // false, // 10 // )}` // ); if (options.watch) { return eachValueFrom( zip(buildSubscriber, tscSubscriber).pipe( map(([buildResults, tscResults]) => { // console.log('\x1Bc'); console.log(tscResults.message); console.log(buildResults.message); return { success: buildResults?.success && tscResults?.success, outfile, }; }) ) ); } return eachValueFrom( zip( buildSubscriber, tscSubscriber, packageCopySubscriber, assetCopySubscriber ).pipe( map( ([buildResults, tscResults, packageCopyResults, assetCopyResults]) => { // console.log('\x1Bc'); console.log(tscResults.message); console.log(buildResults.message); if (packageCopyResults.message.length !== 0) { console.log( `Copied node_modules: ${inspect( packageCopyResults.message, false, 10, true )}` ); } if (assetCopyResults.error) { console.error(`Error copying assets: ${assetCopyResults.error}`); } return { success: buildResults?.success && tscResults?.success && packageCopyResults.success && assetCopyResults.success, outfile, }; } ) ) ); } interface RunBuildResponse { buildResult: BuildResult | null; buildFailure: BuildFailure | null; } function runBuild( options: BuildOptions, watchDir?: string ): Observable<RunBuildResponse> { return new Observable<RunBuildResponse>((subscriber) => { const cwd = watchDir || options.absWorkingDir || process.cwd(); // We will use the org watch settings with node-watch for better refresh performance const { watch: buildWatch, ...opts } = options; build(opts) .then((buildResult) => { subscriber.next({ buildResult, buildFailure: null }); // Helper to send back data for watch events & supporting existing esbuild settings const watchNext = ({ buildFailure, buildResult }: RunBuildResponse) => { subscriber.next({ buildFailure, buildResult }); if (typeof buildWatch === 'object' && buildWatch.onRebuild) { buildWatch.onRebuild(buildFailure, buildResult); } }; // When in watch mode, it will continue to report back if (buildWatch) { watch(cwd, { recursive: true }, () => { buildResult .rebuild() .then((watchResult) => { watchNext({ buildFailure: null, buildResult: watchResult, }); }) .catch((watchFailure: BuildFailure) => { watchNext({ buildFailure: watchFailure, buildResult: null, }); }); }); } else { subscriber.complete(); } }) .catch((buildFailure: BuildFailure) => { subscriber.next({ buildResult: null, buildFailure }); subscriber.complete(); }); }); } interface RunTscOptions { tsconfigPath: string; watch?: boolean; root?: string; useGlobal?: boolean; } function runTsc({ tsconfigPath, watch, root, useGlobal }: RunTscOptions) { return new Observable<{ info?: string; error?: string; tscError?: Error; end?: string; }>((subscriber) => { // Build command const modeModulesPath = useGlobal ? '' : (root ? root + '/' : './') + 'node_modules/typescript/bin/'; const command = `${modeModulesPath}tsc`; // Build arguments const args: string[] = ['--noEmit']; // --noEmit so as to not save out data if (watch) { args.push('-w'); } args.push('-p'); args.push(tsconfigPath); let errorCount = 0; // Run command const child = spawn(command, args, { shell: true }); child.stdout.on('data', (data) => { const decoded = data.toString(); // eslint-disable-next-line no-control-regex if (decoded.match(/\x1Bc/g)) return; if (decoded.includes('): error T')) { errorCount++; subscriber.next({ error: decoded }); } else { subscriber.next({ info: decoded }); } }); child.stderr.on('error', (tscError) => { subscriber.next({ tscError }); }); child.stdout.on('end', () => { subscriber.next({ info: `Type check complete. Found ${errorCount} errors`, }); }); }); } interface RunCopyPackagesResponse { copyResult: string[]; success: boolean; error?: any; } function runCopyPackages( root: string, destination: string, external: string[] = [] ) { return new Observable<RunCopyPackagesResponse>((subscriber) => { getPackagesToCopy(root, external) .then((modules) => copyPackages(root, destination, modules)) .then((directories) => { subscriber.next({ copyResult: directories ?? [], success: true, }); }) .catch((error) => { subscriber.next({ copyResult: [], success: false, error, }); }); }); } interface RunCopyAssetsResponse { success: boolean; error?: string; } function runCopyAssets(assets: string[], root: string, destination: string) { return new Observable<RunCopyAssetsResponse>((subscriber) => { copyAssets(assets, root, destination) .then((response) => { subscriber.next(response); }) .catch((error) => { subscriber.next({ success: false, error, }); }); }); } function bufferUntil<T>( predicate: (value: T) => boolean ): OperatorFunction<T, T[]> { return function (source) { const share$ = source.pipe(share()); const until$ = share$.pipe(filter(predicate), delay(0)); return share$.pipe(buffer(until$)); }; } export default buildExecutor;