@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 +68 -6
- package/dist/index.mjs +69 -7
- package/package.json +7 -2
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
|
-
*
|
|
2607
|
-
*
|
|
2608
|
-
*
|
|
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 (
|
|
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
|
|
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
|
-
*
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
140
|
+
"version": "2.3.2"
|
|
136
141
|
}
|