@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.mjs
CHANGED
|
@@ -1282,18 +1282,24 @@ function isPublished(article, isProduction) {
|
|
|
1282
1282
|
* locale collection: existing files only, drafts dropped in production, sorted
|
|
1283
1283
|
* date-descending. The single load+filter+sort step behind {@link createContentApi.loadAll}.
|
|
1284
1284
|
*
|
|
1285
|
+
* When a `cached` map is supplied (incremental dev rebuild), a slug already present in it
|
|
1286
|
+
* is reused as-is (skipping the re-read + Markdown/Shiki re-render); only slugs absent
|
|
1287
|
+
* from it — the ones a preceding `invalidate()` dropped, plus any never-loaded — are
|
|
1288
|
+
* resolved fresh. With no `cached` map every slug is resolved (the full load).
|
|
1289
|
+
*
|
|
1285
1290
|
* @param ctx - Kernel-free domain context (provider + i18n helpers + stage).
|
|
1286
1291
|
* @param slugs - Every known article slug from the provider.
|
|
1287
1292
|
* @param locale - The locale to resolve and collect.
|
|
1293
|
+
* @param cached - Optional per-locale article cache to reuse for unchanged slugs.
|
|
1288
1294
|
* @returns The published (date-descending) articles for this locale.
|
|
1289
1295
|
* @example
|
|
1290
1296
|
* ```ts
|
|
1291
1297
|
* const present = await loadAndFilterArticles(ctx, slugs, "en");
|
|
1292
1298
|
* ```
|
|
1293
1299
|
*/
|
|
1294
|
-
async function loadAndFilterArticles(ctx, slugs, locale) {
|
|
1300
|
+
async function loadAndFilterArticles(ctx, slugs, locale, cached) {
|
|
1295
1301
|
const isProduction = ctx.global.stage === "production";
|
|
1296
|
-
return (await Promise.all(slugs.map((slug) => resolveArticle(ctx, slug, locale)))).filter((article) => article !== null).filter((article) => isPublished(article, isProduction)).toSorted(byDateDescending);
|
|
1302
|
+
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);
|
|
1297
1303
|
}
|
|
1298
1304
|
/**
|
|
1299
1305
|
* Derive the article slug from a source file path — the parent directory name
|
|
@@ -1330,19 +1336,31 @@ function createContentApi(ctx) {
|
|
|
1330
1336
|
* Load every article across every active locale (locale fallback, production
|
|
1331
1337
|
* draft exclusion, date sort, `contentId` after sort), cache them, emit `content:ready`.
|
|
1332
1338
|
*
|
|
1339
|
+
* Cache-first by default: repeated calls return the per-build memo (list-route loaders
|
|
1340
|
+
* call this once PER PAGE — without the memo every page would re-read + re-render every
|
|
1341
|
+
* article, the dev-loop killer), and a rebuild after `invalidate()` re-resolves only the
|
|
1342
|
+
* dropped slugs while reusing the cached articles for the rest (`contentId` ordinals are
|
|
1343
|
+
* still recomputed across the FULL sorted set, so ids + order match a full load). Pass
|
|
1344
|
+
* `{ reuse: false }` to force a FRESH full reload (cold build / an unclassifiable change
|
|
1345
|
+
* where the caller cannot pinpoint what changed) — this bypasses the memo + per-slug cache.
|
|
1346
|
+
*
|
|
1347
|
+
* @param options - Optional load behaviour (`reuse`, default `true`).
|
|
1333
1348
|
* @returns A locale-keyed map of date-descending articles.
|
|
1334
1349
|
* @example
|
|
1335
1350
|
* ```ts
|
|
1336
1351
|
* const byLocale = await api.loadAll();
|
|
1337
1352
|
* ```
|
|
1338
1353
|
*/
|
|
1339
|
-
async loadAll() {
|
|
1354
|
+
async loadAll(options) {
|
|
1355
|
+
const reuse = options?.reuse !== false;
|
|
1356
|
+
const memo = ctx.state.loadedAll;
|
|
1357
|
+
if (reuse && memo !== null) return memo;
|
|
1340
1358
|
const slugs = await ctx.provider.slugs();
|
|
1341
1359
|
const locales = ctx.locales();
|
|
1342
1360
|
const result = /* @__PURE__ */ new Map();
|
|
1343
1361
|
let total = 0;
|
|
1344
1362
|
for (const locale of locales) {
|
|
1345
|
-
const present = await loadAndFilterArticles(ctx, slugs, locale);
|
|
1363
|
+
const present = await loadAndFilterArticles(ctx, slugs, locale, reuse ? ctx.state.articles.get(locale) : void 0);
|
|
1346
1364
|
const cache = /* @__PURE__ */ new Map();
|
|
1347
1365
|
let index = 0;
|
|
1348
1366
|
for (const article of present) {
|
|
@@ -1354,6 +1372,7 @@ function createContentApi(ctx) {
|
|
|
1354
1372
|
result.set(locale, present);
|
|
1355
1373
|
total += present.length;
|
|
1356
1374
|
}
|
|
1375
|
+
ctx.state.loadedAll = result;
|
|
1357
1376
|
ctx.emit("content:ready", {
|
|
1358
1377
|
locales,
|
|
1359
1378
|
articleCount: total
|
|
@@ -1417,6 +1436,7 @@ function createContentApi(ctx) {
|
|
|
1417
1436
|
if (slug === void 0) continue;
|
|
1418
1437
|
for (const cache of ctx.state.articles.values()) cache.delete(slug);
|
|
1419
1438
|
}
|
|
1439
|
+
if (accepted.length > 0) ctx.state.loadedAll = null;
|
|
1420
1440
|
ctx.emit("content:invalidated", { paths: accepted });
|
|
1421
1441
|
},
|
|
1422
1442
|
/**
|
|
@@ -1486,14 +1506,17 @@ const contentEvents = (register) => ({
|
|
|
1486
1506
|
* @param _ctx - Minimal context with global and config.
|
|
1487
1507
|
* @param _ctx.global - Global plugin registry.
|
|
1488
1508
|
* @param _ctx.config - Resolved plugin configuration.
|
|
1489
|
-
* @returns Fresh content shell state: an empty article cache.
|
|
1509
|
+
* @returns Fresh content shell state: an empty article cache + an empty loadAll memo.
|
|
1490
1510
|
* @example
|
|
1491
1511
|
* ```ts
|
|
1492
1512
|
* const state = createContentState({ global: {}, config: { providers: [] } });
|
|
1493
1513
|
* ```
|
|
1494
1514
|
*/
|
|
1495
1515
|
function createContentState(_ctx) {
|
|
1496
|
-
return {
|
|
1516
|
+
return {
|
|
1517
|
+
articles: /* @__PURE__ */ new Map(),
|
|
1518
|
+
loadedAll: null
|
|
1519
|
+
};
|
|
1497
1520
|
}
|
|
1498
1521
|
//#endregion
|
|
1499
1522
|
//#region src/plugins/content/validate.ts
|
|
@@ -3343,7 +3366,11 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
|
3343
3366
|
/**
|
|
3344
3367
|
* Bundles CSS and JS into the output directory via two separate runner passes
|
|
3345
3368
|
* (dodging Bun's mixed-entrypoint segfault), honoring `config.minify`, and caches
|
|
3346
|
-
* the resulting hashed asset paths in `state.buildCache` for downstream phases.
|
|
3369
|
+
* the resulting hashed asset paths in `state.buildCache` for downstream phases. The
|
|
3370
|
+
* two passes run CONCURRENTLY (`Promise.all`) — they target disjoint hashed outputs
|
|
3371
|
+
* and distinct `buildCache` keys (`css`/`js`), so overlapping them ~halves bundle
|
|
3372
|
+
* wall-time with no shared-state hazard. The CSS pass is still dispatched first, so
|
|
3373
|
+
* the runner's invocation order stays css-then-js.
|
|
3347
3374
|
*
|
|
3348
3375
|
* @param ctx - Plugin context (provides `state`, `config`, `log`).
|
|
3349
3376
|
* @param options - Optional dependency-injection seam (runner + entrypoints).
|
|
@@ -3356,10 +3383,10 @@ async function runOne(ctx, runner, kind, entrypoints, outDir, outdir, minify) {
|
|
|
3356
3383
|
async function bundle(ctx, options = {}) {
|
|
3357
3384
|
const runner = options.runner ?? defaultRunner;
|
|
3358
3385
|
const { minify, outDir } = ctx.config;
|
|
3386
|
+
const assetsDir = path.join(outDir, "assets");
|
|
3359
3387
|
const cssEntrypoints = options.cssEntrypoints ?? resolveEntrypoints(CSS_ENTRY_CANDIDATES);
|
|
3360
3388
|
const jsEntrypoints = options.jsEntrypoints ?? resolveJsEntrypoints(ctx);
|
|
3361
|
-
await runOne(ctx, runner, "css", cssEntrypoints, outDir,
|
|
3362
|
-
await runOne(ctx, runner, "js", jsEntrypoints, outDir, path.join(outDir, "assets"), minify);
|
|
3389
|
+
await Promise.all([runOne(ctx, runner, "css", cssEntrypoints, outDir, assetsDir, minify), runOne(ctx, runner, "js", jsEntrypoints, outDir, assetsDir, minify)]);
|
|
3363
3390
|
}
|
|
3364
3391
|
//#endregion
|
|
3365
3392
|
//#region src/plugins/build/phases/content.ts
|
|
@@ -3376,15 +3403,24 @@ const CONTENT_CACHE_KEY = "content";
|
|
|
3376
3403
|
* pages/feeds/og-images phases. Performs NO Markdown parsing itself — the
|
|
3377
3404
|
* content plugin owns rendering (god-plugin invariant).
|
|
3378
3405
|
*
|
|
3406
|
+
* On a dev incremental rebuild (`options.changed` set) it first `invalidate()`s the
|
|
3407
|
+
* changed Markdown so `loadAll({ reuse: true })` re-reads + re-renders ONLY those
|
|
3408
|
+
* articles, reusing the cached HTML for the rest. With no options it does a full load.
|
|
3409
|
+
*
|
|
3379
3410
|
* @param ctx - Plugin context (provides `require`, `state`, `log`).
|
|
3411
|
+
* @param options - Optional incremental hints; omit for a full load.
|
|
3412
|
+
* @param options.reuse - Reuse cached content for slugs not invalidated (dev incremental rebuild).
|
|
3413
|
+
* @param options.changed - The changed Markdown paths to invalidate before loading.
|
|
3380
3414
|
* @returns The locale-keyed article map returned by the content plugin.
|
|
3381
3415
|
* @example
|
|
3382
3416
|
* ```ts
|
|
3383
3417
|
* const byLocale = await loadContent(ctx);
|
|
3384
3418
|
* ```
|
|
3385
3419
|
*/
|
|
3386
|
-
async function loadContent(ctx) {
|
|
3387
|
-
const
|
|
3420
|
+
async function loadContent(ctx, options) {
|
|
3421
|
+
const content = ctx.require(contentPlugin);
|
|
3422
|
+
if (options?.changed && options.changed.length > 0) content.invalidate(options.changed);
|
|
3423
|
+
const byLocale = await content.loadAll({ reuse: options?.reuse ?? false });
|
|
3388
3424
|
ctx.state.buildCache.set(CONTENT_CACHE_KEY, byLocale);
|
|
3389
3425
|
ctx.log.debug("build:content", { locales: byLocale.size });
|
|
3390
3426
|
return byLocale;
|
|
@@ -4738,6 +4774,71 @@ function renderBody(definition, routeContext) {
|
|
|
4738
4774
|
return renderToString(definition._handlers.layout ? definition._handlers.layout(layoutContext, vnode) : vnode);
|
|
4739
4775
|
}
|
|
4740
4776
|
/**
|
|
4777
|
+
* Hash a page's render inputs (its loaded data) for the render cache. `null` when the
|
|
4778
|
+
* data is not JSON-serializable — such a page is never cached and always re-renders.
|
|
4779
|
+
*
|
|
4780
|
+
* @param data - The route's loaded data (the only per-page input besides params/locale/code).
|
|
4781
|
+
* @returns The hex SHA-256 of the serialized data, or `null` when it cannot be serialized.
|
|
4782
|
+
* @example
|
|
4783
|
+
* ```ts
|
|
4784
|
+
* hashData({ title: "Hi" }); // "9f8e…"
|
|
4785
|
+
* ```
|
|
4786
|
+
*/
|
|
4787
|
+
function hashData(data) {
|
|
4788
|
+
try {
|
|
4789
|
+
const serialized = JSON.stringify(data) ?? "";
|
|
4790
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
4791
|
+
} catch {
|
|
4792
|
+
return null;
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
/**
|
|
4796
|
+
* The render-cache key for one page instance: name + params + locale (the stable identity
|
|
4797
|
+
* that, together with the data hash, determines its body). NUL-joined so no value collides.
|
|
4798
|
+
*
|
|
4799
|
+
* @param instance - The page instance.
|
|
4800
|
+
* @returns The cache key string.
|
|
4801
|
+
* @example
|
|
4802
|
+
* ```ts
|
|
4803
|
+
* renderCacheKey(instance); // "article{\"slug\":\"x\"}en"
|
|
4804
|
+
* ```
|
|
4805
|
+
*/
|
|
4806
|
+
function renderCacheKey(instance) {
|
|
4807
|
+
return `${instance.name}${JSON.stringify(instance.params)}${instance.locale}`;
|
|
4808
|
+
}
|
|
4809
|
+
/**
|
|
4810
|
+
* Render one page's body, reusing the cached body when this page's data is unchanged.
|
|
4811
|
+
* The body is the synchronous, dominant-cost step ({@link renderBody}); an incremental
|
|
4812
|
+
* dev rebuild (`reuse`, code unchanged) skips it for every page whose data hash matches
|
|
4813
|
+
* the cache, and a changed page (or a non-`reuse` run) renders + refreshes the cache.
|
|
4814
|
+
*
|
|
4815
|
+
* @param ctx - Plugin context (provides the cross-run `state.renderCache`).
|
|
4816
|
+
* @param instance - The page instance being rendered.
|
|
4817
|
+
* @param routeContext - The route context passed to `.render()`/`.layout()`.
|
|
4818
|
+
* @param data - The route's loaded data (hashed to detect a change).
|
|
4819
|
+
* @param reuse - Whether this run may reuse a cached body (incremental, no code change).
|
|
4820
|
+
* @returns The SSR-rendered body HTML.
|
|
4821
|
+
* @example
|
|
4822
|
+
* ```ts
|
|
4823
|
+
* const body = renderBodyCached(ctx, instance, routeContext, data, true);
|
|
4824
|
+
* ```
|
|
4825
|
+
*/
|
|
4826
|
+
function renderBodyCached(ctx, instance, routeContext, data, reuse) {
|
|
4827
|
+
const cache = ctx.state.renderCache;
|
|
4828
|
+
const key = renderCacheKey(instance);
|
|
4829
|
+
const hash = hashData(data);
|
|
4830
|
+
if (reuse && hash !== null) {
|
|
4831
|
+
const hit = cache.get(key);
|
|
4832
|
+
if (hit?.dataHash === hash) return hit.body;
|
|
4833
|
+
}
|
|
4834
|
+
const body = renderBody(instance.definition, routeContext);
|
|
4835
|
+
if (hash !== null) cache.set(key, {
|
|
4836
|
+
dataHash: hash,
|
|
4837
|
+
body
|
|
4838
|
+
});
|
|
4839
|
+
return body;
|
|
4840
|
+
}
|
|
4841
|
+
/**
|
|
4741
4842
|
* Write a rendered page document to its on-disk path. The path comes from the
|
|
4742
4843
|
* compiled `TypedRoute.toFile(params)` (honoring any route-level `.toFile()`
|
|
4743
4844
|
* override), resolved under the build `outDir`; parent directories are created first.
|
|
@@ -4766,13 +4867,14 @@ async function writeDocument(outDir, entry, params, html) {
|
|
|
4766
4867
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `has`).
|
|
4767
4868
|
* @param instance - The concrete page instance to render.
|
|
4768
4869
|
* @param shell - Per-build wiring shared across instances (asset tags + template).
|
|
4870
|
+
* @param reuse - Whether this run may reuse a cached body (incremental, no code change).
|
|
4769
4871
|
* @returns The instance's URL, rendered HTML, loaded data, and client-nav flag.
|
|
4770
4872
|
* @example
|
|
4771
4873
|
* ```ts
|
|
4772
|
-
* await renderInstance(ctx, instance, { assets: "", template: null });
|
|
4874
|
+
* await renderInstance(ctx, instance, { assets: "", template: null }, false);
|
|
4773
4875
|
* ```
|
|
4774
4876
|
*/
|
|
4775
|
-
async function renderInstance(ctx, instance, shell) {
|
|
4877
|
+
async function renderInstance(ctx, instance, shell, reuse) {
|
|
4776
4878
|
const { definition, entry, params, locale } = instance;
|
|
4777
4879
|
const router = ctx.require(routerPlugin);
|
|
4778
4880
|
const data = await loadRouteData(definition, params, locale, ctx);
|
|
@@ -4785,7 +4887,7 @@ async function renderInstance(ctx, instance, shell) {
|
|
|
4785
4887
|
};
|
|
4786
4888
|
const parts = {
|
|
4787
4889
|
head: composeHeadHtml(ctx, instance, url, routeContext, data),
|
|
4788
|
-
body:
|
|
4890
|
+
body: renderBodyCached(ctx, instance, routeContext, data, reuse),
|
|
4789
4891
|
assets: shell.assets,
|
|
4790
4892
|
locale
|
|
4791
4893
|
};
|
|
@@ -4886,6 +4988,12 @@ function findRootHtml(rendered) {
|
|
|
4886
4988
|
*/
|
|
4887
4989
|
const RENDER_BATCH_SIZE = 2;
|
|
4888
4990
|
/**
|
|
4991
|
+
* Batch size for an incremental (`reuse`) rebuild. Most instances are cheap cache hits, so
|
|
4992
|
+
* a larger batch cuts the per-batch `setImmediate` round-trips (which would otherwise add
|
|
4993
|
+
* pure latency to an otherwise-fast rebuild) without starving the dev spinner.
|
|
4994
|
+
*/
|
|
4995
|
+
const INCREMENTAL_BATCH_SIZE = 32;
|
|
4996
|
+
/**
|
|
4889
4997
|
* Render `items` through `worker` in bounded-size batches, yielding a macrotask
|
|
4890
4998
|
* (`setImmediate`) between batches. Beyond bounding peak concurrency/memory for large
|
|
4891
4999
|
* sites, the yield lets the single JS thread breathe: one un-yielded `Promise.all` over
|
|
@@ -4921,21 +5029,29 @@ async function renderInBatches(items, batchSize, worker) {
|
|
|
4921
5029
|
* bounded batches ({@link renderInBatches}) → write data sidecars (hybrid/spa) →
|
|
4922
5030
|
* capture the root page's HTML for the root-index phase.
|
|
4923
5031
|
*
|
|
5032
|
+
* On an incremental rebuild (`options.reuse`) the cross-run render cache is kept and each
|
|
5033
|
+
* unchanged-data page reuses its cached body; a full render clears the cache first so a
|
|
5034
|
+
* removed/renamed route's stale body never lingers.
|
|
5035
|
+
*
|
|
4924
5036
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `log`, `has`).
|
|
5037
|
+
* @param options - Optional incremental hint; omit for a full render.
|
|
5038
|
+
* @param options.reuse - Reuse cached page bodies for unchanged-data pages (dev incremental rebuild).
|
|
4925
5039
|
* @returns The number of pages rendered and the captured default-page HTML.
|
|
4926
5040
|
* @example
|
|
4927
5041
|
* ```ts
|
|
4928
5042
|
* const { pageCount, rootHtml } = await renderPages(ctx);
|
|
4929
5043
|
* ```
|
|
4930
5044
|
*/
|
|
4931
|
-
async function renderPages(ctx) {
|
|
5045
|
+
async function renderPages(ctx, options) {
|
|
5046
|
+
const reuse = options?.reuse === true;
|
|
4932
5047
|
const router = ctx.require(routerPlugin);
|
|
4933
5048
|
const manifest = router.manifest();
|
|
4934
5049
|
ctx.state.manifest = [...manifest];
|
|
4935
5050
|
const locales = ctx.require(i18nPlugin).locales();
|
|
4936
5051
|
const byPattern = makeEntryMap(router);
|
|
5052
|
+
if (!reuse) ctx.state.renderCache.clear();
|
|
4937
5053
|
const shell = await prepareShell(ctx);
|
|
4938
|
-
const rendered = await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell));
|
|
5054
|
+
const rendered = await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), reuse ? INCREMENTAL_BATCH_SIZE : RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell, reuse));
|
|
4939
5055
|
await writeDataSidecars(ctx, rendered, router.mode());
|
|
4940
5056
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
4941
5057
|
return {
|
|
@@ -5143,6 +5259,40 @@ async function generateSitemap(ctx) {
|
|
|
5143
5259
|
* @file build plugin — pipeline driver. Sequences the fixed multi-phase build,
|
|
5144
5260
|
* emits `build:phase` boundaries, and runs intra-phase work via `Promise.all`.
|
|
5145
5261
|
*/
|
|
5262
|
+
/** Matches a Markdown source path (a content edit). */
|
|
5263
|
+
const MARKDOWN_PATH = /\.md$/;
|
|
5264
|
+
/** Matches a stylesheet path (a CSS edit — does not change rendered page bodies). */
|
|
5265
|
+
const STYLE_PATH = /\.css$/;
|
|
5266
|
+
/** Matches a code path (TS/JS/JSON — may change ANY page's render output). */
|
|
5267
|
+
const CODE_PATH = /\.(?:tsx?|jsx?|mjs|cjs|json)$/;
|
|
5268
|
+
/**
|
|
5269
|
+
* Derive the {@link ChangePlan} for a run from its changed-path set (see the type docs
|
|
5270
|
+
* for the rules).
|
|
5271
|
+
*
|
|
5272
|
+
* @param changed - Absolute/relative changed paths, or `undefined` for a full build.
|
|
5273
|
+
* @returns The reuse plan for this run.
|
|
5274
|
+
* @example
|
|
5275
|
+
* ```ts
|
|
5276
|
+
* const plan = planIncrementalRebuild(options?.changed);
|
|
5277
|
+
* ```
|
|
5278
|
+
*/
|
|
5279
|
+
function planIncrementalRebuild(changed) {
|
|
5280
|
+
if (changed === void 0 || changed.length === 0) return {
|
|
5281
|
+
contentChanged: [],
|
|
5282
|
+
contentReuse: false,
|
|
5283
|
+
renderReuse: false
|
|
5284
|
+
};
|
|
5285
|
+
if (!changed.every((file) => MARKDOWN_PATH.test(file) || STYLE_PATH.test(file) || CODE_PATH.test(file))) return {
|
|
5286
|
+
contentChanged: [],
|
|
5287
|
+
contentReuse: false,
|
|
5288
|
+
renderReuse: false
|
|
5289
|
+
};
|
|
5290
|
+
return {
|
|
5291
|
+
contentChanged: changed.filter((file) => MARKDOWN_PATH.test(file)),
|
|
5292
|
+
contentReuse: true,
|
|
5293
|
+
renderReuse: !changed.some((file) => CODE_PATH.test(file))
|
|
5294
|
+
};
|
|
5295
|
+
}
|
|
5146
5296
|
/**
|
|
5147
5297
|
* The static ordered list of pipeline phase names.
|
|
5148
5298
|
*
|
|
@@ -5256,8 +5406,7 @@ async function runOutputs(ctx) {
|
|
|
5256
5406
|
* `build:phase` boundary per phase and `build:complete` once at the end.
|
|
5257
5407
|
*
|
|
5258
5408
|
* @param ctx - Plugin context (provides `require`, `emit`, `state`, `config`, `log`).
|
|
5259
|
-
* @param options - Optional run overrides.
|
|
5260
|
-
* @param options.outDir - Override the configured output directory for this run.
|
|
5409
|
+
* @param options - Optional per-run overrides ({@link RunOptions}).
|
|
5261
5410
|
* @returns The build result (outDir, pageCount, durationMs).
|
|
5262
5411
|
* @example
|
|
5263
5412
|
* ```ts
|
|
@@ -5272,17 +5421,22 @@ async function runPipeline(ctx, options) {
|
|
|
5272
5421
|
...ctx,
|
|
5273
5422
|
config: {
|
|
5274
5423
|
...ctx.config,
|
|
5275
|
-
outDir
|
|
5424
|
+
outDir,
|
|
5425
|
+
...options?.overrides
|
|
5276
5426
|
}
|
|
5277
5427
|
};
|
|
5278
|
-
|
|
5428
|
+
const plan = planIncrementalRebuild(options?.changed);
|
|
5429
|
+
if (!options?.skipClean) await rm(outDir, {
|
|
5279
5430
|
recursive: true,
|
|
5280
5431
|
force: true
|
|
5281
5432
|
});
|
|
5282
5433
|
await mkdir(outDir, { recursive: true });
|
|
5283
5434
|
await withPhase(phaseContext, "bundle", () => bundle(phaseContext));
|
|
5284
|
-
await Promise.all([withPhase(phaseContext, "content", () => loadContent(phaseContext
|
|
5285
|
-
|
|
5435
|
+
await Promise.all([withPhase(phaseContext, "content", () => loadContent(phaseContext, {
|
|
5436
|
+
reuse: plan.contentReuse,
|
|
5437
|
+
changed: plan.contentChanged
|
|
5438
|
+
})), withPhase(phaseContext, "images", () => processImages(phaseContext))]);
|
|
5439
|
+
const pages = await withPhase(phaseContext, "pages", () => renderPages(phaseContext, { reuse: plan.renderReuse }));
|
|
5286
5440
|
await withPhase(phaseContext, "content-images", () => copyContentImages(phaseContext));
|
|
5287
5441
|
await runOutputs(phaseContext);
|
|
5288
5442
|
await withPhase(phaseContext, "root-index", async () => {
|
|
@@ -5335,10 +5489,12 @@ const defaultConfig$2 = {
|
|
|
5335
5489
|
function createApi$3(ctx) {
|
|
5336
5490
|
return {
|
|
5337
5491
|
/**
|
|
5338
|
-
* Run the full SSG pipeline and write the site to disk.
|
|
5492
|
+
* Run the full SSG pipeline and write the site to disk. With no options a full
|
|
5493
|
+
* production build runs; dev callers pass `skipClean`/`overrides`/`changed` for a
|
|
5494
|
+
* fast incremental rebuild (all gated behind opt-in fields — the default path is
|
|
5495
|
+
* unchanged).
|
|
5339
5496
|
*
|
|
5340
|
-
* @param options - Optional run overrides.
|
|
5341
|
-
* @param options.outDir - Override the configured output directory for this run.
|
|
5497
|
+
* @param options - Optional per-run overrides (outDir / skipClean / overrides / changed).
|
|
5342
5498
|
* @returns The build result (outDir, pageCount, durationMs).
|
|
5343
5499
|
* @example
|
|
5344
5500
|
* ```ts
|
|
@@ -5431,8 +5587,8 @@ function createEvents(register) {
|
|
|
5431
5587
|
/**
|
|
5432
5588
|
* Creates initial `build` plugin state: a frozen config snapshot plus empty
|
|
5433
5589
|
* per-run caches (`manifest`, `buildCache`, `runId`) and the cross-run OG
|
|
5434
|
-
* content-hash
|
|
5435
|
-
* duplicated here (pulled fresh via `ctx.require` each run).
|
|
5590
|
+
* content-hash + page-render caches. Holds caches and config only — no domain data
|
|
5591
|
+
* is duplicated here (pulled fresh via `ctx.require` each run).
|
|
5436
5592
|
*
|
|
5437
5593
|
* @param ctx - Minimal context with global and config.
|
|
5438
5594
|
* @param ctx.global - Global plugin registry (unused; caches are config-driven).
|
|
@@ -5449,7 +5605,8 @@ function createState$3(ctx) {
|
|
|
5449
5605
|
manifest: null,
|
|
5450
5606
|
buildCache: /* @__PURE__ */ new Map(),
|
|
5451
5607
|
runId: null,
|
|
5452
|
-
ogImageHashCache: /* @__PURE__ */ new Map()
|
|
5608
|
+
ogImageHashCache: /* @__PURE__ */ new Map(),
|
|
5609
|
+
renderCache: /* @__PURE__ */ new Map()
|
|
5453
5610
|
};
|
|
5454
5611
|
}
|
|
5455
5612
|
//#endregion
|
|
@@ -5673,12 +5830,37 @@ function buildWranglerArgs(input) {
|
|
|
5673
5830
|
branch
|
|
5674
5831
|
];
|
|
5675
5832
|
}
|
|
5833
|
+
/**
|
|
5834
|
+
* Assemble the argv for `wrangler pages project create` (no shell). Guards the
|
|
5835
|
+
* production branch against flag injection; the slug is already a safe `toSlug` output.
|
|
5836
|
+
*
|
|
5837
|
+
* @param input - The resolved project-create inputs.
|
|
5838
|
+
* @param input.slug - Cloudflare project-name slug (`toSlug(site.name())`).
|
|
5839
|
+
* @param input.branch - Production branch (guarded by `/^[a-zA-Z0-9/_.-]+$/`).
|
|
5840
|
+
* @returns The wrangler argv array.
|
|
5841
|
+
* @throws {Error} `ERR_DEPLOY_INVALID_BRANCH` when the branch fails the guard.
|
|
5842
|
+
* @example
|
|
5843
|
+
* buildProjectCreateArgs({ slug: "my-site", branch: "main" });
|
|
5844
|
+
*/
|
|
5845
|
+
function buildProjectCreateArgs(input) {
|
|
5846
|
+
const branch = guardBranch(input.branch);
|
|
5847
|
+
return [
|
|
5848
|
+
"bunx",
|
|
5849
|
+
"wrangler",
|
|
5850
|
+
"pages",
|
|
5851
|
+
"project",
|
|
5852
|
+
"create",
|
|
5853
|
+
input.slug,
|
|
5854
|
+
"--production-branch",
|
|
5855
|
+
branch
|
|
5856
|
+
];
|
|
5857
|
+
}
|
|
5676
5858
|
/** Lowercased substring matchers for the wrangler error taxonomy. */
|
|
5677
5859
|
const ERROR_SIGNATURES = [
|
|
5678
5860
|
{
|
|
5679
5861
|
match: ["could not find project", "project not found"],
|
|
5680
5862
|
kind: "ERR_DEPLOY_PROJECT_NOT_FOUND",
|
|
5681
|
-
advice: "The Cloudflare Pages project does not exist.
|
|
5863
|
+
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.)"
|
|
5682
5864
|
},
|
|
5683
5865
|
{
|
|
5684
5866
|
match: [
|
|
@@ -6257,15 +6439,16 @@ function validateConfig$1(ctx) {
|
|
|
6257
6439
|
* Run wrangler for the prepared argv and surface its scrubbed result, translating
|
|
6258
6440
|
* a non-zero exit into the classified deploy error. The API token is read from env
|
|
6259
6441
|
* here so it never crosses a logging boundary; only scrubbed output is returned.
|
|
6442
|
+
* Shared by `run()` (deploy) and `createProject()` (project create).
|
|
6260
6443
|
*
|
|
6261
6444
|
* @param ctx - Plugin context (provides `state.spawn`, `config`, `env`).
|
|
6262
6445
|
* @param args - The fully-built, pre-validated wrangler argv.
|
|
6263
6446
|
* @returns The wrangler `stdout` plus the scrubbed `stderr` to log on success.
|
|
6264
6447
|
* @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
|
|
6265
6448
|
* @example
|
|
6266
|
-
* const { stdout, scrubbedStderr } = await
|
|
6449
|
+
* const { stdout, scrubbedStderr } = await executeWrangler(ctx, args);
|
|
6267
6450
|
*/
|
|
6268
|
-
async function
|
|
6451
|
+
async function executeWrangler(ctx, args) {
|
|
6269
6452
|
const token = ctx.env.require("CLOUDFLARE_API_TOKEN");
|
|
6270
6453
|
const { stdout, scrubbedStderr, exitCode } = await runWrangler({
|
|
6271
6454
|
spawn: ctx.state.spawn,
|
|
@@ -6337,7 +6520,7 @@ function createApi$2(ctx) {
|
|
|
6337
6520
|
root
|
|
6338
6521
|
});
|
|
6339
6522
|
const start = Date.now();
|
|
6340
|
-
const { stdout, scrubbedStderr } = await
|
|
6523
|
+
const { stdout, scrubbedStderr } = await executeWrangler(ctx, args);
|
|
6341
6524
|
ctx.log.info(scrubbedStderr);
|
|
6342
6525
|
const result = buildDeployResult(stdout, branch, start);
|
|
6343
6526
|
ctx.state.lastDeployment = result;
|
|
@@ -6376,6 +6559,38 @@ function createApi$2(ctx) {
|
|
|
6376
6559
|
cwd: process.cwd(),
|
|
6377
6560
|
options
|
|
6378
6561
|
});
|
|
6562
|
+
},
|
|
6563
|
+
/**
|
|
6564
|
+
* The Cloudflare Pages project name this app deploys to (`toSlug(site.name())`).
|
|
6565
|
+
*
|
|
6566
|
+
* @returns The project-name slug.
|
|
6567
|
+
* @example
|
|
6568
|
+
* api.projectName(); // "my-site"
|
|
6569
|
+
*/
|
|
6570
|
+
projectName() {
|
|
6571
|
+
return toSlug(ctx.require(sitePlugin).name());
|
|
6572
|
+
},
|
|
6573
|
+
/**
|
|
6574
|
+
* Create the remote Cloudflare Pages project via wrangler, so a first deploy has a
|
|
6575
|
+
* target. Derives the slug from `site.name()` and the production branch from config.
|
|
6576
|
+
*
|
|
6577
|
+
* @returns The created project name + production branch.
|
|
6578
|
+
* @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
|
|
6579
|
+
* @example
|
|
6580
|
+
* await api.createProject(); // { name: "my-site", branch: "main" }
|
|
6581
|
+
*/
|
|
6582
|
+
async createProject() {
|
|
6583
|
+
const name = toSlug(ctx.require(sitePlugin).name());
|
|
6584
|
+
const branch = ctx.config.productionBranch ?? "main";
|
|
6585
|
+
const { scrubbedStderr } = await executeWrangler(ctx, buildProjectCreateArgs({
|
|
6586
|
+
slug: name,
|
|
6587
|
+
branch
|
|
6588
|
+
}));
|
|
6589
|
+
ctx.log.info(scrubbedStderr);
|
|
6590
|
+
return {
|
|
6591
|
+
name,
|
|
6592
|
+
branch
|
|
6593
|
+
};
|
|
6379
6594
|
}
|
|
6380
6595
|
};
|
|
6381
6596
|
}
|
|
@@ -6509,21 +6724,56 @@ const ACCOUNT_HELP = [
|
|
|
6509
6724
|
"right-hand sidebar (also in the dashboard URL). Then make it available:",
|
|
6510
6725
|
" export CLOUDFLARE_ACCOUNT_ID=… or add it to .env."
|
|
6511
6726
|
].join("\n");
|
|
6727
|
+
/** Shown when a credential is in the raw environment but the app's env providers did not resolve it. */
|
|
6728
|
+
const PROVIDERS_HELP = [
|
|
6729
|
+
"Found in your shell/.env but the app's env plugin did not resolve it — its providers",
|
|
6730
|
+
"are not wired. Add the Node providers in createApp so the deploy can read it:",
|
|
6731
|
+
" pluginConfigs.env = { providers: [processEnv(), dotenv()] } (import them from @moku-labs/web)."
|
|
6732
|
+
].join("\n");
|
|
6512
6733
|
/** The GitHub repo secrets the generated workflow consumes. */
|
|
6513
6734
|
const SECRETS_HELP = ["Add these repo secrets (GitHub → Settings → Secrets and variables → Actions):", "CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID"].join("\n");
|
|
6514
6735
|
/**
|
|
6736
|
+
* Build one credential prerequisite by reading the SAME source the deploy reads — the
|
|
6737
|
+
* resolved `ctx.env` table — so a ✓ guarantees `ctx.env.require(key)` will succeed. When
|
|
6738
|
+
* the value is present in the raw `process.env` but unresolved by the app's providers
|
|
6739
|
+
* (the silent "deploy can't see it" trap a bare `process.env` check would mark green),
|
|
6740
|
+
* the fix hint points at wiring the providers instead of re-adding the value.
|
|
6741
|
+
*
|
|
6742
|
+
* @param ctx - The cli plugin context (provides the resolved `ctx.env`).
|
|
6743
|
+
* @param key - The credential variable name.
|
|
6744
|
+
* @param label - The diagnostic line label.
|
|
6745
|
+
* @param missingHelp - The fix hint when the credential is genuinely absent everywhere.
|
|
6746
|
+
* @returns The credential prerequisite check.
|
|
6747
|
+
* @example
|
|
6748
|
+
* credentialPrereq(ctx, "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN is set", TOKEN_HELP);
|
|
6749
|
+
*/
|
|
6750
|
+
function credentialPrereq(ctx, key, label, missingHelp) {
|
|
6751
|
+
if ((ctx.env.get(key) ?? "") !== "") return {
|
|
6752
|
+
ok: true,
|
|
6753
|
+
label,
|
|
6754
|
+
detail: void 0,
|
|
6755
|
+
scaffoldable: false
|
|
6756
|
+
};
|
|
6757
|
+
return {
|
|
6758
|
+
ok: false,
|
|
6759
|
+
label,
|
|
6760
|
+
detail: (process.env[key] ?? "") !== "" ? PROVIDERS_HELP : missingHelp,
|
|
6761
|
+
scaffoldable: false
|
|
6762
|
+
};
|
|
6763
|
+
}
|
|
6764
|
+
/**
|
|
6515
6765
|
* Evaluate the three deploy prerequisites against the current project: the Cloudflare
|
|
6516
|
-
* wrangler config exists, and both Cloudflare credentials
|
|
6766
|
+
* wrangler config exists, and both Cloudflare credentials resolve through `ctx.env` (the
|
|
6767
|
+
* deploy's own source of truth — not a bare `process.env` read that can diverge from it).
|
|
6517
6768
|
*
|
|
6769
|
+
* @param ctx - The cli plugin context (provides the resolved `ctx.env`).
|
|
6518
6770
|
* @param cwd - The project root (where `wrangler.jsonc` lives).
|
|
6519
6771
|
* @returns The ordered prerequisite checks.
|
|
6520
6772
|
* @example
|
|
6521
|
-
* const prereqs = diagnose(process.cwd());
|
|
6773
|
+
* const prereqs = diagnose(ctx, process.cwd());
|
|
6522
6774
|
*/
|
|
6523
|
-
function diagnose(cwd) {
|
|
6775
|
+
function diagnose(ctx, cwd) {
|
|
6524
6776
|
const wranglerOk = existsSync(path.join(cwd, "wrangler.jsonc"));
|
|
6525
|
-
const tokenOk = (process.env.CLOUDFLARE_API_TOKEN ?? "") !== "";
|
|
6526
|
-
const accountOk = (process.env.CLOUDFLARE_ACCOUNT_ID ?? "") !== "";
|
|
6527
6777
|
return [
|
|
6528
6778
|
{
|
|
6529
6779
|
ok: wranglerOk,
|
|
@@ -6531,18 +6781,8 @@ function diagnose(cwd) {
|
|
|
6531
6781
|
detail: wranglerOk ? void 0 : "Missing — scaffold it (offered below) or run app.deploy.init().",
|
|
6532
6782
|
scaffoldable: true
|
|
6533
6783
|
},
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
label: "CLOUDFLARE_API_TOKEN is set",
|
|
6537
|
-
detail: tokenOk ? void 0 : TOKEN_HELP,
|
|
6538
|
-
scaffoldable: false
|
|
6539
|
-
},
|
|
6540
|
-
{
|
|
6541
|
-
ok: accountOk,
|
|
6542
|
-
label: "CLOUDFLARE_ACCOUNT_ID is set",
|
|
6543
|
-
detail: accountOk ? void 0 : ACCOUNT_HELP,
|
|
6544
|
-
scaffoldable: false
|
|
6545
|
-
}
|
|
6784
|
+
credentialPrereq(ctx, "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_TOKEN is set", TOKEN_HELP),
|
|
6785
|
+
credentialPrereq(ctx, "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_ACCOUNT_ID is set", ACCOUNT_HELP)
|
|
6546
6786
|
];
|
|
6547
6787
|
}
|
|
6548
6788
|
/**
|
|
@@ -6640,8 +6880,139 @@ async function offerWorkflowSetup(ctx) {
|
|
|
6640
6880
|
ctx.state.render.info(SECRETS_HELP);
|
|
6641
6881
|
}
|
|
6642
6882
|
/**
|
|
6643
|
-
*
|
|
6644
|
-
*
|
|
6883
|
+
* Read the taxonomy `code` off a thrown value, when present. Deploy errors carry a
|
|
6884
|
+
* `code` (e.g. `ERR_DEPLOY_PROJECT_NOT_FOUND`) so the wizard can tailor the fix hint.
|
|
6885
|
+
*
|
|
6886
|
+
* @param error - The thrown value.
|
|
6887
|
+
* @returns The `code` string, or `undefined` when absent.
|
|
6888
|
+
* @example
|
|
6889
|
+
* codeOf(deployError("ERR_DEPLOY_AUTH", "…")); // "ERR_DEPLOY_AUTH"
|
|
6890
|
+
*/
|
|
6891
|
+
function codeOf(error) {
|
|
6892
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
6893
|
+
const { code } = error;
|
|
6894
|
+
return typeof code === "string" ? code : void 0;
|
|
6895
|
+
}
|
|
6896
|
+
}
|
|
6897
|
+
/**
|
|
6898
|
+
* A copy-pasteable "create the project yourself" hint, shown when the user declines the
|
|
6899
|
+
* offer to auto-create. Spells out that the remote project is what's missing (init only
|
|
6900
|
+
* scaffolds local config).
|
|
6901
|
+
*
|
|
6902
|
+
* @param name - The Cloudflare Pages project name (the deploy slug).
|
|
6903
|
+
* @returns The multi-line hint (newline-separated; rendered indented under a `›`).
|
|
6904
|
+
* @example
|
|
6905
|
+
* ctx.state.render.info(projectNotFoundHint("my-site"));
|
|
6906
|
+
*/
|
|
6907
|
+
function projectNotFoundHint(name) {
|
|
6908
|
+
return [
|
|
6909
|
+
"how to fix: the Cloudflare Pages project does not exist yet — create it once, then",
|
|
6910
|
+
"re-run `bun run deploy`. (app.deploy.init() only scaffolds local config; it does not",
|
|
6911
|
+
"create the remote project.)",
|
|
6912
|
+
` • CLI: bunx wrangler pages project create ${name} --production-branch main`,
|
|
6913
|
+
" • Dashboard: Cloudflare → Workers & Pages → Create → Pages"
|
|
6914
|
+
].join("\n");
|
|
6915
|
+
}
|
|
6916
|
+
/**
|
|
6917
|
+
* An actionable, error-specific "how to fix" hint for a failed deploy (other than the
|
|
6918
|
+
* project-not-found case, which the wizard handles interactively), so the user never
|
|
6919
|
+
* lands on a raw stack trace.
|
|
6920
|
+
*
|
|
6921
|
+
* @param error - The thrown deploy error.
|
|
6922
|
+
* @returns The fix hint line.
|
|
6923
|
+
* @example
|
|
6924
|
+
* ctx.state.render.info(deployFailureHint(err));
|
|
6925
|
+
*/
|
|
6926
|
+
function deployFailureHint$1(error) {
|
|
6927
|
+
const code = codeOf(error);
|
|
6928
|
+
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`.";
|
|
6929
|
+
if (code === "ERR_DEPLOY_NETWORK") return "how to fix: a network error reached Cloudflare — check connectivity, then re-run `bun run deploy`.";
|
|
6930
|
+
return "how to fix: resolve the error above, then re-run `bun run deploy`.";
|
|
6931
|
+
}
|
|
6932
|
+
/**
|
|
6933
|
+
* Render a styled deploy failure (✗ + fix hint) and return the `"failed"` outcome, so a
|
|
6934
|
+
* caught error surfaces consistently instead of as a raw throw.
|
|
6935
|
+
*
|
|
6936
|
+
* @param ctx - The cli plugin context.
|
|
6937
|
+
* @param error - The thrown deploy error.
|
|
6938
|
+
* @returns The `"failed"` deploy outcome.
|
|
6939
|
+
* @example
|
|
6940
|
+
* return renderFailure(ctx, error);
|
|
6941
|
+
*/
|
|
6942
|
+
function renderFailure(ctx, error) {
|
|
6943
|
+
ctx.state.render.error("deploy failed", error);
|
|
6944
|
+
ctx.state.render.info(deployFailureHint$1(error));
|
|
6945
|
+
return {
|
|
6946
|
+
deployed: false,
|
|
6947
|
+
reason: "failed"
|
|
6948
|
+
};
|
|
6949
|
+
}
|
|
6950
|
+
/**
|
|
6951
|
+
* Deploy once via the deploy plugin and wrap the result as a successful outcome. Throws
|
|
6952
|
+
* the classified deploy error on failure (the caller decides how to surface it).
|
|
6953
|
+
*
|
|
6954
|
+
* @param ctx - The cli plugin context.
|
|
6955
|
+
* @param options - The deploy options (branch override).
|
|
6956
|
+
* @returns The successful deploy outcome.
|
|
6957
|
+
* @throws {Error} With a `code` from the deploy error taxonomy on any failure.
|
|
6958
|
+
* @example
|
|
6959
|
+
* const outcome = await deployOnce(ctx, { branch: "main" });
|
|
6960
|
+
*/
|
|
6961
|
+
async function deployOnce(ctx, options) {
|
|
6962
|
+
return {
|
|
6963
|
+
deployed: true,
|
|
6964
|
+
...await ctx.require(deployPlugin).run(options.branch === void 0 ? {} : { branch: options.branch })
|
|
6965
|
+
};
|
|
6966
|
+
}
|
|
6967
|
+
/**
|
|
6968
|
+
* Handle a project-not-found deploy failure interactively: ask (a confirmation step)
|
|
6969
|
+
* before creating a real Cloudflare resource, create the Pages project via the deploy
|
|
6970
|
+
* plugin, then retry the deploy once. A declined offer (or a create failure) returns the
|
|
6971
|
+
* `"failed"` outcome with an actionable hint — never a raw stack trace.
|
|
6972
|
+
*
|
|
6973
|
+
* @param ctx - The cli plugin context.
|
|
6974
|
+
* @param options - The deploy options (branch override).
|
|
6975
|
+
* @param originalError - The project-not-found error from the first attempt.
|
|
6976
|
+
* @returns The deploy outcome (deployed after a successful create + retry, else failed).
|
|
6977
|
+
* @example
|
|
6978
|
+
* return createProjectThenRetry(ctx, options, error);
|
|
6979
|
+
*/
|
|
6980
|
+
async function createProjectThenRetry(ctx, options, originalError) {
|
|
6981
|
+
const deploy = ctx.require(deployPlugin);
|
|
6982
|
+
const name = deploy.projectName();
|
|
6983
|
+
ctx.state.render.warn(`The Cloudflare Pages project "${name}" does not exist yet.`);
|
|
6984
|
+
if (!await ctx.state.confirm(`Create the Cloudflare Pages project "${name}" now?`)) {
|
|
6985
|
+
ctx.state.render.error("deploy failed", originalError);
|
|
6986
|
+
ctx.state.render.info(projectNotFoundHint(name));
|
|
6987
|
+
return {
|
|
6988
|
+
deployed: false,
|
|
6989
|
+
reason: "failed"
|
|
6990
|
+
};
|
|
6991
|
+
}
|
|
6992
|
+
try {
|
|
6993
|
+
const created = await deploy.createProject();
|
|
6994
|
+
ctx.state.render.check(true, `created Cloudflare Pages project "${created.name}"`);
|
|
6995
|
+
} catch (error) {
|
|
6996
|
+
ctx.state.render.error("could not create the Pages project", error);
|
|
6997
|
+
ctx.state.render.info(deployFailureHint$1(error));
|
|
6998
|
+
return {
|
|
6999
|
+
deployed: false,
|
|
7000
|
+
reason: "failed"
|
|
7001
|
+
};
|
|
7002
|
+
}
|
|
7003
|
+
ctx.state.render.info("project created — retrying the deploy…");
|
|
7004
|
+
try {
|
|
7005
|
+
return await deployOnce(ctx, options);
|
|
7006
|
+
} catch (error) {
|
|
7007
|
+
return renderFailure(ctx, error);
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
/**
|
|
7011
|
+
* Run the deploy step: confirm (unless `yes`), then deploy via the deploy plugin. A
|
|
7012
|
+
* declined confirm returns `{ deployed: false, reason: "declined" }`. A project-not-found
|
|
7013
|
+
* failure offers to create the project (with a confirmation step) and retries; any other
|
|
7014
|
+
* runtime failure is surfaced as a styled error + fix hint, returning
|
|
7015
|
+
* `{ deployed: false, reason: "failed" }` — never a raw stack trace.
|
|
6645
7016
|
*
|
|
6646
7017
|
* @param ctx - The cli plugin context.
|
|
6647
7018
|
* @param options - The deploy options (branch override + `yes`).
|
|
@@ -6658,10 +7029,12 @@ async function runDeployStep(ctx, options) {
|
|
|
6658
7029
|
reason: "declined"
|
|
6659
7030
|
};
|
|
6660
7031
|
}
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
7032
|
+
try {
|
|
7033
|
+
return await deployOnce(ctx, options);
|
|
7034
|
+
} catch (error) {
|
|
7035
|
+
if (codeOf(error) === "ERR_DEPLOY_PROJECT_NOT_FOUND") return createProjectThenRetry(ctx, options, error);
|
|
7036
|
+
return renderFailure(ctx, error);
|
|
7037
|
+
}
|
|
6665
7038
|
}
|
|
6666
7039
|
/**
|
|
6667
7040
|
* Run the guided deploy wizard end to end: diagnose prerequisites (offering to scaffold
|
|
@@ -6679,10 +7052,10 @@ async function runDeployStep(ctx, options) {
|
|
|
6679
7052
|
async function runDeployWizard(ctx, options) {
|
|
6680
7053
|
const cwd = process.cwd();
|
|
6681
7054
|
ctx.state.render.heading("Checking prerequisites");
|
|
6682
|
-
for (const item of diagnose(cwd)) ctx.state.render.check(item.ok, item.label, item.detail);
|
|
6683
|
-
await offerScaffold(ctx, diagnose(cwd));
|
|
7055
|
+
for (const item of diagnose(ctx, cwd)) ctx.state.render.check(item.ok, item.label, item.detail);
|
|
7056
|
+
await offerScaffold(ctx, diagnose(ctx, cwd));
|
|
6684
7057
|
await offerEnvScaffold(ctx, cwd);
|
|
6685
|
-
const blockers = diagnose(cwd).filter((item) => !item.ok);
|
|
7058
|
+
const blockers = diagnose(ctx, cwd).filter((item) => !item.ok);
|
|
6686
7059
|
if (blockers.length > 0) {
|
|
6687
7060
|
ctx.state.render.heading("Not ready to deploy");
|
|
6688
7061
|
for (const item of blockers) ctx.state.render.check(false, item.label, item.detail);
|
|
@@ -6699,7 +7072,7 @@ async function runDeployWizard(ctx, options) {
|
|
|
6699
7072
|
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).");
|
|
6700
7073
|
ctx.state.render.info("Tip: run `bun run preview` to eyeball the built site before deploying.");
|
|
6701
7074
|
const outcome = await runDeployStep(ctx, options);
|
|
6702
|
-
await offerWorkflowSetup(ctx);
|
|
7075
|
+
if (!(outcome.deployed === false && outcome.reason === "failed")) await offerWorkflowSetup(ctx);
|
|
6703
7076
|
return outcome;
|
|
6704
7077
|
}
|
|
6705
7078
|
//#endregion
|
|
@@ -6739,25 +7112,26 @@ function injectReloadClient(html) {
|
|
|
6739
7112
|
* Run one rebuild and report the result. Announces the start (`onRebuildStart`), then
|
|
6740
7113
|
* routes success to `onReloaded` and failure to `onError`.
|
|
6741
7114
|
*
|
|
6742
|
-
* @param input - The rebuild dependencies + the changed file.
|
|
6743
|
-
* @param input.runBuild - Runs one build and resolves with its summary.
|
|
7115
|
+
* @param input - The rebuild dependencies + the changed file/paths.
|
|
7116
|
+
* @param input.runBuild - Runs one build (given the changed paths) and resolves with its summary.
|
|
6744
7117
|
* @param input.onRebuildStart - Called with the changed file just before the build runs.
|
|
6745
|
-
* @param input.onReloaded - Called with the changed file + summary after a rebuild.
|
|
7118
|
+
* @param input.onReloaded - Called with the changed file + summary + the built `changed` set after a rebuild.
|
|
6746
7119
|
* @param input.onError - Called when a rebuild throws.
|
|
6747
7120
|
* @param input.file - The changed file to report alongside the summary.
|
|
7121
|
+
* @param input.changed - The accumulated changed paths handed to `runBuild` (incremental).
|
|
6748
7122
|
* @returns Resolves once the rebuild settles (always — errors are routed, not thrown).
|
|
6749
7123
|
* @example
|
|
6750
|
-
* await runOneRebuild({ runBuild, onReloaded, onError, file: "a.md" });
|
|
7124
|
+
* await runOneRebuild({ runBuild, onReloaded, onError, file: "a.md", changed: ["a.md"] });
|
|
6751
7125
|
*/
|
|
6752
7126
|
async function runOneRebuild(input) {
|
|
6753
7127
|
input.onRebuildStart?.(input.file);
|
|
6754
7128
|
try {
|
|
6755
|
-
const summary = await input.runBuild();
|
|
7129
|
+
const summary = await input.runBuild(input.changed);
|
|
6756
7130
|
input.onReloaded({
|
|
6757
7131
|
file: input.file,
|
|
6758
7132
|
pageCount: summary.pageCount,
|
|
6759
7133
|
durationMs: summary.durationMs
|
|
6760
|
-
});
|
|
7134
|
+
}, input.changed);
|
|
6761
7135
|
} catch (error) {
|
|
6762
7136
|
input.onError(error);
|
|
6763
7137
|
}
|
|
@@ -6771,9 +7145,9 @@ async function runOneRebuild(input) {
|
|
|
6771
7145
|
*
|
|
6772
7146
|
* @param input - The rebuild dependencies.
|
|
6773
7147
|
* @param input.debounceMs - Debounce window in milliseconds.
|
|
6774
|
-
* @param input.runBuild - Runs one build and resolves with its summary.
|
|
7148
|
+
* @param input.runBuild - Runs one build (given the changed paths) and resolves with its summary.
|
|
6775
7149
|
* @param input.onRebuildStart - Called with the changed file just before each build runs.
|
|
6776
|
-
* @param input.onReloaded - Called with the changed file + summary after a rebuild.
|
|
7150
|
+
* @param input.onReloaded - Called with the changed file + summary + the built `changed` set after a rebuild.
|
|
6777
7151
|
* @param input.onError - Called when a rebuild throws.
|
|
6778
7152
|
* @returns The debounced rebuild driver.
|
|
6779
7153
|
* @example
|
|
@@ -6782,12 +7156,13 @@ async function runOneRebuild(input) {
|
|
|
6782
7156
|
function createRebuilder(input) {
|
|
6783
7157
|
let timer;
|
|
6784
7158
|
let pendingFile = "";
|
|
7159
|
+
const pendingChanged = /* @__PURE__ */ new Set();
|
|
6785
7160
|
let building = false;
|
|
6786
7161
|
let dirty = false;
|
|
6787
7162
|
/**
|
|
6788
|
-
* Rebuild repeatedly until no change arrived mid-flight: each pass clears
|
|
6789
|
-
* runs one build, then loops again if a
|
|
6790
|
-
* no change is dropped.
|
|
7163
|
+
* Rebuild repeatedly until no change arrived mid-flight: each pass snapshots + clears
|
|
7164
|
+
* the accumulated changed paths, runs one build over them, then loops again if a
|
|
7165
|
+
* `schedule()` set `dirty` (and added more paths) while it ran, so no change is dropped.
|
|
6791
7166
|
*
|
|
6792
7167
|
* @returns Resolves once a pass completes with no pending change (errors are routed,
|
|
6793
7168
|
* never thrown).
|
|
@@ -6797,12 +7172,15 @@ function createRebuilder(input) {
|
|
|
6797
7172
|
const drainPendingRebuilds = async () => {
|
|
6798
7173
|
do {
|
|
6799
7174
|
dirty = false;
|
|
7175
|
+
const changed = [...pendingChanged];
|
|
7176
|
+
pendingChanged.clear();
|
|
6800
7177
|
await runOneRebuild({
|
|
6801
7178
|
runBuild: input.runBuild,
|
|
6802
7179
|
...input.onRebuildStart ? { onRebuildStart: input.onRebuildStart } : {},
|
|
6803
7180
|
onReloaded: input.onReloaded,
|
|
6804
7181
|
onError: input.onError,
|
|
6805
|
-
file: pendingFile
|
|
7182
|
+
file: pendingFile,
|
|
7183
|
+
changed
|
|
6806
7184
|
});
|
|
6807
7185
|
} while (dirty);
|
|
6808
7186
|
};
|
|
@@ -6829,14 +7207,15 @@ function createRebuilder(input) {
|
|
|
6829
7207
|
};
|
|
6830
7208
|
return {
|
|
6831
7209
|
/**
|
|
6832
|
-
* Queue a rebuild for the given
|
|
7210
|
+
* Queue a rebuild for the given changed path (debounced + coalesced + accumulated).
|
|
6833
7211
|
*
|
|
6834
|
-
* @param file - The
|
|
7212
|
+
* @param file - The changed path reported as `ReloadInfo.file` and added to the changed set.
|
|
6835
7213
|
* @example
|
|
6836
|
-
* rebuilder.schedule("content");
|
|
7214
|
+
* rebuilder.schedule("content/intro/en.md");
|
|
6837
7215
|
*/
|
|
6838
7216
|
schedule(file) {
|
|
6839
7217
|
pendingFile = file;
|
|
7218
|
+
pendingChanged.add(file);
|
|
6840
7219
|
if (timer) clearTimeout(timer);
|
|
6841
7220
|
timer = setTimeout(fire, input.debounceMs);
|
|
6842
7221
|
},
|
|
@@ -6866,26 +7245,33 @@ function isNoisePath(filename) {
|
|
|
6866
7245
|
return filename.split(/[/\\]/).some((segment) => segment.startsWith(".")) || filename.endsWith("~");
|
|
6867
7246
|
}
|
|
6868
7247
|
/**
|
|
6869
|
-
* Create a {@link ChangeGate} that drops
|
|
6870
|
-
*
|
|
6871
|
-
* `outDir` (the build's own output — a loop guard),
|
|
6872
|
-
*
|
|
6873
|
-
*
|
|
6874
|
-
*
|
|
6875
|
-
*
|
|
6876
|
-
*
|
|
7248
|
+
* Create a {@link ChangeGate} that drops four kinds of spurious change events before they
|
|
7249
|
+
* reach the debounced rebuilder: editor/OS noise (dotfiles, backups), writes under
|
|
7250
|
+
* `outDir` (the build's own output — a loop guard), the stale duplicate/parent-dir echoes
|
|
7251
|
+
* macOS fires for one save (a build-start high-water mark — a change whose mtime is at or
|
|
7252
|
+
* before the last build we started was already captured), and — when a `fileHash` seam is
|
|
7253
|
+
* supplied — a NO-OP save whose bytes are identical to the last successfully-built version
|
|
7254
|
+
* (a double Ctrl-S, a `touch`, a format-on-save that reverts). The no-op baseline is
|
|
7255
|
+
* recorded ONLY by {@link ChangeGate.commitBuilt} on build SUCCESS, scoped to that build's
|
|
7256
|
+
* paths — so a failed build commits nothing (a retry save always rebuilds) and a file
|
|
7257
|
+
* edited mid-build is never falsely baselined by another file's success. A genuinely newer
|
|
7258
|
+
* edit (even mid-build) and a deletion (missing file) always pass.
|
|
6877
7259
|
*
|
|
6878
7260
|
* @param input - The gate dependencies.
|
|
6879
7261
|
* @param input.outDir - The build output directory whose writes must never re-trigger a build.
|
|
6880
7262
|
* @param input.fileMtime - Resolves a path's mtime in ms (or `null` when missing).
|
|
6881
7263
|
* @param input.now - Monotonic wall clock (ms) used for the build-start high-water mark.
|
|
7264
|
+
* @param input.fileHash - Resolves a path's content hash (or `null` when missing). Optional;
|
|
7265
|
+
* defaults to `() => null`, which disables the no-op-save short-circuit (every edit passes).
|
|
6882
7266
|
* @returns The change gate.
|
|
6883
7267
|
* @example
|
|
6884
|
-
* const gate = createChangeGate({ outDir: "dist", fileMtime: state.fileMtime, now: state.clock });
|
|
7268
|
+
* const gate = createChangeGate({ outDir: "dist", fileMtime: state.fileMtime, now: state.clock, fileHash: state.fileHash });
|
|
6885
7269
|
*/
|
|
6886
7270
|
function createChangeGate(input) {
|
|
6887
7271
|
const outDirAbs = path.resolve(input.outDir);
|
|
7272
|
+
const fileHash = input.fileHash ?? (() => null);
|
|
6888
7273
|
let lastBuildStartedAt = input.now();
|
|
7274
|
+
const committedHash = /* @__PURE__ */ new Map();
|
|
6889
7275
|
return {
|
|
6890
7276
|
/**
|
|
6891
7277
|
* Decide whether a change beneath `dir` warrants a rebuild (see {@link ChangeGate.accept}).
|
|
@@ -6903,6 +7289,12 @@ function createChangeGate(input) {
|
|
|
6903
7289
|
if (changed === outDirAbs || changed.startsWith(`${outDirAbs}${path.sep}`)) return false;
|
|
6904
7290
|
const mtime = input.fileMtime(changed);
|
|
6905
7291
|
if (mtime !== null && mtime < lastBuildStartedAt) return false;
|
|
7292
|
+
const hash = fileHash(changed);
|
|
7293
|
+
if (hash === null) {
|
|
7294
|
+
committedHash.delete(changed);
|
|
7295
|
+
return true;
|
|
7296
|
+
}
|
|
7297
|
+
if (committedHash.get(changed) === hash) return false;
|
|
6906
7298
|
return true;
|
|
6907
7299
|
},
|
|
6908
7300
|
/**
|
|
@@ -6913,6 +7305,20 @@ function createChangeGate(input) {
|
|
|
6913
7305
|
*/
|
|
6914
7306
|
markBuildStart() {
|
|
6915
7307
|
lastBuildStartedAt = input.now();
|
|
7308
|
+
},
|
|
7309
|
+
/**
|
|
7310
|
+
* Baseline exactly the paths the just-succeeded build consumed (see {@link ChangeGate.commitBuilt}).
|
|
7311
|
+
*
|
|
7312
|
+
* @param changed - The paths the just-succeeded build consumed.
|
|
7313
|
+
* @example
|
|
7314
|
+
* gate.commitBuilt(["content/intro/en.md"]);
|
|
7315
|
+
*/
|
|
7316
|
+
commitBuilt(changed) {
|
|
7317
|
+
for (const file of changed) {
|
|
7318
|
+
const key = path.resolve(file);
|
|
7319
|
+
const hash = fileHash(key);
|
|
7320
|
+
if (hash !== null) committedHash.set(key, hash);
|
|
7321
|
+
}
|
|
6916
7322
|
}
|
|
6917
7323
|
};
|
|
6918
7324
|
}
|
|
@@ -7105,19 +7511,46 @@ function createDevHandler(ctx, hub) {
|
|
|
7105
7511
|
};
|
|
7106
7512
|
}
|
|
7107
7513
|
/**
|
|
7514
|
+
* Build the per-run {@link BuildRunOverrides} for a dev build from the session feature
|
|
7515
|
+
* opt-ins: minification is always off in dev (no benefit, slower), and each expensive,
|
|
7516
|
+
* NON-navigational output stays off unless its flag re-enables it (`ogImage: false`
|
|
7517
|
+
* disables OG generation regardless of the persisted config). Locale-redirects are NOT
|
|
7518
|
+
* overridden — they produce navigable pages (the bare `/` → `/{defaultLocale}/` redirect),
|
|
7519
|
+
* so they follow the app's own config. The persisted plugin config is never mutated.
|
|
7520
|
+
*
|
|
7521
|
+
* @param features - The resolved per-session dev feature opt-ins.
|
|
7522
|
+
* @returns The config overrides merged into the dev build run.
|
|
7523
|
+
* @example
|
|
7524
|
+
* devBuildOverrides({ og: false, sitemap: false, feeds: false });
|
|
7525
|
+
*/
|
|
7526
|
+
function devBuildOverrides(features) {
|
|
7527
|
+
return {
|
|
7528
|
+
minify: false,
|
|
7529
|
+
...features.feeds ? {} : { feeds: false },
|
|
7530
|
+
...features.sitemap ? {} : { sitemap: false },
|
|
7531
|
+
...features.og ? {} : { ogImage: false }
|
|
7532
|
+
};
|
|
7533
|
+
}
|
|
7534
|
+
/**
|
|
7108
7535
|
* Run the dev loop: an initial build, an in-process static server that injects the
|
|
7109
7536
|
* live-reload client, a recursive watcher over `config.watchDirs`, and a debounced
|
|
7110
7537
|
* rebuild that re-renders and pushes a browser reload. Resolves on SIGINT/SIGTERM,
|
|
7111
|
-
* which stops the server, closes the watchers, and cancels any pending rebuild.
|
|
7538
|
+
* which stops the server, closes the watchers, and cancels any pending rebuild. The dev
|
|
7539
|
+
* build disables minification + expensive outputs (per {@link devBuildOverrides}); every
|
|
7540
|
+
* rebuild also skips the clean so caches + unchanged assets survive (no mid-rebuild 404).
|
|
7541
|
+
* Because rebuilds skip the clean, a DELETED or renamed content slug's stale page lingers
|
|
7542
|
+
* (and is served) until you restart `serve` or run a production `build`.
|
|
7112
7543
|
*
|
|
7113
7544
|
* @param ctx - The cli plugin context (config, state seams, `require`).
|
|
7114
7545
|
* @param port - The port to bind the dev server to.
|
|
7546
|
+
* @param features - Per-session dev feature opt-ins (`og`/`sitemap`/`feeds`/`localeRedirects`).
|
|
7115
7547
|
* @returns Resolves once the server has been torn down by a termination signal.
|
|
7116
7548
|
* @example
|
|
7117
|
-
* await runDevServer(ctx, 4173);
|
|
7549
|
+
* await runDevServer(ctx, 4173, { og: false, sitemap: false, feeds: false, localeRedirects: false });
|
|
7118
7550
|
*/
|
|
7119
|
-
async function runDevServer(ctx, port) {
|
|
7120
|
-
|
|
7551
|
+
async function runDevServer(ctx, port, features) {
|
|
7552
|
+
const overrides = devBuildOverrides(features);
|
|
7553
|
+
await ctx.require(buildPlugin).run({ overrides });
|
|
7121
7554
|
const hub = createReloadHub();
|
|
7122
7555
|
const server = ctx.state.serveStatic({
|
|
7123
7556
|
port,
|
|
@@ -7127,19 +7560,27 @@ async function runDevServer(ctx, port) {
|
|
|
7127
7560
|
const gate = createChangeGate({
|
|
7128
7561
|
outDir: ctx.config.outDir,
|
|
7129
7562
|
fileMtime: ctx.state.fileMtime,
|
|
7130
|
-
now: ctx.state.clock
|
|
7563
|
+
now: ctx.state.clock,
|
|
7564
|
+
fileHash: ctx.state.fileHash
|
|
7131
7565
|
});
|
|
7132
7566
|
const rebuilder = createRebuilder({
|
|
7133
7567
|
debounceMs: ctx.config.debounceMs,
|
|
7134
7568
|
/**
|
|
7135
|
-
* Re-run the SSG build for a rebuild
|
|
7569
|
+
* Re-run the SSG build for a rebuild: skip the clean so the prior assets + on-disk
|
|
7570
|
+
* caches survive (and no in-flight request hits an empty outDir), with the dev
|
|
7571
|
+
* overrides applied.
|
|
7136
7572
|
*
|
|
7573
|
+
* @param changed - The paths changed since the last build (incremental rebuild hint).
|
|
7137
7574
|
* @returns The rebuild summary.
|
|
7138
7575
|
* @example
|
|
7139
|
-
* await runBuild();
|
|
7576
|
+
* await runBuild(["content/intro/en.md"]);
|
|
7140
7577
|
*/
|
|
7141
|
-
runBuild() {
|
|
7142
|
-
return ctx.require(buildPlugin).run(
|
|
7578
|
+
runBuild(changed) {
|
|
7579
|
+
return ctx.require(buildPlugin).run({
|
|
7580
|
+
skipClean: true,
|
|
7581
|
+
overrides,
|
|
7582
|
+
changed
|
|
7583
|
+
});
|
|
7143
7584
|
},
|
|
7144
7585
|
/**
|
|
7145
7586
|
* Show the compact in-place "rebuilding {label}" line before the build runs.
|
|
@@ -7156,15 +7597,18 @@ async function runDevServer(ctx, port) {
|
|
|
7156
7597
|
* Render the reload line and push a browser reload after a rebuild.
|
|
7157
7598
|
*
|
|
7158
7599
|
* @param info - The changed file plus the rebuild's page count and duration.
|
|
7600
|
+
* @param changed - The paths this successful build consumed (baselined for no-op drops).
|
|
7159
7601
|
* @example
|
|
7160
|
-
* onReloaded({ file: "a.md", pageCount: 1, durationMs: 10 });
|
|
7602
|
+
* onReloaded({ file: "a.md", pageCount: 1, durationMs: 10 }, ["content/a.md"]);
|
|
7161
7603
|
*/
|
|
7162
|
-
onReloaded(info) {
|
|
7604
|
+
onReloaded(info, changed) {
|
|
7605
|
+
gate.commitBuilt(changed);
|
|
7163
7606
|
ctx.state.render.reload(info);
|
|
7164
7607
|
hub.reloadAll();
|
|
7165
7608
|
},
|
|
7166
7609
|
/**
|
|
7167
|
-
* Render a rebuild failure (the dev loop keeps running).
|
|
7610
|
+
* Render a rebuild failure (the dev loop keeps running). A failed build baselines
|
|
7611
|
+
* nothing (commitBuilt only runs on success), so an identical retry save still rebuilds.
|
|
7168
7612
|
*
|
|
7169
7613
|
* @param error - The thrown rebuild error.
|
|
7170
7614
|
* @example
|
|
@@ -7175,7 +7619,8 @@ async function runDevServer(ctx, port) {
|
|
|
7175
7619
|
}
|
|
7176
7620
|
});
|
|
7177
7621
|
const watchers = ctx.config.watchDirs.map((dir) => ctx.state.watch(dir, (filename) => {
|
|
7178
|
-
if (gate.accept(dir, filename))
|
|
7622
|
+
if (!gate.accept(dir, filename)) return;
|
|
7623
|
+
rebuilder.schedule(filename === void 0 ? dir : path.join(dir, filename));
|
|
7179
7624
|
}));
|
|
7180
7625
|
ctx.state.render.serverReady({
|
|
7181
7626
|
local: `http://localhost:${port}`,
|
|
@@ -7442,17 +7887,25 @@ function createApi$1(ctx) {
|
|
|
7442
7887
|
},
|
|
7443
7888
|
/**
|
|
7444
7889
|
* Dev loop: build once, serve `dist/` in-process (live-reload injected), watch
|
|
7445
|
-
* `watchDirs`, debounced rebuild + reload.
|
|
7890
|
+
* `watchDirs`, debounced + incremental rebuild + reload. For a fast rebuild the dev
|
|
7891
|
+
* build disables minification + expensive, NON-navigational outputs (feeds / sitemap /
|
|
7892
|
+
* og-images); pass `og`/`sitemap`/`feeds` to re-enable any of them for the session.
|
|
7893
|
+
* Locale-redirects are always built per the app config (they emit the navigable bare-path
|
|
7894
|
+
* `/` → `/{defaultLocale}/` redirect). Resolves on SIGINT/SIGTERM.
|
|
7446
7895
|
*
|
|
7447
|
-
* @param options - Optional port override
|
|
7896
|
+
* @param options - Optional port override + per-session dev feature opt-ins.
|
|
7448
7897
|
* @returns Resolves once the server has been torn down.
|
|
7449
7898
|
* @example
|
|
7450
|
-
* await api.serve({ port: 3000 });
|
|
7899
|
+
* await api.serve({ port: 3000, og: true });
|
|
7451
7900
|
*/
|
|
7452
7901
|
serve(options = {}) {
|
|
7453
7902
|
const { port = ctx.config.port } = options;
|
|
7454
7903
|
ctx.state.render.header("serve");
|
|
7455
|
-
return runDevServer(ctx, port
|
|
7904
|
+
return runDevServer(ctx, port, {
|
|
7905
|
+
og: options.og ?? false,
|
|
7906
|
+
sitemap: options.sitemap ?? false,
|
|
7907
|
+
feeds: options.feeds ?? false
|
|
7908
|
+
});
|
|
7456
7909
|
},
|
|
7457
7910
|
/**
|
|
7458
7911
|
* Static preview of the built `dist/` with CF-Pages clean-URL resolution.
|
|
@@ -8535,6 +8988,23 @@ function defaultFileMtime(filePath) {
|
|
|
8535
8988
|
}
|
|
8536
8989
|
}
|
|
8537
8990
|
/**
|
|
8991
|
+
* Default file-content-hash probe — `sha256` of the file bytes, returning `null` for a
|
|
8992
|
+
* missing/unreadable path. serve()'s change gate compares this against the last
|
|
8993
|
+
* successfully-built bytes to drop a no-op save (a byte-identical double Ctrl-S).
|
|
8994
|
+
*
|
|
8995
|
+
* @param filePath - The absolute path to hash.
|
|
8996
|
+
* @returns The hex SHA-256 of the file's bytes, or `null` when it cannot be read.
|
|
8997
|
+
* @example
|
|
8998
|
+
* const hash = defaultFileHash("/abs/content/a.md");
|
|
8999
|
+
*/
|
|
9000
|
+
function defaultFileHash(filePath) {
|
|
9001
|
+
try {
|
|
9002
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
9003
|
+
} catch {
|
|
9004
|
+
return null;
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
/**
|
|
8538
9008
|
* Default LAN network-URL deriver — wraps {@link networkUrl} so the production seam
|
|
8539
9009
|
* reads `node:os` interfaces while tests can inject a deterministic value.
|
|
8540
9010
|
*
|
|
@@ -8667,7 +9137,8 @@ function createState$1(_ctx) {
|
|
|
8667
9137
|
serveStatic: defaultServeStatic,
|
|
8668
9138
|
fileResponse: defaultFileResponse,
|
|
8669
9139
|
networkUrl: defaultNetworkUrl,
|
|
8670
|
-
fileMtime: defaultFileMtime
|
|
9140
|
+
fileMtime: defaultFileMtime,
|
|
9141
|
+
fileHash: defaultFileHash
|
|
8671
9142
|
};
|
|
8672
9143
|
}
|
|
8673
9144
|
//#endregion
|