@moku-labs/web 1.2.0 → 1.3.1
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/browser.d.mts +37 -5
- package/dist/browser.mjs +29 -6
- package/dist/index.cjs +572 -101
- package/dist/index.d.cts +165 -17
- package/dist/index.d.mts +165 -17
- package/dist/index.mjs +572 -101
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1295,18 +1295,24 @@ function isPublished(article, isProduction) {
|
|
|
1295
1295
|
* locale collection: existing files only, drafts dropped in production, sorted
|
|
1296
1296
|
* date-descending. The single load+filter+sort step behind {@link createContentApi.loadAll}.
|
|
1297
1297
|
*
|
|
1298
|
+
* When a `cached` map is supplied (incremental dev rebuild), a slug already present in it
|
|
1299
|
+
* is reused as-is (skipping the re-read + Markdown/Shiki re-render); only slugs absent
|
|
1300
|
+
* from it — the ones a preceding `invalidate()` dropped, plus any never-loaded — are
|
|
1301
|
+
* resolved fresh. With no `cached` map every slug is resolved (the full load).
|
|
1302
|
+
*
|
|
1298
1303
|
* @param ctx - Kernel-free domain context (provider + i18n helpers + stage).
|
|
1299
1304
|
* @param slugs - Every known article slug from the provider.
|
|
1300
1305
|
* @param locale - The locale to resolve and collect.
|
|
1306
|
+
* @param cached - Optional per-locale article cache to reuse for unchanged slugs.
|
|
1301
1307
|
* @returns The published (date-descending) articles for this locale.
|
|
1302
1308
|
* @example
|
|
1303
1309
|
* ```ts
|
|
1304
1310
|
* const present = await loadAndFilterArticles(ctx, slugs, "en");
|
|
1305
1311
|
* ```
|
|
1306
1312
|
*/
|
|
1307
|
-
async function loadAndFilterArticles(ctx, slugs, locale) {
|
|
1313
|
+
async function loadAndFilterArticles(ctx, slugs, locale, cached) {
|
|
1308
1314
|
const isProduction = ctx.global.stage === "production";
|
|
1309
|
-
return (await Promise.all(slugs.map((slug) => resolveArticle(ctx, slug, locale)))).filter((article) => article !== null).filter((article) => isPublished(article, isProduction)).toSorted(byDateDescending);
|
|
1315
|
+
return (await Promise.all(slugs.map((slug) => cached?.get(slug) ?? resolveArticle(ctx, slug, locale)))).filter((article) => article !== null).filter((article) => isPublished(article, isProduction)).toSorted(byDateDescending);
|
|
1310
1316
|
}
|
|
1311
1317
|
/**
|
|
1312
1318
|
* Derive the article slug from a source file path — the parent directory name
|
|
@@ -1343,19 +1349,31 @@ function createContentApi(ctx) {
|
|
|
1343
1349
|
* Load every article across every active locale (locale fallback, production
|
|
1344
1350
|
* draft exclusion, date sort, `contentId` after sort), cache them, emit `content:ready`.
|
|
1345
1351
|
*
|
|
1352
|
+
* Cache-first by default: repeated calls return the per-build memo (list-route loaders
|
|
1353
|
+
* call this once PER PAGE — without the memo every page would re-read + re-render every
|
|
1354
|
+
* article, the dev-loop killer), and a rebuild after `invalidate()` re-resolves only the
|
|
1355
|
+
* dropped slugs while reusing the cached articles for the rest (`contentId` ordinals are
|
|
1356
|
+
* still recomputed across the FULL sorted set, so ids + order match a full load). Pass
|
|
1357
|
+
* `{ reuse: false }` to force a FRESH full reload (cold build / an unclassifiable change
|
|
1358
|
+
* where the caller cannot pinpoint what changed) — this bypasses the memo + per-slug cache.
|
|
1359
|
+
*
|
|
1360
|
+
* @param options - Optional load behaviour (`reuse`, default `true`).
|
|
1346
1361
|
* @returns A locale-keyed map of date-descending articles.
|
|
1347
1362
|
* @example
|
|
1348
1363
|
* ```ts
|
|
1349
1364
|
* const byLocale = await api.loadAll();
|
|
1350
1365
|
* ```
|
|
1351
1366
|
*/
|
|
1352
|
-
async loadAll() {
|
|
1367
|
+
async loadAll(options) {
|
|
1368
|
+
const reuse = options?.reuse !== false;
|
|
1369
|
+
const memo = ctx.state.loadedAll;
|
|
1370
|
+
if (reuse && memo !== null) return memo;
|
|
1353
1371
|
const slugs = await ctx.provider.slugs();
|
|
1354
1372
|
const locales = ctx.locales();
|
|
1355
1373
|
const result = /* @__PURE__ */ new Map();
|
|
1356
1374
|
let total = 0;
|
|
1357
1375
|
for (const locale of locales) {
|
|
1358
|
-
const present = await loadAndFilterArticles(ctx, slugs, locale);
|
|
1376
|
+
const present = await loadAndFilterArticles(ctx, slugs, locale, reuse ? ctx.state.articles.get(locale) : void 0);
|
|
1359
1377
|
const cache = /* @__PURE__ */ new Map();
|
|
1360
1378
|
let index = 0;
|
|
1361
1379
|
for (const article of present) {
|
|
@@ -1367,6 +1385,7 @@ function createContentApi(ctx) {
|
|
|
1367
1385
|
result.set(locale, present);
|
|
1368
1386
|
total += present.length;
|
|
1369
1387
|
}
|
|
1388
|
+
ctx.state.loadedAll = result;
|
|
1370
1389
|
ctx.emit("content:ready", {
|
|
1371
1390
|
locales,
|
|
1372
1391
|
articleCount: total
|
|
@@ -1430,6 +1449,7 @@ function createContentApi(ctx) {
|
|
|
1430
1449
|
if (slug === void 0) continue;
|
|
1431
1450
|
for (const cache of ctx.state.articles.values()) cache.delete(slug);
|
|
1432
1451
|
}
|
|
1452
|
+
if (accepted.length > 0) ctx.state.loadedAll = null;
|
|
1433
1453
|
ctx.emit("content:invalidated", { paths: accepted });
|
|
1434
1454
|
},
|
|
1435
1455
|
/**
|
|
@@ -1499,14 +1519,17 @@ const contentEvents = (register) => ({
|
|
|
1499
1519
|
* @param _ctx - Minimal context with global and config.
|
|
1500
1520
|
* @param _ctx.global - Global plugin registry.
|
|
1501
1521
|
* @param _ctx.config - Resolved plugin configuration.
|
|
1502
|
-
* @returns Fresh content shell state: an empty article cache.
|
|
1522
|
+
* @returns Fresh content shell state: an empty article cache + an empty loadAll memo.
|
|
1503
1523
|
* @example
|
|
1504
1524
|
* ```ts
|
|
1505
1525
|
* const state = createContentState({ global: {}, config: { providers: [] } });
|
|
1506
1526
|
* ```
|
|
1507
1527
|
*/
|
|
1508
1528
|
function createContentState(_ctx) {
|
|
1509
|
-
return {
|
|
1529
|
+
return {
|
|
1530
|
+
articles: /* @__PURE__ */ new Map(),
|
|
1531
|
+
loadedAll: null
|
|
1532
|
+
};
|
|
1510
1533
|
}
|
|
1511
1534
|
//#endregion
|
|
1512
1535
|
//#region src/plugins/content/validate.ts
|
|
@@ -3356,7 +3379,11 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
|
3356
3379
|
/**
|
|
3357
3380
|
* Bundles CSS and JS into the output directory via two separate runner passes
|
|
3358
3381
|
* (dodging Bun's mixed-entrypoint segfault), honoring `config.minify`, and caches
|
|
3359
|
-
* the resulting hashed asset paths in `state.buildCache` for downstream phases.
|
|
3382
|
+
* the resulting hashed asset paths in `state.buildCache` for downstream phases. The
|
|
3383
|
+
* two passes run CONCURRENTLY (`Promise.all`) — they target disjoint hashed outputs
|
|
3384
|
+
* and distinct `buildCache` keys (`css`/`js`), so overlapping them ~halves bundle
|
|
3385
|
+
* wall-time with no shared-state hazard. The CSS pass is still dispatched first, so
|
|
3386
|
+
* the runner's invocation order stays css-then-js.
|
|
3360
3387
|
*
|
|
3361
3388
|
* @param ctx - Plugin context (provides `state`, `config`, `log`).
|
|
3362
3389
|
* @param options - Optional dependency-injection seam (runner + entrypoints).
|
|
@@ -3369,10 +3396,10 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
|
3369
3396
|
async function bundle(ctx, options = {}) {
|
|
3370
3397
|
const runner = options.runner ?? defaultRunner;
|
|
3371
3398
|
const { minify, outDir } = ctx.config;
|
|
3399
|
+
const assetsDir = node_path$1.default.join(outDir, "assets");
|
|
3372
3400
|
const cssEntrypoints = options.cssEntrypoints ?? resolveEntrypoints(CSS_ENTRY_CANDIDATES);
|
|
3373
3401
|
const jsEntrypoints = options.jsEntrypoints ?? resolveJsEntrypoints(ctx);
|
|
3374
|
-
await runOne(ctx, runner, "css", cssEntrypoints, outDir,
|
|
3375
|
-
await runOne(ctx, runner, "js", jsEntrypoints, outDir, node_path$1.default.join(outDir, "assets"), minify);
|
|
3402
|
+
await Promise.all([runOne(ctx, runner, "css", cssEntrypoints, outDir, assetsDir, minify), runOne(ctx, runner, "js", jsEntrypoints, outDir, assetsDir, minify)]);
|
|
3376
3403
|
}
|
|
3377
3404
|
//#endregion
|
|
3378
3405
|
//#region src/plugins/build/phases/content.ts
|
|
@@ -3389,15 +3416,24 @@ const CONTENT_CACHE_KEY = "content";
|
|
|
3389
3416
|
* pages/feeds/og-images phases. Performs NO Markdown parsing itself — the
|
|
3390
3417
|
* content plugin owns rendering (god-plugin invariant).
|
|
3391
3418
|
*
|
|
3419
|
+
* On a dev incremental rebuild (`options.changed` set) it first `invalidate()`s the
|
|
3420
|
+
* changed Markdown so `loadAll({ reuse: true })` re-reads + re-renders ONLY those
|
|
3421
|
+
* articles, reusing the cached HTML for the rest. With no options it does a full load.
|
|
3422
|
+
*
|
|
3392
3423
|
* @param ctx - Plugin context (provides `require`, `state`, `log`).
|
|
3424
|
+
* @param options - Optional incremental hints; omit for a full load.
|
|
3425
|
+
* @param options.reuse - Reuse cached content for slugs not invalidated (dev incremental rebuild).
|
|
3426
|
+
* @param options.changed - The changed Markdown paths to invalidate before loading.
|
|
3393
3427
|
* @returns The locale-keyed article map returned by the content plugin.
|
|
3394
3428
|
* @example
|
|
3395
3429
|
* ```ts
|
|
3396
3430
|
* const byLocale = await loadContent(ctx);
|
|
3397
3431
|
* ```
|
|
3398
3432
|
*/
|
|
3399
|
-
async function loadContent(ctx) {
|
|
3400
|
-
const
|
|
3433
|
+
async function loadContent(ctx, options) {
|
|
3434
|
+
const content = ctx.require(contentPlugin);
|
|
3435
|
+
if (options?.changed && options.changed.length > 0) content.invalidate(options.changed);
|
|
3436
|
+
const byLocale = await content.loadAll({ reuse: options?.reuse ?? false });
|
|
3401
3437
|
ctx.state.buildCache.set(CONTENT_CACHE_KEY, byLocale);
|
|
3402
3438
|
ctx.log.debug("build:content", { locales: byLocale.size });
|
|
3403
3439
|
return byLocale;
|
|
@@ -4751,6 +4787,71 @@ function renderBody(definition, routeContext) {
|
|
|
4751
4787
|
return (0, preact_render_to_string.renderToString)(definition._handlers.layout ? definition._handlers.layout(layoutContext, vnode) : vnode);
|
|
4752
4788
|
}
|
|
4753
4789
|
/**
|
|
4790
|
+
* Hash a page's render inputs (its loaded data) for the render cache. `null` when the
|
|
4791
|
+
* data is not JSON-serializable — such a page is never cached and always re-renders.
|
|
4792
|
+
*
|
|
4793
|
+
* @param data - The route's loaded data (the only per-page input besides params/locale/code).
|
|
4794
|
+
* @returns The hex SHA-256 of the serialized data, or `null` when it cannot be serialized.
|
|
4795
|
+
* @example
|
|
4796
|
+
* ```ts
|
|
4797
|
+
* hashData({ title: "Hi" }); // "9f8e…"
|
|
4798
|
+
* ```
|
|
4799
|
+
*/
|
|
4800
|
+
function hashData(data) {
|
|
4801
|
+
try {
|
|
4802
|
+
const serialized = JSON.stringify(data) ?? "";
|
|
4803
|
+
return (0, node_crypto.createHash)("sha256").update(serialized).digest("hex");
|
|
4804
|
+
} catch {
|
|
4805
|
+
return null;
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
/**
|
|
4809
|
+
* The render-cache key for one page instance: name + params + locale (the stable identity
|
|
4810
|
+
* that, together with the data hash, determines its body). NUL-joined so no value collides.
|
|
4811
|
+
*
|
|
4812
|
+
* @param instance - The page instance.
|
|
4813
|
+
* @returns The cache key string.
|
|
4814
|
+
* @example
|
|
4815
|
+
* ```ts
|
|
4816
|
+
* renderCacheKey(instance); // "article{\"slug\":\"x\"}en"
|
|
4817
|
+
* ```
|
|
4818
|
+
*/
|
|
4819
|
+
function renderCacheKey(instance) {
|
|
4820
|
+
return `${instance.name}${JSON.stringify(instance.params)}${instance.locale}`;
|
|
4821
|
+
}
|
|
4822
|
+
/**
|
|
4823
|
+
* Render one page's body, reusing the cached body when this page's data is unchanged.
|
|
4824
|
+
* The body is the synchronous, dominant-cost step ({@link renderBody}); an incremental
|
|
4825
|
+
* dev rebuild (`reuse`, code unchanged) skips it for every page whose data hash matches
|
|
4826
|
+
* the cache, and a changed page (or a non-`reuse` run) renders + refreshes the cache.
|
|
4827
|
+
*
|
|
4828
|
+
* @param ctx - Plugin context (provides the cross-run `state.renderCache`).
|
|
4829
|
+
* @param instance - The page instance being rendered.
|
|
4830
|
+
* @param routeContext - The route context passed to `.render()`/`.layout()`.
|
|
4831
|
+
* @param data - The route's loaded data (hashed to detect a change).
|
|
4832
|
+
* @param reuse - Whether this run may reuse a cached body (incremental, no code change).
|
|
4833
|
+
* @returns The SSR-rendered body HTML.
|
|
4834
|
+
* @example
|
|
4835
|
+
* ```ts
|
|
4836
|
+
* const body = renderBodyCached(ctx, instance, routeContext, data, true);
|
|
4837
|
+
* ```
|
|
4838
|
+
*/
|
|
4839
|
+
function renderBodyCached(ctx, instance, routeContext, data, reuse) {
|
|
4840
|
+
const cache = ctx.state.renderCache;
|
|
4841
|
+
const key = renderCacheKey(instance);
|
|
4842
|
+
const hash = hashData(data);
|
|
4843
|
+
if (reuse && hash !== null) {
|
|
4844
|
+
const hit = cache.get(key);
|
|
4845
|
+
if (hit?.dataHash === hash) return hit.body;
|
|
4846
|
+
}
|
|
4847
|
+
const body = renderBody(instance.definition, routeContext);
|
|
4848
|
+
if (hash !== null) cache.set(key, {
|
|
4849
|
+
dataHash: hash,
|
|
4850
|
+
body
|
|
4851
|
+
});
|
|
4852
|
+
return body;
|
|
4853
|
+
}
|
|
4854
|
+
/**
|
|
4754
4855
|
* Write a rendered page document to its on-disk path. The path comes from the
|
|
4755
4856
|
* compiled `TypedRoute.toFile(params)` (honoring any route-level `.toFile()`
|
|
4756
4857
|
* override), resolved under the build `outDir`; parent directories are created first.
|
|
@@ -4779,13 +4880,14 @@ async function writeDocument(outDir, entry, params, html) {
|
|
|
4779
4880
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `has`).
|
|
4780
4881
|
* @param instance - The concrete page instance to render.
|
|
4781
4882
|
* @param shell - Per-build wiring shared across instances (asset tags + template).
|
|
4883
|
+
* @param reuse - Whether this run may reuse a cached body (incremental, no code change).
|
|
4782
4884
|
* @returns The instance's URL, rendered HTML, loaded data, and client-nav flag.
|
|
4783
4885
|
* @example
|
|
4784
4886
|
* ```ts
|
|
4785
|
-
* await renderInstance(ctx, instance, { assets: "", template: null });
|
|
4887
|
+
* await renderInstance(ctx, instance, { assets: "", template: null }, false);
|
|
4786
4888
|
* ```
|
|
4787
4889
|
*/
|
|
4788
|
-
async function renderInstance(ctx, instance, shell) {
|
|
4890
|
+
async function renderInstance(ctx, instance, shell, reuse) {
|
|
4789
4891
|
const { definition, entry, params, locale } = instance;
|
|
4790
4892
|
const router = ctx.require(routerPlugin);
|
|
4791
4893
|
const data = await loadRouteData(definition, params, locale, ctx);
|
|
@@ -4798,7 +4900,7 @@ async function renderInstance(ctx, instance, shell) {
|
|
|
4798
4900
|
};
|
|
4799
4901
|
const parts = {
|
|
4800
4902
|
head: composeHeadHtml(ctx, instance, url, routeContext, data),
|
|
4801
|
-
body:
|
|
4903
|
+
body: renderBodyCached(ctx, instance, routeContext, data, reuse),
|
|
4802
4904
|
assets: shell.assets,
|
|
4803
4905
|
locale
|
|
4804
4906
|
};
|
|
@@ -4899,6 +5001,12 @@ function findRootHtml(rendered) {
|
|
|
4899
5001
|
*/
|
|
4900
5002
|
const RENDER_BATCH_SIZE = 2;
|
|
4901
5003
|
/**
|
|
5004
|
+
* Batch size for an incremental (`reuse`) rebuild. Most instances are cheap cache hits, so
|
|
5005
|
+
* a larger batch cuts the per-batch `setImmediate` round-trips (which would otherwise add
|
|
5006
|
+
* pure latency to an otherwise-fast rebuild) without starving the dev spinner.
|
|
5007
|
+
*/
|
|
5008
|
+
const INCREMENTAL_BATCH_SIZE = 32;
|
|
5009
|
+
/**
|
|
4902
5010
|
* Render `items` through `worker` in bounded-size batches, yielding a macrotask
|
|
4903
5011
|
* (`setImmediate`) between batches. Beyond bounding peak concurrency/memory for large
|
|
4904
5012
|
* sites, the yield lets the single JS thread breathe: one un-yielded `Promise.all` over
|
|
@@ -4934,21 +5042,29 @@ async function renderInBatches(items, batchSize, worker) {
|
|
|
4934
5042
|
* bounded batches ({@link renderInBatches}) → write data sidecars (hybrid/spa) →
|
|
4935
5043
|
* capture the root page's HTML for the root-index phase.
|
|
4936
5044
|
*
|
|
5045
|
+
* On an incremental rebuild (`options.reuse`) the cross-run render cache is kept and each
|
|
5046
|
+
* unchanged-data page reuses its cached body; a full render clears the cache first so a
|
|
5047
|
+
* removed/renamed route's stale body never lingers.
|
|
5048
|
+
*
|
|
4937
5049
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `log`, `has`).
|
|
5050
|
+
* @param options - Optional incremental hint; omit for a full render.
|
|
5051
|
+
* @param options.reuse - Reuse cached page bodies for unchanged-data pages (dev incremental rebuild).
|
|
4938
5052
|
* @returns The number of pages rendered and the captured default-page HTML.
|
|
4939
5053
|
* @example
|
|
4940
5054
|
* ```ts
|
|
4941
5055
|
* const { pageCount, rootHtml } = await renderPages(ctx);
|
|
4942
5056
|
* ```
|
|
4943
5057
|
*/
|
|
4944
|
-
async function renderPages(ctx) {
|
|
5058
|
+
async function renderPages(ctx, options) {
|
|
5059
|
+
const reuse = options?.reuse === true;
|
|
4945
5060
|
const router = ctx.require(routerPlugin);
|
|
4946
5061
|
const manifest = router.manifest();
|
|
4947
5062
|
ctx.state.manifest = [...manifest];
|
|
4948
5063
|
const locales = ctx.require(i18nPlugin).locales();
|
|
4949
5064
|
const byPattern = makeEntryMap(router);
|
|
5065
|
+
if (!reuse) ctx.state.renderCache.clear();
|
|
4950
5066
|
const shell = await prepareShell(ctx);
|
|
4951
|
-
const rendered = await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell));
|
|
5067
|
+
const rendered = await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), reuse ? INCREMENTAL_BATCH_SIZE : RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell, reuse));
|
|
4952
5068
|
await writeDataSidecars(ctx, rendered, router.mode());
|
|
4953
5069
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
4954
5070
|
return {
|
|
@@ -5156,6 +5272,40 @@ async function generateSitemap(ctx) {
|
|
|
5156
5272
|
* @file build plugin — pipeline driver. Sequences the fixed multi-phase build,
|
|
5157
5273
|
* emits `build:phase` boundaries, and runs intra-phase work via `Promise.all`.
|
|
5158
5274
|
*/
|
|
5275
|
+
/** Matches a Markdown source path (a content edit). */
|
|
5276
|
+
const MARKDOWN_PATH = /\.md$/;
|
|
5277
|
+
/** Matches a stylesheet path (a CSS edit — does not change rendered page bodies). */
|
|
5278
|
+
const STYLE_PATH = /\.css$/;
|
|
5279
|
+
/** Matches a code path (TS/JS/JSON — may change ANY page's render output). */
|
|
5280
|
+
const CODE_PATH = /\.(?:tsx?|jsx?|mjs|cjs|json)$/;
|
|
5281
|
+
/**
|
|
5282
|
+
* Derive the {@link ChangePlan} for a run from its changed-path set (see the type docs
|
|
5283
|
+
* for the rules).
|
|
5284
|
+
*
|
|
5285
|
+
* @param changed - Absolute/relative changed paths, or `undefined` for a full build.
|
|
5286
|
+
* @returns The reuse plan for this run.
|
|
5287
|
+
* @example
|
|
5288
|
+
* ```ts
|
|
5289
|
+
* const plan = planIncrementalRebuild(options?.changed);
|
|
5290
|
+
* ```
|
|
5291
|
+
*/
|
|
5292
|
+
function planIncrementalRebuild(changed) {
|
|
5293
|
+
if (changed === void 0 || changed.length === 0) return {
|
|
5294
|
+
contentChanged: [],
|
|
5295
|
+
contentReuse: false,
|
|
5296
|
+
renderReuse: false
|
|
5297
|
+
};
|
|
5298
|
+
if (!changed.every((file) => MARKDOWN_PATH.test(file) || STYLE_PATH.test(file) || CODE_PATH.test(file))) return {
|
|
5299
|
+
contentChanged: [],
|
|
5300
|
+
contentReuse: false,
|
|
5301
|
+
renderReuse: false
|
|
5302
|
+
};
|
|
5303
|
+
return {
|
|
5304
|
+
contentChanged: changed.filter((file) => MARKDOWN_PATH.test(file)),
|
|
5305
|
+
contentReuse: true,
|
|
5306
|
+
renderReuse: !changed.some((file) => CODE_PATH.test(file))
|
|
5307
|
+
};
|
|
5308
|
+
}
|
|
5159
5309
|
/**
|
|
5160
5310
|
* The static ordered list of pipeline phase names.
|
|
5161
5311
|
*
|
|
@@ -5269,8 +5419,7 @@ async function runOutputs(ctx) {
|
|
|
5269
5419
|
* `build:phase` boundary per phase and `build:complete` once at the end.
|
|
5270
5420
|
*
|
|
5271
5421
|
* @param ctx - Plugin context (provides `require`, `emit`, `state`, `config`, `log`).
|
|
5272
|
-
* @param options - Optional run overrides.
|
|
5273
|
-
* @param options.outDir - Override the configured output directory for this run.
|
|
5422
|
+
* @param options - Optional per-run overrides ({@link RunOptions}).
|
|
5274
5423
|
* @returns The build result (outDir, pageCount, durationMs).
|
|
5275
5424
|
* @example
|
|
5276
5425
|
* ```ts
|
|
@@ -5285,17 +5434,22 @@ async function runPipeline(ctx, options) {
|
|
|
5285
5434
|
...ctx,
|
|
5286
5435
|
config: {
|
|
5287
5436
|
...ctx.config,
|
|
5288
|
-
outDir
|
|
5437
|
+
outDir,
|
|
5438
|
+
...options?.overrides
|
|
5289
5439
|
}
|
|
5290
5440
|
};
|
|
5291
|
-
|
|
5441
|
+
const plan = planIncrementalRebuild(options?.changed);
|
|
5442
|
+
if (!options?.skipClean) await (0, node_fs_promises.rm)(outDir, {
|
|
5292
5443
|
recursive: true,
|
|
5293
5444
|
force: true
|
|
5294
5445
|
});
|
|
5295
5446
|
await (0, node_fs_promises.mkdir)(outDir, { recursive: true });
|
|
5296
5447
|
await withPhase(phaseContext, "bundle", () => bundle(phaseContext));
|
|
5297
|
-
await Promise.all([withPhase(phaseContext, "content", () => loadContent(phaseContext
|
|
5298
|
-
|
|
5448
|
+
await Promise.all([withPhase(phaseContext, "content", () => loadContent(phaseContext, {
|
|
5449
|
+
reuse: plan.contentReuse,
|
|
5450
|
+
changed: plan.contentChanged
|
|
5451
|
+
})), withPhase(phaseContext, "images", () => processImages(phaseContext))]);
|
|
5452
|
+
const pages = await withPhase(phaseContext, "pages", () => renderPages(phaseContext, { reuse: plan.renderReuse }));
|
|
5299
5453
|
await withPhase(phaseContext, "content-images", () => copyContentImages(phaseContext));
|
|
5300
5454
|
await runOutputs(phaseContext);
|
|
5301
5455
|
await withPhase(phaseContext, "root-index", async () => {
|
|
@@ -5348,10 +5502,12 @@ const defaultConfig$2 = {
|
|
|
5348
5502
|
function createApi$3(ctx) {
|
|
5349
5503
|
return {
|
|
5350
5504
|
/**
|
|
5351
|
-
* Run the full SSG pipeline and write the site to disk.
|
|
5505
|
+
* Run the full SSG pipeline and write the site to disk. With no options a full
|
|
5506
|
+
* production build runs; dev callers pass `skipClean`/`overrides`/`changed` for a
|
|
5507
|
+
* fast incremental rebuild (all gated behind opt-in fields — the default path is
|
|
5508
|
+
* unchanged).
|
|
5352
5509
|
*
|
|
5353
|
-
* @param options - Optional run overrides.
|
|
5354
|
-
* @param options.outDir - Override the configured output directory for this run.
|
|
5510
|
+
* @param options - Optional per-run overrides (outDir / skipClean / overrides / changed).
|
|
5355
5511
|
* @returns The build result (outDir, pageCount, durationMs).
|
|
5356
5512
|
* @example
|
|
5357
5513
|
* ```ts
|
|
@@ -5444,8 +5600,8 @@ function createEvents(register) {
|
|
|
5444
5600
|
/**
|
|
5445
5601
|
* Creates initial `build` plugin state: a frozen config snapshot plus empty
|
|
5446
5602
|
* per-run caches (`manifest`, `buildCache`, `runId`) and the cross-run OG
|
|
5447
|
-
* content-hash
|
|
5448
|
-
* duplicated here (pulled fresh via `ctx.require` each run).
|
|
5603
|
+
* content-hash + page-render caches. Holds caches and config only — no domain data
|
|
5604
|
+
* is duplicated here (pulled fresh via `ctx.require` each run).
|
|
5449
5605
|
*
|
|
5450
5606
|
* @param ctx - Minimal context with global and config.
|
|
5451
5607
|
* @param ctx.global - Global plugin registry (unused; caches are config-driven).
|
|
@@ -5462,7 +5618,8 @@ function createState$3(ctx) {
|
|
|
5462
5618
|
manifest: null,
|
|
5463
5619
|
buildCache: /* @__PURE__ */ new Map(),
|
|
5464
5620
|
runId: null,
|
|
5465
|
-
ogImageHashCache: /* @__PURE__ */ new Map()
|
|
5621
|
+
ogImageHashCache: /* @__PURE__ */ new Map(),
|
|
5622
|
+
renderCache: /* @__PURE__ */ new Map()
|
|
5466
5623
|
};
|
|
5467
5624
|
}
|
|
5468
5625
|
//#endregion
|
|
@@ -5686,12 +5843,37 @@ function buildWranglerArgs(input) {
|
|
|
5686
5843
|
branch
|
|
5687
5844
|
];
|
|
5688
5845
|
}
|
|
5846
|
+
/**
|
|
5847
|
+
* Assemble the argv for `wrangler pages project create` (no shell). Guards the
|
|
5848
|
+
* production branch against flag injection; the slug is already a safe `toSlug` output.
|
|
5849
|
+
*
|
|
5850
|
+
* @param input - The resolved project-create inputs.
|
|
5851
|
+
* @param input.slug - Cloudflare project-name slug (`toSlug(site.name())`).
|
|
5852
|
+
* @param input.branch - Production branch (guarded by `/^[a-zA-Z0-9/_.-]+$/`).
|
|
5853
|
+
* @returns The wrangler argv array.
|
|
5854
|
+
* @throws {Error} `ERR_DEPLOY_INVALID_BRANCH` when the branch fails the guard.
|
|
5855
|
+
* @example
|
|
5856
|
+
* buildProjectCreateArgs({ slug: "my-site", branch: "main" });
|
|
5857
|
+
*/
|
|
5858
|
+
function buildProjectCreateArgs(input) {
|
|
5859
|
+
const branch = guardBranch(input.branch);
|
|
5860
|
+
return [
|
|
5861
|
+
"bunx",
|
|
5862
|
+
"wrangler",
|
|
5863
|
+
"pages",
|
|
5864
|
+
"project",
|
|
5865
|
+
"create",
|
|
5866
|
+
input.slug,
|
|
5867
|
+
"--production-branch",
|
|
5868
|
+
branch
|
|
5869
|
+
];
|
|
5870
|
+
}
|
|
5689
5871
|
/** Lowercased substring matchers for the wrangler error taxonomy. */
|
|
5690
5872
|
const ERROR_SIGNATURES = [
|
|
5691
5873
|
{
|
|
5692
5874
|
match: ["could not find project", "project not found"],
|
|
5693
5875
|
kind: "ERR_DEPLOY_PROJECT_NOT_FOUND",
|
|
5694
|
-
advice: "The Cloudflare Pages project does not exist.
|
|
5876
|
+
advice: "The Cloudflare Pages project does not exist yet. Create it in the dashboard (Workers & Pages → Create → Pages) or with `bunx wrangler pages project create <name>`, then retry. (app.deploy.init() only scaffolds local config — it does not create the remote project.)"
|
|
5695
5877
|
},
|
|
5696
5878
|
{
|
|
5697
5879
|
match: [
|
|
@@ -6270,15 +6452,16 @@ function validateConfig$1(ctx) {
|
|
|
6270
6452
|
* Run wrangler for the prepared argv and surface its scrubbed result, translating
|
|
6271
6453
|
* a non-zero exit into the classified deploy error. The API token is read from env
|
|
6272
6454
|
* here so it never crosses a logging boundary; only scrubbed output is returned.
|
|
6455
|
+
* Shared by `run()` (deploy) and `createProject()` (project create).
|
|
6273
6456
|
*
|
|
6274
6457
|
* @param ctx - Plugin context (provides `state.spawn`, `config`, `env`).
|
|
6275
6458
|
* @param args - The fully-built, pre-validated wrangler argv.
|
|
6276
6459
|
* @returns The wrangler `stdout` plus the scrubbed `stderr` to log on success.
|
|
6277
6460
|
* @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
|
|
6278
6461
|
* @example
|
|
6279
|
-
* const { stdout, scrubbedStderr } = await
|
|
6462
|
+
* const { stdout, scrubbedStderr } = await executeWrangler(ctx, args);
|
|
6280
6463
|
*/
|
|
6281
|
-
async function
|
|
6464
|
+
async function executeWrangler(ctx, args) {
|
|
6282
6465
|
const token = ctx.env.require("CLOUDFLARE_API_TOKEN");
|
|
6283
6466
|
const { stdout, scrubbedStderr, exitCode } = await runWrangler({
|
|
6284
6467
|
spawn: ctx.state.spawn,
|
|
@@ -6350,7 +6533,7 @@ function createApi$2(ctx) {
|
|
|
6350
6533
|
root
|
|
6351
6534
|
});
|
|
6352
6535
|
const start = Date.now();
|
|
6353
|
-
const { stdout, scrubbedStderr } = await
|
|
6536
|
+
const { stdout, scrubbedStderr } = await executeWrangler(ctx, args);
|
|
6354
6537
|
ctx.log.info(scrubbedStderr);
|
|
6355
6538
|
const result = buildDeployResult(stdout, branch, start);
|
|
6356
6539
|
ctx.state.lastDeployment = result;
|
|
@@ -6389,6 +6572,38 @@ function createApi$2(ctx) {
|
|
|
6389
6572
|
cwd: process.cwd(),
|
|
6390
6573
|
options
|
|
6391
6574
|
});
|
|
6575
|
+
},
|
|
6576
|
+
/**
|
|
6577
|
+
* The Cloudflare Pages project name this app deploys to (`toSlug(site.name())`).
|
|
6578
|
+
*
|
|
6579
|
+
* @returns The project-name slug.
|
|
6580
|
+
* @example
|
|
6581
|
+
* api.projectName(); // "my-site"
|
|
6582
|
+
*/
|
|
6583
|
+
projectName() {
|
|
6584
|
+
return toSlug(ctx.require(sitePlugin).name());
|
|
6585
|
+
},
|
|
6586
|
+
/**
|
|
6587
|
+
* Create the remote Cloudflare Pages project via wrangler, so a first deploy has a
|
|
6588
|
+
* target. Derives the slug from `site.name()` and the production branch from config.
|
|
6589
|
+
*
|
|
6590
|
+
* @returns The created project name + production branch.
|
|
6591
|
+
* @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
|
|
6592
|
+
* @example
|
|
6593
|
+
* await api.createProject(); // { name: "my-site", branch: "main" }
|
|
6594
|
+
*/
|
|
6595
|
+
async createProject() {
|
|
6596
|
+
const name = toSlug(ctx.require(sitePlugin).name());
|
|
6597
|
+
const branch = ctx.config.productionBranch ?? "main";
|
|
6598
|
+
const { scrubbedStderr } = await executeWrangler(ctx, buildProjectCreateArgs({
|
|
6599
|
+
slug: name,
|
|
6600
|
+
branch
|
|
6601
|
+
}));
|
|
6602
|
+
ctx.log.info(scrubbedStderr);
|
|
6603
|
+
return {
|
|
6604
|
+
name,
|
|
6605
|
+
branch
|
|
6606
|
+
};
|
|
6392
6607
|
}
|
|
6393
6608
|
};
|
|
6394
6609
|
}
|
|
@@ -6522,21 +6737,56 @@ const ACCOUNT_HELP = [
|
|
|
6522
6737
|
"right-hand sidebar (also in the dashboard URL). Then make it available:",
|
|
6523
6738
|
" export CLOUDFLARE_ACCOUNT_ID=… or add it to .env."
|
|
6524
6739
|
].join("\n");
|
|
6740
|
+
/** Shown when a credential is in the raw environment but the app's env providers did not resolve it. */
|
|
6741
|
+
const PROVIDERS_HELP = [
|
|
6742
|
+
"Found in your shell/.env but the app's env plugin did not resolve it — its providers",
|
|
6743
|
+
"are not wired. Add the Node providers in createApp so the deploy can read it:",
|
|
6744
|
+
" pluginConfigs.env = { providers: [processEnv(), dotenv()] } (import them from @moku-labs/web)."
|
|
6745
|
+
].join("\n");
|
|
6525
6746
|
/** The GitHub repo secrets the generated workflow consumes. */
|
|
6526
6747
|
const SECRETS_HELP = ["Add these repo secrets (GitHub → Settings → Secrets and variables → Actions):", "CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID"].join("\n");
|
|
6527
6748
|
/**
|
|
6749
|
+
* Build one credential prerequisite by reading the SAME source the deploy reads — the
|
|
6750
|
+
* resolved `ctx.env` table — so a ✓ guarantees `ctx.env.require(key)` will succeed. When
|
|
6751
|
+
* the value is present in the raw `process.env` but unresolved by the app's providers
|
|
6752
|
+
* (the silent "deploy can't see it" trap a bare `process.env` check would mark green),
|
|
6753
|
+
* the fix hint points at wiring the providers instead of re-adding the value.
|
|
6754
|
+
*
|
|
6755
|
+
* @param ctx - The cli plugin context (provides the resolved `ctx.env`).
|
|
6756
|
+
* @param key - The credential variable name.
|
|
6757
|
+
* @param label - The diagnostic line label.
|
|
6758
|
+
* @param missingHelp - The fix hint when the credential is genuinely absent everywhere.
|
|
6759
|
+
* @returns The credential prerequisite check.
|
|
6760
|
+
* @example
|
|
6761
|
+
* credentialPrereq(ctx, "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN is set", TOKEN_HELP);
|
|
6762
|
+
*/
|
|
6763
|
+
function credentialPrereq(ctx, key, label, missingHelp) {
|
|
6764
|
+
if ((ctx.env.get(key) ?? "") !== "") return {
|
|
6765
|
+
ok: true,
|
|
6766
|
+
label,
|
|
6767
|
+
detail: void 0,
|
|
6768
|
+
scaffoldable: false
|
|
6769
|
+
};
|
|
6770
|
+
return {
|
|
6771
|
+
ok: false,
|
|
6772
|
+
label,
|
|
6773
|
+
detail: (process.env[key] ?? "") !== "" ? PROVIDERS_HELP : missingHelp,
|
|
6774
|
+
scaffoldable: false
|
|
6775
|
+
};
|
|
6776
|
+
}
|
|
6777
|
+
/**
|
|
6528
6778
|
* Evaluate the three deploy prerequisites against the current project: the Cloudflare
|
|
6529
|
-
* wrangler config exists, and both Cloudflare credentials
|
|
6779
|
+
* wrangler config exists, and both Cloudflare credentials resolve through `ctx.env` (the
|
|
6780
|
+
* deploy's own source of truth — not a bare `process.env` read that can diverge from it).
|
|
6530
6781
|
*
|
|
6782
|
+
* @param ctx - The cli plugin context (provides the resolved `ctx.env`).
|
|
6531
6783
|
* @param cwd - The project root (where `wrangler.jsonc` lives).
|
|
6532
6784
|
* @returns The ordered prerequisite checks.
|
|
6533
6785
|
* @example
|
|
6534
|
-
* const prereqs = diagnose(process.cwd());
|
|
6786
|
+
* const prereqs = diagnose(ctx, process.cwd());
|
|
6535
6787
|
*/
|
|
6536
|
-
function diagnose(cwd) {
|
|
6788
|
+
function diagnose(ctx, cwd) {
|
|
6537
6789
|
const wranglerOk = (0, node_fs.existsSync)(node_path$1.default.join(cwd, "wrangler.jsonc"));
|
|
6538
|
-
const tokenOk = (process.env.CLOUDFLARE_API_TOKEN ?? "") !== "";
|
|
6539
|
-
const accountOk = (process.env.CLOUDFLARE_ACCOUNT_ID ?? "") !== "";
|
|
6540
6790
|
return [
|
|
6541
6791
|
{
|
|
6542
6792
|
ok: wranglerOk,
|
|
@@ -6544,18 +6794,8 @@ function diagnose(cwd) {
|
|
|
6544
6794
|
detail: wranglerOk ? void 0 : "Missing — scaffold it (offered below) or run app.deploy.init().",
|
|
6545
6795
|
scaffoldable: true
|
|
6546
6796
|
},
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
label: "CLOUDFLARE_API_TOKEN is set",
|
|
6550
|
-
detail: tokenOk ? void 0 : TOKEN_HELP,
|
|
6551
|
-
scaffoldable: false
|
|
6552
|
-
},
|
|
6553
|
-
{
|
|
6554
|
-
ok: accountOk,
|
|
6555
|
-
label: "CLOUDFLARE_ACCOUNT_ID is set",
|
|
6556
|
-
detail: accountOk ? void 0 : ACCOUNT_HELP,
|
|
6557
|
-
scaffoldable: false
|
|
6558
|
-
}
|
|
6797
|
+
credentialPrereq(ctx, "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN is set", TOKEN_HELP),
|
|
6798
|
+
credentialPrereq(ctx, "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_ACCOUNT_ID is set", ACCOUNT_HELP)
|
|
6559
6799
|
];
|
|
6560
6800
|
}
|
|
6561
6801
|
/**
|
|
@@ -6653,8 +6893,139 @@ async function offerWorkflowSetup(ctx) {
|
|
|
6653
6893
|
ctx.state.render.info(SECRETS_HELP);
|
|
6654
6894
|
}
|
|
6655
6895
|
/**
|
|
6656
|
-
*
|
|
6657
|
-
*
|
|
6896
|
+
* Read the taxonomy `code` off a thrown value, when present. Deploy errors carry a
|
|
6897
|
+
* `code` (e.g. `ERR_DEPLOY_PROJECT_NOT_FOUND`) so the wizard can tailor the fix hint.
|
|
6898
|
+
*
|
|
6899
|
+
* @param error - The thrown value.
|
|
6900
|
+
* @returns The `code` string, or `undefined` when absent.
|
|
6901
|
+
* @example
|
|
6902
|
+
* codeOf(deployError("ERR_DEPLOY_AUTH", "…")); // "ERR_DEPLOY_AUTH"
|
|
6903
|
+
*/
|
|
6904
|
+
function codeOf(error) {
|
|
6905
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
6906
|
+
const { code } = error;
|
|
6907
|
+
return typeof code === "string" ? code : void 0;
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
/**
|
|
6911
|
+
* A copy-pasteable "create the project yourself" hint, shown when the user declines the
|
|
6912
|
+
* offer to auto-create. Spells out that the remote project is what's missing (init only
|
|
6913
|
+
* scaffolds local config).
|
|
6914
|
+
*
|
|
6915
|
+
* @param name - The Cloudflare Pages project name (the deploy slug).
|
|
6916
|
+
* @returns The multi-line hint (newline-separated; rendered indented under a `›`).
|
|
6917
|
+
* @example
|
|
6918
|
+
* ctx.state.render.info(projectNotFoundHint("my-site"));
|
|
6919
|
+
*/
|
|
6920
|
+
function projectNotFoundHint(name) {
|
|
6921
|
+
return [
|
|
6922
|
+
"how to fix: the Cloudflare Pages project does not exist yet — create it once, then",
|
|
6923
|
+
"re-run `bun run deploy`. (app.deploy.init() only scaffolds local config; it does not",
|
|
6924
|
+
"create the remote project.)",
|
|
6925
|
+
` • CLI: bunx wrangler pages project create ${name} --production-branch main`,
|
|
6926
|
+
" • Dashboard: Cloudflare → Workers & Pages → Create → Pages"
|
|
6927
|
+
].join("\n");
|
|
6928
|
+
}
|
|
6929
|
+
/**
|
|
6930
|
+
* An actionable, error-specific "how to fix" hint for a failed deploy (other than the
|
|
6931
|
+
* project-not-found case, which the wizard handles interactively), so the user never
|
|
6932
|
+
* lands on a raw stack trace.
|
|
6933
|
+
*
|
|
6934
|
+
* @param error - The thrown deploy error.
|
|
6935
|
+
* @returns The fix hint line.
|
|
6936
|
+
* @example
|
|
6937
|
+
* ctx.state.render.info(deployFailureHint(err));
|
|
6938
|
+
*/
|
|
6939
|
+
function deployFailureHint$1(error) {
|
|
6940
|
+
const code = codeOf(error);
|
|
6941
|
+
if (code === "ERR_DEPLOY_AUTH" || code === "ERR_DEPLOY_AUTH_EXPIRED") return "how to fix: refresh CLOUDFLARE_API_TOKEN (scope: Account › Cloudflare Pages › Edit), then re-run `bun run deploy`.";
|
|
6942
|
+
if (code === "ERR_DEPLOY_NETWORK") return "how to fix: a network error reached Cloudflare — check connectivity, then re-run `bun run deploy`.";
|
|
6943
|
+
return "how to fix: resolve the error above, then re-run `bun run deploy`.";
|
|
6944
|
+
}
|
|
6945
|
+
/**
|
|
6946
|
+
* Render a styled deploy failure (✗ + fix hint) and return the `"failed"` outcome, so a
|
|
6947
|
+
* caught error surfaces consistently instead of as a raw throw.
|
|
6948
|
+
*
|
|
6949
|
+
* @param ctx - The cli plugin context.
|
|
6950
|
+
* @param error - The thrown deploy error.
|
|
6951
|
+
* @returns The `"failed"` deploy outcome.
|
|
6952
|
+
* @example
|
|
6953
|
+
* return renderFailure(ctx, error);
|
|
6954
|
+
*/
|
|
6955
|
+
function renderFailure(ctx, error) {
|
|
6956
|
+
ctx.state.render.error("deploy failed", error);
|
|
6957
|
+
ctx.state.render.info(deployFailureHint$1(error));
|
|
6958
|
+
return {
|
|
6959
|
+
deployed: false,
|
|
6960
|
+
reason: "failed"
|
|
6961
|
+
};
|
|
6962
|
+
}
|
|
6963
|
+
/**
|
|
6964
|
+
* Deploy once via the deploy plugin and wrap the result as a successful outcome. Throws
|
|
6965
|
+
* the classified deploy error on failure (the caller decides how to surface it).
|
|
6966
|
+
*
|
|
6967
|
+
* @param ctx - The cli plugin context.
|
|
6968
|
+
* @param options - The deploy options (branch override).
|
|
6969
|
+
* @returns The successful deploy outcome.
|
|
6970
|
+
* @throws {Error} With a `code` from the deploy error taxonomy on any failure.
|
|
6971
|
+
* @example
|
|
6972
|
+
* const outcome = await deployOnce(ctx, { branch: "main" });
|
|
6973
|
+
*/
|
|
6974
|
+
async function deployOnce(ctx, options) {
|
|
6975
|
+
return {
|
|
6976
|
+
deployed: true,
|
|
6977
|
+
...await ctx.require(deployPlugin).run(options.branch === void 0 ? {} : { branch: options.branch })
|
|
6978
|
+
};
|
|
6979
|
+
}
|
|
6980
|
+
/**
|
|
6981
|
+
* Handle a project-not-found deploy failure interactively: ask (a confirmation step)
|
|
6982
|
+
* before creating a real Cloudflare resource, create the Pages project via the deploy
|
|
6983
|
+
* plugin, then retry the deploy once. A declined offer (or a create failure) returns the
|
|
6984
|
+
* `"failed"` outcome with an actionable hint — never a raw stack trace.
|
|
6985
|
+
*
|
|
6986
|
+
* @param ctx - The cli plugin context.
|
|
6987
|
+
* @param options - The deploy options (branch override).
|
|
6988
|
+
* @param originalError - The project-not-found error from the first attempt.
|
|
6989
|
+
* @returns The deploy outcome (deployed after a successful create + retry, else failed).
|
|
6990
|
+
* @example
|
|
6991
|
+
* return createProjectThenRetry(ctx, options, error);
|
|
6992
|
+
*/
|
|
6993
|
+
async function createProjectThenRetry(ctx, options, originalError) {
|
|
6994
|
+
const deploy = ctx.require(deployPlugin);
|
|
6995
|
+
const name = deploy.projectName();
|
|
6996
|
+
ctx.state.render.warn(`The Cloudflare Pages project "${name}" does not exist yet.`);
|
|
6997
|
+
if (!await ctx.state.confirm(`Create the Cloudflare Pages project "${name}" now?`)) {
|
|
6998
|
+
ctx.state.render.error("deploy failed", originalError);
|
|
6999
|
+
ctx.state.render.info(projectNotFoundHint(name));
|
|
7000
|
+
return {
|
|
7001
|
+
deployed: false,
|
|
7002
|
+
reason: "failed"
|
|
7003
|
+
};
|
|
7004
|
+
}
|
|
7005
|
+
try {
|
|
7006
|
+
const created = await deploy.createProject();
|
|
7007
|
+
ctx.state.render.check(true, `created Cloudflare Pages project "${created.name}"`);
|
|
7008
|
+
} catch (error) {
|
|
7009
|
+
ctx.state.render.error("could not create the Pages project", error);
|
|
7010
|
+
ctx.state.render.info(deployFailureHint$1(error));
|
|
7011
|
+
return {
|
|
7012
|
+
deployed: false,
|
|
7013
|
+
reason: "failed"
|
|
7014
|
+
};
|
|
7015
|
+
}
|
|
7016
|
+
ctx.state.render.info("project created — retrying the deploy…");
|
|
7017
|
+
try {
|
|
7018
|
+
return await deployOnce(ctx, options);
|
|
7019
|
+
} catch (error) {
|
|
7020
|
+
return renderFailure(ctx, error);
|
|
7021
|
+
}
|
|
7022
|
+
}
|
|
7023
|
+
/**
|
|
7024
|
+
* Run the deploy step: confirm (unless `yes`), then deploy via the deploy plugin. A
|
|
7025
|
+
* declined confirm returns `{ deployed: false, reason: "declined" }`. A project-not-found
|
|
7026
|
+
* failure offers to create the project (with a confirmation step) and retries; any other
|
|
7027
|
+
* runtime failure is surfaced as a styled error + fix hint, returning
|
|
7028
|
+
* `{ deployed: false, reason: "failed" }` — never a raw stack trace.
|
|
6658
7029
|
*
|
|
6659
7030
|
* @param ctx - The cli plugin context.
|
|
6660
7031
|
* @param options - The deploy options (branch override + `yes`).
|
|
@@ -6671,10 +7042,12 @@ async function runDeployStep(ctx, options) {
|
|
|
6671
7042
|
reason: "declined"
|
|
6672
7043
|
};
|
|
6673
7044
|
}
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
7045
|
+
try {
|
|
7046
|
+
return await deployOnce(ctx, options);
|
|
7047
|
+
} catch (error) {
|
|
7048
|
+
if (codeOf(error) === "ERR_DEPLOY_PROJECT_NOT_FOUND") return createProjectThenRetry(ctx, options, error);
|
|
7049
|
+
return renderFailure(ctx, error);
|
|
7050
|
+
}
|
|
6678
7051
|
}
|
|
6679
7052
|
/**
|
|
6680
7053
|
* Run the guided deploy wizard end to end: diagnose prerequisites (offering to scaffold
|
|
@@ -6692,10 +7065,10 @@ async function runDeployStep(ctx, options) {
|
|
|
6692
7065
|
async function runDeployWizard(ctx, options) {
|
|
6693
7066
|
const cwd = process.cwd();
|
|
6694
7067
|
ctx.state.render.heading("Checking prerequisites");
|
|
6695
|
-
for (const item of diagnose(cwd)) ctx.state.render.check(item.ok, item.label, item.detail);
|
|
6696
|
-
await offerScaffold(ctx, diagnose(cwd));
|
|
7068
|
+
for (const item of diagnose(ctx, cwd)) ctx.state.render.check(item.ok, item.label, item.detail);
|
|
7069
|
+
await offerScaffold(ctx, diagnose(ctx, cwd));
|
|
6697
7070
|
await offerEnvScaffold(ctx, cwd);
|
|
6698
|
-
const blockers = diagnose(cwd).filter((item) => !item.ok);
|
|
7071
|
+
const blockers = diagnose(ctx, cwd).filter((item) => !item.ok);
|
|
6699
7072
|
if (blockers.length > 0) {
|
|
6700
7073
|
ctx.state.render.heading("Not ready to deploy");
|
|
6701
7074
|
for (const item of blockers) ctx.state.render.check(false, item.label, item.detail);
|
|
@@ -6712,7 +7085,7 @@ async function runDeployWizard(ctx, options) {
|
|
|
6712
7085
|
ctx.state.render.check(notFoundOk, `${ctx.config.notFoundFile} present`, notFoundOk ? void 0 : "Set build.notFound so the SSG emits it (CF Pages else flips to SPA mode).");
|
|
6713
7086
|
ctx.state.render.info("Tip: run `bun run preview` to eyeball the built site before deploying.");
|
|
6714
7087
|
const outcome = await runDeployStep(ctx, options);
|
|
6715
|
-
await offerWorkflowSetup(ctx);
|
|
7088
|
+
if (!(outcome.deployed === false && outcome.reason === "failed")) await offerWorkflowSetup(ctx);
|
|
6716
7089
|
return outcome;
|
|
6717
7090
|
}
|
|
6718
7091
|
//#endregion
|
|
@@ -6752,25 +7125,26 @@ function injectReloadClient(html) {
|
|
|
6752
7125
|
* Run one rebuild and report the result. Announces the start (`onRebuildStart`), then
|
|
6753
7126
|
* routes success to `onReloaded` and failure to `onError`.
|
|
6754
7127
|
*
|
|
6755
|
-
* @param input - The rebuild dependencies + the changed file.
|
|
6756
|
-
* @param input.runBuild - Runs one build and resolves with its summary.
|
|
7128
|
+
* @param input - The rebuild dependencies + the changed file/paths.
|
|
7129
|
+
* @param input.runBuild - Runs one build (given the changed paths) and resolves with its summary.
|
|
6757
7130
|
* @param input.onRebuildStart - Called with the changed file just before the build runs.
|
|
6758
|
-
* @param input.onReloaded - Called with the changed file + summary after a rebuild.
|
|
7131
|
+
* @param input.onReloaded - Called with the changed file + summary + the built `changed` set after a rebuild.
|
|
6759
7132
|
* @param input.onError - Called when a rebuild throws.
|
|
6760
7133
|
* @param input.file - The changed file to report alongside the summary.
|
|
7134
|
+
* @param input.changed - The accumulated changed paths handed to `runBuild` (incremental).
|
|
6761
7135
|
* @returns Resolves once the rebuild settles (always — errors are routed, not thrown).
|
|
6762
7136
|
* @example
|
|
6763
|
-
* await runOneRebuild({ runBuild, onReloaded, onError, file: "a.md" });
|
|
7137
|
+
* await runOneRebuild({ runBuild, onReloaded, onError, file: "a.md", changed: ["a.md"] });
|
|
6764
7138
|
*/
|
|
6765
7139
|
async function runOneRebuild(input) {
|
|
6766
7140
|
input.onRebuildStart?.(input.file);
|
|
6767
7141
|
try {
|
|
6768
|
-
const summary = await input.runBuild();
|
|
7142
|
+
const summary = await input.runBuild(input.changed);
|
|
6769
7143
|
input.onReloaded({
|
|
6770
7144
|
file: input.file,
|
|
6771
7145
|
pageCount: summary.pageCount,
|
|
6772
7146
|
durationMs: summary.durationMs
|
|
6773
|
-
});
|
|
7147
|
+
}, input.changed);
|
|
6774
7148
|
} catch (error) {
|
|
6775
7149
|
input.onError(error);
|
|
6776
7150
|
}
|
|
@@ -6784,9 +7158,9 @@ async function runOneRebuild(input) {
|
|
|
6784
7158
|
*
|
|
6785
7159
|
* @param input - The rebuild dependencies.
|
|
6786
7160
|
* @param input.debounceMs - Debounce window in milliseconds.
|
|
6787
|
-
* @param input.runBuild - Runs one build and resolves with its summary.
|
|
7161
|
+
* @param input.runBuild - Runs one build (given the changed paths) and resolves with its summary.
|
|
6788
7162
|
* @param input.onRebuildStart - Called with the changed file just before each build runs.
|
|
6789
|
-
* @param input.onReloaded - Called with the changed file + summary after a rebuild.
|
|
7163
|
+
* @param input.onReloaded - Called with the changed file + summary + the built `changed` set after a rebuild.
|
|
6790
7164
|
* @param input.onError - Called when a rebuild throws.
|
|
6791
7165
|
* @returns The debounced rebuild driver.
|
|
6792
7166
|
* @example
|
|
@@ -6795,12 +7169,13 @@ async function runOneRebuild(input) {
|
|
|
6795
7169
|
function createRebuilder(input) {
|
|
6796
7170
|
let timer;
|
|
6797
7171
|
let pendingFile = "";
|
|
7172
|
+
const pendingChanged = /* @__PURE__ */ new Set();
|
|
6798
7173
|
let building = false;
|
|
6799
7174
|
let dirty = false;
|
|
6800
7175
|
/**
|
|
6801
|
-
* Rebuild repeatedly until no change arrived mid-flight: each pass clears
|
|
6802
|
-
* runs one build, then loops again if a
|
|
6803
|
-
* no change is dropped.
|
|
7176
|
+
* Rebuild repeatedly until no change arrived mid-flight: each pass snapshots + clears
|
|
7177
|
+
* the accumulated changed paths, runs one build over them, then loops again if a
|
|
7178
|
+
* `schedule()` set `dirty` (and added more paths) while it ran, so no change is dropped.
|
|
6804
7179
|
*
|
|
6805
7180
|
* @returns Resolves once a pass completes with no pending change (errors are routed,
|
|
6806
7181
|
* never thrown).
|
|
@@ -6810,12 +7185,15 @@ function createRebuilder(input) {
|
|
|
6810
7185
|
const drainPendingRebuilds = async () => {
|
|
6811
7186
|
do {
|
|
6812
7187
|
dirty = false;
|
|
7188
|
+
const changed = [...pendingChanged];
|
|
7189
|
+
pendingChanged.clear();
|
|
6813
7190
|
await runOneRebuild({
|
|
6814
7191
|
runBuild: input.runBuild,
|
|
6815
7192
|
...input.onRebuildStart ? { onRebuildStart: input.onRebuildStart } : {},
|
|
6816
7193
|
onReloaded: input.onReloaded,
|
|
6817
7194
|
onError: input.onError,
|
|
6818
|
-
file: pendingFile
|
|
7195
|
+
file: pendingFile,
|
|
7196
|
+
changed
|
|
6819
7197
|
});
|
|
6820
7198
|
} while (dirty);
|
|
6821
7199
|
};
|
|
@@ -6842,14 +7220,15 @@ function createRebuilder(input) {
|
|
|
6842
7220
|
};
|
|
6843
7221
|
return {
|
|
6844
7222
|
/**
|
|
6845
|
-
* Queue a rebuild for the given
|
|
7223
|
+
* Queue a rebuild for the given changed path (debounced + coalesced + accumulated).
|
|
6846
7224
|
*
|
|
6847
|
-
* @param file - The
|
|
7225
|
+
* @param file - The changed path reported as `ReloadInfo.file` and added to the changed set.
|
|
6848
7226
|
* @example
|
|
6849
|
-
* rebuilder.schedule("content");
|
|
7227
|
+
* rebuilder.schedule("content/intro/en.md");
|
|
6850
7228
|
*/
|
|
6851
7229
|
schedule(file) {
|
|
6852
7230
|
pendingFile = file;
|
|
7231
|
+
pendingChanged.add(file);
|
|
6853
7232
|
if (timer) clearTimeout(timer);
|
|
6854
7233
|
timer = setTimeout(fire, input.debounceMs);
|
|
6855
7234
|
},
|
|
@@ -6879,26 +7258,33 @@ function isNoisePath(filename) {
|
|
|
6879
7258
|
return filename.split(/[/\\]/).some((segment) => segment.startsWith(".")) || filename.endsWith("~");
|
|
6880
7259
|
}
|
|
6881
7260
|
/**
|
|
6882
|
-
* Create a {@link ChangeGate} that drops
|
|
6883
|
-
*
|
|
6884
|
-
* `outDir` (the build's own output — a loop guard),
|
|
6885
|
-
*
|
|
6886
|
-
*
|
|
6887
|
-
*
|
|
6888
|
-
*
|
|
6889
|
-
*
|
|
7261
|
+
* Create a {@link ChangeGate} that drops four kinds of spurious change events before they
|
|
7262
|
+
* reach the debounced rebuilder: editor/OS noise (dotfiles, backups), writes under
|
|
7263
|
+
* `outDir` (the build's own output — a loop guard), the stale duplicate/parent-dir echoes
|
|
7264
|
+
* macOS fires for one save (a build-start high-water mark — a change whose mtime is at or
|
|
7265
|
+
* before the last build we started was already captured), and — when a `fileHash` seam is
|
|
7266
|
+
* supplied — a NO-OP save whose bytes are identical to the last successfully-built version
|
|
7267
|
+
* (a double Ctrl-S, a `touch`, a format-on-save that reverts). The no-op baseline is
|
|
7268
|
+
* recorded ONLY by {@link ChangeGate.commitBuilt} on build SUCCESS, scoped to that build's
|
|
7269
|
+
* paths — so a failed build commits nothing (a retry save always rebuilds) and a file
|
|
7270
|
+
* edited mid-build is never falsely baselined by another file's success. A genuinely newer
|
|
7271
|
+
* edit (even mid-build) and a deletion (missing file) always pass.
|
|
6890
7272
|
*
|
|
6891
7273
|
* @param input - The gate dependencies.
|
|
6892
7274
|
* @param input.outDir - The build output directory whose writes must never re-trigger a build.
|
|
6893
7275
|
* @param input.fileMtime - Resolves a path's mtime in ms (or `null` when missing).
|
|
6894
7276
|
* @param input.now - Monotonic wall clock (ms) used for the build-start high-water mark.
|
|
7277
|
+
* @param input.fileHash - Resolves a path's content hash (or `null` when missing). Optional;
|
|
7278
|
+
* defaults to `() => null`, which disables the no-op-save short-circuit (every edit passes).
|
|
6895
7279
|
* @returns The change gate.
|
|
6896
7280
|
* @example
|
|
6897
|
-
* const gate = createChangeGate({ outDir: "dist", fileMtime: state.fileMtime, now: state.clock });
|
|
7281
|
+
* const gate = createChangeGate({ outDir: "dist", fileMtime: state.fileMtime, now: state.clock, fileHash: state.fileHash });
|
|
6898
7282
|
*/
|
|
6899
7283
|
function createChangeGate(input) {
|
|
6900
7284
|
const outDirAbs = node_path$1.default.resolve(input.outDir);
|
|
7285
|
+
const fileHash = input.fileHash ?? (() => null);
|
|
6901
7286
|
let lastBuildStartedAt = input.now();
|
|
7287
|
+
const committedHash = /* @__PURE__ */ new Map();
|
|
6902
7288
|
return {
|
|
6903
7289
|
/**
|
|
6904
7290
|
* Decide whether a change beneath `dir` warrants a rebuild (see {@link ChangeGate.accept}).
|
|
@@ -6916,6 +7302,12 @@ function createChangeGate(input) {
|
|
|
6916
7302
|
if (changed === outDirAbs || changed.startsWith(`${outDirAbs}${node_path$1.default.sep}`)) return false;
|
|
6917
7303
|
const mtime = input.fileMtime(changed);
|
|
6918
7304
|
if (mtime !== null && mtime < lastBuildStartedAt) return false;
|
|
7305
|
+
const hash = fileHash(changed);
|
|
7306
|
+
if (hash === null) {
|
|
7307
|
+
committedHash.delete(changed);
|
|
7308
|
+
return true;
|
|
7309
|
+
}
|
|
7310
|
+
if (committedHash.get(changed) === hash) return false;
|
|
6919
7311
|
return true;
|
|
6920
7312
|
},
|
|
6921
7313
|
/**
|
|
@@ -6926,6 +7318,20 @@ function createChangeGate(input) {
|
|
|
6926
7318
|
*/
|
|
6927
7319
|
markBuildStart() {
|
|
6928
7320
|
lastBuildStartedAt = input.now();
|
|
7321
|
+
},
|
|
7322
|
+
/**
|
|
7323
|
+
* Baseline exactly the paths the just-succeeded build consumed (see {@link ChangeGate.commitBuilt}).
|
|
7324
|
+
*
|
|
7325
|
+
* @param changed - The paths the just-succeeded build consumed.
|
|
7326
|
+
* @example
|
|
7327
|
+
* gate.commitBuilt(["content/intro/en.md"]);
|
|
7328
|
+
*/
|
|
7329
|
+
commitBuilt(changed) {
|
|
7330
|
+
for (const file of changed) {
|
|
7331
|
+
const key = node_path$1.default.resolve(file);
|
|
7332
|
+
const hash = fileHash(key);
|
|
7333
|
+
if (hash !== null) committedHash.set(key, hash);
|
|
7334
|
+
}
|
|
6929
7335
|
}
|
|
6930
7336
|
};
|
|
6931
7337
|
}
|
|
@@ -7118,19 +7524,46 @@ function createDevHandler(ctx, hub) {
|
|
|
7118
7524
|
};
|
|
7119
7525
|
}
|
|
7120
7526
|
/**
|
|
7527
|
+
* Build the per-run {@link BuildRunOverrides} for a dev build from the session feature
|
|
7528
|
+
* opt-ins: minification is always off in dev (no benefit, slower), and each expensive,
|
|
7529
|
+
* NON-navigational output stays off unless its flag re-enables it (`ogImage: false`
|
|
7530
|
+
* disables OG generation regardless of the persisted config). Locale-redirects are NOT
|
|
7531
|
+
* overridden — they produce navigable pages (the bare `/` → `/{defaultLocale}/` redirect),
|
|
7532
|
+
* so they follow the app's own config. The persisted plugin config is never mutated.
|
|
7533
|
+
*
|
|
7534
|
+
* @param features - The resolved per-session dev feature opt-ins.
|
|
7535
|
+
* @returns The config overrides merged into the dev build run.
|
|
7536
|
+
* @example
|
|
7537
|
+
* devBuildOverrides({ og: false, sitemap: false, feeds: false });
|
|
7538
|
+
*/
|
|
7539
|
+
function devBuildOverrides(features) {
|
|
7540
|
+
return {
|
|
7541
|
+
minify: false,
|
|
7542
|
+
...features.feeds ? {} : { feeds: false },
|
|
7543
|
+
...features.sitemap ? {} : { sitemap: false },
|
|
7544
|
+
...features.og ? {} : { ogImage: false }
|
|
7545
|
+
};
|
|
7546
|
+
}
|
|
7547
|
+
/**
|
|
7121
7548
|
* Run the dev loop: an initial build, an in-process static server that injects the
|
|
7122
7549
|
* live-reload client, a recursive watcher over `config.watchDirs`, and a debounced
|
|
7123
7550
|
* rebuild that re-renders and pushes a browser reload. Resolves on SIGINT/SIGTERM,
|
|
7124
|
-
* which stops the server, closes the watchers, and cancels any pending rebuild.
|
|
7551
|
+
* which stops the server, closes the watchers, and cancels any pending rebuild. The dev
|
|
7552
|
+
* build disables minification + expensive outputs (per {@link devBuildOverrides}); every
|
|
7553
|
+
* rebuild also skips the clean so caches + unchanged assets survive (no mid-rebuild 404).
|
|
7554
|
+
* Because rebuilds skip the clean, a DELETED or renamed content slug's stale page lingers
|
|
7555
|
+
* (and is served) until you restart `serve` or run a production `build`.
|
|
7125
7556
|
*
|
|
7126
7557
|
* @param ctx - The cli plugin context (config, state seams, `require`).
|
|
7127
7558
|
* @param port - The port to bind the dev server to.
|
|
7559
|
+
* @param features - Per-session dev feature opt-ins (`og`/`sitemap`/`feeds`/`localeRedirects`).
|
|
7128
7560
|
* @returns Resolves once the server has been torn down by a termination signal.
|
|
7129
7561
|
* @example
|
|
7130
|
-
* await runDevServer(ctx, 4173);
|
|
7562
|
+
* await runDevServer(ctx, 4173, { og: false, sitemap: false, feeds: false, localeRedirects: false });
|
|
7131
7563
|
*/
|
|
7132
|
-
async function runDevServer(ctx, port) {
|
|
7133
|
-
|
|
7564
|
+
async function runDevServer(ctx, port, features) {
|
|
7565
|
+
const overrides = devBuildOverrides(features);
|
|
7566
|
+
await ctx.require(buildPlugin).run({ overrides });
|
|
7134
7567
|
const hub = createReloadHub();
|
|
7135
7568
|
const server = ctx.state.serveStatic({
|
|
7136
7569
|
port,
|
|
@@ -7140,19 +7573,27 @@ async function runDevServer(ctx, port) {
|
|
|
7140
7573
|
const gate = createChangeGate({
|
|
7141
7574
|
outDir: ctx.config.outDir,
|
|
7142
7575
|
fileMtime: ctx.state.fileMtime,
|
|
7143
|
-
now: ctx.state.clock
|
|
7576
|
+
now: ctx.state.clock,
|
|
7577
|
+
fileHash: ctx.state.fileHash
|
|
7144
7578
|
});
|
|
7145
7579
|
const rebuilder = createRebuilder({
|
|
7146
7580
|
debounceMs: ctx.config.debounceMs,
|
|
7147
7581
|
/**
|
|
7148
|
-
* Re-run the SSG build for a rebuild
|
|
7582
|
+
* Re-run the SSG build for a rebuild: skip the clean so the prior assets + on-disk
|
|
7583
|
+
* caches survive (and no in-flight request hits an empty outDir), with the dev
|
|
7584
|
+
* overrides applied.
|
|
7149
7585
|
*
|
|
7586
|
+
* @param changed - The paths changed since the last build (incremental rebuild hint).
|
|
7150
7587
|
* @returns The rebuild summary.
|
|
7151
7588
|
* @example
|
|
7152
|
-
* await runBuild();
|
|
7589
|
+
* await runBuild(["content/intro/en.md"]);
|
|
7153
7590
|
*/
|
|
7154
|
-
runBuild() {
|
|
7155
|
-
return ctx.require(buildPlugin).run(
|
|
7591
|
+
runBuild(changed) {
|
|
7592
|
+
return ctx.require(buildPlugin).run({
|
|
7593
|
+
skipClean: true,
|
|
7594
|
+
overrides,
|
|
7595
|
+
changed
|
|
7596
|
+
});
|
|
7156
7597
|
},
|
|
7157
7598
|
/**
|
|
7158
7599
|
* Show the compact in-place "rebuilding {label}" line before the build runs.
|
|
@@ -7169,15 +7610,18 @@ async function runDevServer(ctx, port) {
|
|
|
7169
7610
|
* Render the reload line and push a browser reload after a rebuild.
|
|
7170
7611
|
*
|
|
7171
7612
|
* @param info - The changed file plus the rebuild's page count and duration.
|
|
7613
|
+
* @param changed - The paths this successful build consumed (baselined for no-op drops).
|
|
7172
7614
|
* @example
|
|
7173
|
-
* onReloaded({ file: "a.md", pageCount: 1, durationMs: 10 });
|
|
7615
|
+
* onReloaded({ file: "a.md", pageCount: 1, durationMs: 10 }, ["content/a.md"]);
|
|
7174
7616
|
*/
|
|
7175
|
-
onReloaded(info) {
|
|
7617
|
+
onReloaded(info, changed) {
|
|
7618
|
+
gate.commitBuilt(changed);
|
|
7176
7619
|
ctx.state.render.reload(info);
|
|
7177
7620
|
hub.reloadAll();
|
|
7178
7621
|
},
|
|
7179
7622
|
/**
|
|
7180
|
-
* Render a rebuild failure (the dev loop keeps running).
|
|
7623
|
+
* Render a rebuild failure (the dev loop keeps running). A failed build baselines
|
|
7624
|
+
* nothing (commitBuilt only runs on success), so an identical retry save still rebuilds.
|
|
7181
7625
|
*
|
|
7182
7626
|
* @param error - The thrown rebuild error.
|
|
7183
7627
|
* @example
|
|
@@ -7188,7 +7632,8 @@ async function runDevServer(ctx, port) {
|
|
|
7188
7632
|
}
|
|
7189
7633
|
});
|
|
7190
7634
|
const watchers = ctx.config.watchDirs.map((dir) => ctx.state.watch(dir, (filename) => {
|
|
7191
|
-
if (gate.accept(dir, filename))
|
|
7635
|
+
if (!gate.accept(dir, filename)) return;
|
|
7636
|
+
rebuilder.schedule(filename === void 0 ? dir : node_path$1.default.join(dir, filename));
|
|
7192
7637
|
}));
|
|
7193
7638
|
ctx.state.render.serverReady({
|
|
7194
7639
|
local: `http://localhost:${port}`,
|
|
@@ -7455,17 +7900,25 @@ function createApi$1(ctx) {
|
|
|
7455
7900
|
},
|
|
7456
7901
|
/**
|
|
7457
7902
|
* Dev loop: build once, serve `dist/` in-process (live-reload injected), watch
|
|
7458
|
-
* `watchDirs`, debounced rebuild + reload.
|
|
7903
|
+
* `watchDirs`, debounced + incremental rebuild + reload. For a fast rebuild the dev
|
|
7904
|
+
* build disables minification + expensive, NON-navigational outputs (feeds / sitemap /
|
|
7905
|
+
* og-images); pass `og`/`sitemap`/`feeds` to re-enable any of them for the session.
|
|
7906
|
+
* Locale-redirects are always built per the app config (they emit the navigable bare-path
|
|
7907
|
+
* `/` → `/{defaultLocale}/` redirect). Resolves on SIGINT/SIGTERM.
|
|
7459
7908
|
*
|
|
7460
|
-
* @param options - Optional port override
|
|
7909
|
+
* @param options - Optional port override + per-session dev feature opt-ins.
|
|
7461
7910
|
* @returns Resolves once the server has been torn down.
|
|
7462
7911
|
* @example
|
|
7463
|
-
* await api.serve({ port: 3000 });
|
|
7912
|
+
* await api.serve({ port: 3000, og: true });
|
|
7464
7913
|
*/
|
|
7465
7914
|
serve(options = {}) {
|
|
7466
7915
|
const { port = ctx.config.port } = options;
|
|
7467
7916
|
ctx.state.render.header("serve");
|
|
7468
|
-
return runDevServer(ctx, port
|
|
7917
|
+
return runDevServer(ctx, port, {
|
|
7918
|
+
og: options.og ?? false,
|
|
7919
|
+
sitemap: options.sitemap ?? false,
|
|
7920
|
+
feeds: options.feeds ?? false
|
|
7921
|
+
});
|
|
7469
7922
|
},
|
|
7470
7923
|
/**
|
|
7471
7924
|
* Static preview of the built `dist/` with CF-Pages clean-URL resolution.
|
|
@@ -8548,6 +9001,23 @@ function defaultFileMtime(filePath) {
|
|
|
8548
9001
|
}
|
|
8549
9002
|
}
|
|
8550
9003
|
/**
|
|
9004
|
+
* Default file-content-hash probe — `sha256` of the file bytes, returning `null` for a
|
|
9005
|
+
* missing/unreadable path. serve()'s change gate compares this against the last
|
|
9006
|
+
* successfully-built bytes to drop a no-op save (a byte-identical double Ctrl-S).
|
|
9007
|
+
*
|
|
9008
|
+
* @param filePath - The absolute path to hash.
|
|
9009
|
+
* @returns The hex SHA-256 of the file's bytes, or `null` when it cannot be read.
|
|
9010
|
+
* @example
|
|
9011
|
+
* const hash = defaultFileHash("/abs/content/a.md");
|
|
9012
|
+
*/
|
|
9013
|
+
function defaultFileHash(filePath) {
|
|
9014
|
+
try {
|
|
9015
|
+
return (0, node_crypto.createHash)("sha256").update((0, node_fs.readFileSync)(filePath)).digest("hex");
|
|
9016
|
+
} catch {
|
|
9017
|
+
return null;
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
/**
|
|
8551
9021
|
* Default LAN network-URL deriver — wraps {@link networkUrl} so the production seam
|
|
8552
9022
|
* reads `node:os` interfaces while tests can inject a deterministic value.
|
|
8553
9023
|
*
|
|
@@ -8680,7 +9150,8 @@ function createState$1(_ctx) {
|
|
|
8680
9150
|
serveStatic: defaultServeStatic,
|
|
8681
9151
|
fileResponse: defaultFileResponse,
|
|
8682
9152
|
networkUrl: defaultNetworkUrl,
|
|
8683
|
-
fileMtime: defaultFileMtime
|
|
9153
|
+
fileMtime: defaultFileMtime,
|
|
9154
|
+
fileHash: defaultFileHash
|
|
8684
9155
|
};
|
|
8685
9156
|
}
|
|
8686
9157
|
//#endregion
|