From e7c00fc4542c6b15555acbf355adf28940b2ff71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 09:23:10 +0800 Subject: [PATCH 01/10] feat: rolldown watcher --- src/features/watch.ts | 25 ++++++------------ src/index.ts | 59 ++++++++++++++++++++++++++++++++----------- src/run.ts | 2 ++ 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/features/watch.ts b/src/features/watch.ts index 497516b12..7b4cfc9fb 100644 --- a/src/features/watch.ts +++ b/src/features/watch.ts @@ -1,8 +1,6 @@ -import { blue } from 'ansis' -import { debounce, toArray } from '../utils/general' +import { debounce, resolveComma, toArray } from '../utils/general' import { logger } from '../utils/logger' import type { ResolvedOptions } from '../options' -import type { FSWatcher } from 'chokidar' const endsWithConfig = /[\\/](?:package\.json|tsdown\.config.*)$/ @@ -11,20 +9,12 @@ export async function watchBuild( configFiles: string[], rebuild: () => void, restart: () => void, -): Promise { - if (typeof options.watch === 'boolean' && options.outDir === options.cwd) { - throw new Error( - `Watch is enabled, but output directory is the same as the current working directory.` + - `Please specify a different watch directory using ${blue`watch`} option,` + - `or set ${blue`outDir`} to a different directory.`, - ) - } - - const files = toArray( - typeof options.watch === 'boolean' ? options.cwd : options.watch, +): Promise { + const files = resolveComma( + toArray(typeof options.watch === 'string' ? options.watch : []), ) - logger.info(`Watching for changes in ${files.join(', ')}`) files.push(...configFiles) + logger.info(`Watching for changes...`) const { watch } = await import('chokidar') const debouncedRebuild = debounce(rebuild, 100) @@ -35,7 +25,6 @@ export async function watchBuild( ignored: [ /[\\/]\.git[\\/]/, /[\\/]node_modules[\\/]/, - options.outDir, ...toArray(options.ignoreWatch), ], }) @@ -51,5 +40,7 @@ export async function watchBuild( debouncedRebuild() }) - return watcher + return { + [Symbol.asyncDispose]: () => watcher.close(), + } } diff --git a/src/index.ts b/src/index.ts index 3e9cccf18..268957365 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,11 +4,13 @@ import { fileURLToPath } from 'node:url' import { green } from 'ansis' import { build as rolldownBuild, + watch as rolldownWatch, type BuildOptions, type OutputAsset, type OutputChunk, type OutputOptions, type RolldownPluginOption, + type RolldownWatcher, } from 'rolldown' import { exec } from 'tinyexec' import { attw } from './features/attw' @@ -64,8 +66,16 @@ export async function build(userOptions: Options = {}): Promise { const rebuild = rebuilds[i] if (!rebuild) continue - const watcher = await watchBuild(config, configFiles, rebuild, restart) - cleanCbs.push(() => watcher.close()) + const tsdownWatcher = await watchBuild( + config, + configFiles, + rebuild, + restart, + ) + cleanCbs.push(async () => { + await tsdownWatcher[Symbol.asyncDispose]() + rebuild[Symbol.asyncDispose]?.() + }) } if (cleanCbs.length) { @@ -98,18 +108,28 @@ export type TsdownChunks = Partial< export async function buildSingle( config: ResolvedOptions, clean: () => Promise, -): Promise<(() => Promise) | undefined> { +): Promise<(AsyncDisposable & (() => Promise)) | undefined> { const { format: formats, dts, watch, onSuccess } = config let ab: AbortController | undefined const { hooks, context } = await createHooks(config) + const rolldownWatchers: RolldownWatcher[] = [] + async function dispose() { + ab?.abort() + for (const watcher of rolldownWatchers) { + await watcher.close() + } + } + await rebuild(true) if (watch) { - return () => rebuild() + const rebuildFn = () => rebuild() + rebuildFn[Symbol.asyncDispose] = dispose + return rebuildFn } - async function rebuild(first?: boolean) { + async function rebuild(first?: boolean): Promise { const startTime = performance.now() await hooks.callHook('build:prepare', context) @@ -133,13 +153,24 @@ export async function buildSingle( ...context, buildOptions, }) - const { output } = await rolldownBuild(buildOptions) - chunks[format] = output - if (format === 'cjs' && dts) { - const { output } = await rolldownBuild( - await getBuildOptions(config, format, isMultiFormat, true), - ) - chunks[format].push(...output) + if (watch) { + rolldownWatchers.push(rolldownWatch(buildOptions)) + if (format === 'cjs' && dts) { + rolldownWatchers.push( + rolldownWatch( + await getBuildOptions(config, format, isMultiFormat, true), + ), + ) + } + } else { + const { output } = await rolldownBuild(buildOptions) + chunks[format] = output + if (format === 'cjs' && dts) { + const { output } = await rolldownBuild( + await getBuildOptions(config, format, isMultiFormat, true), + ) + chunks[format].push(...output) + } } } catch (error) { if (watch) { @@ -152,9 +183,7 @@ export async function buildSingle( }), ) - if (hasErrors) { - return - } + if (hasErrors) return await Promise.all([writeExports(config, chunks), copy(config)]) await Promise.all([publint(config), attw(config)]) diff --git a/src/run.ts b/src/run.ts index a6e06f109..d08aac0a6 100644 --- a/src/run.ts +++ b/src/run.ts @@ -5,4 +5,6 @@ import { runCLI } from './cli' try { module.enableCompileCache?.() } catch {} +// @ts-ignore +Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose') runCLI() From 402d52069e5218d5379bb67b912786f7781807e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 09:24:49 +0800 Subject: [PATCH 02/10] refactor --- src/index.ts | 75 ++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/src/index.ts b/src/index.ts index 268957365..ac5d28112 100644 --- a/src/index.ts +++ b/src/index.ts @@ -140,48 +140,47 @@ export async function buildSingle( let hasErrors = false const isMultiFormat = formats.length > 1 const chunks: TsdownChunks = {} - await Promise.all( - formats.map(async (format) => { - try { - const buildOptions = await getBuildOptions( - config, - format, - isMultiFormat, - false, - ) - await hooks.callHook('build:before', { - ...context, - buildOptions, - }) - if (watch) { - rolldownWatchers.push(rolldownWatch(buildOptions)) - if (format === 'cjs' && dts) { - rolldownWatchers.push( - rolldownWatch( - await getBuildOptions(config, format, isMultiFormat, true), - ), - ) - } - } else { - const { output } = await rolldownBuild(buildOptions) - chunks[format] = output - if (format === 'cjs' && dts) { - const { output } = await rolldownBuild( + async function buildByFormat(format: NormalizedFormat) { + try { + const buildOptions = await getBuildOptions( + config, + format, + isMultiFormat, + false, + ) + await hooks.callHook('build:before', { + ...context, + buildOptions, + }) + if (watch) { + rolldownWatchers.push(rolldownWatch(buildOptions)) + if (format === 'cjs' && dts) { + rolldownWatchers.push( + rolldownWatch( await getBuildOptions(config, format, isMultiFormat, true), - ) - chunks[format].push(...output) - } + ), + ) } - } catch (error) { - if (watch) { - logger.error(error) - hasErrors = true - return + } else { + const { output } = await rolldownBuild(buildOptions) + chunks[format] = output + if (format === 'cjs' && dts) { + const { output } = await rolldownBuild( + await getBuildOptions(config, format, isMultiFormat, true), + ) + chunks[format].push(...output) } - throw error } - }), - ) + } catch (error) { + if (watch) { + logger.error(error) + hasErrors = true + return + } + throw error + } + } + await Promise.all(formats.map(buildByFormat)) if (hasErrors) return From 37f7f39564b2be0acb4240ad31fadb2fea8e3189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 09:33:51 +0800 Subject: [PATCH 03/10] refactor --- src/index.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index ac5d28112..53b857d73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -154,21 +154,24 @@ export async function buildSingle( }) if (watch) { rolldownWatchers.push(rolldownWatch(buildOptions)) - if (format === 'cjs' && dts) { - rolldownWatchers.push( - rolldownWatch( - await getBuildOptions(config, format, isMultiFormat, true), - ), - ) - } } else { const { output } = await rolldownBuild(buildOptions) chunks[format] = output - if (format === 'cjs' && dts) { - const { output } = await rolldownBuild( - await getBuildOptions(config, format, isMultiFormat, true), - ) - chunks[format].push(...output) + } + + // build cjs dts + if (format === 'cjs' && dts) { + const buildOptions = await getBuildOptions( + config, + format, + isMultiFormat, + true, + ) + if (watch) { + rolldownWatchers.push(rolldownWatch(buildOptions)) + } else { + const { output } = await rolldownBuild(buildOptions) + chunks[format]!.push(...output) } } } catch (error) { @@ -184,7 +187,7 @@ export async function buildSingle( if (hasErrors) return - await Promise.all([writeExports(config, chunks), copy(config)]) + await Promise.all([!watch && writeExports(config, chunks), copy(config)]) await Promise.all([publint(config), attw(config)]) await hooks.callHook('build:done', context) From bc24962c38bba88984d41aa6f6118b223023df18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 10:05:16 +0800 Subject: [PATCH 04/10] refactor --- src/features/exports.ts | 16 +++++++++++++++- src/features/watch.ts | 10 ++++++++++ src/index.ts | 31 ++++++++++++++++++++++++------- src/utils/general.ts | 11 +++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/features/exports.ts b/src/features/exports.ts index db86afff7..3940edc38 100644 --- a/src/features/exports.ts +++ b/src/features/exports.ts @@ -6,7 +6,7 @@ import type { TsdownChunks } from '..' import type { NormalizedFormat, ResolvedOptions } from '../options' import type { Awaitable } from '../utils/types' import type { PackageJson } from 'pkg-types' -import type { OutputAsset, OutputChunk } from 'rolldown' +import type { OutputAsset, OutputChunk, Plugin } from 'rolldown' export interface ExportsOptions { /** @@ -239,3 +239,17 @@ function exportMeta(exports: Record, all?: boolean) { exports['./package.json'] = './package.json' } } + +export function OutputPlugin( + resolveChunks: (chunks: Array) => void, +): Plugin { + return { + name: 'tsdown:output', + generateBundle: { + order: 'post', + handler(_outputOptions, bundle) { + resolveChunks(Object.values(bundle)) + }, + }, + } +} diff --git a/src/features/watch.ts b/src/features/watch.ts index 7b4cfc9fb..78f70cb80 100644 --- a/src/features/watch.ts +++ b/src/features/watch.ts @@ -1,6 +1,7 @@ import { debounce, resolveComma, toArray } from '../utils/general' import { logger } from '../utils/logger' import type { ResolvedOptions } from '../options' +import type { Plugin } from 'rolldown' const endsWithConfig = /[\\/](?:package\.json|tsdown\.config.*)$/ @@ -44,3 +45,12 @@ export async function watchBuild( [Symbol.asyncDispose]: () => watcher.close(), } } + +export function WatchPlugin(): Plugin { + return { + name: 'tsdown:watch', + watchChange(id) { + logger.info(`Change detected: ${id}`) + }, + } +} diff --git a/src/index.ts b/src/index.ts index 53b857d73..7160899d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { exec } from 'tinyexec' import { attw } from './features/attw' import { cleanOutDir } from './features/clean' import { copy } from './features/copy' -import { writeExports } from './features/exports' +import { OutputPlugin, writeExports } from './features/exports' import { ExternalPlugin } from './features/external' import { createHooks } from './features/hooks' import { LightningCSSPlugin } from './features/lightningcss' @@ -27,7 +27,7 @@ import { ReportPlugin } from './features/report' import { getShimsInject } from './features/shims' import { shortcuts } from './features/shortcuts' import { RuntimeHelperCheckPlugin } from './features/target' -import { watchBuild } from './features/watch' +import { watchBuild, WatchPlugin } from './features/watch' import { mergeUserOptions, resolveOptions, @@ -37,6 +37,7 @@ import { } from './options' import { ShebangPlugin } from './plugins' import { lowestCommonAncestor } from './utils/fs' +import { withResolver } from './utils/general' import { logger, prettyName } from './utils/logger' import type { Options as DtsOptions } from 'rolldown-plugin-dts' @@ -140,11 +141,15 @@ export async function buildSingle( let hasErrors = false const isMultiFormat = formats.length > 1 const chunks: TsdownChunks = {} + async function buildByFormat(format: NormalizedFormat) { try { + const [chunksPromise, resolve] = + withResolver>() const buildOptions = await getBuildOptions( config, format, + resolve, isMultiFormat, false, ) @@ -155,24 +160,28 @@ export async function buildSingle( if (watch) { rolldownWatchers.push(rolldownWatch(buildOptions)) } else { - const { output } = await rolldownBuild(buildOptions) - chunks[format] = output + await rolldownBuild(buildOptions) } + chunks[format] = await chunksPromise // build cjs dts if (format === 'cjs' && dts) { + const [chunksPromise, resolve] = + withResolver>() + const buildOptions = await getBuildOptions( config, format, + resolve, isMultiFormat, true, ) if (watch) { rolldownWatchers.push(rolldownWatch(buildOptions)) } else { - const { output } = await rolldownBuild(buildOptions) - chunks[format]!.push(...output) + await rolldownBuild(buildOptions) } + chunks[format].push(...(await chunksPromise)) } } catch (error) { if (watch) { @@ -187,7 +196,7 @@ export async function buildSingle( if (hasErrors) return - await Promise.all([!watch && writeExports(config, chunks), copy(config)]) + await Promise.all([writeExports(config, chunks), copy(config)]) await Promise.all([publint(config), attw(config)]) await hooks.callHook('build:done', context) @@ -219,6 +228,7 @@ export async function buildSingle( async function getBuildOptions( config: ResolvedOptions, format: NormalizedFormat, + resolveChunks: (chunks: Array) => void, isMultiFormat?: boolean, cjsDts?: boolean, ): Promise { @@ -245,6 +255,7 @@ async function getBuildOptions( loader, name, unbundle, + watch, } = config const plugins: RolldownPluginOption = [] @@ -291,6 +302,12 @@ async function getBuildOptions( plugins.push(userPlugins) } + if (watch) { + plugins.push(WatchPlugin()) + } + + plugins.push(OutputPlugin(resolveChunks)) + const inputOptions = await mergeUserOptions( { input: entry, diff --git a/src/utils/general.ts b/src/utils/general.ts index c286f8647..d9b6373ce 100644 --- a/src/utils/general.ts +++ b/src/utils/general.ts @@ -42,3 +42,14 @@ export function slash(string: string): string { } export const noop = (v: T): T => v + +export function withResolver(): [ + promise: Promise, + resolve: (value: T) => void, +] { + let resolve: (value: T) => void + const promise = new Promise((_resolve) => { + resolve = _resolve + }) + return [promise, resolve!] +} From c6d3ddc84882f5d0c21519326cc7c2eaebcce39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 10:41:59 +0800 Subject: [PATCH 05/10] feat!: drop chokidar --- package.json | 1 - pnpm-lock.yaml | 6 -- pnpm-workspace.yaml | 1 - src/features/watch.ts | 59 +++-------- src/index.ts | 226 ++++++++++++++++++++---------------------- 5 files changed, 124 insertions(+), 169 deletions(-) diff --git a/package.json b/package.json index abcf00e76..25dfcd23d 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "dependencies": { "ansis": "catalog:prod", "cac": "catalog:prod", - "chokidar": "catalog:prod", "debug": "catalog:prod", "diff": "catalog:prod", "empathic": "catalog:prod", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4ce4c252..2bae851b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,9 +98,6 @@ catalogs: cac: specifier: ^6.7.14 version: 6.7.14 - chokidar: - specifier: ^4.0.3 - version: 4.0.3 debug: specifier: ^4.4.1 version: 4.4.1 @@ -143,9 +140,6 @@ importers: cac: specifier: catalog:prod version: 6.7.14 - chokidar: - specifier: catalog:prod - version: 4.0.3 debug: specifier: catalog:prod version: 4.4.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 036ceaa49..21bb84eb2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -35,7 +35,6 @@ catalogs: prod: ansis: ^4.1.0 cac: ^6.7.14 - chokidar: ^4.0.3 debug: ^4.4.1 diff: ^8.0.2 empathic: ^2.0.0 diff --git a/src/features/watch.ts b/src/features/watch.ts index 78f70cb80..23da59ef2 100644 --- a/src/features/watch.ts +++ b/src/features/watch.ts @@ -1,56 +1,27 @@ -import { debounce, resolveComma, toArray } from '../utils/general' import { logger } from '../utils/logger' -import type { ResolvedOptions } from '../options' import type { Plugin } from 'rolldown' const endsWithConfig = /[\\/](?:package\.json|tsdown\.config.*)$/ -export async function watchBuild( - options: ResolvedOptions, +export function WatchPlugin( configFiles: string[], - rebuild: () => void, restart: () => void, -): Promise { - const files = resolveComma( - toArray(typeof options.watch === 'string' ? options.watch : []), - ) - files.push(...configFiles) - logger.info(`Watching for changes...`) - - const { watch } = await import('chokidar') - const debouncedRebuild = debounce(rebuild, 100) - - const watcher = watch(files, { - ignoreInitial: true, - ignorePermissionErrors: true, - ignored: [ - /[\\/]\.git[\\/]/, - /[\\/]node_modules[\\/]/, - ...toArray(options.ignoreWatch), - ], - }) - - watcher.on('all', (type: string, file: string) => { - if (configFiles.includes(file) || endsWithConfig.test(file)) { - logger.info(`Reload config: ${file}`) - restart() - return - } - - logger.info(`Change detected: ${type} ${file}`) - debouncedRebuild() - }) - - return { - [Symbol.asyncDispose]: () => watcher.close(), - } -} - -export function WatchPlugin(): Plugin { +): Plugin { return { name: 'tsdown:watch', - watchChange(id) { - logger.info(`Change detected: ${id}`) + buildStart() { + for (const file of configFiles) { + this.addWatchFile(file) + } + }, + watchChange(id, event) { + if (configFiles.includes(id) || endsWithConfig.test(id)) { + logger.info(`Reload config: ${id}`) + restart() + } else { + console.info('') + logger.info(`File ${event.event}d: ${id}`) + } }, } } diff --git a/src/index.ts b/src/index.ts index 7160899d6..409a88bf6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,7 @@ import { ReportPlugin } from './features/report' import { getShimsInject } from './features/shims' import { shortcuts } from './features/shortcuts' import { RuntimeHelperCheckPlugin } from './features/target' -import { watchBuild, WatchPlugin } from './features/watch' +import { WatchPlugin } from './features/watch' import { mergeUserOptions, resolveOptions, @@ -57,37 +57,26 @@ export async function build(userOptions: Options = {}): Promise { return (cleanPromise = cleanOutDir(configs)) } + let restarting = false + logger.info('Build start') - const rebuilds = await Promise.all( - configs.map((options) => buildSingle(options, clean)), + const task = Promise.all( + configs.map((options) => buildSingle(options, clean, configFiles, restart)), ) - const cleanCbs: (() => Promise)[] = [] - - for (const [i, config] of configs.entries()) { - const rebuild = rebuilds[i] - if (!rebuild) continue - - const tsdownWatcher = await watchBuild( - config, - configFiles, - rebuild, - restart, - ) - cleanCbs.push(async () => { - await tsdownWatcher[Symbol.asyncDispose]() - rebuild[Symbol.asyncDispose]?.() - }) - } - - if (cleanCbs.length) { + if ((await task).some((d) => !!d)) { shortcuts(restart) } async function restart() { - for (const clean of cleanCbs) { - await clean() + if (restarting) return + + restarting = true + for (const dispose of await task) { + await dispose?.[Symbol.asyncDispose]?.() } + logger.info('Restarting build...\n') build(userOptions) + restarting = false } } @@ -109,118 +98,119 @@ export type TsdownChunks = Partial< export async function buildSingle( config: ResolvedOptions, clean: () => Promise, -): Promise<(AsyncDisposable & (() => Promise)) | undefined> { + configFiles: string[], + restart: () => void, +): Promise { const { format: formats, dts, watch, onSuccess } = config let ab: AbortController | undefined const { hooks, context } = await createHooks(config) + const watchers: RolldownWatcher[] = [] + + const startTime = performance.now() + + await hooks.callHook('build:prepare', context) + ab?.abort() + + await clean() + + let hasErrors = false + const isMultiFormat = formats.length > 1 + const chunks: TsdownChunks = {} + + async function buildByFormat(format: NormalizedFormat) { + try { + const [chunksPromise, resolve] = + withResolver>() + const buildOptions = await getBuildOptions( + config, + format, + configFiles, + restart, + resolve, + isMultiFormat, + false, + ) + await hooks.callHook('build:before', { + ...context, + buildOptions, + }) + if (watch) { + watchers.push(rolldownWatch(buildOptions)) + } else { + await rolldownBuild(buildOptions) + } + chunks[format] = await chunksPromise - const rolldownWatchers: RolldownWatcher[] = [] - async function dispose() { - ab?.abort() - for (const watcher of rolldownWatchers) { - await watcher.close() - } - } - - await rebuild(true) - if (watch) { - const rebuildFn = () => rebuild() - rebuildFn[Symbol.asyncDispose] = dispose - return rebuildFn - } - - async function rebuild(first?: boolean): Promise { - const startTime = performance.now() - - await hooks.callHook('build:prepare', context) - ab?.abort() - - await clean() - - let hasErrors = false - const isMultiFormat = formats.length > 1 - const chunks: TsdownChunks = {} - - async function buildByFormat(format: NormalizedFormat) { - try { + // build cjs dts + if (format === 'cjs' && dts) { const [chunksPromise, resolve] = withResolver>() + const buildOptions = await getBuildOptions( config, format, + configFiles, + restart, resolve, isMultiFormat, - false, + true, ) - await hooks.callHook('build:before', { - ...context, - buildOptions, - }) if (watch) { - rolldownWatchers.push(rolldownWatch(buildOptions)) + watchers.push(rolldownWatch(buildOptions)) } else { await rolldownBuild(buildOptions) } - chunks[format] = await chunksPromise - - // build cjs dts - if (format === 'cjs' && dts) { - const [chunksPromise, resolve] = - withResolver>() - - const buildOptions = await getBuildOptions( - config, - format, - resolve, - isMultiFormat, - true, - ) - if (watch) { - rolldownWatchers.push(rolldownWatch(buildOptions)) - } else { - await rolldownBuild(buildOptions) - } - chunks[format].push(...(await chunksPromise)) - } - } catch (error) { - if (watch) { - logger.error(error) - hasErrors = true - return - } - throw error + chunks[format].push(...(await chunksPromise)) + } + } catch (error) { + if (watch) { + logger.error(error) + hasErrors = true + return } + throw error } - await Promise.all(formats.map(buildByFormat)) - - if (hasErrors) return - - await Promise.all([writeExports(config, chunks), copy(config)]) - await Promise.all([publint(config), attw(config)]) - - await hooks.callHook('build:done', context) - - logger.success( - prettyName(config.name), - `${first ? 'Build' : 'Rebuild'} complete in ${green(`${Math.round(performance.now() - startTime)}ms`)}`, - ) - ab = new AbortController() - if (typeof onSuccess === 'string') { - const p = exec(onSuccess, [], { - nodeOptions: { - shell: true, - stdio: 'inherit', - signal: ab.signal, - }, - }) - p.then(({ exitCode }) => { - if (exitCode) { - process.exitCode = exitCode + } + await Promise.all(formats.map(buildByFormat)) + + if (hasErrors) return + + await Promise.all([writeExports(config, chunks), copy(config)]) + await Promise.all([publint(config), attw(config)]) + + await hooks.callHook('build:done', context) + + logger.success( + prettyName(config.name), + `Build complete in ${green(`${Math.round(performance.now() - startTime)}ms`)}`, + ) + ab = new AbortController() + if (typeof onSuccess === 'string') { + const p = exec(onSuccess, [], { + nodeOptions: { + shell: true, + stdio: 'inherit', + signal: ab.signal, + }, + }) + p.then(({ exitCode }) => { + if (exitCode) { + process.exitCode = exitCode + } + }) + } else { + await onSuccess?.(config, ab.signal) + } + + if (watch) { + return { + async [Symbol.asyncDispose]() { + ab?.abort() + for (const watcher of watchers) { + await watcher.close() } - }) - } else { - await onSuccess?.(config, ab.signal) + }, } } } @@ -228,6 +218,8 @@ export async function buildSingle( async function getBuildOptions( config: ResolvedOptions, format: NormalizedFormat, + configFiles: string[], + restart: () => void, resolveChunks: (chunks: Array) => void, isMultiFormat?: boolean, cjsDts?: boolean, @@ -303,7 +295,7 @@ async function getBuildOptions( } if (watch) { - plugins.push(WatchPlugin()) + plugins.push(WatchPlugin(configFiles, restart)) } plugins.push(OutputPlugin(resolveChunks)) From b2dc1f62c56bf8b9f90af1a0446d68f3648eb49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 10:54:01 +0800 Subject: [PATCH 06/10] refactor --- src/features/watch.ts | 21 ++++++++++++++++++--- src/index.ts | 2 +- src/options/index.ts | 4 ++-- src/options/types.ts | 5 +++-- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/features/watch.ts b/src/features/watch.ts index 23da59ef2..80fc42287 100644 --- a/src/features/watch.ts +++ b/src/features/watch.ts @@ -1,26 +1,41 @@ -import { logger } from '../utils/logger' +import { resolveComma, toArray } from '../utils/general' +import { logger, prettyName } from '../utils/logger' +import type { ResolvedOptions } from '../options' import type { Plugin } from 'rolldown' const endsWithConfig = /[\\/](?:package\.json|tsdown\.config.*)$/ export function WatchPlugin( + options: ResolvedOptions, configFiles: string[], restart: () => void, ): Plugin { return { name: 'tsdown:watch', + options: options.ignoreWatch.length + ? (inputOptions) => { + inputOptions.watch ||= {} + inputOptions.watch.exclude = toArray(inputOptions.watch.exclude) + inputOptions.watch.exclude.push(...toArray(options.ignoreWatch)) + } + : undefined, buildStart() { for (const file of configFiles) { this.addWatchFile(file) } + if (typeof options.watch !== 'boolean') { + for (const file of resolveComma(toArray(options.watch))) { + this.addWatchFile(file) + } + } }, watchChange(id, event) { if (configFiles.includes(id) || endsWithConfig.test(id)) { - logger.info(`Reload config: ${id}`) + logger.info(prettyName(options.name), `Reload config: ${id}`) restart() } else { console.info('') - logger.info(`File ${event.event}d: ${id}`) + logger.info(prettyName(options.name), `File ${event.event}d: ${id}`) } }, } diff --git a/src/index.ts b/src/index.ts index 409a88bf6..4962b396e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -295,7 +295,7 @@ async function getBuildOptions( } if (watch) { - plugins.push(WatchPlugin(configFiles, restart)) + plugins.push(WatchPlugin(config, configFiles, restart)) } plugins.push(OutputPlugin(resolveChunks)) diff --git a/src/options/index.ts b/src/options/index.ts index 6e4afb931..9086f538b 100644 --- a/src/options/index.ts +++ b/src/options/index.ts @@ -7,7 +7,7 @@ import { resolveClean } from '../features/clean' import { resolveEntry } from '../features/entry' import { resolveTarget } from '../features/target' import { resolveTsconfig } from '../features/tsconfig' -import { resolveRegex } from '../utils/general' +import { resolveRegex, toArray } from '../utils/general' import { logger } from '../utils/logger' import { normalizeFormat, readPackageJson } from '../utils/package' import type { Awaitable } from '../utils/types' @@ -302,7 +302,7 @@ async function resolveConfig( report: report === true ? {} : report, unused, watch, - ignoreWatch, + ignoreWatch: toArray(ignoreWatch), shims, skipNodeModulesBundle, publint, diff --git a/src/options/types.ts b/src/options/types.ts index 8f4156e4f..adda392bd 100644 --- a/src/options/types.ts +++ b/src/options/types.ts @@ -205,8 +205,8 @@ export interface Options { */ config?: boolean | string /** @default false */ - watch?: boolean | string | string[] - ignoreWatch?: string | string[] + watch?: boolean | Arrayable + ignoreWatch?: Arrayable /** * You can specify command to be executed after a successful build, specially useful for Watch mode @@ -400,6 +400,7 @@ export type ResolvedOptions = Omit< pkg?: PackageJson exports: false | ExportsOptions nodeProtocol: 'strip' | boolean + ignoreWatch: (string | RegExp)[] } >, 'config' | 'fromVite' From 80b0a68df8270ced32ad8e4cd87f24b1476d4e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 11:11:38 +0800 Subject: [PATCH 07/10] fix closeBundle --- src/features/exports.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/features/exports.ts b/src/features/exports.ts index 3940edc38..e3134f7aa 100644 --- a/src/features/exports.ts +++ b/src/features/exports.ts @@ -243,13 +243,17 @@ function exportMeta(exports: Record, all?: boolean) { export function OutputPlugin( resolveChunks: (chunks: Array) => void, ): Plugin { + let chunks: Array return { name: 'tsdown:output', generateBundle: { order: 'post', handler(_outputOptions, bundle) { - resolveChunks(Object.values(bundle)) + chunks = Object.values(bundle) }, }, + closeBundle() { + resolveChunks(chunks) + }, } } From 07b145a5b62f661cc01cffefe63ecb6f58458f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 22:06:22 +0800 Subject: [PATCH 08/10] refactor --- src/features/exports.ts | 20 +--- src/features/watch.ts | 14 ++- src/index.ts | 198 ++++++++++++++++++++++------------------ src/utils/general.ts | 11 --- 4 files changed, 120 insertions(+), 123 deletions(-) diff --git a/src/features/exports.ts b/src/features/exports.ts index e3134f7aa..db86afff7 100644 --- a/src/features/exports.ts +++ b/src/features/exports.ts @@ -6,7 +6,7 @@ import type { TsdownChunks } from '..' import type { NormalizedFormat, ResolvedOptions } from '../options' import type { Awaitable } from '../utils/types' import type { PackageJson } from 'pkg-types' -import type { OutputAsset, OutputChunk, Plugin } from 'rolldown' +import type { OutputAsset, OutputChunk } from 'rolldown' export interface ExportsOptions { /** @@ -239,21 +239,3 @@ function exportMeta(exports: Record, all?: boolean) { exports['./package.json'] = './package.json' } } - -export function OutputPlugin( - resolveChunks: (chunks: Array) => void, -): Plugin { - let chunks: Array - return { - name: 'tsdown:output', - generateBundle: { - order: 'post', - handler(_outputOptions, bundle) { - chunks = Object.values(bundle) - }, - }, - closeBundle() { - resolveChunks(chunks) - }, - } -} diff --git a/src/features/watch.ts b/src/features/watch.ts index 80fc42287..58b3c3c6c 100644 --- a/src/features/watch.ts +++ b/src/features/watch.ts @@ -1,11 +1,12 @@ import { resolveComma, toArray } from '../utils/general' import { logger, prettyName } from '../utils/logger' import type { ResolvedOptions } from '../options' -import type { Plugin } from 'rolldown' +import type { OutputAsset, OutputChunk, Plugin } from 'rolldown' const endsWithConfig = /[\\/](?:package\.json|tsdown\.config.*)$/ export function WatchPlugin( + chunks: Array, options: ResolvedOptions, configFiles: string[], restart: () => void, @@ -29,14 +30,17 @@ export function WatchPlugin( } } }, - watchChange(id, event) { + watchChange(id) { if (configFiles.includes(id) || endsWithConfig.test(id)) { logger.info(prettyName(options.name), `Reload config: ${id}`) restart() - } else { - console.info('') - logger.info(prettyName(options.name), `File ${event.event}d: ${id}`) } }, + generateBundle: { + order: 'post', + handler(_outputOptions, bundle) { + chunks.push(...Object.values(bundle)) + }, + }, } } diff --git a/src/index.ts b/src/index.ts index 4962b396e..1e6d6ffb7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' -import { green } from 'ansis' +import { bold } from 'ansis' import { build as rolldownBuild, watch as rolldownWatch, @@ -16,7 +16,7 @@ import { exec } from 'tinyexec' import { attw } from './features/attw' import { cleanOutDir } from './features/clean' import { copy } from './features/copy' -import { OutputPlugin, writeExports } from './features/exports' +import { writeExports } from './features/exports' import { ExternalPlugin } from './features/external' import { createHooks } from './features/hooks' import { LightningCSSPlugin } from './features/lightningcss' @@ -37,8 +37,7 @@ import { } from './options' import { ShebangPlugin } from './plugins' import { lowestCommonAncestor } from './utils/fs' -import { withResolver } from './utils/general' -import { logger, prettyName } from './utils/logger' +import { logger } from './utils/logger' import type { Options as DtsOptions } from 'rolldown-plugin-dts' /** @@ -105,113 +104,138 @@ export async function buildSingle( let ab: AbortController | undefined const { hooks, context } = await createHooks(config) - const watchers: RolldownWatcher[] = [] - - const startTime = performance.now() + let watcher: RolldownWatcher await hooks.callHook('build:prepare', context) ab?.abort() await clean() - let hasErrors = false const isMultiFormat = formats.length > 1 const chunks: TsdownChunks = {} async function buildByFormat(format: NormalizedFormat) { - try { - const [chunksPromise, resolve] = - withResolver>() - const buildOptions = await getBuildOptions( - config, - format, - configFiles, - restart, - resolve, - isMultiFormat, - false, - ) - await hooks.callHook('build:before', { - ...context, - buildOptions, - }) - if (watch) { - watchers.push(rolldownWatch(buildOptions)) - } else { - await rolldownBuild(buildOptions) - } - chunks[format] = await chunksPromise - - // build cjs dts - if (format === 'cjs' && dts) { - const [chunksPromise, resolve] = - withResolver>() - - const buildOptions = await getBuildOptions( + const buildOptions = await getBuildOptions( + config, + format, + configFiles, + restart, + (chunks[format] = []), + isMultiFormat, + false, + ) + await hooks.callHook('build:before', { + ...context, + buildOptions, + }) + + const buildOptionsList = [buildOptions] + if (format === 'cjs' && dts) { + buildOptionsList.push( + await getBuildOptions( config, format, configFiles, restart, - resolve, + chunks[format], isMultiFormat, true, - ) - if (watch) { - watchers.push(rolldownWatch(buildOptions)) - } else { - await rolldownBuild(buildOptions) + ), + ) + } + + if (watch) { + watcher = rolldownWatch(buildOptionsList) + const changedFile: string[] = [] + let hasError = false + watcher.on('change', (id, event) => { + if (event.event === 'update') { + changedFile.push(id) } - chunks[format].push(...(await chunksPromise)) - } - } catch (error) { - if (watch) { - logger.error(error) - hasErrors = true - return - } - throw error + }) + watcher.on('event', async (event) => { + switch (event.code) { + case 'START': { + chunks[format]!.length = 0 + hasError = false + break + } + + case 'END': { + if (!hasError) { + await postBuild() + } + break + } + + case 'BUNDLE_START': { + if (changedFile.length > 0) { + console.info('') + logger.info( + `Found ${bold(changedFile.join(', '))} changed, rebuilding...`, + ) + } + changedFile.length = 0 + break + } + + case 'BUNDLE_END': { + await event.result.close() + logger.success(`Rebuilt in ${event.duration}ms.`) + break + } + + case 'ERROR': { + await event.result.close() + logger.error(event.error) + hasError = true + break + } + } + }) + } else { + const output = (await rolldownBuild(buildOptionsList)).flatMap( + ({ output }) => output, + ) + chunks[format] = output } } await Promise.all(formats.map(buildByFormat)) - if (hasErrors) return - - await Promise.all([writeExports(config, chunks), copy(config)]) - await Promise.all([publint(config), attw(config)]) - - await hooks.callHook('build:done', context) - - logger.success( - prettyName(config.name), - `Build complete in ${green(`${Math.round(performance.now() - startTime)}ms`)}`, - ) - ab = new AbortController() - if (typeof onSuccess === 'string') { - const p = exec(onSuccess, [], { - nodeOptions: { - shell: true, - stdio: 'inherit', - signal: ab.signal, - }, - }) - p.then(({ exitCode }) => { - if (exitCode) { - process.exitCode = exitCode - } - }) - } else { - await onSuccess?.(config, ab.signal) - } - if (watch) { return { async [Symbol.asyncDispose]() { ab?.abort() - for (const watcher of watchers) { - await watcher.close() - } + await watcher.close() }, } + } else { + await postBuild() + } + + async function postBuild() { + await Promise.all([writeExports(config, chunks), copy(config)]) + await Promise.all([publint(config), attw(config)]) + + await hooks.callHook('build:done', context) + + ab = new AbortController() + if (typeof onSuccess === 'string') { + const p = exec(onSuccess, [], { + nodeOptions: { + shell: true, + stdio: 'inherit', + signal: ab.signal, + }, + }) + p.then(({ exitCode }) => { + if (exitCode) { + process.exitCode = exitCode + } + }) + } else { + await onSuccess?.(config, ab.signal) + } } } @@ -220,7 +244,7 @@ async function getBuildOptions( format: NormalizedFormat, configFiles: string[], restart: () => void, - resolveChunks: (chunks: Array) => void, + chunks: Array, isMultiFormat?: boolean, cjsDts?: boolean, ): Promise { @@ -295,11 +319,9 @@ async function getBuildOptions( } if (watch) { - plugins.push(WatchPlugin(config, configFiles, restart)) + plugins.push(WatchPlugin(chunks!, config, configFiles, restart)) } - plugins.push(OutputPlugin(resolveChunks)) - const inputOptions = await mergeUserOptions( { input: entry, diff --git a/src/utils/general.ts b/src/utils/general.ts index d9b6373ce..c286f8647 100644 --- a/src/utils/general.ts +++ b/src/utils/general.ts @@ -42,14 +42,3 @@ export function slash(string: string): string { } export const noop = (v: T): T => v - -export function withResolver(): [ - promise: Promise, - resolve: (value: T) => void, -] { - let resolve: (value: T) => void - const promise = new Promise((_resolve) => { - resolve = _resolve - }) - return [promise, resolve!] -} From f9061506cbf61eacf5c7fa863fdb446d0a0fbc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 19 Jun 2025 22:07:25 +0800 Subject: [PATCH 09/10] refactor --- src/features/watch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/watch.ts b/src/features/watch.ts index 58b3c3c6c..fc229abf9 100644 --- a/src/features/watch.ts +++ b/src/features/watch.ts @@ -38,7 +38,7 @@ export function WatchPlugin( }, generateBundle: { order: 'post', - handler(_outputOptions, bundle) { + handler(outputOptions, bundle) { chunks.push(...Object.values(bundle)) }, }, From a91c8b3d85874d06e89bc1876cf40f0a6eb4ac47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Fri, 20 Jun 2025 23:26:19 +0800 Subject: [PATCH 10/10] wip --- src/features/watch.ts | 2 +- src/index.ts | 121 ++++++++++++++++++++++++------------------ 2 files changed, 70 insertions(+), 53 deletions(-) diff --git a/src/features/watch.ts b/src/features/watch.ts index fc229abf9..148e4c579 100644 --- a/src/features/watch.ts +++ b/src/features/watch.ts @@ -3,7 +3,7 @@ import { logger, prettyName } from '../utils/logger' import type { ResolvedOptions } from '../options' import type { OutputAsset, OutputChunk, Plugin } from 'rolldown' -const endsWithConfig = /[\\/](?:package\.json|tsdown\.config.*)$/ +const endsWithConfig = /[\\/]tsdown\.config.*$/ export function WatchPlugin( chunks: Array, diff --git a/src/index.ts b/src/index.ts index 1e6d6ffb7..004bc3924 100644 --- a/src/index.ts +++ b/src/index.ts @@ -104,7 +104,6 @@ export async function buildSingle( let ab: AbortController | undefined const { hooks, context } = await createHooks(config) - let watcher: RolldownWatcher await hooks.callHook('build:prepare', context) ab?.abort() @@ -113,8 +112,9 @@ export async function buildSingle( const isMultiFormat = formats.length > 1 const chunks: TsdownChunks = {} + const watchers: RolldownWatcher[] = [] - async function buildByFormat(format: NormalizedFormat) { + async function buildOptionsByFormat(format: NormalizedFormat) { const buildOptions = await getBuildOptions( config, format, @@ -129,9 +129,13 @@ export async function buildSingle( buildOptions, }) - const buildOptionsList = [buildOptions] + const buildOptionsList: [ + format: NormalizedFormat, + buildOptions: BuildOptions, + ][] = [[format, buildOptions]] if (format === 'cjs' && dts) { - buildOptionsList.push( + buildOptionsList.push([ + format, await getBuildOptions( config, format, @@ -141,72 +145,85 @@ export async function buildSingle( isMultiFormat, true, ), - ) + ]) } - if (watch) { - watcher = rolldownWatch(buildOptionsList) - const changedFile: string[] = [] - let hasError = false - watcher.on('change', (id, event) => { - if (event.event === 'update') { - changedFile.push(id) - } - }) - watcher.on('event', async (event) => { - switch (event.code) { - case 'START': { + return buildOptionsList + } + const buildOptionsList = ( + await Promise.all(formats.map(buildOptionsByFormat)) + ).flat() + + if (watch) { + const watcher = rolldownWatch(buildOptionsList.map((item) => item[1])) + const changedFile: string[] = [] + let hasError = false + watcher.on('change', (id, event) => { + if (event.event === 'update') { + changedFile.push(id) + } + }) + watcher.on('event', async (event) => { + switch (event.code) { + case 'START': { + for (const format of formats) { chunks[format]!.length = 0 - hasError = false - break } + hasError = false + break + } - case 'END': { - if (!hasError) { - await postBuild() - } - break + case 'END': { + if (!hasError) { + await postBuild() } + break + } - case 'BUNDLE_START': { - if (changedFile.length > 0) { - console.info('') - logger.info( - `Found ${bold(changedFile.join(', '))} changed, rebuilding...`, - ) - } - changedFile.length = 0 - break + case 'BUNDLE_START': { + if (changedFile.length > 0) { + console.info('') + logger.info( + `Found ${bold(changedFile.join(', '))} changed, rebuilding...`, + ) } + changedFile.length = 0 + break + } - case 'BUNDLE_END': { - await event.result.close() - logger.success(`Rebuilt in ${event.duration}ms.`) - break - } + case 'BUNDLE_END': { + await event.result.close() + logger.success(`Rebuilt in ${event.duration}ms.`) + break + } - case 'ERROR': { - await event.result.close() - logger.error(event.error) - hasError = true - break - } + case 'ERROR': { + await event.result.close() + logger.error(event.error) + hasError = true + break } - }) - } else { - const output = (await rolldownBuild(buildOptionsList)).flatMap( - ({ output }) => output, - ) - chunks[format] = output + } + }) + watchers.push(watcher) + } else { + const outputs = ( + await rolldownBuild(buildOptionsList.map((item) => item[1])) + ).map(({ output }) => output) + for (const [i, output] of outputs.entries()) { + const format = buildOptionsList[i][0] + chunks[format] ||= [] + chunks[format].push(...output) } } - await Promise.all(formats.map(buildByFormat)) if (watch) { return { async [Symbol.asyncDispose]() { ab?.abort() - await watcher.close() + for (const watcher of watchers) { + await watcher.close() + } }, } } else {