@moku-labs/web 1.1.0 → 1.2.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/README.md +1 -1
- package/dist/index.cjs +552 -95
- package/dist/index.d.cts +11 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +553 -96
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -10,7 +10,9 @@ node_path = require_convention.__toESM(node_path);
|
|
|
10
10
|
let feed = require("feed");
|
|
11
11
|
let preact = require("preact");
|
|
12
12
|
let preact_render_to_string = require("preact-render-to-string");
|
|
13
|
+
let node_child_process = require("node:child_process");
|
|
13
14
|
let node_readline = require("node:readline");
|
|
15
|
+
let node_url = require("node:url");
|
|
14
16
|
let node_os = require("node:os");
|
|
15
17
|
let gray_matter = require("gray-matter");
|
|
16
18
|
gray_matter = require_convention.__toESM(gray_matter, 1);
|
|
@@ -4890,10 +4892,47 @@ function findRootHtml(rendered) {
|
|
|
4890
4892
|
return rendered.find((page) => page.url === "/" || page.url === "")?.html ?? null;
|
|
4891
4893
|
}
|
|
4892
4894
|
/**
|
|
4895
|
+
* Pages rendered concurrently per batch. Kept small so the macrotask yield between
|
|
4896
|
+
* batches fires frequently — a large batch renders for seconds before yielding, which
|
|
4897
|
+
* leaves a watching dev server's spinner repainting only every few seconds (sluggish).
|
|
4898
|
+
* Smaller batches trade a little write-concurrency for a smooth, responsive spinner.
|
|
4899
|
+
*/
|
|
4900
|
+
const RENDER_BATCH_SIZE = 2;
|
|
4901
|
+
/**
|
|
4902
|
+
* Render `items` through `worker` in bounded-size batches, yielding a macrotask
|
|
4903
|
+
* (`setImmediate`) between batches. Beyond bounding peak concurrency/memory for large
|
|
4904
|
+
* sites, the yield lets the single JS thread breathe: one un-yielded `Promise.all` over
|
|
4905
|
+
* hundreds of synchronous `renderToString` calls starves the event loop, which freezes a
|
|
4906
|
+
* watching dev server's progress spinner until the whole phase resolves. Output order is
|
|
4907
|
+
* preserved (batch order + `Promise.all` order within a batch).
|
|
4908
|
+
*
|
|
4909
|
+
* @template Item - The input item type.
|
|
4910
|
+
* @template Out - The rendered output type.
|
|
4911
|
+
* @param items - The items to render.
|
|
4912
|
+
* @param batchSize - Maximum items rendered concurrently per batch.
|
|
4913
|
+
* @param worker - Renders one item to its output.
|
|
4914
|
+
* @returns All rendered outputs in input order.
|
|
4915
|
+
* @example
|
|
4916
|
+
* ```ts
|
|
4917
|
+
* const pages = await renderInBatches(instances, 32, i => renderInstance(ctx, i, shell));
|
|
4918
|
+
* ```
|
|
4919
|
+
*/
|
|
4920
|
+
async function renderInBatches(items, batchSize, worker) {
|
|
4921
|
+
const out = [];
|
|
4922
|
+
for (let start = 0; start < items.length; start += batchSize) {
|
|
4923
|
+
const batch = items.slice(start, start + batchSize);
|
|
4924
|
+
out.push(...await Promise.all(batch.map((item) => worker(item))));
|
|
4925
|
+
if (start + batchSize < items.length) await new Promise((resolve) => {
|
|
4926
|
+
setImmediate(resolve);
|
|
4927
|
+
});
|
|
4928
|
+
}
|
|
4929
|
+
return out;
|
|
4930
|
+
}
|
|
4931
|
+
/**
|
|
4893
4932
|
* Renders every route in the manifest to `outDir/<path>/index.html`. Reads as a
|
|
4894
|
-
* pipeline: resolve deps → prepare the shared shell → expand instances → render
|
|
4895
|
-
*
|
|
4896
|
-
*
|
|
4933
|
+
* pipeline: resolve deps → prepare the shared shell → expand instances → render in
|
|
4934
|
+
* bounded batches ({@link renderInBatches}) → write data sidecars (hybrid/spa) →
|
|
4935
|
+
* capture the root page's HTML for the root-index phase.
|
|
4897
4936
|
*
|
|
4898
4937
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `log`, `has`).
|
|
4899
4938
|
* @returns The number of pages rendered and the captured default-page HTML.
|
|
@@ -4909,8 +4948,7 @@ async function renderPages(ctx) {
|
|
|
4909
4948
|
const locales = ctx.require(i18nPlugin).locales();
|
|
4910
4949
|
const byPattern = makeEntryMap(router);
|
|
4911
4950
|
const shell = await prepareShell(ctx);
|
|
4912
|
-
const
|
|
4913
|
-
const rendered = await Promise.all(instances.map((instance) => renderInstance(ctx, instance, shell)));
|
|
4951
|
+
const rendered = await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell));
|
|
4914
4952
|
await writeDataSidecars(ctx, rendered, router.mode());
|
|
4915
4953
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
4916
4954
|
return {
|
|
@@ -6462,13 +6500,14 @@ const deployPlugin = createPlugin$1("deploy", {
|
|
|
6462
6500
|
//#endregion
|
|
6463
6501
|
//#region src/plugins/cli/deploy-wizard.ts
|
|
6464
6502
|
/**
|
|
6465
|
-
* @file cli plugin — the guided deploy wizard (`cli.deploy({ guided: true })
|
|
6466
|
-
*
|
|
6467
|
-
* Cloudflare
|
|
6468
|
-
*
|
|
6469
|
-
*
|
|
6470
|
-
*
|
|
6471
|
-
*
|
|
6503
|
+
* @file cli plugin — the guided deploy wizard (`cli.deploy({ guided: true })`, the default
|
|
6504
|
+
* for `bun run deploy`; the direct `--cli` path stays in `api.ts`). Walks a human through a
|
|
6505
|
+
* Cloudflare Pages deploy: checks prerequisites (wrangler config + the Cloudflare
|
|
6506
|
+
* credentials) with concrete fix guidance, offers to scaffold what is missing (a
|
|
6507
|
+
* `wrangler.jsonc`, and a placeholder `.env` for any missing credentials), HARD-GATES the
|
|
6508
|
+
* deploy on everything being green, runs a local build smoke test, confirms, deploys, then
|
|
6509
|
+
* offers to scaffold a GitHub Actions workflow (auto on push to main, or a versioned/manual
|
|
6510
|
+
* trigger). Every prompt + line of output flows through injectable `state` seams.
|
|
6472
6511
|
*/
|
|
6473
6512
|
/** How to create a Cloudflare API token + where to make it available locally. */
|
|
6474
6513
|
const TOKEN_HELP = [
|
|
@@ -6535,6 +6574,43 @@ async function offerScaffold(ctx, prereqs) {
|
|
|
6535
6574
|
await ctx.require(deployPlugin).init({});
|
|
6536
6575
|
ctx.state.render.check(true, "wrangler.jsonc scaffolded");
|
|
6537
6576
|
}
|
|
6577
|
+
/** The Cloudflare credentials the deploy needs, with the comment written above each in a scaffolded `.env`. */
|
|
6578
|
+
const ENV_CREDENTIALS = [{
|
|
6579
|
+
key: "CLOUDFLARE_API_TOKEN",
|
|
6580
|
+
comment: "# Cloudflare API token — https://dash.cloudflare.com/profile/api-tokens (template: Cloudflare Pages — Edit)"
|
|
6581
|
+
}, {
|
|
6582
|
+
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
6583
|
+
comment: "# Cloudflare account id — dashboard → Workers & Pages → right-hand sidebar"
|
|
6584
|
+
}];
|
|
6585
|
+
/**
|
|
6586
|
+
* Offer to scaffold a `.env` with placeholders for whichever Cloudflare credentials are
|
|
6587
|
+
* missing — created when absent, appended to (never clobbering a key already present)
|
|
6588
|
+
* when it exists. The placeholders are empty, so the deploy still hard-gates until the
|
|
6589
|
+
* user fills them in; this just removes the "where do I even put these?" friction.
|
|
6590
|
+
*
|
|
6591
|
+
* @param ctx - The cli plugin context.
|
|
6592
|
+
* @param cwd - The project root (where `.env` lives).
|
|
6593
|
+
* @returns Resolves once any accepted scaffold has been written.
|
|
6594
|
+
* @example
|
|
6595
|
+
* await offerEnvScaffold(ctx, process.cwd());
|
|
6596
|
+
*/
|
|
6597
|
+
async function offerEnvScaffold(ctx, cwd) {
|
|
6598
|
+
const missing = ENV_CREDENTIALS.filter(({ key }) => (process.env[key] ?? "") === "");
|
|
6599
|
+
if (missing.length === 0) return;
|
|
6600
|
+
const envPath = node_path$1.default.join(cwd, ".env");
|
|
6601
|
+
const exists = (0, node_fs.existsSync)(envPath);
|
|
6602
|
+
const verb = exists ? "Add placeholders for the missing secret(s) to" : "Create";
|
|
6603
|
+
if (!await ctx.state.confirm(`${verb} .env?`)) return;
|
|
6604
|
+
const lines = exists ? (0, node_fs.readFileSync)(envPath, "utf8").split(/\r?\n/) : [];
|
|
6605
|
+
const toAdd = missing.filter(({ key }) => !lines.some((line) => line.trimStart().startsWith(`${key}=`)));
|
|
6606
|
+
if (toAdd.length === 0) {
|
|
6607
|
+
ctx.state.render.info(".env already lists those keys — fill in their values, then re-run.");
|
|
6608
|
+
return;
|
|
6609
|
+
}
|
|
6610
|
+
(0, node_fs.appendFileSync)(envPath, `${exists ? "\n" : "# Cloudflare Pages deploy credentials — fill these in (keep .env gitignored).\n"}${toAdd.map(({ key, comment }) => `${comment}\n${key}=`).join("\n\n")}\n`);
|
|
6611
|
+
const names = toAdd.map(({ key }) => key).join(", ");
|
|
6612
|
+
ctx.state.render.check(true, `${exists ? "added placeholders to" : "created"} .env`, `fill in ${names}, then re-run \`bun run deploy\`.`);
|
|
6613
|
+
}
|
|
6538
6614
|
/**
|
|
6539
6615
|
* Map a top-level workflow choice (and, for the versioned option, a sub-choice) to the
|
|
6540
6616
|
* concrete {@link WorkflowTrigger}, or `null` when the user chose to skip setup.
|
|
@@ -6618,6 +6694,7 @@ async function runDeployWizard(ctx, options) {
|
|
|
6618
6694
|
ctx.state.render.heading("Checking prerequisites");
|
|
6619
6695
|
for (const item of diagnose(cwd)) ctx.state.render.check(item.ok, item.label, item.detail);
|
|
6620
6696
|
await offerScaffold(ctx, diagnose(cwd));
|
|
6697
|
+
await offerEnvScaffold(ctx, cwd);
|
|
6621
6698
|
const blockers = diagnose(cwd).filter((item) => !item.ok);
|
|
6622
6699
|
if (blockers.length > 0) {
|
|
6623
6700
|
ctx.state.render.heading("Not ready to deploy");
|
|
@@ -7123,6 +7200,7 @@ async function runDevServer(ctx, port) {
|
|
|
7123
7200
|
for (const watcher of watchers) watcher.close();
|
|
7124
7201
|
hub.close();
|
|
7125
7202
|
server.stop();
|
|
7203
|
+
ctx.state.render.dispose();
|
|
7126
7204
|
});
|
|
7127
7205
|
}
|
|
7128
7206
|
//#endregion
|
|
@@ -7323,6 +7401,28 @@ async function confirmDeploy(ctx, yes) {
|
|
|
7323
7401
|
if (!confirmed) ctx.state.render.warn("deploy skipped");
|
|
7324
7402
|
return confirmed;
|
|
7325
7403
|
}
|
|
7404
|
+
/** Matches the prerequisite/credential failures a direct deploy most often hits (missing token/account). */
|
|
7405
|
+
const PREREQUISITE_ERROR = /required variable|not defined|cloudflare|token|account|unauthor|wrangler/i;
|
|
7406
|
+
/**
|
|
7407
|
+
* A short, actionable "how to fix" hint for a failed deploy, rendered under the error so
|
|
7408
|
+
* the user is never left at a raw stack trace. A missing-credential/prerequisite failure
|
|
7409
|
+
* (the common case for a first direct deploy) gets the concrete secret-setup steps;
|
|
7410
|
+
* anything else points at the guided deploy, which diagnoses prerequisites step by step.
|
|
7411
|
+
*
|
|
7412
|
+
* @param error - The thrown deploy error.
|
|
7413
|
+
* @returns The multi-line hint (newline-separated; rendered indented under a `›`).
|
|
7414
|
+
* @example
|
|
7415
|
+
* render.info(deployFailureHint(err));
|
|
7416
|
+
*/
|
|
7417
|
+
function deployFailureHint(error) {
|
|
7418
|
+
if (PREREQUISITE_ERROR.test(String(error))) return [
|
|
7419
|
+
"how to fix:",
|
|
7420
|
+
"1. run `bun run deploy` (without `--cli`) — the guided setup diagnoses prerequisites and offers to create a starter .env",
|
|
7421
|
+
"2. or set them yourself in .env: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID",
|
|
7422
|
+
" token: https://dash.cloudflare.com/profile/api-tokens (template: Cloudflare Pages — Edit)"
|
|
7423
|
+
].join("\n");
|
|
7424
|
+
return "how to fix: run `bun run deploy` (without `--cli`) — the guided setup diagnoses prerequisites — then retry";
|
|
7425
|
+
}
|
|
7326
7426
|
/**
|
|
7327
7427
|
* Create the cli plugin API surface — exactly `build`, `serve`, `preview`, `deploy`.
|
|
7328
7428
|
* Each method renders `state.render.header(<command>)` first, then does its work;
|
|
@@ -7397,15 +7497,21 @@ function createApi$1(ctx) {
|
|
|
7397
7497
|
const { branch, yes = false } = options;
|
|
7398
7498
|
ctx.state.render.header("deploy");
|
|
7399
7499
|
if (options.guided === true) return runDeployWizard(ctx, options);
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7500
|
+
try {
|
|
7501
|
+
await ctx.require(deployPlugin).init({ ci: true });
|
|
7502
|
+
if (!await confirmDeploy(ctx, yes)) return {
|
|
7503
|
+
deployed: false,
|
|
7504
|
+
reason: "declined"
|
|
7505
|
+
};
|
|
7506
|
+
return {
|
|
7507
|
+
deployed: true,
|
|
7508
|
+
...await ctx.require(deployPlugin).run(branch === void 0 ? {} : { branch })
|
|
7509
|
+
};
|
|
7510
|
+
} catch (error) {
|
|
7511
|
+
ctx.state.render.error("deploy failed", error);
|
|
7512
|
+
ctx.state.render.info(deployFailureHint(error));
|
|
7513
|
+
throw error;
|
|
7514
|
+
}
|
|
7409
7515
|
}
|
|
7410
7516
|
};
|
|
7411
7517
|
}
|
|
@@ -7496,6 +7602,28 @@ const ANSI = {
|
|
|
7496
7602
|
cyan: `${ESC}[36m`,
|
|
7497
7603
|
gray: `${ESC}[90m`
|
|
7498
7604
|
};
|
|
7605
|
+
/**
|
|
7606
|
+
* The Moku brand pink (`#FF1E6F`) as an RGB triple, used for 24-bit truecolor output.
|
|
7607
|
+
* Degrades to {@link ANSI.magenta} on a 16-color TTY and to plain text off a TTY.
|
|
7608
|
+
*/
|
|
7609
|
+
const BRAND_PINK = {
|
|
7610
|
+
r: 255,
|
|
7611
|
+
g: 30,
|
|
7612
|
+
b: 111
|
|
7613
|
+
};
|
|
7614
|
+
/**
|
|
7615
|
+
* Build a 24-bit (truecolor) SGR foreground escape for the given RGB triple.
|
|
7616
|
+
*
|
|
7617
|
+
* @param r - Red channel (0–255).
|
|
7618
|
+
* @param g - Green channel (0–255).
|
|
7619
|
+
* @param b - Blue channel (0–255).
|
|
7620
|
+
* @returns The `ESC[38;2;r;g;bm` foreground sequence.
|
|
7621
|
+
* @example
|
|
7622
|
+
* fg24(255, 30, 111); // "\x1b[38;2;255;30;111m"
|
|
7623
|
+
*/
|
|
7624
|
+
function fg24(r, g, b) {
|
|
7625
|
+
return `${ESC}[38;2;${r};${g};${b}m`;
|
|
7626
|
+
}
|
|
7499
7627
|
/** ANSI: erase the entire current line, leaving the cursor where it is. */
|
|
7500
7628
|
const CLEAR_LINE = `${ESC}[2K`;
|
|
7501
7629
|
/** ANSI: erase from the cursor to the end of the screen (drops stale trailing rows). */
|
|
@@ -7567,6 +7695,36 @@ function supportsColor(stream = process.stdout, noColor = process.env.NO_COLOR)
|
|
|
7567
7695
|
return stream.isTTY === true && noColor === void 0;
|
|
7568
7696
|
}
|
|
7569
7697
|
/**
|
|
7698
|
+
* Whether the terminal advertises 24-bit (truecolor) support via `COLORTERM`, so the
|
|
7699
|
+
* renderer may emit the exact brand pink ({@link BRAND_PINK}) instead of the 16-color
|
|
7700
|
+
* `magenta` approximation. Always layered on top of {@link supportsColor} — truecolor
|
|
7701
|
+
* is never used when color itself is disabled.
|
|
7702
|
+
*
|
|
7703
|
+
* @param colorTerm - The `COLORTERM` value (defaults to `process.env.COLORTERM`).
|
|
7704
|
+
* @returns `true` when `COLORTERM` is `truecolor` or `24bit`.
|
|
7705
|
+
* @example
|
|
7706
|
+
* supportsTruecolor("truecolor"); // true
|
|
7707
|
+
*/
|
|
7708
|
+
function supportsTruecolor(colorTerm = process.env.COLORTERM) {
|
|
7709
|
+
return colorTerm === "truecolor" || colorTerm === "24bit";
|
|
7710
|
+
}
|
|
7711
|
+
/**
|
|
7712
|
+
* The braille spinner glyph for a given elapsed time, advancing one frame per
|
|
7713
|
+
* `frameMs`. Deriving the frame from wall-clock elapsed (rather than a tick counter)
|
|
7714
|
+
* keeps the spinner correct even when the animation ticker is briefly starved by a
|
|
7715
|
+
* synchronous build phase and several ticks coalesce — the glyph still reflects real
|
|
7716
|
+
* elapsed time instead of freezing on a stale frame.
|
|
7717
|
+
*
|
|
7718
|
+
* @param elapsedMs - Milliseconds since the live region opened.
|
|
7719
|
+
* @param frameMs - Milliseconds per frame (defaults to `80`).
|
|
7720
|
+
* @returns The active spinner glyph.
|
|
7721
|
+
* @example
|
|
7722
|
+
* spinnerFrameAt(240); // "⠹" (the 4th frame at 80ms/frame)
|
|
7723
|
+
*/
|
|
7724
|
+
function spinnerFrameAt(elapsedMs, frameMs = 80) {
|
|
7725
|
+
return SPINNER_FRAMES[Math.floor(Math.max(0, elapsedMs) / frameMs) % SPINNER_FRAMES.length] ?? "⠋";
|
|
7726
|
+
}
|
|
7727
|
+
/**
|
|
7570
7728
|
* Select the box glyph set for the given color mode (Unicode on a TTY, ASCII off it).
|
|
7571
7729
|
*
|
|
7572
7730
|
* @param color - Whether color/Unicode output is enabled.
|
|
@@ -7594,12 +7752,14 @@ function visibleWidth(text) {
|
|
|
7594
7752
|
* output in CI/pipes.
|
|
7595
7753
|
*
|
|
7596
7754
|
* @param color - Whether color is enabled (typically `supportsColor()`).
|
|
7755
|
+
* @param truecolor - Whether 24-bit output is enabled (typically `supportsTruecolor()`);
|
|
7756
|
+
* only consulted by {@link Palette.pink}. Defaults to `false` (16-color magenta).
|
|
7597
7757
|
* @returns The bound color palette.
|
|
7598
7758
|
* @example
|
|
7599
|
-
* const palette = makePalette(supportsColor());
|
|
7759
|
+
* const palette = makePalette(supportsColor(), supportsTruecolor());
|
|
7600
7760
|
* const line = palette.green("done");
|
|
7601
7761
|
*/
|
|
7602
|
-
function makePalette(color) {
|
|
7762
|
+
function makePalette(color, truecolor = false) {
|
|
7603
7763
|
return {
|
|
7604
7764
|
enabled: color,
|
|
7605
7765
|
/**
|
|
@@ -7679,23 +7839,39 @@ function makePalette(color) {
|
|
|
7679
7839
|
*/
|
|
7680
7840
|
cyan(text) {
|
|
7681
7841
|
return this.paint(ANSI.cyan, text);
|
|
7842
|
+
},
|
|
7843
|
+
/**
|
|
7844
|
+
* Color the given text the Moku brand pink: exact `#FF1E6F` (24-bit) when truecolor
|
|
7845
|
+
* is enabled, the 16-color `magenta` approximation otherwise, unchanged in plain mode.
|
|
7846
|
+
*
|
|
7847
|
+
* @param text - The text to colorize.
|
|
7848
|
+
* @returns The pink (or unchanged) text.
|
|
7849
|
+
* @example
|
|
7850
|
+
* palette.pink("▟▙ moku web");
|
|
7851
|
+
*/
|
|
7852
|
+
pink(text) {
|
|
7853
|
+
if (!color) return text;
|
|
7854
|
+
if (truecolor) return `${fg24(BRAND_PINK.r, BRAND_PINK.g, BRAND_PINK.b)}${text}${ANSI.reset}`;
|
|
7855
|
+
return this.paint(ANSI.magenta, text);
|
|
7682
7856
|
}
|
|
7683
7857
|
};
|
|
7684
7858
|
}
|
|
7685
7859
|
/**
|
|
7686
7860
|
* Frame a list of already-rendered content lines in a box, padding each line to the
|
|
7687
|
-
*
|
|
7688
|
-
*
|
|
7861
|
+
* widest visible line (or `minInnerWidth`, whichever is larger — so several boxes can be
|
|
7862
|
+
* forced to a shared width). Uses Unicode borders when `color` is enabled and ASCII
|
|
7863
|
+
* otherwise. Visible width ignores embedded ANSI so colored lines align.
|
|
7689
7864
|
*
|
|
7690
7865
|
* @param lines - The content lines (may contain ANSI color codes).
|
|
7691
7866
|
* @param color - Whether to use Unicode borders (and assume color-capable output).
|
|
7867
|
+
* @param minInnerWidth - Minimum inner (content) width to pad every row to. Defaults to `0`.
|
|
7692
7868
|
* @returns The boxed lines (top border, content rows, bottom border).
|
|
7693
7869
|
* @example
|
|
7694
|
-
* box(["Local: http://localhost:4173"], true);
|
|
7870
|
+
* box(["Local: http://localhost:4173"], true, 62);
|
|
7695
7871
|
*/
|
|
7696
|
-
function box(lines, color) {
|
|
7872
|
+
function box(lines, color, minInnerWidth = 0) {
|
|
7697
7873
|
const glyphs = boxGlyphs(color);
|
|
7698
|
-
const inner = Math.max(0, ...lines.map((line) => visibleWidth(line)));
|
|
7874
|
+
const inner = Math.max(0, minInnerWidth, ...lines.map((line) => visibleWidth(line)));
|
|
7699
7875
|
const horizontal = glyphs.horizontal.repeat(inner + 2);
|
|
7700
7876
|
const top = `${glyphs.topLeft}${horizontal}${glyphs.topRight}`;
|
|
7701
7877
|
const bottom = `${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`;
|
|
@@ -7710,13 +7886,70 @@ function box(lines, color) {
|
|
|
7710
7886
|
}
|
|
7711
7887
|
//#endregion
|
|
7712
7888
|
//#region src/plugins/cli/render/panel.ts
|
|
7713
|
-
/** Per-command label shown
|
|
7889
|
+
/** Per-command label shown beside the lockup wordmark. */
|
|
7714
7890
|
const COMMAND_LABEL = {
|
|
7715
7891
|
build: "build",
|
|
7716
7892
|
serve: "serve · dev",
|
|
7717
7893
|
preview: "preview",
|
|
7718
7894
|
deploy: "deploy"
|
|
7719
7895
|
};
|
|
7896
|
+
/** Total visible width the header rule spans and the per-row timing column right-aligns to. */
|
|
7897
|
+
const RAIL_WIDTH = 66;
|
|
7898
|
+
/** Animation repaint cadence (ms) — how often the live region is redrawn when the loop is free. */
|
|
7899
|
+
const TICK_MS = 40;
|
|
7900
|
+
/** Spinner frame interval (ms) — one braille glyph advance per this many elapsed ms. */
|
|
7901
|
+
const SPIN_MS = 60;
|
|
7902
|
+
/** Inner (content) width of the BUILD/server boxes so their right edge lines up with the phase tree. */
|
|
7903
|
+
const BOX_INNER = RAIL_WIDTH - 4;
|
|
7904
|
+
/** The eight block glyphs the per-phase time-profile sparkline maps durations onto. */
|
|
7905
|
+
const SPARK_BARS = "▁▂▃▄▅▆▇█";
|
|
7906
|
+
/**
|
|
7907
|
+
* Build a sparkline from a list of values — one block glyph per value, height scaled to
|
|
7908
|
+
* the largest value so the tallest bar is `█`. A real micro-histogram (no fake data):
|
|
7909
|
+
* under the BUILD summary each bar is one phase's duration, so the slowest phase stands
|
|
7910
|
+
* out at a glance. Returns `""` for an empty list.
|
|
7911
|
+
*
|
|
7912
|
+
* @param values - The values to plot (e.g. per-phase durations in ms).
|
|
7913
|
+
* @returns The sparkline string.
|
|
7914
|
+
* @example
|
|
7915
|
+
* sparkline([12, 1701, 19698, 9]); // "▁▁█▁"
|
|
7916
|
+
*/
|
|
7917
|
+
function sparkline(values) {
|
|
7918
|
+
if (values.length === 0) return "";
|
|
7919
|
+
const max = Math.max(...values, 1);
|
|
7920
|
+
return values.map((value) => {
|
|
7921
|
+
return SPARK_BARS[Math.min(7, Math.floor(value / max * 7))] ?? SPARK_BARS[0];
|
|
7922
|
+
}).join("");
|
|
7923
|
+
}
|
|
7924
|
+
/**
|
|
7925
|
+
* The structural glyph set for the active color mode: Unicode on a color-capable TTY,
|
|
7926
|
+
* ASCII fallbacks off it. Only the NEW Velocity chrome (cube, rule, tree, bar, live
|
|
7927
|
+
* dot) degrades here — the `✓ ✗ ~ ➜ ›` status marks stay as-is in both modes.
|
|
7928
|
+
*
|
|
7929
|
+
* @param color - Whether color/Unicode output is enabled.
|
|
7930
|
+
* @returns The matching glyph set.
|
|
7931
|
+
* @example
|
|
7932
|
+
* const g = glyphSet(true);
|
|
7933
|
+
*/
|
|
7934
|
+
function glyphSet(color) {
|
|
7935
|
+
return color ? {
|
|
7936
|
+
cube: "▟▙",
|
|
7937
|
+
rule: "─",
|
|
7938
|
+
tree: "├─",
|
|
7939
|
+
barFill: "━",
|
|
7940
|
+
barTrack: "╴",
|
|
7941
|
+
liveOn: "◍",
|
|
7942
|
+
liveOff: "○"
|
|
7943
|
+
} : {
|
|
7944
|
+
cube: "*",
|
|
7945
|
+
rule: "-",
|
|
7946
|
+
tree: "-",
|
|
7947
|
+
barFill: "#",
|
|
7948
|
+
barTrack: "-",
|
|
7949
|
+
liveOn: "*",
|
|
7950
|
+
liveOff: "*"
|
|
7951
|
+
};
|
|
7952
|
+
}
|
|
7720
7953
|
/**
|
|
7721
7954
|
* Render one human-readable duration suffix (e.g. `· 84ms`).
|
|
7722
7955
|
*
|
|
@@ -7731,15 +7964,49 @@ function durationSuffix(palette, durationMs) {
|
|
|
7731
7964
|
return ` ${palette.dim(`· ${durationMs}ms`)}`;
|
|
7732
7965
|
}
|
|
7733
7966
|
/**
|
|
7967
|
+
* Right-align `right` against `left` within {@link RAIL_WIDTH}, measuring visible width
|
|
7968
|
+
* so embedded ANSI never throws the timing column off.
|
|
7969
|
+
*
|
|
7970
|
+
* @param left - The left segment (may contain ANSI).
|
|
7971
|
+
* @param right - The right segment (may contain ANSI).
|
|
7972
|
+
* @param width - Total visible width to fill (defaults to {@link RAIL_WIDTH}).
|
|
7973
|
+
* @returns The padded line.
|
|
7974
|
+
* @example
|
|
7975
|
+
* railLine(" ├─ ✓ pages", "· 12ms");
|
|
7976
|
+
*/
|
|
7977
|
+
function railLine(left, right, width = RAIL_WIDTH) {
|
|
7978
|
+
const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
|
|
7979
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
7980
|
+
}
|
|
7981
|
+
/**
|
|
7982
|
+
* The runtime facts line shown under the banner: the pinned core version (when known)
|
|
7983
|
+
* plus the live Node/Bun versions + platform — the ACTUAL running runtime, not the
|
|
7984
|
+
* `engines` floor. Every value is real (read from `@moku-labs/core`'s pinned dependency
|
|
7985
|
+
* and `process.versions`), so nothing on this line is faked.
|
|
7986
|
+
*
|
|
7987
|
+
* @param coreVersion - The pinned `@moku-labs/core` version (appended last — it rarely
|
|
7988
|
+
* matters — and omitted entirely when unknown).
|
|
7989
|
+
* @returns The facts string (e.g. `node 24.3.0 · bun 1.3.9 · darwin arm64 · core 0.1.0-alpha.6`).
|
|
7990
|
+
* @example
|
|
7991
|
+
* runtimeFacts("0.1.0-alpha.6");
|
|
7992
|
+
*/
|
|
7993
|
+
function runtimeFacts(coreVersion) {
|
|
7994
|
+
const node = `node ${process.versions.node}`;
|
|
7995
|
+
const bun = process.versions.bun ? ` · bun ${process.versions.bun}` : "";
|
|
7996
|
+
const core = coreVersion ? ` · core ${coreVersion}` : "";
|
|
7997
|
+
return `${node}${bun} · ${process.platform} ${process.arch}${core}`;
|
|
7998
|
+
}
|
|
7999
|
+
/**
|
|
7734
8000
|
* Create the Panel {@link CliRenderer}. Output is written through the injected sink
|
|
7735
|
-
* (default `console.log`/`console.error`) and colorized only when color is enabled,
|
|
7736
|
-
*
|
|
7737
|
-
* ASCII lines in CI/pipes.
|
|
8001
|
+
* (default `console.log`/`console.error`) and colorized only when color is enabled, so
|
|
8002
|
+
* the identical render path yields the animated, box-free Velocity UI on a TTY and
|
|
8003
|
+
* plain ASCII lines in CI/pipes.
|
|
7738
8004
|
*
|
|
7739
|
-
* @param options - Optional sinks
|
|
8005
|
+
* @param options - Optional sinks, color/truecolor overrides, clock, and version (see
|
|
8006
|
+
* {@link PanelOptions}).
|
|
7740
8007
|
* @returns The renderer mounted on `state.render` and driven by the API + hooks.
|
|
7741
8008
|
* @example
|
|
7742
|
-
* const render = createPanelRenderer();
|
|
8009
|
+
* const render = createPanelRenderer({ version: "0.1.0-alpha" });
|
|
7743
8010
|
* render.header("build");
|
|
7744
8011
|
*/
|
|
7745
8012
|
function createPanelRenderer(options = {}) {
|
|
@@ -7750,26 +8017,24 @@ function createPanelRenderer(options = {}) {
|
|
|
7750
8017
|
});
|
|
7751
8018
|
const now = options.now ?? Date.now;
|
|
7752
8019
|
const color = options.color ?? supportsColor();
|
|
7753
|
-
const palette = makePalette(color);
|
|
8020
|
+
const palette = makePalette(color, options.truecolor ?? (color && supportsTruecolor()));
|
|
8021
|
+
const version = options.version ?? "dev";
|
|
8022
|
+
const coreVersion = options.coreVersion;
|
|
8023
|
+
const g = glyphSet(color);
|
|
7754
8024
|
let phaseRows = [];
|
|
7755
8025
|
let phaseDrawn = 0;
|
|
7756
8026
|
let phaseOpen = false;
|
|
8027
|
+
let blockStartedAt = 0;
|
|
7757
8028
|
let rebuilding = false;
|
|
7758
8029
|
let rebuildLabel = "";
|
|
7759
8030
|
let rebuildStartedAt = 0;
|
|
7760
|
-
let
|
|
8031
|
+
let idle = false;
|
|
8032
|
+
let idleStartedAt = 0;
|
|
8033
|
+
let serveMode = false;
|
|
7761
8034
|
let ticker;
|
|
7762
8035
|
/**
|
|
7763
|
-
*
|
|
7764
|
-
*
|
|
7765
|
-
* @returns The active braille spinner frame.
|
|
7766
|
-
* @example
|
|
7767
|
-
* frameGlyph(); // "⠙"
|
|
7768
|
-
*/
|
|
7769
|
-
const frameGlyph = () => SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length] ?? "⠋";
|
|
7770
|
-
/**
|
|
7771
|
-
* Render one phase row: a green `✓ name · time` when done, else a spinning cyan glyph
|
|
7772
|
-
* before the dim name.
|
|
8036
|
+
* Render one phase-tree row: a spinning cyan glyph + dim name while running, or a green
|
|
8037
|
+
* `✓` + name with the duration right-aligned in the dim timing column once done.
|
|
7773
8038
|
*
|
|
7774
8039
|
* @param row - The phase row to render.
|
|
7775
8040
|
* @returns The rendered row line (no trailing newline).
|
|
@@ -7777,12 +8042,34 @@ function createPanelRenderer(options = {}) {
|
|
|
7777
8042
|
* renderPhaseRow({ name: "pages", done: true, durationMs: 12 });
|
|
7778
8043
|
*/
|
|
7779
8044
|
const renderPhaseRow = (row) => {
|
|
7780
|
-
|
|
7781
|
-
return ` ${palette.
|
|
8045
|
+
const branch = palette.dim(g.tree);
|
|
8046
|
+
if (row.done) return railLine(` ${branch} ${palette.green("✓")} ${row.name}`, palette.dim(`· ${row.durationMs}ms`));
|
|
8047
|
+
return ` ${branch} ${palette.cyan(spinnerFrameAt(now() - blockStartedAt, SPIN_MS))} ${palette.dim(row.name)}`;
|
|
7782
8048
|
};
|
|
7783
8049
|
/**
|
|
7784
|
-
*
|
|
7785
|
-
*
|
|
8050
|
+
* Render the indeterminate "comet" build bar — a short pink fill window sweeping across
|
|
8051
|
+
* a dim track — for the given elapsed time. Animated purely from wall-clock elapsed so
|
|
8052
|
+
* it never needs a known phase total.
|
|
8053
|
+
*
|
|
8054
|
+
* @param elapsedMs - Milliseconds since the phase block opened.
|
|
8055
|
+
* @returns The rendered bar row (no trailing newline).
|
|
8056
|
+
* @example
|
|
8057
|
+
* renderBuildBar(300);
|
|
8058
|
+
*/
|
|
8059
|
+
const renderBuildBar = (elapsedMs) => {
|
|
8060
|
+
const length = 28;
|
|
8061
|
+
const window = 6;
|
|
8062
|
+
const head = Math.floor(elapsedMs / 28) % 34;
|
|
8063
|
+
let bar = "";
|
|
8064
|
+
for (let index = 0; index < length; index++) {
|
|
8065
|
+
const lit = index <= head && index > head - window;
|
|
8066
|
+
bar += lit ? palette.pink(g.barFill) : palette.dim(g.barTrack);
|
|
8067
|
+
}
|
|
8068
|
+
return ` ${bar}`;
|
|
8069
|
+
};
|
|
8070
|
+
/**
|
|
8071
|
+
* Repaint the live phase block in place (tree rows + animated build bar): move up over
|
|
8072
|
+
* the prior draw, rewrite each row, then the bar, clearing any stale trailing lines.
|
|
7786
8073
|
*
|
|
7787
8074
|
* @example
|
|
7788
8075
|
* paintPhaseBlock();
|
|
@@ -7790,8 +8077,9 @@ function createPanelRenderer(options = {}) {
|
|
|
7790
8077
|
const paintPhaseBlock = () => {
|
|
7791
8078
|
let frame = cursorUp(phaseDrawn);
|
|
7792
8079
|
for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
|
|
8080
|
+
frame += `${CLEAR_LINE}${renderBuildBar(now() - blockStartedAt)}\n`;
|
|
7793
8081
|
writeRaw(frame + CLEAR_BELOW);
|
|
7794
|
-
phaseDrawn = phaseRows.length;
|
|
8082
|
+
phaseDrawn = phaseRows.length + 1;
|
|
7795
8083
|
};
|
|
7796
8084
|
/**
|
|
7797
8085
|
* Repaint the single in-place rebuild line (spinner + label + live elapsed seconds).
|
|
@@ -7800,20 +8088,31 @@ function createPanelRenderer(options = {}) {
|
|
|
7800
8088
|
* paintRebuildLine();
|
|
7801
8089
|
*/
|
|
7802
8090
|
const paintRebuildLine = () => {
|
|
7803
|
-
const
|
|
7804
|
-
const
|
|
7805
|
-
writeRaw(`\r${CLEAR_LINE} ${
|
|
8091
|
+
const spinner = palette.cyan(spinnerFrameAt(now() - rebuildStartedAt, SPIN_MS));
|
|
8092
|
+
const elapsed = palette.dim(`· ${((now() - rebuildStartedAt) / 1e3).toFixed(1)}s`);
|
|
8093
|
+
writeRaw(`\r${CLEAR_LINE} ${spinner} rebuilding ${rebuildLabel} ${elapsed}`);
|
|
8094
|
+
};
|
|
8095
|
+
/**
|
|
8096
|
+
* Repaint the persistent in-place `◍ live` idle pulse beneath the serve panel — the
|
|
8097
|
+
* dot breathes (pink → dim) on a calm ~0.6s cycle so a quiet dev session always reads
|
|
8098
|
+
* as alive without strobing.
|
|
8099
|
+
*
|
|
8100
|
+
* @example
|
|
8101
|
+
* paintIdleLine();
|
|
8102
|
+
*/
|
|
8103
|
+
const paintIdleLine = () => {
|
|
8104
|
+
writeRaw(`\r${CLEAR_LINE} ${Math.floor((now() - idleStartedAt) / 450) % 2 === 0 ? palette.pink(g.liveOn) : palette.dim(g.liveOff)} ${palette.dim("live · waiting for changes…")}`);
|
|
7806
8105
|
};
|
|
7807
8106
|
/**
|
|
7808
|
-
* Advance
|
|
8107
|
+
* Advance whichever live region is active by one frame (driven by the shared ticker).
|
|
7809
8108
|
*
|
|
7810
8109
|
* @example
|
|
7811
8110
|
* onTick();
|
|
7812
8111
|
*/
|
|
7813
8112
|
const onTick = () => {
|
|
7814
|
-
spinnerFrame += 1;
|
|
7815
8113
|
if (rebuilding) paintRebuildLine();
|
|
7816
8114
|
else if (phaseOpen) paintPhaseBlock();
|
|
8115
|
+
else if (idle) paintIdleLine();
|
|
7817
8116
|
};
|
|
7818
8117
|
/**
|
|
7819
8118
|
* Start the animation ticker (TTY only; idempotent; `unref`'d so it never blocks exit).
|
|
@@ -7823,7 +8122,7 @@ function createPanelRenderer(options = {}) {
|
|
|
7823
8122
|
*/
|
|
7824
8123
|
const startTicker = () => {
|
|
7825
8124
|
if (!color || ticker) return;
|
|
7826
|
-
ticker = setInterval(onTick,
|
|
8125
|
+
ticker = setInterval(onTick, TICK_MS);
|
|
7827
8126
|
ticker.unref?.();
|
|
7828
8127
|
};
|
|
7829
8128
|
/**
|
|
@@ -7846,22 +8145,47 @@ function createPanelRenderer(options = {}) {
|
|
|
7846
8145
|
const writeBlock = (lines) => {
|
|
7847
8146
|
for (const line of lines) write(line);
|
|
7848
8147
|
};
|
|
8148
|
+
/**
|
|
8149
|
+
* Resume the serve idle pulse on a fresh bottom line (TTY serve sessions only). A no-op
|
|
8150
|
+
* outside serve so standalone rebuild/error calls in unit tests never leave a ticker
|
|
8151
|
+
* running.
|
|
8152
|
+
*
|
|
8153
|
+
* @example
|
|
8154
|
+
* resumeIdle();
|
|
8155
|
+
*/
|
|
8156
|
+
const resumeIdle = () => {
|
|
8157
|
+
if (!(color && serveMode)) {
|
|
8158
|
+
stopTicker();
|
|
8159
|
+
return;
|
|
8160
|
+
}
|
|
8161
|
+
idle = true;
|
|
8162
|
+
idleStartedAt = now();
|
|
8163
|
+
paintIdleLine();
|
|
8164
|
+
startTicker();
|
|
8165
|
+
};
|
|
7849
8166
|
return {
|
|
7850
8167
|
/**
|
|
7851
|
-
* Render the
|
|
8168
|
+
* Render the `▟▙ moku web` lockup + per-command label, a dim rule, and the runtime
|
|
8169
|
+
* facts line (live Node/Bun versions + platform). Called once per command (one
|
|
8170
|
+
* command = one process), so it never repeats within a run.
|
|
7852
8171
|
*
|
|
7853
|
-
* @param command - The command being run, shown beside the
|
|
8172
|
+
* @param command - The command being run, shown beside the wordmark.
|
|
7854
8173
|
* @example
|
|
7855
8174
|
* render.header("serve");
|
|
7856
8175
|
*/
|
|
7857
8176
|
header(command) {
|
|
7858
|
-
writeBlock(
|
|
8177
|
+
writeBlock([
|
|
8178
|
+
railLine(` ${palette.pink(g.cube)} ${palette.pink(palette.bold("moku web"))} ${palette.dim(COMMAND_LABEL[command])}`, palette.dim(version)),
|
|
8179
|
+
` ${palette.dim(g.rule.repeat(RAIL_WIDTH - 1))}`,
|
|
8180
|
+
` ${palette.dim(runtimeFacts(coreVersion))}`
|
|
8181
|
+
]);
|
|
7859
8182
|
},
|
|
7860
8183
|
/**
|
|
7861
|
-
* Render a per-phase row from a `build:phase` event. On a TTY each phase is ONE
|
|
7862
|
-
* that updates in place (spinning glyph while running → green ✓ + duration
|
|
7863
|
-
*
|
|
7864
|
-
* no-op while a serve()
|
|
8184
|
+
* Render a live per-phase row from a `build:phase` event. On a TTY each phase is ONE
|
|
8185
|
+
* tree row that updates in place (spinning glyph while running → green ✓ + duration
|
|
8186
|
+
* when done) beneath an animated indeterminate build bar; off a TTY one line is
|
|
8187
|
+
* printed per completed phase (no start/done duplication). A no-op while a serve()
|
|
8188
|
+
* rebuild is in flight — those show the compact rebuild line.
|
|
7865
8189
|
*
|
|
7866
8190
|
* @param phase - The `build:phase` payload.
|
|
7867
8191
|
* @example
|
|
@@ -7877,6 +8201,7 @@ function createPanelRenderer(options = {}) {
|
|
|
7877
8201
|
phaseRows = [];
|
|
7878
8202
|
phaseDrawn = 0;
|
|
7879
8203
|
phaseOpen = true;
|
|
8204
|
+
blockStartedAt = now();
|
|
7880
8205
|
}
|
|
7881
8206
|
const done = phase.status === "done";
|
|
7882
8207
|
const existing = phaseRows.find((row) => row.name === phase.phase);
|
|
@@ -7892,7 +8217,9 @@ function createPanelRenderer(options = {}) {
|
|
|
7892
8217
|
startTicker();
|
|
7893
8218
|
},
|
|
7894
8219
|
/**
|
|
7895
|
-
* Render the BUILD summary
|
|
8220
|
+
* Render the BUILD summary line + a one-shot throughput sparkline from a
|
|
8221
|
+
* `build:complete` event, finalizing the live phase tree (dropping its animated bar)
|
|
8222
|
+
* first.
|
|
7896
8223
|
*
|
|
7897
8224
|
* @param summary - The `build:complete` payload.
|
|
7898
8225
|
* @example
|
|
@@ -7900,33 +8227,52 @@ function createPanelRenderer(options = {}) {
|
|
|
7900
8227
|
*/
|
|
7901
8228
|
built(summary) {
|
|
7902
8229
|
if (rebuilding) return;
|
|
8230
|
+
if (color && phaseOpen) {
|
|
8231
|
+
let frame = cursorUp(phaseDrawn);
|
|
8232
|
+
for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
|
|
8233
|
+
writeRaw(frame + CLEAR_BELOW);
|
|
8234
|
+
}
|
|
8235
|
+
const phaseDurations = phaseRows.map((row) => row.durationMs).filter((value) => value !== void 0);
|
|
7903
8236
|
phaseOpen = false;
|
|
7904
8237
|
phaseDrawn = 0;
|
|
7905
8238
|
stopTicker();
|
|
7906
8239
|
const pages = palette.bold(String(summary.pageCount));
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
8240
|
+
const dot = palette.dim("·");
|
|
8241
|
+
const lines = [railLine(`${palette.green("✓")} ${palette.bold("BUILD")} ${dot} ${pages} pages`, `${summary.durationMs}ms ${dot} ${summary.outDir}/`, BOX_INNER)];
|
|
8242
|
+
if (color && summary.durationMs > 0) {
|
|
8243
|
+
const rate = Math.max(1, Math.round(summary.pageCount / (summary.durationMs / 1e3)));
|
|
8244
|
+
const spark = phaseDurations.length > 0 ? palette.pink(sparkline(phaseDurations)) : "";
|
|
8245
|
+
const rateLabel = palette.dim(`${rate} pages/s`);
|
|
8246
|
+
lines.push(railLine(spark, rateLabel, BOX_INNER));
|
|
8247
|
+
}
|
|
8248
|
+
writeBlock(box(lines, color, BOX_INNER));
|
|
7913
8249
|
},
|
|
7914
8250
|
/**
|
|
7915
|
-
* Render the
|
|
8251
|
+
* Render the server-ready rail (Local / Network URLs + watched dirs) and, on a TTY,
|
|
8252
|
+
* begin the persistent breathing `◍ live` idle pulse beneath it.
|
|
7916
8253
|
*
|
|
7917
8254
|
* @param info - Local/Network URLs and optionally the watched directories.
|
|
7918
8255
|
* @example
|
|
7919
8256
|
* render.serverReady({ local: "http://localhost:4173", network: null });
|
|
7920
8257
|
*/
|
|
7921
8258
|
serverReady(info) {
|
|
7922
|
-
const
|
|
7923
|
-
|
|
7924
|
-
|
|
8259
|
+
const network = info.network ? palette.cyan(info.network) : palette.dim("unavailable");
|
|
8260
|
+
const lines = [`${palette.green("➜")} ${palette.bold("Local")} ${palette.cyan(info.local)}`, `${palette.green("➜")} ${palette.bold("Network")} ${network}`];
|
|
8261
|
+
if (info.watching && info.watching.length > 0) lines.push(`${palette.dim("watching")} ${palette.dim(info.watching.join(", "))}`);
|
|
8262
|
+
writeBlock(box(lines, color, BOX_INNER));
|
|
8263
|
+
if (color) {
|
|
8264
|
+
serveMode = true;
|
|
8265
|
+
idle = true;
|
|
8266
|
+
idleStartedAt = now();
|
|
8267
|
+
paintIdleLine();
|
|
8268
|
+
startTicker();
|
|
8269
|
+
}
|
|
7925
8270
|
},
|
|
7926
8271
|
/**
|
|
7927
8272
|
* Begin a serve() rebuild: show ONE compact "rebuilding {label}" line (an animated
|
|
7928
|
-
* spinner with live elapsed on a TTY; a plain "~ {label}" line otherwise)
|
|
7929
|
-
* the verbose phase
|
|
8273
|
+
* spinner with live elapsed on a TTY; a plain "~ {label}" line otherwise), taking over
|
|
8274
|
+
* the idle-pulse line, and mute the verbose phase tree + BUILD summary until
|
|
8275
|
+
* {@link reload}/{@link error} settles it.
|
|
7930
8276
|
*
|
|
7931
8277
|
* @param label - The changed watch target shown in the line.
|
|
7932
8278
|
* @example
|
|
@@ -7934,9 +8280,9 @@ function createPanelRenderer(options = {}) {
|
|
|
7934
8280
|
*/
|
|
7935
8281
|
rebuildStart(label) {
|
|
7936
8282
|
rebuilding = true;
|
|
8283
|
+
idle = false;
|
|
7937
8284
|
rebuildLabel = label;
|
|
7938
8285
|
rebuildStartedAt = now();
|
|
7939
|
-
spinnerFrame = 0;
|
|
7940
8286
|
if (!color) {
|
|
7941
8287
|
write(` ${palette.yellow("~")} ${label}`);
|
|
7942
8288
|
return;
|
|
@@ -7946,9 +8292,9 @@ function createPanelRenderer(options = {}) {
|
|
|
7946
8292
|
},
|
|
7947
8293
|
/**
|
|
7948
8294
|
* Settle the current rebuild: replace the in-place "rebuilding…" line with a compact
|
|
7949
|
-
* "✓ rebuilt N pages · Xms · reloaded" (
|
|
7950
|
-
* Called standalone (no preceding {@link rebuildStart}) it also
|
|
7951
|
-
* line so the changed target stays visible.
|
|
8295
|
+
* "✓ rebuilt N pages · Xms · reloaded", then (in a serve session) resume the idle pulse
|
|
8296
|
+
* on a fresh bottom line. Called standalone (no preceding {@link rebuildStart}) it also
|
|
8297
|
+
* prints the "~ file" line so the changed target stays visible.
|
|
7952
8298
|
*
|
|
7953
8299
|
* @param info - The changed file plus the rebuild's page count and duration.
|
|
7954
8300
|
* @example
|
|
@@ -7957,30 +8303,26 @@ function createPanelRenderer(options = {}) {
|
|
|
7957
8303
|
reload(info) {
|
|
7958
8304
|
const settledRebuild = rebuilding;
|
|
7959
8305
|
rebuilding = false;
|
|
7960
|
-
stopTicker();
|
|
7961
8306
|
const line = ` ${palette.green("✓")} rebuilt ${palette.bold(String(info.pageCount))} pages ${palette.dim(`· ${info.durationMs}ms · reloaded`)}`;
|
|
7962
8307
|
if (settledRebuild && color) {
|
|
7963
8308
|
writeRaw(`\r${CLEAR_LINE}${line}\n`);
|
|
8309
|
+
resumeIdle();
|
|
7964
8310
|
return;
|
|
7965
8311
|
}
|
|
7966
8312
|
if (!settledRebuild) write(` ${palette.yellow("~")} ${info.file}`);
|
|
7967
8313
|
write(line);
|
|
7968
8314
|
},
|
|
7969
8315
|
/**
|
|
7970
|
-
* Render the deploy result
|
|
8316
|
+
* Render the deploy result from a `deploy:complete` event: a `✓ DEPLOYED → url` line
|
|
8317
|
+
* with the URL the hero value, then a dim `branch · id · time` line beneath it.
|
|
7971
8318
|
*
|
|
7972
8319
|
* @param result - The `deploy:complete` payload.
|
|
7973
8320
|
* @example
|
|
7974
8321
|
* render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
|
|
7975
8322
|
*/
|
|
7976
8323
|
deployed(result) {
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
`${palette.dim("url")} ${palette.cyan(result.url)}`,
|
|
7980
|
-
`${palette.dim("branch")} ${result.branch}`,
|
|
7981
|
-
`${palette.dim("id")} ${result.deploymentId}`,
|
|
7982
|
-
`${palette.dim("time")} ${result.durationMs}ms`
|
|
7983
|
-
], color));
|
|
8324
|
+
const meta = palette.dim(`branch ${result.branch} · ${result.deploymentId} · ${result.durationMs}ms`);
|
|
8325
|
+
writeBlock([` ${palette.green("✓")} ${palette.bold("DEPLOYED")} ${palette.dim("→")} ${palette.cyan(result.url)}`, ` ${meta}`]);
|
|
7984
8326
|
},
|
|
7985
8327
|
/**
|
|
7986
8328
|
* Render a neutral informational line.
|
|
@@ -8005,7 +8347,8 @@ function createPanelRenderer(options = {}) {
|
|
|
8005
8347
|
writeError(` ${palette.yellow("⚠")} ${message}`);
|
|
8006
8348
|
},
|
|
8007
8349
|
/**
|
|
8008
|
-
* Render an error line (to stderr), optionally with a cause.
|
|
8350
|
+
* Render an error line (to stderr), optionally with a cause. A failing rebuild settles
|
|
8351
|
+
* its in-place spinner line first; in a serve session the idle pulse then resumes.
|
|
8009
8352
|
*
|
|
8010
8353
|
* @param message - The error summary to print.
|
|
8011
8354
|
* @param cause - Optional underlying error/value to print beneath the summary.
|
|
@@ -8013,16 +8356,18 @@ function createPanelRenderer(options = {}) {
|
|
|
8013
8356
|
* render.error("build failed", err);
|
|
8014
8357
|
*/
|
|
8015
8358
|
error(message, cause) {
|
|
8359
|
+
const wasRebuilding = rebuilding;
|
|
8016
8360
|
if (rebuilding) {
|
|
8017
8361
|
rebuilding = false;
|
|
8018
|
-
stopTicker();
|
|
8019
8362
|
if (color) writeRaw(`\r${CLEAR_LINE}`);
|
|
8020
8363
|
}
|
|
8021
8364
|
writeError(` ${palette.red("✗")} ${message}`);
|
|
8022
8365
|
if (cause !== void 0) writeError(String(cause));
|
|
8366
|
+
if (wasRebuilding) resumeIdle();
|
|
8367
|
+
else stopTicker();
|
|
8023
8368
|
},
|
|
8024
8369
|
/**
|
|
8025
|
-
* Render a section heading (a blank line + a bold
|
|
8370
|
+
* Render a section heading (a blank line + a bold pink label) for a multi-step flow.
|
|
8026
8371
|
*
|
|
8027
8372
|
* @param text - The heading label.
|
|
8028
8373
|
* @example
|
|
@@ -8030,7 +8375,7 @@ function createPanelRenderer(options = {}) {
|
|
|
8030
8375
|
*/
|
|
8031
8376
|
heading(text) {
|
|
8032
8377
|
write("");
|
|
8033
|
-
write(` ${palette.bold(palette.
|
|
8378
|
+
write(` ${palette.bold(palette.pink(text))}`);
|
|
8034
8379
|
},
|
|
8035
8380
|
/**
|
|
8036
8381
|
* Render a diagnostic line: green `✓` (pass) or red `✗` (fail) + label, with optional
|
|
@@ -8045,6 +8390,19 @@ function createPanelRenderer(options = {}) {
|
|
|
8045
8390
|
check(ok, label, detail) {
|
|
8046
8391
|
write(` ${ok ? palette.green("✓") : palette.red("✗")} ${label}`);
|
|
8047
8392
|
if (detail !== void 0) for (const line of detail.split("\n")) write(` ${palette.dim(line)}`);
|
|
8393
|
+
},
|
|
8394
|
+
/**
|
|
8395
|
+
* Stop every animation and release the interval timer (serve()'s teardown calls this).
|
|
8396
|
+
*
|
|
8397
|
+
* @example
|
|
8398
|
+
* render.dispose();
|
|
8399
|
+
*/
|
|
8400
|
+
dispose() {
|
|
8401
|
+
stopTicker();
|
|
8402
|
+
idle = false;
|
|
8403
|
+
rebuilding = false;
|
|
8404
|
+
phaseOpen = false;
|
|
8405
|
+
serveMode = false;
|
|
8048
8406
|
}
|
|
8049
8407
|
};
|
|
8050
8408
|
}
|
|
@@ -8201,6 +8559,101 @@ function defaultFileMtime(filePath) {
|
|
|
8201
8559
|
function defaultNetworkUrl(port) {
|
|
8202
8560
|
return networkUrl(port);
|
|
8203
8561
|
}
|
|
8562
|
+
/** Memoized banner facts — resolution touches the filesystem + git once, then caches. */
|
|
8563
|
+
let cachedBanner;
|
|
8564
|
+
/**
|
|
8565
|
+
* Run a read-only `git` command in `dir`, returning its trimmed stdout (`undefined` on
|
|
8566
|
+
* any failure — not a checkout, git missing, etc.). A thin wrapper so the version
|
|
8567
|
+
* resolver can issue a couple of git queries without repeating the spawn boilerplate.
|
|
8568
|
+
*
|
|
8569
|
+
* @param dir - The working directory to run git in.
|
|
8570
|
+
* @param args - The git arguments (no user input is ever interpolated).
|
|
8571
|
+
* @returns The trimmed command output, or `undefined` on failure.
|
|
8572
|
+
* @example
|
|
8573
|
+
* git("/Users/me/moku/web", ["tag", "--list", "v*"]);
|
|
8574
|
+
*/
|
|
8575
|
+
function git(dir, args) {
|
|
8576
|
+
try {
|
|
8577
|
+
return (0, node_child_process.execFileSync)("git", args, {
|
|
8578
|
+
cwd: dir,
|
|
8579
|
+
encoding: "utf8",
|
|
8580
|
+
stdio: [
|
|
8581
|
+
"ignore",
|
|
8582
|
+
"pipe",
|
|
8583
|
+
"ignore"
|
|
8584
|
+
]
|
|
8585
|
+
}).trim();
|
|
8586
|
+
} catch {
|
|
8587
|
+
return;
|
|
8588
|
+
}
|
|
8589
|
+
}
|
|
8590
|
+
/**
|
|
8591
|
+
* The framework's source/dev version, derived the SAME way the publish workflow computes
|
|
8592
|
+
* a release: the highest semver `v*` tag is the source of truth (`@moku-labs/web` is
|
|
8593
|
+
* released tag-only — the working-tree `package.json` deliberately carries no `version`).
|
|
8594
|
+
* A `-dev` suffix marks it as a local build off that release line (e.g. `v1.1.0-dev`), so
|
|
8595
|
+
* it never masquerades as the published release. Falls back to the short commit (then
|
|
8596
|
+
* `undefined`) only when no tags exist. `undefined` when `dir` is not a git checkout (a
|
|
8597
|
+
* published npm install — which carries its real `version` instead).
|
|
8598
|
+
*
|
|
8599
|
+
* @param dir - A directory inside the framework's own repository (the realpath of the
|
|
8600
|
+
* package root, so a symlinked local checkout reports the framework's tag — not the
|
|
8601
|
+
* consumer's).
|
|
8602
|
+
* @returns The dev version (e.g. `v1.1.0-dev`), or `undefined`.
|
|
8603
|
+
* @example
|
|
8604
|
+
* devVersion("/Users/me/moku/web"); // "v1.1.0-dev"
|
|
8605
|
+
*/
|
|
8606
|
+
function devVersion(dir) {
|
|
8607
|
+
const latestTag = git(dir, [
|
|
8608
|
+
"tag",
|
|
8609
|
+
"--list",
|
|
8610
|
+
"v*",
|
|
8611
|
+
"--sort=-v:refname"
|
|
8612
|
+
])?.split("\n")[0]?.trim();
|
|
8613
|
+
if (latestTag) return `${latestTag}-dev`;
|
|
8614
|
+
const sha = git(dir, [
|
|
8615
|
+
"rev-parse",
|
|
8616
|
+
"--short",
|
|
8617
|
+
"HEAD"
|
|
8618
|
+
]);
|
|
8619
|
+
return sha ? `${sha}-dev` : void 0;
|
|
8620
|
+
}
|
|
8621
|
+
/**
|
|
8622
|
+
* Resolve the real version/runtime facts shown in the Panel banner (memoized). Reads the
|
|
8623
|
+
* `package.json` shipped beside the built bundle (`dist/../package.json`): a PUBLISHED
|
|
8624
|
+
* release carries a `version` and reports `v{version}`; a source/dev build (no `version`
|
|
8625
|
+
* field — `@moku-labs/web` is released tag-only) reports the latest semver tag + `-dev`
|
|
8626
|
+
* (e.g. `v1.1.0-dev`, the same tag the publish workflow treats as the version source), or
|
|
8627
|
+
* `"dev"` when git is unavailable. The pinned `@moku-labs/core` version comes from the
|
|
8628
|
+
* same file's `dependencies`.
|
|
8629
|
+
*
|
|
8630
|
+
* @returns The resolved {@link BannerFacts}.
|
|
8631
|
+
* @example
|
|
8632
|
+
* resolveBanner(); // { version: "v1.1.0-dev", coreVersion: "0.1.0-alpha.6" }
|
|
8633
|
+
*/
|
|
8634
|
+
function resolveBanner() {
|
|
8635
|
+
if (cachedBanner) return cachedBanner;
|
|
8636
|
+
let pkg = {};
|
|
8637
|
+
let pkgDir;
|
|
8638
|
+
try {
|
|
8639
|
+
const pkgUrl = new URL("../package.json", require("url").pathToFileURL(__filename).href);
|
|
8640
|
+
pkgDir = (0, node_fs.realpathSync)(node_path$1.default.dirname((0, node_url.fileURLToPath)(pkgUrl)));
|
|
8641
|
+
pkg = JSON.parse((0, node_fs.readFileSync)(pkgUrl, "utf8"));
|
|
8642
|
+
} catch {}
|
|
8643
|
+
const coreVersion = (pkg.dependencies?.["@moku-labs/core"] ?? "").replace(/^\D*/, "") || "unknown";
|
|
8644
|
+
const released = pkg.version;
|
|
8645
|
+
let version = "dev";
|
|
8646
|
+
if (released) version = `v${released}`;
|
|
8647
|
+
else {
|
|
8648
|
+
const dev = devVersion(pkgDir ?? process.cwd());
|
|
8649
|
+
if (dev) version = dev;
|
|
8650
|
+
}
|
|
8651
|
+
cachedBanner = {
|
|
8652
|
+
version,
|
|
8653
|
+
coreVersion
|
|
8654
|
+
};
|
|
8655
|
+
return cachedBanner;
|
|
8656
|
+
}
|
|
8204
8657
|
/**
|
|
8205
8658
|
* Create the initial cli plugin state with the production seams wired. Every field is
|
|
8206
8659
|
* an injectable seam (`render`, `confirm`, `clock`, `watch`, the server factories,
|
|
@@ -8214,8 +8667,12 @@ function defaultNetworkUrl(port) {
|
|
|
8214
8667
|
* const state = createState({ global: {}, config });
|
|
8215
8668
|
*/
|
|
8216
8669
|
function createState$1(_ctx) {
|
|
8670
|
+
const banner = resolveBanner();
|
|
8217
8671
|
return {
|
|
8218
|
-
render: createPanelRenderer(
|
|
8672
|
+
render: createPanelRenderer({
|
|
8673
|
+
version: banner.version,
|
|
8674
|
+
coreVersion: banner.coreVersion
|
|
8675
|
+
}),
|
|
8219
8676
|
confirm: defaultConfirm,
|
|
8220
8677
|
select: defaultSelect,
|
|
8221
8678
|
clock: Date.now,
|