diff --git a/src/cli/build/esbuild.ts b/src/cli/build/esbuild.ts index 2df8a16ba..e3ce852a8 100644 --- a/src/cli/build/esbuild.ts +++ b/src/cli/build/esbuild.ts @@ -1,24 +1,21 @@ import { inspect } from 'util'; import tsconfigPaths from '@esbuild-plugins/tsconfig-paths'; -import { build } from 'esbuild'; -import { ModuleKind, ModuleResolutionKind, ScriptTarget } from 'typescript'; - -import { createLogger } from '../../utils/logging'; +import { type BuildOptions, build } from 'esbuild'; +import { type CompilerOptions, ModuleKind, ScriptTarget } from 'typescript'; import { parseTscArgs } from './args'; -import { readTsconfig, tsc } from './tsc'; +import type { RunnerParams } from './runner'; +import { tsc } from './tsc'; -interface EsbuildParameters { - debug: boolean; -} +export type EsbuildParameters = RunnerParams & { + mode: 'build' | 'build-package'; +}; export const esbuild = async ( - { debug }: EsbuildParameters, + { compilerOptions, debug, entryPoints, log, mode }: EsbuildParameters, args = process.argv.slice(2), ) => { - const log = createLogger(debug); - const tscArgs = parseTscArgs(args); if (tscArgs.build) { @@ -29,15 +26,6 @@ export const esbuild = async ( return; } - const parsedCommandLine = readTsconfig(args, log); - - if (!parsedCommandLine || process.exitCode) { - return; - } - - const { fileNames: entryPoints, options: compilerOptions } = - parsedCommandLine; - log.debug(log.bold('Files')); entryPoints.forEach((filepath) => log.debug(filepath)); @@ -46,20 +34,122 @@ export const esbuild = async ( const start = process.hrtime.bigint(); - // TODO: support `bundle`, `minify`, `splitting`, `treeShaking` - const bundle = false; + switch (mode) { + case 'build': + await runBuild({ + compilerOptions, + debug, + entryPoints, + tsconfig: tscArgs.pathname, + }); + + break; + + case 'build-package': + await runBuild({ + bundle: true, + compilerOptions: { + ...compilerOptions, + module: ModuleKind.ESNext, + }, + debug, + entryPoints, + outExtension: { '.js': '.mjs' }, + tsconfig: tscArgs.pathname, + }); + + await runBuild({ + bundle: true, + compilerOptions: { + ...compilerOptions, + module: ModuleKind.NodeNext, + }, + debug, + entryPoints, + outExtension: { '.js': '.js' }, + tsconfig: tscArgs.pathname, + }); + + break; + } + + const end = process.hrtime.bigint(); + + log.plain(`Built in ${log.timing(start, end)}.`); + if (compilerOptions.declaration || mode === 'build-package') { + const removeComments = compilerOptions.removeComments ?? false; + + await tsc([ + '--declaration', + '--emitDeclarationOnly', + ...(tscArgs.project ? ['--project', tscArgs.project] : []), + '--removeComments', + removeComments.toString(), + ]); + } +}; + +const ES_MODULE_KINDS = new Set([ + ModuleKind.ES2015, + ModuleKind.ES2020, + ModuleKind.ES2022, + ModuleKind.ESNext, +]); + +const NODE_MODULE_KINDS = new Set([ + ModuleKind.CommonJS, + ModuleKind.Node16, + ModuleKind.NodeNext, +]); + +const mapModule = ( + compilerOptions: CompilerOptions, +): Pick => { + if (NODE_MODULE_KINDS.has(compilerOptions.module)) { + return { format: 'cjs', platform: 'node' }; + } + + if (ES_MODULE_KINDS.has(compilerOptions.module)) { + return { format: 'esm', platform: 'neutral' }; + } + + return { format: undefined, platform: undefined }; +}; + +type RunEsbuildOptions = { + bundle?: boolean; + compilerOptions: Pick< + CompilerOptions, + 'baseUrl' | 'module' | 'outDir' | 'paths' | 'sourceMap' | 'target' + >; + debug: boolean; + entryPoints: string[]; + outExtension?: BuildOptions['outExtension']; + tsconfig: string; +}; + +const runBuild = async ({ + bundle = false, + compilerOptions, + debug, + entryPoints, + outExtension, + tsconfig, +}: RunEsbuildOptions) => { + const { format, platform } = mapModule(compilerOptions); + + // TODO: support `minify`, `splitting`, `treeShaking` await build({ bundle, entryPoints, - format: compilerOptions.module === ModuleKind.CommonJS ? 'cjs' : undefined, + format, outdir: compilerOptions.outDir, logLevel: debug ? 'debug' : 'info', logLimit: 0, - platform: - compilerOptions.moduleResolution === ModuleResolutionKind.NodeJs - ? 'node' - : undefined, + outExtension, + packages: 'external', + platform, plugins: bundle ? [] : [ @@ -76,18 +166,6 @@ export const esbuild = async ( target: compilerOptions.target ? ScriptTarget[compilerOptions.target].toLocaleLowerCase() : undefined, - tsconfig: tscArgs.pathname, + tsconfig, }); - - const end = process.hrtime.bigint(); - - log.plain(`Built in ${log.timing(start, end)}.`); - - if (compilerOptions.declaration) { - await tsc([ - '--declaration', - '--emitDeclarationOnly', - ...(tscArgs.project ? ['--project', tscArgs.project] : []), - ]); - } }; diff --git a/src/cli/build/index.ts b/src/cli/build/index.ts index 2ac37aa85..344482bee 100644 --- a/src/cli/build/index.ts +++ b/src/cli/build/index.ts @@ -1,58 +1,26 @@ -import chalk from 'chalk'; - -import { hasDebugFlag } from '../../utils/args'; -import { log } from '../../utils/logging'; -import { getStringPropFromConsumerManifest } from '../../utils/manifest'; - import { copyAssets } from './assets'; import { esbuild } from './esbuild'; -import { readTsconfig, tsc } from './tsc'; - -export const build = async (args = process.argv.slice(2)) => { - // TODO: define a unified `package.json#/skuba` schema and parser so we don't - // need all these messy lookups. - const tool = await getStringPropFromConsumerManifest('build'); - - switch (tool) { - case 'esbuild': { - const debug = hasDebugFlag(args); - - log.plain(chalk.yellow('esbuild')); - await esbuild({ debug }, args); - break; - } - - // TODO: flip the default case over to `esbuild` in skuba vNext. - case undefined: - case 'tsc': { - log.plain(chalk.blue('tsc')); - await tsc(args); - break; - } - - default: { - log.err( - 'We don’t support the build tool specified in your', - log.bold('package.json'), - 'yet:', - ); - log.err(log.subtle(JSON.stringify({ skuba: { build: tool } }, null, 2))); - process.exitCode = 1; - return; - } - } - - const parsedCommandLine = readTsconfig(args, log); - - if (!parsedCommandLine || process.exitCode) { - return; - } - - const { options: compilerOptions } = parsedCommandLine; - - if (!compilerOptions.outDir) { - return; - } - - await copyAssets(compilerOptions.outDir); -}; +import { runBuildTool } from './runner'; +import { tsc } from './tsc'; + +export const build = async (args = process.argv.slice(2)) => + runBuildTool( + { + esbuild: async ({ compilerOptions, ...params }) => { + await esbuild({ ...params, compilerOptions, mode: 'build' }, args); + + if (compilerOptions.outDir) { + await copyAssets(compilerOptions.outDir); + } + }, + + tsc: async ({ compilerOptions }) => { + await tsc(args); + + if (compilerOptions.outDir) { + await copyAssets(compilerOptions.outDir); + } + }, + }, + args, + ); diff --git a/src/cli/build/runner.ts b/src/cli/build/runner.ts new file mode 100644 index 000000000..6d90c1313 --- /dev/null +++ b/src/cli/build/runner.ts @@ -0,0 +1,68 @@ +import chalk from 'chalk'; +import type { CompilerOptions } from 'typescript'; + +import { hasDebugFlag } from '../../utils/args'; +import { type Logger, createLogger } from '../../utils/logging'; +import { getStringPropFromConsumerManifest } from '../../utils/manifest'; + +import { readTsconfig } from './tsc'; + +export type RunnerParams = { + compilerOptions: CompilerOptions; + debug: boolean; + entryPoints: string[]; + log: Logger; +}; + +type RunBuildToolParams = { + esbuild: (params: RunnerParams, args: string[]) => Promise; + tsc: (params: RunnerParams, args: string[]) => Promise; +}; + +export const runBuildTool = async (run: RunBuildToolParams, args: string[]) => { + const debug = hasDebugFlag(args); + + const log = createLogger(debug); + + const tsconfig = readTsconfig(args, log); + + if (!tsconfig) { + process.exitCode = 1; + return; + } + + const { compilerOptions, entryPoints } = tsconfig; + + const params = { compilerOptions, debug, entryPoints, log }; + + // TODO: define a unified `package.json#/skuba` schema and parser so we don't + // need all these messy lookups. + const tool = await getStringPropFromConsumerManifest('build'); + + switch (tool) { + case 'esbuild': { + log.plain(chalk.yellow('esbuild')); + await run.esbuild(params, args); + break; + } + + // TODO: flip the default case over to `esbuild` in skuba vNext. + case undefined: + case 'tsc': { + log.plain(chalk.blue('tsc')); + await run.tsc(params, args); + break; + } + + default: { + log.err( + 'We don’t support the build tool specified in your', + log.bold('package.json'), + 'yet:', + ); + log.err(log.subtle(JSON.stringify({ skuba: { build: tool } }, null, 2))); + process.exitCode = 1; + return; + } + } +}; diff --git a/src/cli/build/tsc.ts b/src/cli/build/tsc.ts index 4cf3f38e5..e33b6c21d 100644 --- a/src/cli/build/tsc.ts +++ b/src/cli/build/tsc.ts @@ -46,7 +46,6 @@ export const readTsconfig = (args = process.argv.slice(2), log: Logger) => { ); if (!tsconfigFile) { log.err(`Could not find ${tscArgs.pathname}.`); - process.exitCode = 1; return; } @@ -57,7 +56,6 @@ export const readTsconfig = (args = process.argv.slice(2), log: Logger) => { if (readConfigFile.error) { log.err(`Could not read ${tscArgs.pathname}.`); log.subtle(ts.formatDiagnostic(readConfigFile.error, formatHost)); - process.exitCode = 1; return; } @@ -72,9 +70,11 @@ export const readTsconfig = (args = process.argv.slice(2), log: Logger) => { if (parsedCommandLine.errors.length) { log.err(`Could not parse ${tscArgs.pathname}.`); log.subtle(ts.formatDiagnostics(parsedCommandLine.errors, formatHost)); - process.exitCode = 1; return; } - return parsedCommandLine; + return { + compilerOptions: parsedCommandLine.options, + entryPoints: parsedCommandLine.fileNames, + }; }; diff --git a/src/cli/buildPackage.ts b/src/cli/buildPackage.ts index 7c9f9d5bd..b6904c1ce 100644 --- a/src/cli/buildPackage.ts +++ b/src/cli/buildPackage.ts @@ -1,9 +1,32 @@ +import type { CompilerOptions } from 'typescript'; + import { hasSerialFlag } from '../utils/args'; import { execConcurrently } from '../utils/exec'; -import { copyAssetsConcurrently } from './build/assets'; +import { copyAssets, copyAssetsConcurrently } from './build/assets'; +import { type EsbuildParameters, esbuild as runEsbuild } from './build/esbuild'; +import { runBuildTool } from './build/runner'; + +export const buildPackage = async (args = process.argv.slice(2)) => + runBuildTool({ esbuild, tsc }, args); + +const esbuild = async ( + { compilerOptions, ...params }: Omit, + args: string[], +) => { + await runEsbuild({ ...params, compilerOptions, mode: 'build-package' }, args); + + if (compilerOptions.outDir) { + await copyAssets(compilerOptions.outDir); + } +}; + +const tsc = async ( + { compilerOptions }: { compilerOptions: CompilerOptions }, + args: string[], +) => { + const removeComments = compilerOptions.removeComments ?? false; -export const buildPackage = async (args = process.argv.slice(2)) => { await execConcurrently( [ { @@ -19,8 +42,7 @@ export const buildPackage = async (args = process.argv.slice(2)) => { prefixColor: 'yellow', }, { - command: - 'tsc --allowJS false --declaration --emitDeclarationOnly --outDir lib-types --project tsconfig.build.json', + command: `tsc --allowJS false --declaration --emitDeclarationOnly --outDir lib-types --project tsconfig.build.json --removeComments ${removeComments}`, name: 'types', prefixColor: 'blue', },