diff --git a/src/build.ts b/src/build.ts index ccbb2a23..047ea2fa 100644 --- a/src/build.ts +++ b/src/build.ts @@ -9,6 +9,7 @@ import { defu } from "defu"; import { createHooks } from "hookable"; import prettyBytes from "pretty-bytes"; import { globby } from "globby"; +import type { RollupOptions } from "rollup"; import { dumpObject, rmdir, @@ -18,7 +19,7 @@ import { } from "./utils"; import type { BuildContext, BuildConfig, BuildOptions } from "./types"; import { validatePackage, validateDependencies } from "./validate"; -import { rollupBuild } from "./builder/rollup"; +import { getRollupOptions, rollupBuild } from "./builder/rollup"; import { typesBuild } from "./builder/untyped"; import { mkdistBuild } from "./builder/mkdist"; import { copyBuild } from "./builder/copy"; @@ -42,18 +43,34 @@ export async function build( // Invoke build for every build config defined in build.config.ts const cleanedDirs: string[] = []; + const rollupOptions: RollupOptions[] = []; + + const _watchMode = inputConfig.watch === true; + const _stubMode = !_watchMode && (stub || inputConfig.stub === true); + for (const buildConfig of buildConfigs) { - await _build(rootDir, stub, inputConfig, buildConfig, pkg, cleanedDirs); + await _build( + rootDir, + inputConfig, + buildConfig, + pkg, + cleanedDirs, + rollupOptions, + _stubMode, + _watchMode, + ); } } async function _build( rootDir: string, - stub: boolean, inputConfig: BuildConfig = {}, buildConfig: BuildConfig, pkg: PackageJson & Record<"unbuild" | "build", BuildConfig>, cleanedDirs: string[], + rollupOptions: RollupOptions[], + _stubMode: boolean, + _watchMode: boolean, ) { // Resolve preset const preset = resolvePreset( @@ -78,7 +95,7 @@ async function _build( clean: true, declaration: undefined, outDir: "dist", - stub, + stub: _stubMode, stubOptions: { /** * See https://github.com/unjs/jiti#options @@ -89,6 +106,13 @@ async function _build( alias: {}, }, }, + watch: _watchMode, + watchOptions: _watchMode + ? { + exclude: "node_modules/**", + include: "src/**", + } + : undefined, externals: [ ...Module.builtinModules, ...Module.builtinModules.map((m) => "node:" + m), @@ -102,6 +126,7 @@ async function _build( sourcemap: false, rollup: { emitCJS: false, + watch: false, cjsBridge: false, inlineDependencies: false, preserveDynamicImports: true, @@ -252,8 +277,8 @@ async function _build( // copy await copyBuild(ctx); - // Skip rest for stub - if (options.stub) { + // Skip rest for stub and watch mode + if (options.stub || options.watch) { await ctx.hooks.callHook("build:done", ctx); return; } diff --git a/src/builder/copy.ts b/src/builder/copy.ts index dd0ada96..1ef491a6 100644 --- a/src/builder/copy.ts +++ b/src/builder/copy.ts @@ -3,6 +3,7 @@ import { relative, resolve } from "pathe"; import { globby } from "globby"; import { symlink, rmdir, warn } from "../utils"; import type { CopyBuildEntry, BuildContext } from "../types"; +import consola from "consola"; const copy = fsp.cp || fsp.copyFile; @@ -51,4 +52,8 @@ export async function copyBuild(ctx: BuildContext) { } } await ctx.hooks.callHook("copy:done", ctx); + + if (entries.length > 0 && ctx.options.watch) { + consola.warn("`untyped` builder does not support watch mode yet."); + } } diff --git a/src/builder/mkdist.ts b/src/builder/mkdist.ts index eaf379b0..a8aaf7c5 100644 --- a/src/builder/mkdist.ts +++ b/src/builder/mkdist.ts @@ -2,6 +2,7 @@ import { relative } from "pathe"; import { mkdist, MkdistOptions } from "mkdist"; import { symlink, rmdir } from "../utils"; import type { MkdistBuildEntry, BuildContext } from "../types"; +import consola from "consola"; export async function mkdistBuild(ctx: BuildContext) { const entries = ctx.options.entries.filter( @@ -36,4 +37,8 @@ export async function mkdistBuild(ctx: BuildContext) { } } await ctx.hooks.callHook("mkdist:done", ctx); + + if (entries.length > 0 && ctx.options.watch) { + consola.warn("`mkdist` builder does not support watch mode yet."); + } } diff --git a/src/builder/rollup.ts b/src/builder/rollup.ts index 62a6b7d6..2735662b 100644 --- a/src/builder/rollup.ts +++ b/src/builder/rollup.ts @@ -13,8 +13,16 @@ import { nodeResolve } from "@rollup/plugin-node-resolve"; import alias from "@rollup/plugin-alias"; import dts from "rollup-plugin-dts"; import replace from "@rollup/plugin-replace"; -import { resolve, dirname, normalize, extname, isAbsolute } from "pathe"; +import { + resolve, + dirname, + normalize, + extname, + isAbsolute, + relative, +} from "pathe"; import { resolvePath, resolveModuleExportNames } from "mlly"; +import { watch as rollupWatch } from "rollup"; import { arrayIncludes, getpkg, tryResolve, warn } from "../utils"; import type { BuildContext } from "../types"; import { esbuild } from "./plugins/esbuild"; @@ -27,10 +35,14 @@ import { getShebang, removeShebangPlugin, } from "./plugins/shebang"; +import consola from "consola"; +import chalk from "chalk"; const DEFAULT_EXTENSIONS = [ ".ts", ".tsx", + ".mts", + ".cts", ".mjs", ".cjs", ".js", @@ -198,6 +210,16 @@ export async function rollupBuild(ctx: BuildContext) { } } + // Watch + if (ctx.options.watch) { + _watch(rollupOptions); + // TODO: Clone rollup options to continue types watching + if (ctx.options.declaration && ctx.options.watch) { + consola.warn("`rollup` DTS builder does not support watch mode yet."); + } + return; + } + // Types if (ctx.options.declaration) { rollupOptions.plugins = [ @@ -405,3 +427,37 @@ function resolveAliases(ctx: BuildContext) { return aliases; } + +export function _watch(rollupOptions: RollupOptions) { + const watcher = rollupWatch(rollupOptions); + + let inputs: string[]; + if (Array.isArray(rollupOptions.input)) { + inputs = rollupOptions.input; + } else if (typeof rollupOptions.input === "string") { + inputs = [rollupOptions.input]; + } else { + inputs = Object.keys(rollupOptions.input || {}); + } + consola.info( + `[unbuild] [rollup] Starting watchers for entries: ${inputs.map((input) => "./" + relative(process.cwd(), input)).join(", ")}`, + ); + + consola.warn( + "[unbuild] [rollup] Watch mode is experimental and may be unstable", + ); + + watcher.on("change", (id, { event }) => { + consola.info(`${chalk.cyan(relative(".", id))} was ${event}d`); + }); + + watcher.on("restart", () => { + consola.info(chalk.gray("[unbuild] [rollup] Rebuilding bundle")); + }); + + watcher.on("event", (event) => { + if (event.code === "END") { + consola.success(chalk.green("[unbuild] [rollup] Rebuild finished\n")); + } + }); +} diff --git a/src/builder/untyped.ts b/src/builder/untyped.ts index d9d577a8..9c200e37 100644 --- a/src/builder/untyped.ts +++ b/src/builder/untyped.ts @@ -6,6 +6,7 @@ import untypedPlugin from "untyped/babel-plugin"; import jiti from "jiti"; import { pascalCase } from "scule"; import type { BuildContext, UntypedBuildEntry, UntypedOutputs } from "../types"; +import consola from "consola"; export async function typesBuild(ctx: BuildContext) { const entries = ctx.options.entries.filter( @@ -69,4 +70,8 @@ export async function typesBuild(ctx: BuildContext) { } } await ctx.hooks.callHook("untyped:done", ctx); + + if (entries.length > 0 && ctx.options.watch) { + consola.warn("`untyped` builder does not support watch mode yet."); + } } diff --git a/src/cli.ts b/src/cli.ts index f897b4c4..d2b26416 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,9 +17,13 @@ const main = defineCommand({ description: "The directory to build", required: false, }, + watch: { + type: "boolean", + description: "Watch the src dir and rebuild on change (experimental)", + }, stub: { type: "boolean", - description: "Stub build", + description: "Stub the package for JIT compilation", }, minify: { type: "boolean", @@ -34,6 +38,8 @@ const main = defineCommand({ const rootDir = resolve(process.cwd(), args.dir || "."); await build(rootDir, args.stub, { sourcemap: args.sourcemap, + stub: args.stub, + watch: args.watch, rollup: { esbuild: { minify: args.minify, diff --git a/src/types.ts b/src/types.ts index f13cd965..ae593983 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,11 @@ import type { PackageJson } from "pkg-types"; import type { Hookable } from "hookable"; -import type { RollupOptions, RollupBuild, OutputOptions } from "rollup"; +import type { + RollupOptions, + RollupBuild, + OutputOptions, + WatcherOptions, +} from "rollup"; import type { MkdistOptions } from "mkdist"; import type { Schema } from "untyped"; import type { RollupReplaceOptions } from "@rollup/plugin-replace"; @@ -55,6 +60,13 @@ export interface RollupBuildOptions { */ emitCJS?: boolean; + /** + * Enable experimental active watcher + * + * @experimental + */ + watch?: boolean; + /** * If enabled, unbuild generates CommonJS polyfills for ESM builds. */ @@ -168,11 +180,23 @@ export interface BuildOptions { outDir: string; /** - * Whether to generate declaration files. - * [stubbing](https://antfu.me/posts/publish-esm-and-cjs#stubbing) + * Whether to build with JIT stubs. + * Read more: [stubbing](https://antfu.me/posts/publish-esm-and-cjs#stubbing) */ stub: boolean; + /** + * Whether to build and actively watch the file changes. + * + * @experimental This feature is experimental and incomplete. + */ + watch: boolean; + + /** + * Watch mode options. + */ + watchOptions: WatcherOptions; + /** * Stub options, where [jiti](https://github.com/unjs/jiti) * is an object of type `Omit`.