robuild 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,9 +30,12 @@ npx robuild ./src/index.ts
30
30
 
31
31
  # transform
32
32
  npx robuild ./src/runtime/:./dist/runtime
33
+
34
+ # watch mode - rebuild on file changes
35
+ npx robuild ./src/index.ts --watch
33
36
  ```
34
37
 
35
- You can use `--dir` to set the working directory.
38
+ You can use `--dir` to set the working directory and `--watch` to enable watch mode.
36
39
 
37
40
  If paths end with `/`, robuild uses transpile mode using [oxc-transform](https://www.npmjs.com/package/oxc-transform) instead of bundle mode with [rolldown](https://rolldown.rs/).
38
41
 
@@ -85,6 +88,52 @@ export default defineConfig({
85
88
  })
86
89
  ```
87
90
 
91
+ ## Watch Mode
92
+
93
+ For development, robuild provides a watch mode that automatically rebuilds your project when files change.
94
+
95
+ ### CLI Usage
96
+
97
+ ```sh
98
+ # Enable watch mode for any build
99
+ npx robuild ./src/index.ts --watch
100
+
101
+ # Watch mode with transform
102
+ npx robuild ./src/runtime/:./dist/runtime --watch
103
+
104
+ # Watch mode with custom working directory
105
+ npx robuild ./src/index.ts --watch --dir ./my-project
106
+ ```
107
+
108
+ ### Configuration
109
+
110
+ You can configure watch behavior in your `build.config.ts`:
111
+
112
+ ```js
113
+ import { defineConfig } from 'robuild'
114
+
115
+ export default defineConfig({
116
+ entries: ['./src/index.ts'],
117
+ watch: {
118
+ enabled: true, // Enable watch mode by default
119
+ include: ['src/**/*'], // Files to watch
120
+ exclude: ['**/*.test.ts'], // Files to ignore
121
+ delay: 100, // Rebuild delay in ms
122
+ ignoreInitial: false, // Skip initial build
123
+ watchNewFiles: true, // Watch for new files
124
+ },
125
+ })
126
+ ```
127
+
128
+ ### Features
129
+
130
+ - **Real-time rebuilding**: Automatically rebuilds when source files change
131
+ - **Smart file detection**: Automatically determines what files to watch based on your entries
132
+ - **Debounced rebuilds**: Configurable delay to prevent excessive rebuilds
133
+ - **Error recovery**: Continues watching even after build errors
134
+ - **Clear feedback**: Shows file changes and rebuild status
135
+ - **Graceful shutdown**: Clean exit with Ctrl+C
136
+
88
137
  ## Stub Mode
89
138
 
90
139
  When working on a package locally, it can be tedious to rebuild or run the watch command every time.
@@ -16,6 +16,7 @@ import { minify } from "oxc-minify";
16
16
  import MagicString from "magic-string";
17
17
  import { transform } from "oxc-transform";
18
18
  import { glob } from "tinyglobby";
19
+ import { watch } from "chokidar";
19
20
 
20
21
  //#region src/utils.ts
21
22
  function fmtPath(path) {
@@ -325,20 +326,194 @@ async function transformModule(entryPath, entry) {
325
326
  return transformed;
326
327
  }
327
328
 
329
+ //#endregion
330
+ //#region src/watch.ts
331
+ /**
332
+ * Start watching files and rebuild on changes
333
+ */
334
+ async function startWatch(config, ctx, buildFn) {
335
+ const watchOptions = config.watch || {};
336
+ if (!watchOptions.enabled) throw new Error("Watch mode is not enabled");
337
+ const watchCtx = {
338
+ config,
339
+ ctx,
340
+ buildFn,
341
+ isBuilding: false,
342
+ pendingRebuild: false
343
+ };
344
+ const watchPatterns = await getWatchPatterns(config, ctx, watchOptions);
345
+ const ignorePatterns = getIgnorePatterns(watchOptions);
346
+ consola.info(`👀 Starting watch mode...`);
347
+ consola.info(`📁 Watching: ${colors.dim(watchPatterns.join(", "))}`);
348
+ if (ignorePatterns.length > 0) consola.info(`🚫 Ignoring: ${colors.dim(ignorePatterns.join(", "))}`);
349
+ const delay = watchOptions.delay ?? 100;
350
+ if (delay > 0) consola.info(`⏱️ Rebuild delay: ${colors.dim(`${delay}ms`)}`);
351
+ const watcher = watch(watchPatterns, {
352
+ ignored: ignorePatterns,
353
+ ignoreInitial: watchOptions.ignoreInitial ?? false,
354
+ persistent: true,
355
+ followSymlinks: false,
356
+ cwd: ctx.pkgDir
357
+ });
358
+ watcher.on("change", (path) => handleFileChange(watchCtx, path, "changed"));
359
+ watcher.on("add", (path) => {
360
+ if (watchOptions.watchNewFiles !== false) handleFileChange(watchCtx, path, "added");
361
+ });
362
+ watcher.on("unlink", (path) => handleFileChange(watchCtx, path, "removed"));
363
+ watcher.on("error", (error) => {
364
+ consola.error("❌ Watch error:", error);
365
+ });
366
+ if (process.env.DEBUG) {
367
+ watcher.on("addDir", (path) => consola.debug(`📁 Directory added: ${path}`));
368
+ watcher.on("unlinkDir", (path) => consola.debug(`📁 Directory removed: ${path}`));
369
+ }
370
+ await new Promise((resolve$1) => {
371
+ watcher.on("ready", () => {
372
+ const watchedPaths = watcher.getWatched();
373
+ const totalFiles = Object.values(watchedPaths).reduce((sum, files) => sum + files.length, 0);
374
+ consola.success(`🚀 Watch mode ready - watching ${totalFiles} files`);
375
+ consola.info(`💡 Press ${colors.cyan("Ctrl+C")} to stop watching`);
376
+ resolve$1();
377
+ });
378
+ });
379
+ return () => {
380
+ if (watchCtx.rebuildTimer) clearTimeout(watchCtx.rebuildTimer);
381
+ return watcher.close();
382
+ };
383
+ }
384
+ /**
385
+ * Handle file change events
386
+ */
387
+ function handleFileChange(watchCtx, filePath, changeType) {
388
+ const { config, ctx } = watchCtx;
389
+ const watchOptions = config.watch || {};
390
+ const delay = watchOptions.delay ?? 100;
391
+ const relativePath = relative(ctx.pkgDir, join(ctx.pkgDir, filePath));
392
+ const formattedPath = fmtPath(relativePath);
393
+ const changeIcon = changeType === "changed" ? "📝" : changeType === "added" ? "➕" : "➖";
394
+ consola.info(`${changeIcon} ${colors.cyan(formattedPath)} ${changeType}`);
395
+ if (watchCtx.rebuildTimer) clearTimeout(watchCtx.rebuildTimer);
396
+ watchCtx.pendingRebuild = true;
397
+ watchCtx.rebuildTimer = setTimeout(() => {
398
+ triggerRebuild(watchCtx);
399
+ }, delay);
400
+ }
401
+ /**
402
+ * Trigger a rebuild
403
+ */
404
+ async function triggerRebuild(watchCtx) {
405
+ const { config, buildFn } = watchCtx;
406
+ if (watchCtx.isBuilding) return;
407
+ if (!watchCtx.pendingRebuild) return;
408
+ watchCtx.isBuilding = true;
409
+ watchCtx.pendingRebuild = false;
410
+ try {
411
+ consola.info(`🔄 Rebuilding...`);
412
+ const start = Date.now();
413
+ const buildConfig = {
414
+ ...config,
415
+ watch: {
416
+ ...config.watch,
417
+ enabled: false
418
+ }
419
+ };
420
+ await buildFn(buildConfig);
421
+ const duration = Date.now() - start;
422
+ consola.success(`✅ Rebuild completed in ${duration}ms`);
423
+ } catch (error) {
424
+ consola.error("❌ Rebuild failed:");
425
+ if (error instanceof Error) {
426
+ consola.error(` ${error.message}`);
427
+ if (process.env.DEBUG && error.stack) consola.debug(error.stack);
428
+ } else consola.error(` ${String(error)}`);
429
+ consola.info("👀 Still watching for changes...");
430
+ } finally {
431
+ watchCtx.isBuilding = false;
432
+ if (watchCtx.pendingRebuild) setTimeout(() => triggerRebuild(watchCtx), watchCtx.config.watch?.delay ?? 100);
433
+ }
434
+ }
435
+ /**
436
+ * Get patterns for files to watch
437
+ */
438
+ async function getWatchPatterns(config, ctx, watchOptions) {
439
+ if (watchOptions.include && watchOptions.include.length > 0) return watchOptions.include;
440
+ const patterns = [];
441
+ for (const entry of config.entries || []) if (typeof entry === "string") {
442
+ const [input] = entry.split(":");
443
+ if (input.endsWith("/")) patterns.push(`${input}**/*`);
444
+ else {
445
+ const inputs = input.split(",");
446
+ for (const inputFile of inputs) {
447
+ patterns.push(inputFile);
448
+ const dir = inputFile.substring(0, inputFile.lastIndexOf("/"));
449
+ if (dir) patterns.push(`${dir}/**/*`);
450
+ }
451
+ }
452
+ } else if (entry.type === "transform") patterns.push(`${entry.input}/**/*`);
453
+ else {
454
+ const inputs = Array.isArray(entry.input) ? entry.input : [entry.input];
455
+ for (const inputFile of inputs) {
456
+ patterns.push(inputFile);
457
+ const dir = inputFile.substring(0, inputFile.lastIndexOf("/"));
458
+ if (dir) patterns.push(`${dir}/**/*`);
459
+ }
460
+ }
461
+ if (patterns.length === 0) patterns.push("src/**/*", "*.ts", "*.js", "*.mjs", "*.json");
462
+ return [...new Set(patterns)];
463
+ }
464
+ /**
465
+ * Get patterns for files to ignore
466
+ */
467
+ function getIgnorePatterns(watchOptions) {
468
+ const defaultIgnores = [
469
+ "**/node_modules/**",
470
+ "**/dist/**",
471
+ "**/build/**",
472
+ "**/coverage/**",
473
+ "**/.git/**",
474
+ "**/.DS_Store",
475
+ "**/Thumbs.db",
476
+ "**/*.log",
477
+ "**/tmp/**",
478
+ "**/temp/**"
479
+ ];
480
+ if (watchOptions.exclude && watchOptions.exclude.length > 0) return [...defaultIgnores, ...watchOptions.exclude];
481
+ return defaultIgnores;
482
+ }
483
+
328
484
  //#endregion
329
485
  //#region src/build.ts
330
486
  /**
331
487
  * Build dist/ from src/
332
488
  */
333
489
  async function build(config) {
334
- const start = Date.now();
335
490
  const pkgDir = normalizePath(config.cwd);
336
491
  const pkg = await readJSON(join(pkgDir, "package.json")).catch(() => ({}));
337
492
  const ctx = {
338
493
  pkg,
339
494
  pkgDir
340
495
  };
496
+ if (config.watch?.enabled) {
497
+ consola.log(`👀 Starting watch mode for \`${ctx.pkg.name || "<no name>"}\` (\`${ctx.pkgDir}\`)`);
498
+ await performBuild(config, ctx);
499
+ const stopWatch = await startWatch(config, ctx, build);
500
+ const cleanup = () => {
501
+ consola.info("🛑 Stopping watch mode...");
502
+ stopWatch();
503
+ process.exit(0);
504
+ };
505
+ process.on("SIGINT", cleanup);
506
+ process.on("SIGTERM", cleanup);
507
+ return new Promise(() => {});
508
+ }
341
509
  consola.log(`📦 Building \`${ctx.pkg.name || "<no name>"}\` (\`${ctx.pkgDir}\`)`);
510
+ await performBuild(config, ctx);
511
+ }
512
+ /**
513
+ * Perform the actual build process
514
+ */
515
+ async function performBuild(config, ctx) {
516
+ const start = Date.now();
342
517
  const hooks = config.hooks || {};
343
518
  await hooks.start?.(ctx);
344
519
  const entries = (config.entries || []).map((rawEntry) => {
@@ -357,8 +532,8 @@ async function build(config) {
357
532
  } else entry = rawEntry;
358
533
  if (!entry.input) throw new Error(`Build entry missing \`input\`: ${JSON.stringify(entry, null, 2)}`);
359
534
  entry = { ...entry };
360
- entry.outDir = normalizePath(entry.outDir || "dist", pkgDir);
361
- entry.input = Array.isArray(entry.input) ? entry.input.map((p) => normalizePath(p, pkgDir)) : normalizePath(entry.input, pkgDir);
535
+ entry.outDir = normalizePath(entry.outDir || "dist", ctx.pkgDir);
536
+ entry.input = Array.isArray(entry.input) ? entry.input.map((p) => normalizePath(p, ctx.pkgDir)) : normalizePath(entry.input, ctx.pkgDir);
362
537
  return entry;
363
538
  });
364
539
  await hooks.entries?.(entries, ctx);
@@ -87,10 +87,49 @@ interface BuildHooks {
87
87
  rolldownConfig?: (cfg: InputOptions, ctx: BuildContext) => void | Promise<void>;
88
88
  rolldownOutput?: (cfg: OutputOptions, res: RolldownBuild, ctx: BuildContext) => void | Promise<void>;
89
89
  }
90
+ interface WatchOptions {
91
+ /**
92
+ * Enable watch mode.
93
+ *
94
+ * Defaults to `false` if not provided.
95
+ */
96
+ enabled?: boolean;
97
+ /**
98
+ * Glob patterns for files to watch.
99
+ *
100
+ * Defaults to watching all source files if not provided.
101
+ */
102
+ include?: string[];
103
+ /**
104
+ * Glob patterns for files to ignore.
105
+ *
106
+ * Defaults to common ignore patterns if not provided.
107
+ */
108
+ exclude?: string[];
109
+ /**
110
+ * Delay in milliseconds before rebuilding after a file change.
111
+ *
112
+ * Defaults to `100` if not provided.
113
+ */
114
+ delay?: number;
115
+ /**
116
+ * Whether to ignore the initial build when starting watch mode.
117
+ *
118
+ * Defaults to `false` if not provided.
119
+ */
120
+ ignoreInitial?: boolean;
121
+ /**
122
+ * Whether to watch for new files being added.
123
+ *
124
+ * Defaults to `true` if not provided.
125
+ */
126
+ watchNewFiles?: boolean;
127
+ }
90
128
  interface BuildConfig {
91
129
  cwd?: string | URL;
92
130
  entries?: (BuildEntry | string)[];
93
131
  hooks?: BuildHooks;
132
+ watch?: WatchOptions;
94
133
  }
95
134
  //#endregion
96
135
  //#region src/config.d.ts
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { build } from "./_chunks/build-eIjZ3Fk8.mjs";
2
+ import { build } from "./_chunks/build-DuPhfTzX.mjs";
3
3
  import { consola } from "consola";
4
4
  import { parseArgs } from "node:util";
5
5
  import { loadConfig } from "c12";
@@ -16,6 +16,11 @@ const args = parseArgs({
16
16
  stub: {
17
17
  type: "boolean",
18
18
  default: false
19
+ },
20
+ watch: {
21
+ type: "boolean",
22
+ default: false,
23
+ short: "w"
19
24
  }
20
25
  }
21
26
  });
@@ -48,7 +53,11 @@ if (rawEntries.length === 0) {
48
53
  await build({
49
54
  cwd: args.values.dir,
50
55
  ...config,
51
- entries
56
+ entries,
57
+ watch: args.values.watch ? {
58
+ enabled: true,
59
+ ...config.watch
60
+ } : config.watch
52
61
  });
53
62
 
54
63
  //#endregion
package/dist/config.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { defineConfig } from "./_chunks/config-DxLkhDt6.mjs";
1
+ import { defineConfig } from "./_chunks/config-BZW4dLYD.mjs";
2
2
  export { defineConfig };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { BuildConfig, BuildEntry, BundleEntry, TransformEntry, defineConfig } from "./_chunks/config-DxLkhDt6.mjs";
1
+ import { BuildConfig, BuildEntry, BundleEntry, TransformEntry, defineConfig } from "./_chunks/config-BZW4dLYD.mjs";
2
2
 
3
3
  //#region src/build.d.ts
4
4
 
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { build } from "./_chunks/build-eIjZ3Fk8.mjs";
1
+ import { build } from "./_chunks/build-DuPhfTzX.mjs";
2
2
  import { defineConfig } from "./_chunks/config-B_2eqpNJ.mjs";
3
3
 
4
4
  export { build, defineConfig };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "robuild",
3
3
  "type": "module",
4
- "version": "0.0.5",
4
+ "version": "0.0.6",
5
5
  "packageManager": "pnpm@10.11.1",
6
6
  "description": "Zero-config ESM/TS package builder. Powered by Rolldown and Oxc",
7
7
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "c12": "^3.0.4",
35
+ "chokidar": "^4.0.3",
35
36
  "consola": "^3.4.2",
36
37
  "defu": "^6.1.4",
37
38
  "exsolve": "^1.0.5",