@moku-labs/web 2.3.0 → 2.3.2

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/dist/index.cjs CHANGED
@@ -2603,9 +2603,70 @@ function substituteAssetPlaceholders(ctx, html) {
2603
2603
  return html.replaceAll(ASSETS_PLACEHOLDER, buildAssetTags(ctx)).replaceAll(CSS_ASSETS_PLACEHOLDER, buildAssetTags(ctx, "css")).replaceAll(JS_ASSETS_PLACEHOLDER, buildAssetTags(ctx, "js"));
2604
2604
  }
2605
2605
  /**
2606
- * Copies the configured `publicDir` (default `"public"`) verbatim into `outDir`,
2607
- * preserving the nested directory structure. Skips silently (returns `null`) when
2608
- * the source directory does not exist.
2606
+ * Test whether `destination` is already an up-to-date copy of the `src` file: same
2607
+ * size and at least as new (mtime). A missing destination — or any stat failure —
2608
+ * reports stale: when in doubt, copy.
2609
+ *
2610
+ * @param src - The source file path.
2611
+ * @param destination - The candidate destination file path.
2612
+ * @returns `true` when the destination can be skipped.
2613
+ * @example
2614
+ * ```ts
2615
+ * if (await isFreshCopy(src, destination)) return;
2616
+ * ```
2617
+ */
2618
+ async function isFreshCopy(src, destination) {
2619
+ try {
2620
+ const [srcStat, destinationStat] = await Promise.all([(0, node_fs_promises.stat)(src), (0, node_fs_promises.stat)(destination)]);
2621
+ return destinationStat.size === srcStat.size && destinationStat.mtimeMs >= srcStat.mtimeMs;
2622
+ } catch {
2623
+ return false;
2624
+ }
2625
+ }
2626
+ /**
2627
+ * Recursively copy `from` into `to`, skipping every file whose destination is already
2628
+ * fresh. Skipping matters in dev rebuilds (which keep the previous outDir): re-copying
2629
+ * unchanged assets makes asset-watching dev servers (wrangler) reload on src-only
2630
+ * edits, and on macOS/APFS under Bun a large-file re-copy goes through clonefile(2),
2631
+ * whose FSEvents "ItemCloned" echo on the SOURCE path re-triggers `public/**` watchers
2632
+ * — a self-sustaining rebuild loop. Serial on purpose: public dirs can hold thousands
2633
+ * of assets and the skip path is stat-only, while an unbounded parallel copy risks fd
2634
+ * exhaustion.
2635
+ *
2636
+ * @param from - The source directory.
2637
+ * @param to - The destination directory (created when missing).
2638
+ * @returns The number of entries actually copied.
2639
+ * @example
2640
+ * ```ts
2641
+ * const copied = await copyDirIncremental("public", "dist");
2642
+ * ```
2643
+ */
2644
+ async function copyDirIncremental(from, to) {
2645
+ await (0, node_fs_promises.mkdir)(to, { recursive: true });
2646
+ const entries = await (0, node_fs_promises.readdir)(from, { withFileTypes: true });
2647
+ let copied = 0;
2648
+ for (const entry of entries) {
2649
+ const src = node_path$1.default.join(from, entry.name);
2650
+ const destination = node_path$1.default.join(to, entry.name);
2651
+ if (entry.isDirectory()) copied += await copyDirIncremental(src, destination);
2652
+ else if (entry.isFile()) {
2653
+ if (await isFreshCopy(src, destination)) continue;
2654
+ await (0, node_fs_promises.copyFile)(src, destination);
2655
+ copied += 1;
2656
+ } else {
2657
+ await (0, node_fs_promises.cp)(src, destination, { recursive: true });
2658
+ copied += 1;
2659
+ }
2660
+ }
2661
+ return copied;
2662
+ }
2663
+ /**
2664
+ * Copies the configured `publicDir` (default `"public"`) into `outDir`, preserving the
2665
+ * nested directory structure. The copy is incremental: a file whose destination already
2666
+ * exists with the same size and an mtime at least as new as the source is skipped. A
2667
+ * full build cleans outDir first, so every file misses the compare and the output is
2668
+ * identical to a verbatim copy; a dev rebuild (`skipClean`) rewrites only what changed.
2669
+ * Skips silently (returns `null`) when the source directory does not exist.
2609
2670
  *
2610
2671
  * @param ctx - Plugin context (provides `config`, `log`).
2611
2672
  * @returns The copy result, or `null` when the public directory is absent.
@@ -2623,14 +2684,15 @@ async function copyPublic(ctx) {
2623
2684
  });
2624
2685
  return null;
2625
2686
  }
2626
- await (0, node_fs_promises.cp)(from, ctx.config.outDir, { recursive: true });
2687
+ const copied = await copyDirIncremental(from, ctx.config.outDir);
2627
2688
  ctx.log.debug("build:public", {
2628
2689
  from,
2629
- dest: ctx.config.outDir
2690
+ dest: ctx.config.outDir,
2691
+ copied
2630
2692
  });
2631
2693
  return {
2632
2694
  from: node_path$1.default.normalize(from),
2633
- copied: 1
2695
+ copied
2634
2696
  };
2635
2697
  }
2636
2698
  //#endregion
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { browserEnv, cloudflareBindings, dotenv, envPlugin, envPlugin as envPlug
5
5
  import { createCoreConfig } from "@moku-labs/core";
6
6
  import { appendFileSync, existsSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
- import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8
+ import { copyFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
9
9
  import { homedir, networkInterfaces, tmpdir } from "node:os";
10
10
  import minpath, { default as path } from "node:path";
11
11
  import { Feed } from "feed";
@@ -2559,9 +2559,70 @@ function substituteAssetPlaceholders(ctx, html) {
2559
2559
  return html.replaceAll(ASSETS_PLACEHOLDER, buildAssetTags(ctx)).replaceAll(CSS_ASSETS_PLACEHOLDER, buildAssetTags(ctx, "css")).replaceAll(JS_ASSETS_PLACEHOLDER, buildAssetTags(ctx, "js"));
2560
2560
  }
2561
2561
  /**
2562
- * Copies the configured `publicDir` (default `"public"`) verbatim into `outDir`,
2563
- * preserving the nested directory structure. Skips silently (returns `null`) when
2564
- * the source directory does not exist.
2562
+ * Test whether `destination` is already an up-to-date copy of the `src` file: same
2563
+ * size and at least as new (mtime). A missing destination — or any stat failure —
2564
+ * reports stale: when in doubt, copy.
2565
+ *
2566
+ * @param src - The source file path.
2567
+ * @param destination - The candidate destination file path.
2568
+ * @returns `true` when the destination can be skipped.
2569
+ * @example
2570
+ * ```ts
2571
+ * if (await isFreshCopy(src, destination)) return;
2572
+ * ```
2573
+ */
2574
+ async function isFreshCopy(src, destination) {
2575
+ try {
2576
+ const [srcStat, destinationStat] = await Promise.all([stat(src), stat(destination)]);
2577
+ return destinationStat.size === srcStat.size && destinationStat.mtimeMs >= srcStat.mtimeMs;
2578
+ } catch {
2579
+ return false;
2580
+ }
2581
+ }
2582
+ /**
2583
+ * Recursively copy `from` into `to`, skipping every file whose destination is already
2584
+ * fresh. Skipping matters in dev rebuilds (which keep the previous outDir): re-copying
2585
+ * unchanged assets makes asset-watching dev servers (wrangler) reload on src-only
2586
+ * edits, and on macOS/APFS under Bun a large-file re-copy goes through clonefile(2),
2587
+ * whose FSEvents "ItemCloned" echo on the SOURCE path re-triggers `public/**` watchers
2588
+ * — a self-sustaining rebuild loop. Serial on purpose: public dirs can hold thousands
2589
+ * of assets and the skip path is stat-only, while an unbounded parallel copy risks fd
2590
+ * exhaustion.
2591
+ *
2592
+ * @param from - The source directory.
2593
+ * @param to - The destination directory (created when missing).
2594
+ * @returns The number of entries actually copied.
2595
+ * @example
2596
+ * ```ts
2597
+ * const copied = await copyDirIncremental("public", "dist");
2598
+ * ```
2599
+ */
2600
+ async function copyDirIncremental(from, to) {
2601
+ await mkdir(to, { recursive: true });
2602
+ const entries = await readdir(from, { withFileTypes: true });
2603
+ let copied = 0;
2604
+ for (const entry of entries) {
2605
+ const src = path.join(from, entry.name);
2606
+ const destination = path.join(to, entry.name);
2607
+ if (entry.isDirectory()) copied += await copyDirIncremental(src, destination);
2608
+ else if (entry.isFile()) {
2609
+ if (await isFreshCopy(src, destination)) continue;
2610
+ await copyFile(src, destination);
2611
+ copied += 1;
2612
+ } else {
2613
+ await cp(src, destination, { recursive: true });
2614
+ copied += 1;
2615
+ }
2616
+ }
2617
+ return copied;
2618
+ }
2619
+ /**
2620
+ * Copies the configured `publicDir` (default `"public"`) into `outDir`, preserving the
2621
+ * nested directory structure. The copy is incremental: a file whose destination already
2622
+ * exists with the same size and an mtime at least as new as the source is skipped. A
2623
+ * full build cleans outDir first, so every file misses the compare and the output is
2624
+ * identical to a verbatim copy; a dev rebuild (`skipClean`) rewrites only what changed.
2625
+ * Skips silently (returns `null`) when the source directory does not exist.
2565
2626
  *
2566
2627
  * @param ctx - Plugin context (provides `config`, `log`).
2567
2628
  * @returns The copy result, or `null` when the public directory is absent.
@@ -2579,14 +2640,15 @@ async function copyPublic(ctx) {
2579
2640
  });
2580
2641
  return null;
2581
2642
  }
2582
- await cp(from, ctx.config.outDir, { recursive: true });
2643
+ const copied = await copyDirIncremental(from, ctx.config.outDir);
2583
2644
  ctx.log.debug("build:public", {
2584
2645
  from,
2585
- dest: ctx.config.outDir
2646
+ dest: ctx.config.outDir,
2647
+ copied
2586
2648
  });
2587
2649
  return {
2588
2650
  from: path.normalize(from),
2589
- copied: 1
2651
+ copied
2590
2652
  };
2591
2653
  }
2592
2654
  //#endregion
package/package.json CHANGED
@@ -98,6 +98,7 @@
98
98
  },
99
99
  "devDependencies": {
100
100
  "@biomejs/biome": "2.4.16",
101
+ "@moku-labs/ci": "1.2.5",
101
102
  "@types/bun": "1.3.14",
102
103
  "@vitest/coverage-istanbul": "4.0.18",
103
104
  "eslint": "9.39.3",
@@ -130,7 +131,11 @@
130
131
  "test:integration": "vitest run --project integration",
131
132
  "test:cli-e2e": "bun test --timeout 30000 src/plugins/cli/__tests__/e2e/",
132
133
  "test:build-e2e": "bun test src/plugins/build/__tests__/e2e/",
133
- "test:coverage": "vitest run --project unit --project integration --coverage"
134
+ "test:coverage": "vitest run --project unit --project integration --coverage",
135
+ "typecheck": "tsc --noEmit",
136
+ "release:setup": "moku-release setup",
137
+ "release:doctor": "moku-release doctor",
138
+ "release": "moku-release"
134
139
  },
135
- "version": "2.3.0"
140
+ "version": "2.3.2"
136
141
  }