mirror of
https://github.com/egoist/tsup.git
synced 2025-12-08 20:35:58 +00:00
474 lines
15 KiB
TypeScript
474 lines
15 KiB
TypeScript
import path from 'node:path'
|
|
import fs from 'node:fs'
|
|
import { Worker } from 'node:worker_threads'
|
|
import { loadTsConfig } from 'bundle-require'
|
|
import execa from 'execa'
|
|
import { glob } from 'tinyglobby'
|
|
import kill from 'tree-kill'
|
|
import { version } from '../package.json'
|
|
import { PrettyError, handleError } from './errors'
|
|
import { getAllDepsHash, loadTsupConfig } from './load'
|
|
import {
|
|
type MaybePromise,
|
|
debouncePromise,
|
|
removeFiles,
|
|
slash,
|
|
toObjectEntry,
|
|
} from './utils'
|
|
import { createLogger, setSilent } from './log'
|
|
import { runEsbuild } from './esbuild'
|
|
import { shebang } from './plugins/shebang'
|
|
import { cjsSplitting } from './plugins/cjs-splitting'
|
|
import { PluginContainer } from './plugin'
|
|
import { swcTarget } from './plugins/swc-target'
|
|
import { sizeReporter } from './plugins/size-reporter'
|
|
import { treeShakingPlugin } from './plugins/tree-shaking'
|
|
import { copyPublicDir, isInPublicDir } from './lib/public-dir'
|
|
import { terserPlugin } from './plugins/terser'
|
|
import { runTypeScriptCompiler } from './tsc'
|
|
import { runDtsRollup } from './api-extractor'
|
|
import { cjsInterop } from './plugins/cjs-interop'
|
|
import type { ChildProcess } from 'node:child_process'
|
|
import type { Format, KILL_SIGNAL, NormalizedOptions, Options } from './options'
|
|
|
|
export type { Format, Options, NormalizedOptions }
|
|
|
|
export const defineConfig = (
|
|
options:
|
|
| Options
|
|
| Options[]
|
|
| ((
|
|
/** The options derived from CLI flags */
|
|
overrideOptions: Options,
|
|
) => MaybePromise<Options | Options[]>),
|
|
) => options
|
|
|
|
/**
|
|
* tree-kill use `taskkill` command on Windows to kill the process,
|
|
* it may return 128 as exit code when the process has already exited.
|
|
* @see https://github.com/egoist/tsup/issues/976
|
|
*/
|
|
const isTaskkillCmdProcessNotFoundError = (err: Error) => {
|
|
return (
|
|
process.platform === 'win32' &&
|
|
'cmd' in err &&
|
|
'code' in err &&
|
|
typeof err.cmd === 'string' &&
|
|
err.cmd.startsWith('taskkill') &&
|
|
err.code === 128
|
|
)
|
|
}
|
|
|
|
const killProcess = ({ pid, signal }: { pid: number; signal: KILL_SIGNAL }) =>
|
|
new Promise<void>((resolve, reject) => {
|
|
kill(pid, signal, (err) => {
|
|
if (err && !isTaskkillCmdProcessNotFoundError(err)) return reject(err)
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
const normalizeOptions = async (
|
|
logger: ReturnType<typeof createLogger>,
|
|
optionsFromConfigFile: Options | undefined,
|
|
optionsOverride: Options,
|
|
) => {
|
|
const _options = {
|
|
...optionsFromConfigFile,
|
|
...optionsOverride,
|
|
}
|
|
|
|
const options: Partial<NormalizedOptions> = {
|
|
outDir: 'dist',
|
|
removeNodeProtocol: true,
|
|
..._options,
|
|
format:
|
|
typeof _options.format === 'string'
|
|
? [_options.format as Format]
|
|
: _options.format || ['cjs'],
|
|
dts:
|
|
typeof _options.dts === 'boolean'
|
|
? _options.dts
|
|
? {}
|
|
: undefined
|
|
: typeof _options.dts === 'string'
|
|
? { entry: _options.dts }
|
|
: _options.dts,
|
|
experimentalDts: _options.experimentalDts
|
|
? typeof _options.experimentalDts === 'boolean'
|
|
? _options.experimentalDts
|
|
? { entry: {} }
|
|
: undefined
|
|
: typeof _options.experimentalDts === 'string'
|
|
? {
|
|
entry: toObjectEntry(_options.experimentalDts),
|
|
}
|
|
: {
|
|
..._options.experimentalDts,
|
|
entry: toObjectEntry(_options.experimentalDts.entry || {}),
|
|
}
|
|
: undefined,
|
|
}
|
|
|
|
setSilent(options.silent)
|
|
|
|
const entry = options.entry || options.entryPoints
|
|
|
|
if (!entry || Object.keys(entry).length === 0) {
|
|
throw new PrettyError(`No input files, try "tsup <your-file>" instead`)
|
|
}
|
|
|
|
if (Array.isArray(entry)) {
|
|
options.entry = await glob(entry)
|
|
// Ensure entry exists
|
|
if (!options.entry || options.entry.length === 0) {
|
|
throw new PrettyError(`Cannot find ${entry}`)
|
|
} else {
|
|
logger.info('CLI', `Building entry: ${options.entry.join(', ')}`)
|
|
}
|
|
} else {
|
|
Object.keys(entry).forEach((alias) => {
|
|
const filename = entry[alias]!
|
|
if (!fs.existsSync(filename)) {
|
|
throw new PrettyError(`Cannot find ${alias}: ${filename}`)
|
|
}
|
|
})
|
|
options.entry = entry
|
|
logger.info('CLI', `Building entry: ${JSON.stringify(entry)}`)
|
|
}
|
|
|
|
const tsconfig = loadTsConfig(process.cwd(), options.tsconfig)
|
|
if (tsconfig) {
|
|
logger.info(
|
|
'CLI',
|
|
`Using tsconfig: ${path.relative(process.cwd(), tsconfig.path)}`,
|
|
)
|
|
options.tsconfig = tsconfig.path
|
|
options.tsconfigResolvePaths = tsconfig.data?.compilerOptions?.paths || {}
|
|
options.tsconfigDecoratorMetadata =
|
|
tsconfig.data?.compilerOptions?.emitDecoratorMetadata
|
|
if (options.dts) {
|
|
options.dts.compilerOptions = {
|
|
...(tsconfig.data.compilerOptions || {}),
|
|
...(options.dts.compilerOptions || {}),
|
|
}
|
|
}
|
|
if (options.experimentalDts) {
|
|
options.experimentalDts.compilerOptions = {
|
|
...(tsconfig.data.compilerOptions || {}),
|
|
...(options.experimentalDts.compilerOptions || {}),
|
|
}
|
|
options.experimentalDts.entry = toObjectEntry(
|
|
Object.keys(options.experimentalDts.entry).length > 0
|
|
? options.experimentalDts.entry
|
|
: options.entry,
|
|
)
|
|
}
|
|
if (!options.target) {
|
|
options.target = tsconfig.data?.compilerOptions?.target?.toLowerCase()
|
|
}
|
|
} else if (options.tsconfig) {
|
|
throw new PrettyError(`Cannot find tsconfig: ${options.tsconfig}`)
|
|
}
|
|
|
|
if (!options.target) {
|
|
options.target = 'node16'
|
|
}
|
|
|
|
return options as NormalizedOptions
|
|
}
|
|
|
|
export async function build(_options: Options) {
|
|
const config =
|
|
_options.config === false
|
|
? {}
|
|
: await loadTsupConfig(
|
|
process.cwd(),
|
|
_options.config === true ? undefined : _options.config,
|
|
)
|
|
|
|
const configData =
|
|
typeof config.data === 'function'
|
|
? await config.data(_options)
|
|
: config.data
|
|
|
|
await Promise.all(
|
|
[...(Array.isArray(configData) ? configData : [configData])].map(
|
|
async (item) => {
|
|
const logger = createLogger(item?.name)
|
|
const options = await normalizeOptions(logger, item, _options)
|
|
|
|
logger.info('CLI', `tsup v${version}`)
|
|
|
|
if (config.path) {
|
|
logger.info('CLI', `Using tsup config: ${config.path}`)
|
|
}
|
|
|
|
if (options.watch) {
|
|
logger.info('CLI', 'Running in watch mode')
|
|
}
|
|
|
|
const experimentalDtsTask = async () => {
|
|
if (!options.dts && options.experimentalDts) {
|
|
const exports = runTypeScriptCompiler(options);
|
|
await runDtsRollup(options, exports);
|
|
}
|
|
}
|
|
|
|
const dtsTask = async () => {
|
|
if (options.dts && options.experimentalDts) {
|
|
throw new Error(
|
|
"You can't use both `dts` and `experimentalDts` at the same time",
|
|
)
|
|
}
|
|
|
|
experimentalDtsTask();
|
|
|
|
if (options.dts) {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const worker = new Worker(path.join(__dirname, './rollup.js'))
|
|
|
|
const terminateWorker = () => {
|
|
if (options.watch) return
|
|
worker.terminate()
|
|
}
|
|
|
|
worker.postMessage({
|
|
configName: item?.name,
|
|
options: {
|
|
...options, // functions cannot be cloned
|
|
injectStyle: typeof options.injectStyle === 'function' ? undefined : options.injectStyle,
|
|
banner: undefined,
|
|
footer: undefined,
|
|
esbuildPlugins: undefined,
|
|
esbuildOptions: undefined,
|
|
plugins: undefined,
|
|
treeshake: undefined,
|
|
onSuccess: undefined,
|
|
outExtension: undefined,
|
|
},
|
|
})
|
|
worker.on('message', (data) => {
|
|
if (data === 'error') {
|
|
terminateWorker()
|
|
reject(new Error('error occured in dts build'))
|
|
} else if (data === 'success') {
|
|
terminateWorker()
|
|
resolve()
|
|
} else {
|
|
const { type, text } = data
|
|
if (type === 'log') {
|
|
console.log(text)
|
|
} else if (type === 'error') {
|
|
console.error(text)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
const mainTasks = async () => {
|
|
if (!options.dts?.only) {
|
|
let onSuccessProcess: ChildProcess | undefined
|
|
let onSuccessCleanup: (() => any) | undefined | void
|
|
/** Files imported by the entry */
|
|
const buildDependencies: Set<string> = new Set()
|
|
|
|
let depsHash = await getAllDepsHash(process.cwd())
|
|
|
|
const doOnSuccessCleanup = async () => {
|
|
if (onSuccessProcess) {
|
|
await killProcess({
|
|
pid: onSuccessProcess.pid!,
|
|
signal: options.killSignal || 'SIGTERM',
|
|
})
|
|
} else if (onSuccessCleanup) {
|
|
await onSuccessCleanup()
|
|
}
|
|
// reset them in all occasions anyway
|
|
onSuccessProcess = undefined
|
|
onSuccessCleanup = undefined
|
|
}
|
|
|
|
const debouncedBuildAll = debouncePromise(
|
|
() => {
|
|
return buildAll()
|
|
},
|
|
100,
|
|
handleError,
|
|
)
|
|
|
|
const buildAll = async () => {
|
|
await doOnSuccessCleanup()
|
|
// Store previous build dependencies in case the build failed
|
|
// So we can restore it
|
|
const previousBuildDependencies = new Set(buildDependencies)
|
|
buildDependencies.clear()
|
|
|
|
if (options.clean) {
|
|
const extraPatterns = Array.isArray(options.clean)
|
|
? options.clean
|
|
: []
|
|
// .d.ts files are removed in the `dtsTask` instead
|
|
// `dtsTask` is a separate process, which might start before `mainTasks`
|
|
if (options.dts || options.experimentalDts) {
|
|
extraPatterns.unshift('!**/*.d.{ts,cts,mts}')
|
|
}
|
|
await removeFiles(['**/*', ...extraPatterns], options.outDir)
|
|
logger.info('CLI', 'Cleaning output folder')
|
|
}
|
|
|
|
const css: Map<string, string> = new Map()
|
|
await Promise.all([
|
|
...options.format.map(async (format, index) => {
|
|
const pluginContainer = new PluginContainer([
|
|
shebang(),
|
|
...(options.plugins || []),
|
|
treeShakingPlugin({
|
|
treeshake: options.treeshake,
|
|
name: options.globalName,
|
|
silent: options.silent,
|
|
}),
|
|
cjsSplitting(),
|
|
cjsInterop(),
|
|
swcTarget(),
|
|
sizeReporter(),
|
|
terserPlugin({
|
|
minifyOptions: options.minify,
|
|
format,
|
|
terserOptions: options.terserOptions,
|
|
globalName: options.globalName,
|
|
logger,
|
|
}),
|
|
])
|
|
await runEsbuild(options, {
|
|
pluginContainer,
|
|
format,
|
|
css: index === 0 || options.injectStyle ? css : undefined,
|
|
logger,
|
|
buildDependencies,
|
|
}).catch((error) => {
|
|
previousBuildDependencies.forEach((v) =>
|
|
buildDependencies.add(v),
|
|
)
|
|
throw error
|
|
})
|
|
}),
|
|
])
|
|
|
|
experimentalDtsTask()
|
|
|
|
if (options.onSuccess) {
|
|
if (typeof options.onSuccess === 'function') {
|
|
onSuccessCleanup = await options.onSuccess()
|
|
} else {
|
|
onSuccessProcess = execa(options.onSuccess, {
|
|
shell: true,
|
|
stdio: 'inherit',
|
|
})
|
|
onSuccessProcess.on('exit', (code) => {
|
|
if (code && code !== 0) {
|
|
process.exitCode = code
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const startWatcher = async () => {
|
|
if (!options.watch) return
|
|
|
|
const { watch } = await import('chokidar')
|
|
|
|
const customIgnores = options.ignoreWatch
|
|
? Array.isArray(options.ignoreWatch)
|
|
? options.ignoreWatch
|
|
: [options.ignoreWatch]
|
|
: []
|
|
|
|
const ignored = [
|
|
'**/{.git,node_modules}/**',
|
|
options.outDir,
|
|
...customIgnores,
|
|
]
|
|
|
|
const watchPaths =
|
|
typeof options.watch === 'boolean'
|
|
? '.'
|
|
: Array.isArray(options.watch)
|
|
? options.watch.filter(
|
|
(path): path is string => typeof path === 'string',
|
|
)
|
|
: options.watch
|
|
|
|
logger.info(
|
|
'CLI',
|
|
`Watching for changes in ${
|
|
Array.isArray(watchPaths)
|
|
? watchPaths.map((v) => `"${v}"`).join(' | ')
|
|
: `"${watchPaths}"`
|
|
}`,
|
|
)
|
|
logger.info(
|
|
'CLI',
|
|
`Ignoring changes in ${ignored
|
|
.map((v) => `"${v}"`)
|
|
.join(' | ')}`,
|
|
)
|
|
|
|
const watcher = watch(watchPaths, {
|
|
ignoreInitial: true,
|
|
ignorePermissionErrors: true,
|
|
ignored,
|
|
})
|
|
watcher.on('all', async (type, file) => {
|
|
file = slash(file)
|
|
|
|
if (
|
|
options.publicDir &&
|
|
isInPublicDir(options.publicDir, file)
|
|
) {
|
|
logger.info('CLI', `Change in public dir: ${file}`)
|
|
copyPublicDir(options.publicDir, options.outDir)
|
|
return
|
|
}
|
|
|
|
// By default we only rebuild when imported files change
|
|
// If you specify custom `watch`, a string or multiple strings
|
|
// We rebuild when those files change
|
|
let shouldSkipChange = false
|
|
|
|
if (options.watch === true) {
|
|
if (file === 'package.json' && !buildDependencies.has(file)) {
|
|
const currentHash = await getAllDepsHash(process.cwd())
|
|
shouldSkipChange = currentHash === depsHash
|
|
depsHash = currentHash
|
|
} else if (!buildDependencies.has(file)) {
|
|
shouldSkipChange = true
|
|
}
|
|
}
|
|
|
|
if (shouldSkipChange) {
|
|
return
|
|
}
|
|
|
|
logger.info('CLI', `Change detected: ${type} ${file}`)
|
|
debouncedBuildAll()
|
|
})
|
|
}
|
|
|
|
logger.info('CLI', `Target: ${options.target}`)
|
|
|
|
await buildAll()
|
|
copyPublicDir(options.publicDir, options.outDir)
|
|
|
|
startWatcher()
|
|
}
|
|
}
|
|
|
|
await Promise.all([dtsTask(), mainTasks()])
|
|
},
|
|
),
|
|
)
|
|
}
|