@moku-labs/web 1.3.0 → 1.4.0
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 +21 -9
- package/dist/browser.mjs +18 -8
- package/dist/index.cjs +120 -49
- package/dist/index.d.cts +33 -12
- package/dist/index.d.mts +33 -12
- package/dist/index.mjs +120 -49
- package/package.json +2 -2
package/dist/browser.d.mts
CHANGED
|
@@ -2028,11 +2028,18 @@ type Config = {
|
|
|
2028
2028
|
*
|
|
2029
2029
|
* @example
|
|
2030
2030
|
* ```ts
|
|
2031
|
-
* { articles: new Map() }
|
|
2031
|
+
* { articles: new Map(), loadedAll: null }
|
|
2032
2032
|
* ```
|
|
2033
2033
|
*/
|
|
2034
2034
|
type State = {
|
|
2035
2035
|
/** Article cache keyed locale -> (slug -> Article). Starts empty. */articles: Map<string, Map<string, Article>>;
|
|
2036
|
+
/**
|
|
2037
|
+
* Memoized full `loadAll()` result, or `null` when not yet loaded / invalidated. List-route
|
|
2038
|
+
* loaders call `loadAll()` once PER PAGE, so without this every page re-reads + re-renders
|
|
2039
|
+
* every article (the dev-loop killer). The memo makes repeated calls O(1); `invalidate()`
|
|
2040
|
+
* clears it so a dev rebuild reloads (re-resolving only the changed slugs). Starts `null`.
|
|
2041
|
+
*/
|
|
2042
|
+
loadedAll: Map<string, Article[]> | null;
|
|
2036
2043
|
};
|
|
2037
2044
|
/**
|
|
2038
2045
|
* Notification-only events emitted by the content plugin.
|
|
@@ -2082,11 +2089,13 @@ type ContentApiContext = {
|
|
|
2082
2089
|
*/
|
|
2083
2090
|
type LoadAllOptions = {
|
|
2084
2091
|
/**
|
|
2085
|
-
* Reuse
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2092
|
+
* Reuse the per-build memo + per-slug cache (re-resolving only slugs a preceding
|
|
2093
|
+
* `invalidate()` dropped). Default `true` — this is what keeps repeated `loadAll()` calls
|
|
2094
|
+
* (a list route's loader runs once per page) cheap, and makes a dev rebuild re-render only
|
|
2095
|
+
* changed articles. Set `false` to force a FRESH full reload (cold build / an
|
|
2096
|
+
* unclassifiable change), which re-reads + re-renders every article and rebuilds the memo.
|
|
2097
|
+
* The post-sort `contentId` ordinals are always recomputed across the full set, so order +
|
|
2098
|
+
* ids match a full load either way.
|
|
2090
2099
|
*/
|
|
2091
2100
|
reuse?: boolean;
|
|
2092
2101
|
};
|
|
@@ -2100,10 +2109,13 @@ type LoadAllOptions = {
|
|
|
2100
2109
|
*/
|
|
2101
2110
|
type Api = {
|
|
2102
2111
|
/**
|
|
2103
|
-
* Load every article across every active locale, returning a locale-keyed
|
|
2104
|
-
*
|
|
2112
|
+
* Load every article across every active locale, returning a locale-keyed map of
|
|
2113
|
+
* date-descending Article arrays. Emits content:ready (once per actual load). Cache-first
|
|
2114
|
+
* + memoized: repeated calls (e.g. a list route's loader on every page) return the SAME
|
|
2115
|
+
* cached result with no re-read — so treat the result as READ-ONLY (do not sort/mutate it
|
|
2116
|
+
* in place; slice/copy first). Pass `{ reuse: false }` to force a fresh full reload.
|
|
2105
2117
|
*
|
|
2106
|
-
* @param options - Optional load behaviour ({@link LoadAllOptions});
|
|
2118
|
+
* @param options - Optional load behaviour ({@link LoadAllOptions}); default reuses the cache.
|
|
2107
2119
|
*/
|
|
2108
2120
|
loadAll(options?: LoadAllOptions): Promise<Map<string, Article[]>>;
|
|
2109
2121
|
/**
|
package/dist/browser.mjs
CHANGED
|
@@ -4409,12 +4409,15 @@ function createContentApi(ctx) {
|
|
|
4409
4409
|
* Load every article across every active locale (locale fallback, production
|
|
4410
4410
|
* draft exclusion, date sort, `contentId` after sort), cache them, emit `content:ready`.
|
|
4411
4411
|
*
|
|
4412
|
-
*
|
|
4413
|
-
*
|
|
4414
|
-
*
|
|
4415
|
-
*
|
|
4412
|
+
* Cache-first by default: repeated calls return the per-build memo (list-route loaders
|
|
4413
|
+
* call this once PER PAGE — without the memo every page would re-read + re-render every
|
|
4414
|
+
* article, the dev-loop killer), and a rebuild after `invalidate()` re-resolves only the
|
|
4415
|
+
* dropped slugs while reusing the cached articles for the rest (`contentId` ordinals are
|
|
4416
|
+
* still recomputed across the FULL sorted set, so ids + order match a full load). Pass
|
|
4417
|
+
* `{ reuse: false }` to force a FRESH full reload (cold build / an unclassifiable change
|
|
4418
|
+
* where the caller cannot pinpoint what changed) — this bypasses the memo + per-slug cache.
|
|
4416
4419
|
*
|
|
4417
|
-
* @param options - Optional load behaviour (`reuse`)
|
|
4420
|
+
* @param options - Optional load behaviour (`reuse`, default `true`).
|
|
4418
4421
|
* @returns A locale-keyed map of date-descending articles.
|
|
4419
4422
|
* @example
|
|
4420
4423
|
* ```ts
|
|
@@ -4422,7 +4425,9 @@ function createContentApi(ctx) {
|
|
|
4422
4425
|
* ```
|
|
4423
4426
|
*/
|
|
4424
4427
|
async loadAll(options) {
|
|
4425
|
-
const reuse = options?.reuse
|
|
4428
|
+
const reuse = options?.reuse !== false;
|
|
4429
|
+
const memo = ctx.state.loadedAll;
|
|
4430
|
+
if (reuse && memo !== null) return memo;
|
|
4426
4431
|
const slugs = await ctx.provider.slugs();
|
|
4427
4432
|
const locales = ctx.locales();
|
|
4428
4433
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -4440,6 +4445,7 @@ function createContentApi(ctx) {
|
|
|
4440
4445
|
result.set(locale, present);
|
|
4441
4446
|
total += present.length;
|
|
4442
4447
|
}
|
|
4448
|
+
ctx.state.loadedAll = result;
|
|
4443
4449
|
ctx.emit("content:ready", {
|
|
4444
4450
|
locales,
|
|
4445
4451
|
articleCount: total
|
|
@@ -4503,6 +4509,7 @@ function createContentApi(ctx) {
|
|
|
4503
4509
|
if (slug === void 0) continue;
|
|
4504
4510
|
for (const cache of ctx.state.articles.values()) cache.delete(slug);
|
|
4505
4511
|
}
|
|
4512
|
+
if (accepted.length > 0) ctx.state.loadedAll = null;
|
|
4506
4513
|
ctx.emit("content:invalidated", { paths: accepted });
|
|
4507
4514
|
},
|
|
4508
4515
|
/**
|
|
@@ -4572,14 +4579,17 @@ const contentEvents = (register) => ({
|
|
|
4572
4579
|
* @param _ctx - Minimal context with global and config.
|
|
4573
4580
|
* @param _ctx.global - Global plugin registry.
|
|
4574
4581
|
* @param _ctx.config - Resolved plugin configuration.
|
|
4575
|
-
* @returns Fresh content shell state: an empty article cache.
|
|
4582
|
+
* @returns Fresh content shell state: an empty article cache + an empty loadAll memo.
|
|
4576
4583
|
* @example
|
|
4577
4584
|
* ```ts
|
|
4578
4585
|
* const state = createContentState({ global: {}, config: { providers: [] } });
|
|
4579
4586
|
* ```
|
|
4580
4587
|
*/
|
|
4581
4588
|
function createContentState(_ctx) {
|
|
4582
|
-
return {
|
|
4589
|
+
return {
|
|
4590
|
+
articles: /* @__PURE__ */ new Map(),
|
|
4591
|
+
loadedAll: null
|
|
4592
|
+
};
|
|
4583
4593
|
}
|
|
4584
4594
|
//#endregion
|
|
4585
4595
|
//#region src/plugins/content/validate.ts
|
package/dist/index.cjs
CHANGED
|
@@ -1349,12 +1349,15 @@ function createContentApi(ctx) {
|
|
|
1349
1349
|
* Load every article across every active locale (locale fallback, production
|
|
1350
1350
|
* draft exclusion, date sort, `contentId` after sort), cache them, emit `content:ready`.
|
|
1351
1351
|
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
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.
|
|
1356
1359
|
*
|
|
1357
|
-
* @param options - Optional load behaviour (`reuse`)
|
|
1360
|
+
* @param options - Optional load behaviour (`reuse`, default `true`).
|
|
1358
1361
|
* @returns A locale-keyed map of date-descending articles.
|
|
1359
1362
|
* @example
|
|
1360
1363
|
* ```ts
|
|
@@ -1362,7 +1365,9 @@ function createContentApi(ctx) {
|
|
|
1362
1365
|
* ```
|
|
1363
1366
|
*/
|
|
1364
1367
|
async loadAll(options) {
|
|
1365
|
-
const reuse = options?.reuse
|
|
1368
|
+
const reuse = options?.reuse !== false;
|
|
1369
|
+
const memo = ctx.state.loadedAll;
|
|
1370
|
+
if (reuse && memo !== null) return memo;
|
|
1366
1371
|
const slugs = await ctx.provider.slugs();
|
|
1367
1372
|
const locales = ctx.locales();
|
|
1368
1373
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -1380,6 +1385,7 @@ function createContentApi(ctx) {
|
|
|
1380
1385
|
result.set(locale, present);
|
|
1381
1386
|
total += present.length;
|
|
1382
1387
|
}
|
|
1388
|
+
ctx.state.loadedAll = result;
|
|
1383
1389
|
ctx.emit("content:ready", {
|
|
1384
1390
|
locales,
|
|
1385
1391
|
articleCount: total
|
|
@@ -1443,6 +1449,7 @@ function createContentApi(ctx) {
|
|
|
1443
1449
|
if (slug === void 0) continue;
|
|
1444
1450
|
for (const cache of ctx.state.articles.values()) cache.delete(slug);
|
|
1445
1451
|
}
|
|
1452
|
+
if (accepted.length > 0) ctx.state.loadedAll = null;
|
|
1446
1453
|
ctx.emit("content:invalidated", { paths: accepted });
|
|
1447
1454
|
},
|
|
1448
1455
|
/**
|
|
@@ -1512,14 +1519,17 @@ const contentEvents = (register) => ({
|
|
|
1512
1519
|
* @param _ctx - Minimal context with global and config.
|
|
1513
1520
|
* @param _ctx.global - Global plugin registry.
|
|
1514
1521
|
* @param _ctx.config - Resolved plugin configuration.
|
|
1515
|
-
* @returns Fresh content shell state: an empty article cache.
|
|
1522
|
+
* @returns Fresh content shell state: an empty article cache + an empty loadAll memo.
|
|
1516
1523
|
* @example
|
|
1517
1524
|
* ```ts
|
|
1518
1525
|
* const state = createContentState({ global: {}, config: { providers: [] } });
|
|
1519
1526
|
* ```
|
|
1520
1527
|
*/
|
|
1521
1528
|
function createContentState(_ctx) {
|
|
1522
|
-
return {
|
|
1529
|
+
return {
|
|
1530
|
+
articles: /* @__PURE__ */ new Map(),
|
|
1531
|
+
loadedAll: null
|
|
1532
|
+
};
|
|
1523
1533
|
}
|
|
1524
1534
|
//#endregion
|
|
1525
1535
|
//#region src/plugins/content/validate.ts
|
|
@@ -3832,7 +3842,7 @@ const DEFAULT_BODY = "<h1>404</h1><p>The page you requested could not be found.<
|
|
|
3832
3842
|
* ```
|
|
3833
3843
|
*/
|
|
3834
3844
|
function wrap(body) {
|
|
3835
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3845
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3836
3846
|
}
|
|
3837
3847
|
/**
|
|
3838
3848
|
* Emits `outDir/404.html`. When `config.notFound` is `true`, writes the built-in
|
|
@@ -4516,6 +4526,8 @@ const HEAD_PLACEHOLDER = "<!--moku:head-->";
|
|
|
4516
4526
|
const BODY_PLACEHOLDER = "<!--moku:body-->";
|
|
4517
4527
|
/** Template placeholder for the injected asset `<link>`/`<script>` tags. */
|
|
4518
4528
|
const ASSETS_PLACEHOLDER = "<!--moku:assets-->";
|
|
4529
|
+
/** Template placeholder for the page's locale (`<html lang>`). */
|
|
4530
|
+
const LANG_PLACEHOLDER = "<!--moku:lang-->";
|
|
4519
4531
|
/**
|
|
4520
4532
|
* Read the bundle phase's hashed asset manifest for one kind from `state.buildCache`
|
|
4521
4533
|
* as a typed {@link BuildCacheEntry} (no `Map<string, unknown>` reads).
|
|
@@ -4563,14 +4575,16 @@ function buildAssetTags(ctx) {
|
|
|
4563
4575
|
* ```
|
|
4564
4576
|
*/
|
|
4565
4577
|
function renderDocument(parts) {
|
|
4566
|
-
return `<!DOCTYPE html><html lang="${parts.locale}"><head>${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
|
|
4578
|
+
return `<!DOCTYPE html><html lang="${parts.locale}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
|
|
4567
4579
|
}
|
|
4568
4580
|
/**
|
|
4569
|
-
* Fill a shell template's `<!--moku:
|
|
4570
|
-
* `<!--moku:assets-->` placeholders deterministically at build
|
|
4581
|
+
* Fill a shell template's `<!--moku:lang-->` / `<!--moku:head-->` /
|
|
4582
|
+
* `<!--moku:body-->` / `<!--moku:assets-->` placeholders deterministically at build
|
|
4583
|
+
* time. `<!--moku:lang-->` carries the page locale (for `<html lang>`), so a single
|
|
4584
|
+
* shared template stays locale-correct across every locale.
|
|
4571
4585
|
*
|
|
4572
4586
|
* @param template - The raw shell template HTML.
|
|
4573
|
-
* @param parts - The composed head/body/assets pieces.
|
|
4587
|
+
* @param parts - The composed head/body/assets/locale pieces.
|
|
4574
4588
|
* @returns The filled document string.
|
|
4575
4589
|
* @example
|
|
4576
4590
|
* ```ts
|
|
@@ -4578,7 +4592,7 @@ function renderDocument(parts) {
|
|
|
4578
4592
|
* ```
|
|
4579
4593
|
*/
|
|
4580
4594
|
function fillTemplate(template, parts) {
|
|
4581
|
-
return template.replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
|
|
4595
|
+
return template.replaceAll(LANG_PLACEHOLDER, parts.locale).replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
|
|
4582
4596
|
}
|
|
4583
4597
|
/**
|
|
4584
4598
|
* Resolve the compiled entry for a manifest definition, asserting the router
|
|
@@ -5830,7 +5844,8 @@ function buildWranglerArgs(input) {
|
|
|
5830
5844
|
"--project-name",
|
|
5831
5845
|
input.slug,
|
|
5832
5846
|
"--branch",
|
|
5833
|
-
branch
|
|
5847
|
+
branch,
|
|
5848
|
+
"--commit-dirty=true"
|
|
5834
5849
|
];
|
|
5835
5850
|
}
|
|
5836
5851
|
/**
|
|
@@ -6439,17 +6454,18 @@ function validateConfig$1(ctx) {
|
|
|
6439
6454
|
ctx.require(sitePlugin);
|
|
6440
6455
|
}
|
|
6441
6456
|
/**
|
|
6442
|
-
* Run wrangler for the prepared argv and
|
|
6443
|
-
*
|
|
6444
|
-
*
|
|
6445
|
-
*
|
|
6457
|
+
* Run wrangler for the prepared argv and return its stdout, translating a non-zero
|
|
6458
|
+
* exit into the classified deploy error. The API token is read from env here so it
|
|
6459
|
+
* never crosses a logging boundary; the scrubbed stderr is used only to classify a
|
|
6460
|
+
* failure — it is never logged (that was console noise), so nothing leaks. Shared by
|
|
6461
|
+
* `run()` (deploy) and `createProject()` (project create).
|
|
6446
6462
|
*
|
|
6447
6463
|
* @param ctx - Plugin context (provides `state.spawn`, `config`, `env`).
|
|
6448
6464
|
* @param args - The fully-built, pre-validated wrangler argv.
|
|
6449
|
-
* @returns The wrangler `stdout`
|
|
6465
|
+
* @returns The wrangler `stdout` (for URL/id parsing on a deploy).
|
|
6450
6466
|
* @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
|
|
6451
6467
|
* @example
|
|
6452
|
-
* const
|
|
6468
|
+
* const stdout = await executeWrangler(ctx, args);
|
|
6453
6469
|
*/
|
|
6454
6470
|
async function executeWrangler(ctx, args) {
|
|
6455
6471
|
const token = ctx.env.require("CLOUDFLARE_API_TOKEN");
|
|
@@ -6463,10 +6479,7 @@ async function executeWrangler(ctx, args) {
|
|
|
6463
6479
|
const { code, message } = classifyWranglerError(exitCode, scrubbedStderr);
|
|
6464
6480
|
throw deployError(code, message);
|
|
6465
6481
|
}
|
|
6466
|
-
return
|
|
6467
|
-
stdout,
|
|
6468
|
-
scrubbedStderr
|
|
6469
|
-
};
|
|
6482
|
+
return stdout;
|
|
6470
6483
|
}
|
|
6471
6484
|
/**
|
|
6472
6485
|
* Assemble the public {@link DeployResult} from wrangler's stdout, parsing the
|
|
@@ -6523,9 +6536,7 @@ function createApi$2(ctx) {
|
|
|
6523
6536
|
root
|
|
6524
6537
|
});
|
|
6525
6538
|
const start = Date.now();
|
|
6526
|
-
const
|
|
6527
|
-
ctx.log.info(scrubbedStderr);
|
|
6528
|
-
const result = buildDeployResult(stdout, branch, start);
|
|
6539
|
+
const result = buildDeployResult(await executeWrangler(ctx, args), branch, start);
|
|
6529
6540
|
ctx.state.lastDeployment = result;
|
|
6530
6541
|
ctx.emit("deploy:complete", {
|
|
6531
6542
|
url: result.url,
|
|
@@ -6585,11 +6596,10 @@ function createApi$2(ctx) {
|
|
|
6585
6596
|
async createProject() {
|
|
6586
6597
|
const name = toSlug(ctx.require(sitePlugin).name());
|
|
6587
6598
|
const branch = ctx.config.productionBranch ?? "main";
|
|
6588
|
-
|
|
6599
|
+
await executeWrangler(ctx, buildProjectCreateArgs({
|
|
6589
6600
|
slug: name,
|
|
6590
6601
|
branch
|
|
6591
6602
|
}));
|
|
6592
|
-
ctx.log.info(scrubbedStderr);
|
|
6593
6603
|
return {
|
|
6594
6604
|
name,
|
|
6595
6605
|
branch
|
|
@@ -7515,23 +7525,23 @@ function createDevHandler(ctx, hub) {
|
|
|
7515
7525
|
}
|
|
7516
7526
|
/**
|
|
7517
7527
|
* Build the per-run {@link BuildRunOverrides} for a dev build from the session feature
|
|
7518
|
-
* opt-ins: minification is always off in dev (no benefit, slower), and each expensive
|
|
7519
|
-
* output stays off unless its flag re-enables it (`ogImage: false`
|
|
7520
|
-
* regardless of the persisted config).
|
|
7521
|
-
*
|
|
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.
|
|
7522
7533
|
*
|
|
7523
7534
|
* @param features - The resolved per-session dev feature opt-ins.
|
|
7524
7535
|
* @returns The config overrides merged into the dev build run.
|
|
7525
7536
|
* @example
|
|
7526
|
-
* devBuildOverrides({ og: false, sitemap: false, feeds: false
|
|
7537
|
+
* devBuildOverrides({ og: false, sitemap: false, feeds: false });
|
|
7527
7538
|
*/
|
|
7528
7539
|
function devBuildOverrides(features) {
|
|
7529
7540
|
return {
|
|
7530
7541
|
minify: false,
|
|
7531
7542
|
...features.feeds ? {} : { feeds: false },
|
|
7532
7543
|
...features.sitemap ? {} : { sitemap: false },
|
|
7533
|
-
...features.og ? {} : { ogImage: false }
|
|
7534
|
-
...features.localeRedirects ? {} : { localeRedirects: false }
|
|
7544
|
+
...features.og ? {} : { ogImage: false }
|
|
7535
7545
|
};
|
|
7536
7546
|
}
|
|
7537
7547
|
/**
|
|
@@ -7891,9 +7901,10 @@ function createApi$1(ctx) {
|
|
|
7891
7901
|
/**
|
|
7892
7902
|
* Dev loop: build once, serve `dist/` in-process (live-reload injected), watch
|
|
7893
7903
|
* `watchDirs`, debounced + incremental rebuild + reload. For a fast rebuild the dev
|
|
7894
|
-
* build disables minification + expensive,
|
|
7895
|
-
*
|
|
7896
|
-
*
|
|
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.
|
|
7897
7908
|
*
|
|
7898
7909
|
* @param options - Optional port override + per-session dev feature opt-ins.
|
|
7899
7910
|
* @returns Resolves once the server has been torn down.
|
|
@@ -7906,8 +7917,7 @@ function createApi$1(ctx) {
|
|
|
7906
7917
|
return runDevServer(ctx, port, {
|
|
7907
7918
|
og: options.og ?? false,
|
|
7908
7919
|
sitemap: options.sitemap ?? false,
|
|
7909
|
-
feeds: options.feeds ?? false
|
|
7910
|
-
localeRedirects: options.localeRedirects ?? false
|
|
7920
|
+
feeds: options.feeds ?? false
|
|
7911
7921
|
});
|
|
7912
7922
|
},
|
|
7913
7923
|
/**
|
|
@@ -8756,16 +8766,24 @@ function createPanelRenderer(options = {}) {
|
|
|
8756
8766
|
write(line);
|
|
8757
8767
|
},
|
|
8758
8768
|
/**
|
|
8759
|
-
* Render the deploy result from a `deploy:complete` event
|
|
8760
|
-
*
|
|
8769
|
+
* Render the deploy result from a `deploy:complete` event as a full-width box (matching
|
|
8770
|
+
* the BUILD panel): a `✓ DEPLOYED · branch` header with the elapsed time right-aligned,
|
|
8771
|
+
* then a `→ url · id` row. The url/id row is omitted entirely when wrangler returned no
|
|
8772
|
+
* URL, so a first-deploy with nothing to parse never renders a dangling `→` or `· ·`.
|
|
8761
8773
|
*
|
|
8762
8774
|
* @param result - The `deploy:complete` payload.
|
|
8763
8775
|
* @example
|
|
8764
8776
|
* render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
|
|
8765
8777
|
*/
|
|
8766
8778
|
deployed(result) {
|
|
8767
|
-
const
|
|
8768
|
-
|
|
8779
|
+
const dot = palette.dim("·");
|
|
8780
|
+
const time = result.durationMs >= 1e3 ? `${(result.durationMs / 1e3).toFixed(1)}s` : `${result.durationMs}ms`;
|
|
8781
|
+
const lines = [railLine(`${palette.green("✓")} ${palette.bold("DEPLOYED")} ${dot} ${result.branch}`, palette.dim(time), BOX_INNER)];
|
|
8782
|
+
if (result.url) {
|
|
8783
|
+
const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
|
|
8784
|
+
lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
|
|
8785
|
+
} else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
|
|
8786
|
+
writeBlock(box(lines, color, BOX_INNER));
|
|
8769
8787
|
},
|
|
8770
8788
|
/**
|
|
8771
8789
|
* Render a neutral informational line.
|
|
@@ -8860,6 +8878,29 @@ function createPanelRenderer(options = {}) {
|
|
|
8860
8878
|
*/
|
|
8861
8879
|
/** Matches an explicit affirmative answer (`y`/`yes`, case-insensitive). */
|
|
8862
8880
|
const YES_PATTERN = /^y(es)?$/i;
|
|
8881
|
+
/** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
|
|
8882
|
+
const PROMPT_WIDTH = 66;
|
|
8883
|
+
/** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
|
|
8884
|
+
const PROMPT_COLOR = supportsColor();
|
|
8885
|
+
/** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
|
|
8886
|
+
const PROMPT_PALETTE = makePalette(PROMPT_COLOR, PROMPT_COLOR && supportsTruecolor());
|
|
8887
|
+
/**
|
|
8888
|
+
* Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
|
|
8889
|
+
* a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
|
|
8890
|
+
* to the plain `question [y/N] ` form off a color TTY (CI/pipes), where prompts rarely run.
|
|
8891
|
+
*
|
|
8892
|
+
* @param question - The yes/no question to display.
|
|
8893
|
+
* @returns The readline prompt string (the typed answer follows the caret).
|
|
8894
|
+
* @example
|
|
8895
|
+
* confirmPrompt("Deploy dist/ to Cloudflare Pages?");
|
|
8896
|
+
*/
|
|
8897
|
+
function confirmPrompt(question) {
|
|
8898
|
+
if (!PROMPT_COLOR) return `${question} [y/N] `;
|
|
8899
|
+
const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
|
|
8900
|
+
const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
|
|
8901
|
+
const gap = Math.max(1, PROMPT_WIDTH - visibleWidth(left) - visibleWidth(right));
|
|
8902
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
8903
|
+
}
|
|
8863
8904
|
/**
|
|
8864
8905
|
* Resolve the `Bun` runtime global, or `undefined` when not running under Bun.
|
|
8865
8906
|
*
|
|
@@ -8917,7 +8958,7 @@ function defaultConfirm(question) {
|
|
|
8917
8958
|
input: process.stdin,
|
|
8918
8959
|
output: process.stdout
|
|
8919
8960
|
});
|
|
8920
|
-
readline.question(
|
|
8961
|
+
readline.question(confirmPrompt(question), (answer) => {
|
|
8921
8962
|
readline.close();
|
|
8922
8963
|
resolve(YES_PATTERN.test(answer.trim()));
|
|
8923
8964
|
});
|
|
@@ -8940,8 +8981,8 @@ function defaultSelect(question, choices) {
|
|
|
8940
8981
|
input: process.stdin,
|
|
8941
8982
|
output: process.stdout
|
|
8942
8983
|
});
|
|
8943
|
-
|
|
8944
|
-
readline.question(
|
|
8984
|
+
console.log(selectChoicesBlock(question, choices));
|
|
8985
|
+
readline.question(selectPrompt(question, choices.length), (answer) => {
|
|
8945
8986
|
readline.close();
|
|
8946
8987
|
const picked = Number.parseInt(answer.trim(), 10);
|
|
8947
8988
|
resolve(Number.isInteger(picked) && picked >= 1 && picked <= choices.length ? picked - 1 : 0);
|
|
@@ -8949,6 +8990,36 @@ function defaultSelect(question, choices) {
|
|
|
8949
8990
|
});
|
|
8950
8991
|
}
|
|
8951
8992
|
/**
|
|
8993
|
+
* Render the select block: a brand `◆` marker + the question, then each choice as an
|
|
8994
|
+
* indented dim number + label. Off a color TTY, falls back to the plain ` N) label`
|
|
8995
|
+
* list (the question rides the prompt instead).
|
|
8996
|
+
*
|
|
8997
|
+
* @param question - The prompt shown above the choices (styled mode only).
|
|
8998
|
+
* @param choices - The selectable option labels.
|
|
8999
|
+
* @returns The multi-line choices block.
|
|
9000
|
+
* @example
|
|
9001
|
+
* selectChoicesBlock("Set up a workflow?", ["Auto", "Manual", "Skip"]);
|
|
9002
|
+
*/
|
|
9003
|
+
function selectChoicesBlock(question, choices) {
|
|
9004
|
+
if (!PROMPT_COLOR) return choices.map((choice, index) => ` ${index + 1}) ${choice}`).join("\n");
|
|
9005
|
+
return [` ${PROMPT_PALETTE.pink("◆")} ${question}`, ...choices.map((choice, index) => ` ${PROMPT_PALETTE.dim(String(index + 1))} ${choice}`)].join("\n");
|
|
9006
|
+
}
|
|
9007
|
+
/**
|
|
9008
|
+
* Build the select input prompt: a dim `pick 1–N` hint + cyan `›` caret in styled mode,
|
|
9009
|
+
* or the plain `question [1-N] ` form off a color TTY (where the question is not printed
|
|
9010
|
+
* separately).
|
|
9011
|
+
*
|
|
9012
|
+
* @param question - The prompt (used only by the plain fallback).
|
|
9013
|
+
* @param count - The number of choices.
|
|
9014
|
+
* @returns The readline prompt string.
|
|
9015
|
+
* @example
|
|
9016
|
+
* selectPrompt("Set up a workflow?", 3);
|
|
9017
|
+
*/
|
|
9018
|
+
function selectPrompt(question, count) {
|
|
9019
|
+
if (!PROMPT_COLOR) return `${question} [1-${count}] `;
|
|
9020
|
+
return ` ${PROMPT_PALETTE.dim(`pick 1–${count}`)} ${PROMPT_PALETTE.cyan("›")} `;
|
|
9021
|
+
}
|
|
9022
|
+
/**
|
|
8952
9023
|
* Default recursive directory watcher — wraps `node:fs.watch` with `{ recursive: true }`
|
|
8953
9024
|
* and adapts its handle to {@link WatchHandle}. Tests inject a fake emitter so no real
|
|
8954
9025
|
* FS watch is registered.
|
package/dist/index.d.cts
CHANGED
|
@@ -1615,7 +1615,17 @@ type Config$3 = {
|
|
|
1615
1615
|
body?: string;
|
|
1616
1616
|
}; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
|
|
1617
1617
|
localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
|
|
1618
|
-
clientEntry?: string;
|
|
1618
|
+
clientEntry?: string;
|
|
1619
|
+
/**
|
|
1620
|
+
* Path to a custom HTML document shell, giving the app full control over the
|
|
1621
|
+
* scaffold (charset, viewport, `<html lang>`, body attributes, wrapper markup).
|
|
1622
|
+
* Placeholders, substituted per page at build time:
|
|
1623
|
+
* `<!--moku:lang-->` (page locale for `<html lang>`),
|
|
1624
|
+
* `<!--moku:head-->` (composed `<head>` inner HTML),
|
|
1625
|
+
* `<!--moku:assets-->` (injected `<link>`/`<script>` tags),
|
|
1626
|
+
* `<!--moku:body-->` (SSR body HTML).
|
|
1627
|
+
* When unset, the built-in shell is used (it emits charset + viewport by default).
|
|
1628
|
+
*/
|
|
1619
1629
|
template?: string;
|
|
1620
1630
|
};
|
|
1621
1631
|
/**
|
|
@@ -2460,8 +2470,7 @@ type ServeOptions = {
|
|
|
2460
2470
|
*/
|
|
2461
2471
|
og?: boolean; /** Re-enable `sitemap.xml` + `robots.txt` for this dev session (maps to `--sitemap`). Defaults to `false`. */
|
|
2462
2472
|
sitemap?: boolean; /** Re-enable RSS/Atom/JSON feeds for this dev session (maps to `--feeds`). Defaults to `false`. */
|
|
2463
|
-
feeds?: boolean;
|
|
2464
|
-
localeRedirects?: boolean;
|
|
2473
|
+
feeds?: boolean;
|
|
2465
2474
|
};
|
|
2466
2475
|
/**
|
|
2467
2476
|
* Options for `cli.preview()`.
|
|
@@ -2736,11 +2745,18 @@ type Config = {
|
|
|
2736
2745
|
*
|
|
2737
2746
|
* @example
|
|
2738
2747
|
* ```ts
|
|
2739
|
-
* { articles: new Map() }
|
|
2748
|
+
* { articles: new Map(), loadedAll: null }
|
|
2740
2749
|
* ```
|
|
2741
2750
|
*/
|
|
2742
2751
|
type State = {
|
|
2743
2752
|
/** Article cache keyed locale -> (slug -> Article). Starts empty. */articles: Map<string, Map<string, Article>>;
|
|
2753
|
+
/**
|
|
2754
|
+
* Memoized full `loadAll()` result, or `null` when not yet loaded / invalidated. List-route
|
|
2755
|
+
* loaders call `loadAll()` once PER PAGE, so without this every page re-reads + re-renders
|
|
2756
|
+
* every article (the dev-loop killer). The memo makes repeated calls O(1); `invalidate()`
|
|
2757
|
+
* clears it so a dev rebuild reloads (re-resolving only the changed slugs). Starts `null`.
|
|
2758
|
+
*/
|
|
2759
|
+
loadedAll: Map<string, Article[]> | null;
|
|
2744
2760
|
};
|
|
2745
2761
|
/**
|
|
2746
2762
|
* Notification-only events emitted by the content plugin.
|
|
@@ -2790,11 +2806,13 @@ type ContentApiContext = {
|
|
|
2790
2806
|
*/
|
|
2791
2807
|
type LoadAllOptions = {
|
|
2792
2808
|
/**
|
|
2793
|
-
* Reuse
|
|
2794
|
-
*
|
|
2795
|
-
*
|
|
2796
|
-
*
|
|
2797
|
-
*
|
|
2809
|
+
* Reuse the per-build memo + per-slug cache (re-resolving only slugs a preceding
|
|
2810
|
+
* `invalidate()` dropped). Default `true` — this is what keeps repeated `loadAll()` calls
|
|
2811
|
+
* (a list route's loader runs once per page) cheap, and makes a dev rebuild re-render only
|
|
2812
|
+
* changed articles. Set `false` to force a FRESH full reload (cold build / an
|
|
2813
|
+
* unclassifiable change), which re-reads + re-renders every article and rebuilds the memo.
|
|
2814
|
+
* The post-sort `contentId` ordinals are always recomputed across the full set, so order +
|
|
2815
|
+
* ids match a full load either way.
|
|
2798
2816
|
*/
|
|
2799
2817
|
reuse?: boolean;
|
|
2800
2818
|
};
|
|
@@ -2808,10 +2826,13 @@ type LoadAllOptions = {
|
|
|
2808
2826
|
*/
|
|
2809
2827
|
type Api = {
|
|
2810
2828
|
/**
|
|
2811
|
-
* Load every article across every active locale, returning a locale-keyed
|
|
2812
|
-
*
|
|
2829
|
+
* Load every article across every active locale, returning a locale-keyed map of
|
|
2830
|
+
* date-descending Article arrays. Emits content:ready (once per actual load). Cache-first
|
|
2831
|
+
* + memoized: repeated calls (e.g. a list route's loader on every page) return the SAME
|
|
2832
|
+
* cached result with no re-read — so treat the result as READ-ONLY (do not sort/mutate it
|
|
2833
|
+
* in place; slice/copy first). Pass `{ reuse: false }` to force a fresh full reload.
|
|
2813
2834
|
*
|
|
2814
|
-
* @param options - Optional load behaviour ({@link LoadAllOptions});
|
|
2835
|
+
* @param options - Optional load behaviour ({@link LoadAllOptions}); default reuses the cache.
|
|
2815
2836
|
*/
|
|
2816
2837
|
loadAll(options?: LoadAllOptions): Promise<Map<string, Article[]>>;
|
|
2817
2838
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1615,7 +1615,17 @@ type Config$3 = {
|
|
|
1615
1615
|
body?: string;
|
|
1616
1616
|
}; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
|
|
1617
1617
|
localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
|
|
1618
|
-
clientEntry?: string;
|
|
1618
|
+
clientEntry?: string;
|
|
1619
|
+
/**
|
|
1620
|
+
* Path to a custom HTML document shell, giving the app full control over the
|
|
1621
|
+
* scaffold (charset, viewport, `<html lang>`, body attributes, wrapper markup).
|
|
1622
|
+
* Placeholders, substituted per page at build time:
|
|
1623
|
+
* `<!--moku:lang-->` (page locale for `<html lang>`),
|
|
1624
|
+
* `<!--moku:head-->` (composed `<head>` inner HTML),
|
|
1625
|
+
* `<!--moku:assets-->` (injected `<link>`/`<script>` tags),
|
|
1626
|
+
* `<!--moku:body-->` (SSR body HTML).
|
|
1627
|
+
* When unset, the built-in shell is used (it emits charset + viewport by default).
|
|
1628
|
+
*/
|
|
1619
1629
|
template?: string;
|
|
1620
1630
|
};
|
|
1621
1631
|
/**
|
|
@@ -2460,8 +2470,7 @@ type ServeOptions = {
|
|
|
2460
2470
|
*/
|
|
2461
2471
|
og?: boolean; /** Re-enable `sitemap.xml` + `robots.txt` for this dev session (maps to `--sitemap`). Defaults to `false`. */
|
|
2462
2472
|
sitemap?: boolean; /** Re-enable RSS/Atom/JSON feeds for this dev session (maps to `--feeds`). Defaults to `false`. */
|
|
2463
|
-
feeds?: boolean;
|
|
2464
|
-
localeRedirects?: boolean;
|
|
2473
|
+
feeds?: boolean;
|
|
2465
2474
|
};
|
|
2466
2475
|
/**
|
|
2467
2476
|
* Options for `cli.preview()`.
|
|
@@ -2736,11 +2745,18 @@ type Config = {
|
|
|
2736
2745
|
*
|
|
2737
2746
|
* @example
|
|
2738
2747
|
* ```ts
|
|
2739
|
-
* { articles: new Map() }
|
|
2748
|
+
* { articles: new Map(), loadedAll: null }
|
|
2740
2749
|
* ```
|
|
2741
2750
|
*/
|
|
2742
2751
|
type State = {
|
|
2743
2752
|
/** Article cache keyed locale -> (slug -> Article). Starts empty. */articles: Map<string, Map<string, Article>>;
|
|
2753
|
+
/**
|
|
2754
|
+
* Memoized full `loadAll()` result, or `null` when not yet loaded / invalidated. List-route
|
|
2755
|
+
* loaders call `loadAll()` once PER PAGE, so without this every page re-reads + re-renders
|
|
2756
|
+
* every article (the dev-loop killer). The memo makes repeated calls O(1); `invalidate()`
|
|
2757
|
+
* clears it so a dev rebuild reloads (re-resolving only the changed slugs). Starts `null`.
|
|
2758
|
+
*/
|
|
2759
|
+
loadedAll: Map<string, Article[]> | null;
|
|
2744
2760
|
};
|
|
2745
2761
|
/**
|
|
2746
2762
|
* Notification-only events emitted by the content plugin.
|
|
@@ -2790,11 +2806,13 @@ type ContentApiContext = {
|
|
|
2790
2806
|
*/
|
|
2791
2807
|
type LoadAllOptions = {
|
|
2792
2808
|
/**
|
|
2793
|
-
* Reuse
|
|
2794
|
-
*
|
|
2795
|
-
*
|
|
2796
|
-
*
|
|
2797
|
-
*
|
|
2809
|
+
* Reuse the per-build memo + per-slug cache (re-resolving only slugs a preceding
|
|
2810
|
+
* `invalidate()` dropped). Default `true` — this is what keeps repeated `loadAll()` calls
|
|
2811
|
+
* (a list route's loader runs once per page) cheap, and makes a dev rebuild re-render only
|
|
2812
|
+
* changed articles. Set `false` to force a FRESH full reload (cold build / an
|
|
2813
|
+
* unclassifiable change), which re-reads + re-renders every article and rebuilds the memo.
|
|
2814
|
+
* The post-sort `contentId` ordinals are always recomputed across the full set, so order +
|
|
2815
|
+
* ids match a full load either way.
|
|
2798
2816
|
*/
|
|
2799
2817
|
reuse?: boolean;
|
|
2800
2818
|
};
|
|
@@ -2808,10 +2826,13 @@ type LoadAllOptions = {
|
|
|
2808
2826
|
*/
|
|
2809
2827
|
type Api = {
|
|
2810
2828
|
/**
|
|
2811
|
-
* Load every article across every active locale, returning a locale-keyed
|
|
2812
|
-
*
|
|
2829
|
+
* Load every article across every active locale, returning a locale-keyed map of
|
|
2830
|
+
* date-descending Article arrays. Emits content:ready (once per actual load). Cache-first
|
|
2831
|
+
* + memoized: repeated calls (e.g. a list route's loader on every page) return the SAME
|
|
2832
|
+
* cached result with no re-read — so treat the result as READ-ONLY (do not sort/mutate it
|
|
2833
|
+
* in place; slice/copy first). Pass `{ reuse: false }` to force a fresh full reload.
|
|
2813
2834
|
*
|
|
2814
|
-
* @param options - Optional load behaviour ({@link LoadAllOptions});
|
|
2835
|
+
* @param options - Optional load behaviour ({@link LoadAllOptions}); default reuses the cache.
|
|
2815
2836
|
*/
|
|
2816
2837
|
loadAll(options?: LoadAllOptions): Promise<Map<string, Article[]>>;
|
|
2817
2838
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1336,12 +1336,15 @@ function createContentApi(ctx) {
|
|
|
1336
1336
|
* Load every article across every active locale (locale fallback, production
|
|
1337
1337
|
* draft exclusion, date sort, `contentId` after sort), cache them, emit `content:ready`.
|
|
1338
1338
|
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
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.
|
|
1343
1346
|
*
|
|
1344
|
-
* @param options - Optional load behaviour (`reuse`)
|
|
1347
|
+
* @param options - Optional load behaviour (`reuse`, default `true`).
|
|
1345
1348
|
* @returns A locale-keyed map of date-descending articles.
|
|
1346
1349
|
* @example
|
|
1347
1350
|
* ```ts
|
|
@@ -1349,7 +1352,9 @@ function createContentApi(ctx) {
|
|
|
1349
1352
|
* ```
|
|
1350
1353
|
*/
|
|
1351
1354
|
async loadAll(options) {
|
|
1352
|
-
const reuse = options?.reuse
|
|
1355
|
+
const reuse = options?.reuse !== false;
|
|
1356
|
+
const memo = ctx.state.loadedAll;
|
|
1357
|
+
if (reuse && memo !== null) return memo;
|
|
1353
1358
|
const slugs = await ctx.provider.slugs();
|
|
1354
1359
|
const locales = ctx.locales();
|
|
1355
1360
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -1367,6 +1372,7 @@ function createContentApi(ctx) {
|
|
|
1367
1372
|
result.set(locale, present);
|
|
1368
1373
|
total += present.length;
|
|
1369
1374
|
}
|
|
1375
|
+
ctx.state.loadedAll = result;
|
|
1370
1376
|
ctx.emit("content:ready", {
|
|
1371
1377
|
locales,
|
|
1372
1378
|
articleCount: total
|
|
@@ -1430,6 +1436,7 @@ function createContentApi(ctx) {
|
|
|
1430
1436
|
if (slug === void 0) continue;
|
|
1431
1437
|
for (const cache of ctx.state.articles.values()) cache.delete(slug);
|
|
1432
1438
|
}
|
|
1439
|
+
if (accepted.length > 0) ctx.state.loadedAll = null;
|
|
1433
1440
|
ctx.emit("content:invalidated", { paths: accepted });
|
|
1434
1441
|
},
|
|
1435
1442
|
/**
|
|
@@ -1499,14 +1506,17 @@ const contentEvents = (register) => ({
|
|
|
1499
1506
|
* @param _ctx - Minimal context with global and config.
|
|
1500
1507
|
* @param _ctx.global - Global plugin registry.
|
|
1501
1508
|
* @param _ctx.config - Resolved plugin configuration.
|
|
1502
|
-
* @returns Fresh content shell state: an empty article cache.
|
|
1509
|
+
* @returns Fresh content shell state: an empty article cache + an empty loadAll memo.
|
|
1503
1510
|
* @example
|
|
1504
1511
|
* ```ts
|
|
1505
1512
|
* const state = createContentState({ global: {}, config: { providers: [] } });
|
|
1506
1513
|
* ```
|
|
1507
1514
|
*/
|
|
1508
1515
|
function createContentState(_ctx) {
|
|
1509
|
-
return {
|
|
1516
|
+
return {
|
|
1517
|
+
articles: /* @__PURE__ */ new Map(),
|
|
1518
|
+
loadedAll: null
|
|
1519
|
+
};
|
|
1510
1520
|
}
|
|
1511
1521
|
//#endregion
|
|
1512
1522
|
//#region src/plugins/content/validate.ts
|
|
@@ -3819,7 +3829,7 @@ const DEFAULT_BODY = "<h1>404</h1><p>The page you requested could not be found.<
|
|
|
3819
3829
|
* ```
|
|
3820
3830
|
*/
|
|
3821
3831
|
function wrap(body) {
|
|
3822
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3832
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3823
3833
|
}
|
|
3824
3834
|
/**
|
|
3825
3835
|
* Emits `outDir/404.html`. When `config.notFound` is `true`, writes the built-in
|
|
@@ -4503,6 +4513,8 @@ const HEAD_PLACEHOLDER = "<!--moku:head-->";
|
|
|
4503
4513
|
const BODY_PLACEHOLDER = "<!--moku:body-->";
|
|
4504
4514
|
/** Template placeholder for the injected asset `<link>`/`<script>` tags. */
|
|
4505
4515
|
const ASSETS_PLACEHOLDER = "<!--moku:assets-->";
|
|
4516
|
+
/** Template placeholder for the page's locale (`<html lang>`). */
|
|
4517
|
+
const LANG_PLACEHOLDER = "<!--moku:lang-->";
|
|
4506
4518
|
/**
|
|
4507
4519
|
* Read the bundle phase's hashed asset manifest for one kind from `state.buildCache`
|
|
4508
4520
|
* as a typed {@link BuildCacheEntry} (no `Map<string, unknown>` reads).
|
|
@@ -4550,14 +4562,16 @@ function buildAssetTags(ctx) {
|
|
|
4550
4562
|
* ```
|
|
4551
4563
|
*/
|
|
4552
4564
|
function renderDocument(parts) {
|
|
4553
|
-
return `<!DOCTYPE html><html lang="${parts.locale}"><head>${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
|
|
4565
|
+
return `<!DOCTYPE html><html lang="${parts.locale}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
|
|
4554
4566
|
}
|
|
4555
4567
|
/**
|
|
4556
|
-
* Fill a shell template's `<!--moku:
|
|
4557
|
-
* `<!--moku:assets-->` placeholders deterministically at build
|
|
4568
|
+
* Fill a shell template's `<!--moku:lang-->` / `<!--moku:head-->` /
|
|
4569
|
+
* `<!--moku:body-->` / `<!--moku:assets-->` placeholders deterministically at build
|
|
4570
|
+
* time. `<!--moku:lang-->` carries the page locale (for `<html lang>`), so a single
|
|
4571
|
+
* shared template stays locale-correct across every locale.
|
|
4558
4572
|
*
|
|
4559
4573
|
* @param template - The raw shell template HTML.
|
|
4560
|
-
* @param parts - The composed head/body/assets pieces.
|
|
4574
|
+
* @param parts - The composed head/body/assets/locale pieces.
|
|
4561
4575
|
* @returns The filled document string.
|
|
4562
4576
|
* @example
|
|
4563
4577
|
* ```ts
|
|
@@ -4565,7 +4579,7 @@ function renderDocument(parts) {
|
|
|
4565
4579
|
* ```
|
|
4566
4580
|
*/
|
|
4567
4581
|
function fillTemplate(template, parts) {
|
|
4568
|
-
return template.replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
|
|
4582
|
+
return template.replaceAll(LANG_PLACEHOLDER, parts.locale).replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
|
|
4569
4583
|
}
|
|
4570
4584
|
/**
|
|
4571
4585
|
* Resolve the compiled entry for a manifest definition, asserting the router
|
|
@@ -5817,7 +5831,8 @@ function buildWranglerArgs(input) {
|
|
|
5817
5831
|
"--project-name",
|
|
5818
5832
|
input.slug,
|
|
5819
5833
|
"--branch",
|
|
5820
|
-
branch
|
|
5834
|
+
branch,
|
|
5835
|
+
"--commit-dirty=true"
|
|
5821
5836
|
];
|
|
5822
5837
|
}
|
|
5823
5838
|
/**
|
|
@@ -6426,17 +6441,18 @@ function validateConfig$1(ctx) {
|
|
|
6426
6441
|
ctx.require(sitePlugin);
|
|
6427
6442
|
}
|
|
6428
6443
|
/**
|
|
6429
|
-
* Run wrangler for the prepared argv and
|
|
6430
|
-
*
|
|
6431
|
-
*
|
|
6432
|
-
*
|
|
6444
|
+
* Run wrangler for the prepared argv and return its stdout, translating a non-zero
|
|
6445
|
+
* exit into the classified deploy error. The API token is read from env here so it
|
|
6446
|
+
* never crosses a logging boundary; the scrubbed stderr is used only to classify a
|
|
6447
|
+
* failure — it is never logged (that was console noise), so nothing leaks. Shared by
|
|
6448
|
+
* `run()` (deploy) and `createProject()` (project create).
|
|
6433
6449
|
*
|
|
6434
6450
|
* @param ctx - Plugin context (provides `state.spawn`, `config`, `env`).
|
|
6435
6451
|
* @param args - The fully-built, pre-validated wrangler argv.
|
|
6436
|
-
* @returns The wrangler `stdout`
|
|
6452
|
+
* @returns The wrangler `stdout` (for URL/id parsing on a deploy).
|
|
6437
6453
|
* @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
|
|
6438
6454
|
* @example
|
|
6439
|
-
* const
|
|
6455
|
+
* const stdout = await executeWrangler(ctx, args);
|
|
6440
6456
|
*/
|
|
6441
6457
|
async function executeWrangler(ctx, args) {
|
|
6442
6458
|
const token = ctx.env.require("CLOUDFLARE_API_TOKEN");
|
|
@@ -6450,10 +6466,7 @@ async function executeWrangler(ctx, args) {
|
|
|
6450
6466
|
const { code, message } = classifyWranglerError(exitCode, scrubbedStderr);
|
|
6451
6467
|
throw deployError(code, message);
|
|
6452
6468
|
}
|
|
6453
|
-
return
|
|
6454
|
-
stdout,
|
|
6455
|
-
scrubbedStderr
|
|
6456
|
-
};
|
|
6469
|
+
return stdout;
|
|
6457
6470
|
}
|
|
6458
6471
|
/**
|
|
6459
6472
|
* Assemble the public {@link DeployResult} from wrangler's stdout, parsing the
|
|
@@ -6510,9 +6523,7 @@ function createApi$2(ctx) {
|
|
|
6510
6523
|
root
|
|
6511
6524
|
});
|
|
6512
6525
|
const start = Date.now();
|
|
6513
|
-
const
|
|
6514
|
-
ctx.log.info(scrubbedStderr);
|
|
6515
|
-
const result = buildDeployResult(stdout, branch, start);
|
|
6526
|
+
const result = buildDeployResult(await executeWrangler(ctx, args), branch, start);
|
|
6516
6527
|
ctx.state.lastDeployment = result;
|
|
6517
6528
|
ctx.emit("deploy:complete", {
|
|
6518
6529
|
url: result.url,
|
|
@@ -6572,11 +6583,10 @@ function createApi$2(ctx) {
|
|
|
6572
6583
|
async createProject() {
|
|
6573
6584
|
const name = toSlug(ctx.require(sitePlugin).name());
|
|
6574
6585
|
const branch = ctx.config.productionBranch ?? "main";
|
|
6575
|
-
|
|
6586
|
+
await executeWrangler(ctx, buildProjectCreateArgs({
|
|
6576
6587
|
slug: name,
|
|
6577
6588
|
branch
|
|
6578
6589
|
}));
|
|
6579
|
-
ctx.log.info(scrubbedStderr);
|
|
6580
6590
|
return {
|
|
6581
6591
|
name,
|
|
6582
6592
|
branch
|
|
@@ -7502,23 +7512,23 @@ function createDevHandler(ctx, hub) {
|
|
|
7502
7512
|
}
|
|
7503
7513
|
/**
|
|
7504
7514
|
* Build the per-run {@link BuildRunOverrides} for a dev build from the session feature
|
|
7505
|
-
* opt-ins: minification is always off in dev (no benefit, slower), and each expensive
|
|
7506
|
-
* output stays off unless its flag re-enables it (`ogImage: false`
|
|
7507
|
-
* regardless of the persisted config).
|
|
7508
|
-
*
|
|
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.
|
|
7509
7520
|
*
|
|
7510
7521
|
* @param features - The resolved per-session dev feature opt-ins.
|
|
7511
7522
|
* @returns The config overrides merged into the dev build run.
|
|
7512
7523
|
* @example
|
|
7513
|
-
* devBuildOverrides({ og: false, sitemap: false, feeds: false
|
|
7524
|
+
* devBuildOverrides({ og: false, sitemap: false, feeds: false });
|
|
7514
7525
|
*/
|
|
7515
7526
|
function devBuildOverrides(features) {
|
|
7516
7527
|
return {
|
|
7517
7528
|
minify: false,
|
|
7518
7529
|
...features.feeds ? {} : { feeds: false },
|
|
7519
7530
|
...features.sitemap ? {} : { sitemap: false },
|
|
7520
|
-
...features.og ? {} : { ogImage: false }
|
|
7521
|
-
...features.localeRedirects ? {} : { localeRedirects: false }
|
|
7531
|
+
...features.og ? {} : { ogImage: false }
|
|
7522
7532
|
};
|
|
7523
7533
|
}
|
|
7524
7534
|
/**
|
|
@@ -7878,9 +7888,10 @@ function createApi$1(ctx) {
|
|
|
7878
7888
|
/**
|
|
7879
7889
|
* Dev loop: build once, serve `dist/` in-process (live-reload injected), watch
|
|
7880
7890
|
* `watchDirs`, debounced + incremental rebuild + reload. For a fast rebuild the dev
|
|
7881
|
-
* build disables minification + expensive,
|
|
7882
|
-
*
|
|
7883
|
-
*
|
|
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.
|
|
7884
7895
|
*
|
|
7885
7896
|
* @param options - Optional port override + per-session dev feature opt-ins.
|
|
7886
7897
|
* @returns Resolves once the server has been torn down.
|
|
@@ -7893,8 +7904,7 @@ function createApi$1(ctx) {
|
|
|
7893
7904
|
return runDevServer(ctx, port, {
|
|
7894
7905
|
og: options.og ?? false,
|
|
7895
7906
|
sitemap: options.sitemap ?? false,
|
|
7896
|
-
feeds: options.feeds ?? false
|
|
7897
|
-
localeRedirects: options.localeRedirects ?? false
|
|
7907
|
+
feeds: options.feeds ?? false
|
|
7898
7908
|
});
|
|
7899
7909
|
},
|
|
7900
7910
|
/**
|
|
@@ -8743,16 +8753,24 @@ function createPanelRenderer(options = {}) {
|
|
|
8743
8753
|
write(line);
|
|
8744
8754
|
},
|
|
8745
8755
|
/**
|
|
8746
|
-
* Render the deploy result from a `deploy:complete` event
|
|
8747
|
-
*
|
|
8756
|
+
* Render the deploy result from a `deploy:complete` event as a full-width box (matching
|
|
8757
|
+
* the BUILD panel): a `✓ DEPLOYED · branch` header with the elapsed time right-aligned,
|
|
8758
|
+
* then a `→ url · id` row. The url/id row is omitted entirely when wrangler returned no
|
|
8759
|
+
* URL, so a first-deploy with nothing to parse never renders a dangling `→` or `· ·`.
|
|
8748
8760
|
*
|
|
8749
8761
|
* @param result - The `deploy:complete` payload.
|
|
8750
8762
|
* @example
|
|
8751
8763
|
* render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
|
|
8752
8764
|
*/
|
|
8753
8765
|
deployed(result) {
|
|
8754
|
-
const
|
|
8755
|
-
|
|
8766
|
+
const dot = palette.dim("·");
|
|
8767
|
+
const time = result.durationMs >= 1e3 ? `${(result.durationMs / 1e3).toFixed(1)}s` : `${result.durationMs}ms`;
|
|
8768
|
+
const lines = [railLine(`${palette.green("✓")} ${palette.bold("DEPLOYED")} ${dot} ${result.branch}`, palette.dim(time), BOX_INNER)];
|
|
8769
|
+
if (result.url) {
|
|
8770
|
+
const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
|
|
8771
|
+
lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
|
|
8772
|
+
} else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
|
|
8773
|
+
writeBlock(box(lines, color, BOX_INNER));
|
|
8756
8774
|
},
|
|
8757
8775
|
/**
|
|
8758
8776
|
* Render a neutral informational line.
|
|
@@ -8847,6 +8865,29 @@ function createPanelRenderer(options = {}) {
|
|
|
8847
8865
|
*/
|
|
8848
8866
|
/** Matches an explicit affirmative answer (`y`/`yes`, case-insensitive). */
|
|
8849
8867
|
const YES_PATTERN = /^y(es)?$/i;
|
|
8868
|
+
/** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
|
|
8869
|
+
const PROMPT_WIDTH = 66;
|
|
8870
|
+
/** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
|
|
8871
|
+
const PROMPT_COLOR = supportsColor();
|
|
8872
|
+
/** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
|
|
8873
|
+
const PROMPT_PALETTE = makePalette(PROMPT_COLOR, PROMPT_COLOR && supportsTruecolor());
|
|
8874
|
+
/**
|
|
8875
|
+
* Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
|
|
8876
|
+
* a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
|
|
8877
|
+
* to the plain `question [y/N] ` form off a color TTY (CI/pipes), where prompts rarely run.
|
|
8878
|
+
*
|
|
8879
|
+
* @param question - The yes/no question to display.
|
|
8880
|
+
* @returns The readline prompt string (the typed answer follows the caret).
|
|
8881
|
+
* @example
|
|
8882
|
+
* confirmPrompt("Deploy dist/ to Cloudflare Pages?");
|
|
8883
|
+
*/
|
|
8884
|
+
function confirmPrompt(question) {
|
|
8885
|
+
if (!PROMPT_COLOR) return `${question} [y/N] `;
|
|
8886
|
+
const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
|
|
8887
|
+
const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
|
|
8888
|
+
const gap = Math.max(1, PROMPT_WIDTH - visibleWidth(left) - visibleWidth(right));
|
|
8889
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
8890
|
+
}
|
|
8850
8891
|
/**
|
|
8851
8892
|
* Resolve the `Bun` runtime global, or `undefined` when not running under Bun.
|
|
8852
8893
|
*
|
|
@@ -8904,7 +8945,7 @@ function defaultConfirm(question) {
|
|
|
8904
8945
|
input: process.stdin,
|
|
8905
8946
|
output: process.stdout
|
|
8906
8947
|
});
|
|
8907
|
-
readline.question(
|
|
8948
|
+
readline.question(confirmPrompt(question), (answer) => {
|
|
8908
8949
|
readline.close();
|
|
8909
8950
|
resolve(YES_PATTERN.test(answer.trim()));
|
|
8910
8951
|
});
|
|
@@ -8927,8 +8968,8 @@ function defaultSelect(question, choices) {
|
|
|
8927
8968
|
input: process.stdin,
|
|
8928
8969
|
output: process.stdout
|
|
8929
8970
|
});
|
|
8930
|
-
|
|
8931
|
-
readline.question(
|
|
8971
|
+
console.log(selectChoicesBlock(question, choices));
|
|
8972
|
+
readline.question(selectPrompt(question, choices.length), (answer) => {
|
|
8932
8973
|
readline.close();
|
|
8933
8974
|
const picked = Number.parseInt(answer.trim(), 10);
|
|
8934
8975
|
resolve(Number.isInteger(picked) && picked >= 1 && picked <= choices.length ? picked - 1 : 0);
|
|
@@ -8936,6 +8977,36 @@ function defaultSelect(question, choices) {
|
|
|
8936
8977
|
});
|
|
8937
8978
|
}
|
|
8938
8979
|
/**
|
|
8980
|
+
* Render the select block: a brand `◆` marker + the question, then each choice as an
|
|
8981
|
+
* indented dim number + label. Off a color TTY, falls back to the plain ` N) label`
|
|
8982
|
+
* list (the question rides the prompt instead).
|
|
8983
|
+
*
|
|
8984
|
+
* @param question - The prompt shown above the choices (styled mode only).
|
|
8985
|
+
* @param choices - The selectable option labels.
|
|
8986
|
+
* @returns The multi-line choices block.
|
|
8987
|
+
* @example
|
|
8988
|
+
* selectChoicesBlock("Set up a workflow?", ["Auto", "Manual", "Skip"]);
|
|
8989
|
+
*/
|
|
8990
|
+
function selectChoicesBlock(question, choices) {
|
|
8991
|
+
if (!PROMPT_COLOR) return choices.map((choice, index) => ` ${index + 1}) ${choice}`).join("\n");
|
|
8992
|
+
return [` ${PROMPT_PALETTE.pink("◆")} ${question}`, ...choices.map((choice, index) => ` ${PROMPT_PALETTE.dim(String(index + 1))} ${choice}`)].join("\n");
|
|
8993
|
+
}
|
|
8994
|
+
/**
|
|
8995
|
+
* Build the select input prompt: a dim `pick 1–N` hint + cyan `›` caret in styled mode,
|
|
8996
|
+
* or the plain `question [1-N] ` form off a color TTY (where the question is not printed
|
|
8997
|
+
* separately).
|
|
8998
|
+
*
|
|
8999
|
+
* @param question - The prompt (used only by the plain fallback).
|
|
9000
|
+
* @param count - The number of choices.
|
|
9001
|
+
* @returns The readline prompt string.
|
|
9002
|
+
* @example
|
|
9003
|
+
* selectPrompt("Set up a workflow?", 3);
|
|
9004
|
+
*/
|
|
9005
|
+
function selectPrompt(question, count) {
|
|
9006
|
+
if (!PROMPT_COLOR) return `${question} [1-${count}] `;
|
|
9007
|
+
return ` ${PROMPT_PALETTE.dim(`pick 1–${count}`)} ${PROMPT_PALETTE.cyan("›")} `;
|
|
9008
|
+
}
|
|
9009
|
+
/**
|
|
8939
9010
|
* Default recursive directory watcher — wraps `node:fs.watch` with `{ recursive: true }`
|
|
8940
9011
|
* and adapts its handle to {@link WatchHandle}. Tests inject a fake emitter so no real
|
|
8941
9012
|
* FS watch is registered.
|
package/package.json
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"bun": ">=1.3.14"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@moku-labs/core": "0.1.
|
|
61
|
+
"@moku-labs/core": "0.1.1",
|
|
62
62
|
"@resvg/resvg-js": "2.6.2",
|
|
63
63
|
"@shikijs/rehype": "3.22.0",
|
|
64
64
|
"feed": "5.2.0",
|
|
@@ -113,5 +113,5 @@
|
|
|
113
113
|
"test:cli-e2e": "bun test src/plugins/cli/__tests__/e2e/",
|
|
114
114
|
"test:coverage": "vitest run --project unit --project integration --coverage"
|
|
115
115
|
},
|
|
116
|
-
"version": "1.
|
|
116
|
+
"version": "1.4.0"
|
|
117
117
|
}
|