@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.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
|
|
2
2
|
import { n as relativeDataFile, t as dataSuffix } from "./convention-CepUwWmT.mjs";
|
|
3
3
|
import { createCoreConfig, createCorePlugin } from "@moku-labs/core";
|
|
4
|
-
import { existsSync, readFileSync, readdirSync, statSync, watch } from "node:fs";
|
|
4
|
+
import { appendFileSync, existsSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { Feed } from "feed";
|
|
9
9
|
import { h } from "preact";
|
|
10
10
|
import { renderToString } from "preact-render-to-string";
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
11
12
|
import { createInterface } from "node:readline";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
12
14
|
import { networkInterfaces } from "node:os";
|
|
13
15
|
import matter from "gray-matter";
|
|
14
16
|
import rehypeShiki from "@shikijs/rehype";
|
|
@@ -4877,10 +4879,47 @@ function findRootHtml(rendered) {
|
|
|
4877
4879
|
return rendered.find((page) => page.url === "/" || page.url === "")?.html ?? null;
|
|
4878
4880
|
}
|
|
4879
4881
|
/**
|
|
4882
|
+
* Pages rendered concurrently per batch. Kept small so the macrotask yield between
|
|
4883
|
+
* batches fires frequently — a large batch renders for seconds before yielding, which
|
|
4884
|
+
* leaves a watching dev server's spinner repainting only every few seconds (sluggish).
|
|
4885
|
+
* Smaller batches trade a little write-concurrency for a smooth, responsive spinner.
|
|
4886
|
+
*/
|
|
4887
|
+
const RENDER_BATCH_SIZE = 2;
|
|
4888
|
+
/**
|
|
4889
|
+
* Render `items` through `worker` in bounded-size batches, yielding a macrotask
|
|
4890
|
+
* (`setImmediate`) between batches. Beyond bounding peak concurrency/memory for large
|
|
4891
|
+
* sites, the yield lets the single JS thread breathe: one un-yielded `Promise.all` over
|
|
4892
|
+
* hundreds of synchronous `renderToString` calls starves the event loop, which freezes a
|
|
4893
|
+
* watching dev server's progress spinner until the whole phase resolves. Output order is
|
|
4894
|
+
* preserved (batch order + `Promise.all` order within a batch).
|
|
4895
|
+
*
|
|
4896
|
+
* @template Item - The input item type.
|
|
4897
|
+
* @template Out - The rendered output type.
|
|
4898
|
+
* @param items - The items to render.
|
|
4899
|
+
* @param batchSize - Maximum items rendered concurrently per batch.
|
|
4900
|
+
* @param worker - Renders one item to its output.
|
|
4901
|
+
* @returns All rendered outputs in input order.
|
|
4902
|
+
* @example
|
|
4903
|
+
* ```ts
|
|
4904
|
+
* const pages = await renderInBatches(instances, 32, i => renderInstance(ctx, i, shell));
|
|
4905
|
+
* ```
|
|
4906
|
+
*/
|
|
4907
|
+
async function renderInBatches(items, batchSize, worker) {
|
|
4908
|
+
const out = [];
|
|
4909
|
+
for (let start = 0; start < items.length; start += batchSize) {
|
|
4910
|
+
const batch = items.slice(start, start + batchSize);
|
|
4911
|
+
out.push(...await Promise.all(batch.map((item) => worker(item))));
|
|
4912
|
+
if (start + batchSize < items.length) await new Promise((resolve) => {
|
|
4913
|
+
setImmediate(resolve);
|
|
4914
|
+
});
|
|
4915
|
+
}
|
|
4916
|
+
return out;
|
|
4917
|
+
}
|
|
4918
|
+
/**
|
|
4880
4919
|
* Renders every route in the manifest to `outDir/<path>/index.html`. Reads as a
|
|
4881
|
-
* pipeline: resolve deps → prepare the shared shell → expand instances → render
|
|
4882
|
-
*
|
|
4883
|
-
*
|
|
4920
|
+
* pipeline: resolve deps → prepare the shared shell → expand instances → render in
|
|
4921
|
+
* bounded batches ({@link renderInBatches}) → write data sidecars (hybrid/spa) →
|
|
4922
|
+
* capture the root page's HTML for the root-index phase.
|
|
4884
4923
|
*
|
|
4885
4924
|
* @param ctx - Plugin context (provides `require`, `state`, `config`, `log`, `has`).
|
|
4886
4925
|
* @returns The number of pages rendered and the captured default-page HTML.
|
|
@@ -4896,8 +4935,7 @@ async function renderPages(ctx) {
|
|
|
4896
4935
|
const locales = ctx.require(i18nPlugin).locales();
|
|
4897
4936
|
const byPattern = makeEntryMap(router);
|
|
4898
4937
|
const shell = await prepareShell(ctx);
|
|
4899
|
-
const
|
|
4900
|
-
const rendered = await Promise.all(instances.map((instance) => renderInstance(ctx, instance, shell)));
|
|
4938
|
+
const rendered = await renderInBatches(await expandAllInstances(manifest, locales, byPattern, ctx), RENDER_BATCH_SIZE, (instance) => renderInstance(ctx, instance, shell));
|
|
4901
4939
|
await writeDataSidecars(ctx, rendered, router.mode());
|
|
4902
4940
|
ctx.log.debug("build:pages", { count: rendered.length });
|
|
4903
4941
|
return {
|
|
@@ -6449,13 +6487,14 @@ const deployPlugin = createPlugin$1("deploy", {
|
|
|
6449
6487
|
//#endregion
|
|
6450
6488
|
//#region src/plugins/cli/deploy-wizard.ts
|
|
6451
6489
|
/**
|
|
6452
|
-
* @file cli plugin — the guided deploy wizard (`cli.deploy({ guided: true })
|
|
6453
|
-
*
|
|
6454
|
-
* Cloudflare
|
|
6455
|
-
*
|
|
6456
|
-
*
|
|
6457
|
-
*
|
|
6458
|
-
*
|
|
6490
|
+
* @file cli plugin — the guided deploy wizard (`cli.deploy({ guided: true })`, the default
|
|
6491
|
+
* for `bun run deploy`; the direct `--cli` path stays in `api.ts`). Walks a human through a
|
|
6492
|
+
* Cloudflare Pages deploy: checks prerequisites (wrangler config + the Cloudflare
|
|
6493
|
+
* credentials) with concrete fix guidance, offers to scaffold what is missing (a
|
|
6494
|
+
* `wrangler.jsonc`, and a placeholder `.env` for any missing credentials), HARD-GATES the
|
|
6495
|
+
* deploy on everything being green, runs a local build smoke test, confirms, deploys, then
|
|
6496
|
+
* offers to scaffold a GitHub Actions workflow (auto on push to main, or a versioned/manual
|
|
6497
|
+
* trigger). Every prompt + line of output flows through injectable `state` seams.
|
|
6459
6498
|
*/
|
|
6460
6499
|
/** How to create a Cloudflare API token + where to make it available locally. */
|
|
6461
6500
|
const TOKEN_HELP = [
|
|
@@ -6522,6 +6561,43 @@ async function offerScaffold(ctx, prereqs) {
|
|
|
6522
6561
|
await ctx.require(deployPlugin).init({});
|
|
6523
6562
|
ctx.state.render.check(true, "wrangler.jsonc scaffolded");
|
|
6524
6563
|
}
|
|
6564
|
+
/** The Cloudflare credentials the deploy needs, with the comment written above each in a scaffolded `.env`. */
|
|
6565
|
+
const ENV_CREDENTIALS = [{
|
|
6566
|
+
key: "CLOUDFLARE_API_TOKEN",
|
|
6567
|
+
comment: "# Cloudflare API token — https://dash.cloudflare.com/profile/api-tokens (template: Cloudflare Pages — Edit)"
|
|
6568
|
+
}, {
|
|
6569
|
+
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
6570
|
+
comment: "# Cloudflare account id — dashboard → Workers & Pages → right-hand sidebar"
|
|
6571
|
+
}];
|
|
6572
|
+
/**
|
|
6573
|
+
* Offer to scaffold a `.env` with placeholders for whichever Cloudflare credentials are
|
|
6574
|
+
* missing — created when absent, appended to (never clobbering a key already present)
|
|
6575
|
+
* when it exists. The placeholders are empty, so the deploy still hard-gates until the
|
|
6576
|
+
* user fills them in; this just removes the "where do I even put these?" friction.
|
|
6577
|
+
*
|
|
6578
|
+
* @param ctx - The cli plugin context.
|
|
6579
|
+
* @param cwd - The project root (where `.env` lives).
|
|
6580
|
+
* @returns Resolves once any accepted scaffold has been written.
|
|
6581
|
+
* @example
|
|
6582
|
+
* await offerEnvScaffold(ctx, process.cwd());
|
|
6583
|
+
*/
|
|
6584
|
+
async function offerEnvScaffold(ctx, cwd) {
|
|
6585
|
+
const missing = ENV_CREDENTIALS.filter(({ key }) => (process.env[key] ?? "") === "");
|
|
6586
|
+
if (missing.length === 0) return;
|
|
6587
|
+
const envPath = path.join(cwd, ".env");
|
|
6588
|
+
const exists = existsSync(envPath);
|
|
6589
|
+
const verb = exists ? "Add placeholders for the missing secret(s) to" : "Create";
|
|
6590
|
+
if (!await ctx.state.confirm(`${verb} .env?`)) return;
|
|
6591
|
+
const lines = exists ? readFileSync(envPath, "utf8").split(/\r?\n/) : [];
|
|
6592
|
+
const toAdd = missing.filter(({ key }) => !lines.some((line) => line.trimStart().startsWith(`${key}=`)));
|
|
6593
|
+
if (toAdd.length === 0) {
|
|
6594
|
+
ctx.state.render.info(".env already lists those keys — fill in their values, then re-run.");
|
|
6595
|
+
return;
|
|
6596
|
+
}
|
|
6597
|
+
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`);
|
|
6598
|
+
const names = toAdd.map(({ key }) => key).join(", ");
|
|
6599
|
+
ctx.state.render.check(true, `${exists ? "added placeholders to" : "created"} .env`, `fill in ${names}, then re-run \`bun run deploy\`.`);
|
|
6600
|
+
}
|
|
6525
6601
|
/**
|
|
6526
6602
|
* Map a top-level workflow choice (and, for the versioned option, a sub-choice) to the
|
|
6527
6603
|
* concrete {@link WorkflowTrigger}, or `null` when the user chose to skip setup.
|
|
@@ -6605,6 +6681,7 @@ async function runDeployWizard(ctx, options) {
|
|
|
6605
6681
|
ctx.state.render.heading("Checking prerequisites");
|
|
6606
6682
|
for (const item of diagnose(cwd)) ctx.state.render.check(item.ok, item.label, item.detail);
|
|
6607
6683
|
await offerScaffold(ctx, diagnose(cwd));
|
|
6684
|
+
await offerEnvScaffold(ctx, cwd);
|
|
6608
6685
|
const blockers = diagnose(cwd).filter((item) => !item.ok);
|
|
6609
6686
|
if (blockers.length > 0) {
|
|
6610
6687
|
ctx.state.render.heading("Not ready to deploy");
|
|
@@ -7110,6 +7187,7 @@ async function runDevServer(ctx, port) {
|
|
|
7110
7187
|
for (const watcher of watchers) watcher.close();
|
|
7111
7188
|
hub.close();
|
|
7112
7189
|
server.stop();
|
|
7190
|
+
ctx.state.render.dispose();
|
|
7113
7191
|
});
|
|
7114
7192
|
}
|
|
7115
7193
|
//#endregion
|
|
@@ -7310,6 +7388,28 @@ async function confirmDeploy(ctx, yes) {
|
|
|
7310
7388
|
if (!confirmed) ctx.state.render.warn("deploy skipped");
|
|
7311
7389
|
return confirmed;
|
|
7312
7390
|
}
|
|
7391
|
+
/** Matches the prerequisite/credential failures a direct deploy most often hits (missing token/account). */
|
|
7392
|
+
const PREREQUISITE_ERROR = /required variable|not defined|cloudflare|token|account|unauthor|wrangler/i;
|
|
7393
|
+
/**
|
|
7394
|
+
* A short, actionable "how to fix" hint for a failed deploy, rendered under the error so
|
|
7395
|
+
* the user is never left at a raw stack trace. A missing-credential/prerequisite failure
|
|
7396
|
+
* (the common case for a first direct deploy) gets the concrete secret-setup steps;
|
|
7397
|
+
* anything else points at the guided deploy, which diagnoses prerequisites step by step.
|
|
7398
|
+
*
|
|
7399
|
+
* @param error - The thrown deploy error.
|
|
7400
|
+
* @returns The multi-line hint (newline-separated; rendered indented under a `›`).
|
|
7401
|
+
* @example
|
|
7402
|
+
* render.info(deployFailureHint(err));
|
|
7403
|
+
*/
|
|
7404
|
+
function deployFailureHint(error) {
|
|
7405
|
+
if (PREREQUISITE_ERROR.test(String(error))) return [
|
|
7406
|
+
"how to fix:",
|
|
7407
|
+
"1. run `bun run deploy` (without `--cli`) — the guided setup diagnoses prerequisites and offers to create a starter .env",
|
|
7408
|
+
"2. or set them yourself in .env: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID",
|
|
7409
|
+
" token: https://dash.cloudflare.com/profile/api-tokens (template: Cloudflare Pages — Edit)"
|
|
7410
|
+
].join("\n");
|
|
7411
|
+
return "how to fix: run `bun run deploy` (without `--cli`) — the guided setup diagnoses prerequisites — then retry";
|
|
7412
|
+
}
|
|
7313
7413
|
/**
|
|
7314
7414
|
* Create the cli plugin API surface — exactly `build`, `serve`, `preview`, `deploy`.
|
|
7315
7415
|
* Each method renders `state.render.header(<command>)` first, then does its work;
|
|
@@ -7384,15 +7484,21 @@ function createApi$1(ctx) {
|
|
|
7384
7484
|
const { branch, yes = false } = options;
|
|
7385
7485
|
ctx.state.render.header("deploy");
|
|
7386
7486
|
if (options.guided === true) return runDeployWizard(ctx, options);
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7487
|
+
try {
|
|
7488
|
+
await ctx.require(deployPlugin).init({ ci: true });
|
|
7489
|
+
if (!await confirmDeploy(ctx, yes)) return {
|
|
7490
|
+
deployed: false,
|
|
7491
|
+
reason: "declined"
|
|
7492
|
+
};
|
|
7493
|
+
return {
|
|
7494
|
+
deployed: true,
|
|
7495
|
+
...await ctx.require(deployPlugin).run(branch === void 0 ? {} : { branch })
|
|
7496
|
+
};
|
|
7497
|
+
} catch (error) {
|
|
7498
|
+
ctx.state.render.error("deploy failed", error);
|
|
7499
|
+
ctx.state.render.info(deployFailureHint(error));
|
|
7500
|
+
throw error;
|
|
7501
|
+
}
|
|
7396
7502
|
}
|
|
7397
7503
|
};
|
|
7398
7504
|
}
|
|
@@ -7483,6 +7589,28 @@ const ANSI = {
|
|
|
7483
7589
|
cyan: `${ESC}[36m`,
|
|
7484
7590
|
gray: `${ESC}[90m`
|
|
7485
7591
|
};
|
|
7592
|
+
/**
|
|
7593
|
+
* The Moku brand pink (`#FF1E6F`) as an RGB triple, used for 24-bit truecolor output.
|
|
7594
|
+
* Degrades to {@link ANSI.magenta} on a 16-color TTY and to plain text off a TTY.
|
|
7595
|
+
*/
|
|
7596
|
+
const BRAND_PINK = {
|
|
7597
|
+
r: 255,
|
|
7598
|
+
g: 30,
|
|
7599
|
+
b: 111
|
|
7600
|
+
};
|
|
7601
|
+
/**
|
|
7602
|
+
* Build a 24-bit (truecolor) SGR foreground escape for the given RGB triple.
|
|
7603
|
+
*
|
|
7604
|
+
* @param r - Red channel (0–255).
|
|
7605
|
+
* @param g - Green channel (0–255).
|
|
7606
|
+
* @param b - Blue channel (0–255).
|
|
7607
|
+
* @returns The `ESC[38;2;r;g;bm` foreground sequence.
|
|
7608
|
+
* @example
|
|
7609
|
+
* fg24(255, 30, 111); // "\x1b[38;2;255;30;111m"
|
|
7610
|
+
*/
|
|
7611
|
+
function fg24(r, g, b) {
|
|
7612
|
+
return `${ESC}[38;2;${r};${g};${b}m`;
|
|
7613
|
+
}
|
|
7486
7614
|
/** ANSI: erase the entire current line, leaving the cursor where it is. */
|
|
7487
7615
|
const CLEAR_LINE = `${ESC}[2K`;
|
|
7488
7616
|
/** ANSI: erase from the cursor to the end of the screen (drops stale trailing rows). */
|
|
@@ -7554,6 +7682,36 @@ function supportsColor(stream = process.stdout, noColor = process.env.NO_COLOR)
|
|
|
7554
7682
|
return stream.isTTY === true && noColor === void 0;
|
|
7555
7683
|
}
|
|
7556
7684
|
/**
|
|
7685
|
+
* Whether the terminal advertises 24-bit (truecolor) support via `COLORTERM`, so the
|
|
7686
|
+
* renderer may emit the exact brand pink ({@link BRAND_PINK}) instead of the 16-color
|
|
7687
|
+
* `magenta` approximation. Always layered on top of {@link supportsColor} — truecolor
|
|
7688
|
+
* is never used when color itself is disabled.
|
|
7689
|
+
*
|
|
7690
|
+
* @param colorTerm - The `COLORTERM` value (defaults to `process.env.COLORTERM`).
|
|
7691
|
+
* @returns `true` when `COLORTERM` is `truecolor` or `24bit`.
|
|
7692
|
+
* @example
|
|
7693
|
+
* supportsTruecolor("truecolor"); // true
|
|
7694
|
+
*/
|
|
7695
|
+
function supportsTruecolor(colorTerm = process.env.COLORTERM) {
|
|
7696
|
+
return colorTerm === "truecolor" || colorTerm === "24bit";
|
|
7697
|
+
}
|
|
7698
|
+
/**
|
|
7699
|
+
* The braille spinner glyph for a given elapsed time, advancing one frame per
|
|
7700
|
+
* `frameMs`. Deriving the frame from wall-clock elapsed (rather than a tick counter)
|
|
7701
|
+
* keeps the spinner correct even when the animation ticker is briefly starved by a
|
|
7702
|
+
* synchronous build phase and several ticks coalesce — the glyph still reflects real
|
|
7703
|
+
* elapsed time instead of freezing on a stale frame.
|
|
7704
|
+
*
|
|
7705
|
+
* @param elapsedMs - Milliseconds since the live region opened.
|
|
7706
|
+
* @param frameMs - Milliseconds per frame (defaults to `80`).
|
|
7707
|
+
* @returns The active spinner glyph.
|
|
7708
|
+
* @example
|
|
7709
|
+
* spinnerFrameAt(240); // "⠹" (the 4th frame at 80ms/frame)
|
|
7710
|
+
*/
|
|
7711
|
+
function spinnerFrameAt(elapsedMs, frameMs = 80) {
|
|
7712
|
+
return SPINNER_FRAMES[Math.floor(Math.max(0, elapsedMs) / frameMs) % SPINNER_FRAMES.length] ?? "⠋";
|
|
7713
|
+
}
|
|
7714
|
+
/**
|
|
7557
7715
|
* Select the box glyph set for the given color mode (Unicode on a TTY, ASCII off it).
|
|
7558
7716
|
*
|
|
7559
7717
|
* @param color - Whether color/Unicode output is enabled.
|
|
@@ -7581,12 +7739,14 @@ function visibleWidth(text) {
|
|
|
7581
7739
|
* output in CI/pipes.
|
|
7582
7740
|
*
|
|
7583
7741
|
* @param color - Whether color is enabled (typically `supportsColor()`).
|
|
7742
|
+
* @param truecolor - Whether 24-bit output is enabled (typically `supportsTruecolor()`);
|
|
7743
|
+
* only consulted by {@link Palette.pink}. Defaults to `false` (16-color magenta).
|
|
7584
7744
|
* @returns The bound color palette.
|
|
7585
7745
|
* @example
|
|
7586
|
-
* const palette = makePalette(supportsColor());
|
|
7746
|
+
* const palette = makePalette(supportsColor(), supportsTruecolor());
|
|
7587
7747
|
* const line = palette.green("done");
|
|
7588
7748
|
*/
|
|
7589
|
-
function makePalette(color) {
|
|
7749
|
+
function makePalette(color, truecolor = false) {
|
|
7590
7750
|
return {
|
|
7591
7751
|
enabled: color,
|
|
7592
7752
|
/**
|
|
@@ -7666,23 +7826,39 @@ function makePalette(color) {
|
|
|
7666
7826
|
*/
|
|
7667
7827
|
cyan(text) {
|
|
7668
7828
|
return this.paint(ANSI.cyan, text);
|
|
7829
|
+
},
|
|
7830
|
+
/**
|
|
7831
|
+
* Color the given text the Moku brand pink: exact `#FF1E6F` (24-bit) when truecolor
|
|
7832
|
+
* is enabled, the 16-color `magenta` approximation otherwise, unchanged in plain mode.
|
|
7833
|
+
*
|
|
7834
|
+
* @param text - The text to colorize.
|
|
7835
|
+
* @returns The pink (or unchanged) text.
|
|
7836
|
+
* @example
|
|
7837
|
+
* palette.pink("▟▙ moku web");
|
|
7838
|
+
*/
|
|
7839
|
+
pink(text) {
|
|
7840
|
+
if (!color) return text;
|
|
7841
|
+
if (truecolor) return `${fg24(BRAND_PINK.r, BRAND_PINK.g, BRAND_PINK.b)}${text}${ANSI.reset}`;
|
|
7842
|
+
return this.paint(ANSI.magenta, text);
|
|
7669
7843
|
}
|
|
7670
7844
|
};
|
|
7671
7845
|
}
|
|
7672
7846
|
/**
|
|
7673
7847
|
* Frame a list of already-rendered content lines in a box, padding each line to the
|
|
7674
|
-
*
|
|
7675
|
-
*
|
|
7848
|
+
* widest visible line (or `minInnerWidth`, whichever is larger — so several boxes can be
|
|
7849
|
+
* forced to a shared width). Uses Unicode borders when `color` is enabled and ASCII
|
|
7850
|
+
* otherwise. Visible width ignores embedded ANSI so colored lines align.
|
|
7676
7851
|
*
|
|
7677
7852
|
* @param lines - The content lines (may contain ANSI color codes).
|
|
7678
7853
|
* @param color - Whether to use Unicode borders (and assume color-capable output).
|
|
7854
|
+
* @param minInnerWidth - Minimum inner (content) width to pad every row to. Defaults to `0`.
|
|
7679
7855
|
* @returns The boxed lines (top border, content rows, bottom border).
|
|
7680
7856
|
* @example
|
|
7681
|
-
* box(["Local: http://localhost:4173"], true);
|
|
7857
|
+
* box(["Local: http://localhost:4173"], true, 62);
|
|
7682
7858
|
*/
|
|
7683
|
-
function box(lines, color) {
|
|
7859
|
+
function box(lines, color, minInnerWidth = 0) {
|
|
7684
7860
|
const glyphs = boxGlyphs(color);
|
|
7685
|
-
const inner = Math.max(0, ...lines.map((line) => visibleWidth(line)));
|
|
7861
|
+
const inner = Math.max(0, minInnerWidth, ...lines.map((line) => visibleWidth(line)));
|
|
7686
7862
|
const horizontal = glyphs.horizontal.repeat(inner + 2);
|
|
7687
7863
|
const top = `${glyphs.topLeft}${horizontal}${glyphs.topRight}`;
|
|
7688
7864
|
const bottom = `${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`;
|
|
@@ -7697,13 +7873,70 @@ function box(lines, color) {
|
|
|
7697
7873
|
}
|
|
7698
7874
|
//#endregion
|
|
7699
7875
|
//#region src/plugins/cli/render/panel.ts
|
|
7700
|
-
/** Per-command label shown
|
|
7876
|
+
/** Per-command label shown beside the lockup wordmark. */
|
|
7701
7877
|
const COMMAND_LABEL = {
|
|
7702
7878
|
build: "build",
|
|
7703
7879
|
serve: "serve · dev",
|
|
7704
7880
|
preview: "preview",
|
|
7705
7881
|
deploy: "deploy"
|
|
7706
7882
|
};
|
|
7883
|
+
/** Total visible width the header rule spans and the per-row timing column right-aligns to. */
|
|
7884
|
+
const RAIL_WIDTH = 66;
|
|
7885
|
+
/** Animation repaint cadence (ms) — how often the live region is redrawn when the loop is free. */
|
|
7886
|
+
const TICK_MS = 40;
|
|
7887
|
+
/** Spinner frame interval (ms) — one braille glyph advance per this many elapsed ms. */
|
|
7888
|
+
const SPIN_MS = 60;
|
|
7889
|
+
/** Inner (content) width of the BUILD/server boxes so their right edge lines up with the phase tree. */
|
|
7890
|
+
const BOX_INNER = RAIL_WIDTH - 4;
|
|
7891
|
+
/** The eight block glyphs the per-phase time-profile sparkline maps durations onto. */
|
|
7892
|
+
const SPARK_BARS = "▁▂▃▄▅▆▇█";
|
|
7893
|
+
/**
|
|
7894
|
+
* Build a sparkline from a list of values — one block glyph per value, height scaled to
|
|
7895
|
+
* the largest value so the tallest bar is `█`. A real micro-histogram (no fake data):
|
|
7896
|
+
* under the BUILD summary each bar is one phase's duration, so the slowest phase stands
|
|
7897
|
+
* out at a glance. Returns `""` for an empty list.
|
|
7898
|
+
*
|
|
7899
|
+
* @param values - The values to plot (e.g. per-phase durations in ms).
|
|
7900
|
+
* @returns The sparkline string.
|
|
7901
|
+
* @example
|
|
7902
|
+
* sparkline([12, 1701, 19698, 9]); // "▁▁█▁"
|
|
7903
|
+
*/
|
|
7904
|
+
function sparkline(values) {
|
|
7905
|
+
if (values.length === 0) return "";
|
|
7906
|
+
const max = Math.max(...values, 1);
|
|
7907
|
+
return values.map((value) => {
|
|
7908
|
+
return SPARK_BARS[Math.min(7, Math.floor(value / max * 7))] ?? SPARK_BARS[0];
|
|
7909
|
+
}).join("");
|
|
7910
|
+
}
|
|
7911
|
+
/**
|
|
7912
|
+
* The structural glyph set for the active color mode: Unicode on a color-capable TTY,
|
|
7913
|
+
* ASCII fallbacks off it. Only the NEW Velocity chrome (cube, rule, tree, bar, live
|
|
7914
|
+
* dot) degrades here — the `✓ ✗ ~ ➜ ›` status marks stay as-is in both modes.
|
|
7915
|
+
*
|
|
7916
|
+
* @param color - Whether color/Unicode output is enabled.
|
|
7917
|
+
* @returns The matching glyph set.
|
|
7918
|
+
* @example
|
|
7919
|
+
* const g = glyphSet(true);
|
|
7920
|
+
*/
|
|
7921
|
+
function glyphSet(color) {
|
|
7922
|
+
return color ? {
|
|
7923
|
+
cube: "▟▙",
|
|
7924
|
+
rule: "─",
|
|
7925
|
+
tree: "├─",
|
|
7926
|
+
barFill: "━",
|
|
7927
|
+
barTrack: "╴",
|
|
7928
|
+
liveOn: "◍",
|
|
7929
|
+
liveOff: "○"
|
|
7930
|
+
} : {
|
|
7931
|
+
cube: "*",
|
|
7932
|
+
rule: "-",
|
|
7933
|
+
tree: "-",
|
|
7934
|
+
barFill: "#",
|
|
7935
|
+
barTrack: "-",
|
|
7936
|
+
liveOn: "*",
|
|
7937
|
+
liveOff: "*"
|
|
7938
|
+
};
|
|
7939
|
+
}
|
|
7707
7940
|
/**
|
|
7708
7941
|
* Render one human-readable duration suffix (e.g. `· 84ms`).
|
|
7709
7942
|
*
|
|
@@ -7718,15 +7951,49 @@ function durationSuffix(palette, durationMs) {
|
|
|
7718
7951
|
return ` ${palette.dim(`· ${durationMs}ms`)}`;
|
|
7719
7952
|
}
|
|
7720
7953
|
/**
|
|
7954
|
+
* Right-align `right` against `left` within {@link RAIL_WIDTH}, measuring visible width
|
|
7955
|
+
* so embedded ANSI never throws the timing column off.
|
|
7956
|
+
*
|
|
7957
|
+
* @param left - The left segment (may contain ANSI).
|
|
7958
|
+
* @param right - The right segment (may contain ANSI).
|
|
7959
|
+
* @param width - Total visible width to fill (defaults to {@link RAIL_WIDTH}).
|
|
7960
|
+
* @returns The padded line.
|
|
7961
|
+
* @example
|
|
7962
|
+
* railLine(" ├─ ✓ pages", "· 12ms");
|
|
7963
|
+
*/
|
|
7964
|
+
function railLine(left, right, width = RAIL_WIDTH) {
|
|
7965
|
+
const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
|
|
7966
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
7967
|
+
}
|
|
7968
|
+
/**
|
|
7969
|
+
* The runtime facts line shown under the banner: the pinned core version (when known)
|
|
7970
|
+
* plus the live Node/Bun versions + platform — the ACTUAL running runtime, not the
|
|
7971
|
+
* `engines` floor. Every value is real (read from `@moku-labs/core`'s pinned dependency
|
|
7972
|
+
* and `process.versions`), so nothing on this line is faked.
|
|
7973
|
+
*
|
|
7974
|
+
* @param coreVersion - The pinned `@moku-labs/core` version (appended last — it rarely
|
|
7975
|
+
* matters — and omitted entirely when unknown).
|
|
7976
|
+
* @returns The facts string (e.g. `node 24.3.0 · bun 1.3.9 · darwin arm64 · core 0.1.0-alpha.6`).
|
|
7977
|
+
* @example
|
|
7978
|
+
* runtimeFacts("0.1.0-alpha.6");
|
|
7979
|
+
*/
|
|
7980
|
+
function runtimeFacts(coreVersion) {
|
|
7981
|
+
const node = `node ${process.versions.node}`;
|
|
7982
|
+
const bun = process.versions.bun ? ` · bun ${process.versions.bun}` : "";
|
|
7983
|
+
const core = coreVersion ? ` · core ${coreVersion}` : "";
|
|
7984
|
+
return `${node}${bun} · ${process.platform} ${process.arch}${core}`;
|
|
7985
|
+
}
|
|
7986
|
+
/**
|
|
7721
7987
|
* Create the Panel {@link CliRenderer}. Output is written through the injected sink
|
|
7722
|
-
* (default `console.log`/`console.error`) and colorized only when color is enabled,
|
|
7723
|
-
*
|
|
7724
|
-
* ASCII lines in CI/pipes.
|
|
7988
|
+
* (default `console.log`/`console.error`) and colorized only when color is enabled, so
|
|
7989
|
+
* the identical render path yields the animated, box-free Velocity UI on a TTY and
|
|
7990
|
+
* plain ASCII lines in CI/pipes.
|
|
7725
7991
|
*
|
|
7726
|
-
* @param options - Optional sinks
|
|
7992
|
+
* @param options - Optional sinks, color/truecolor overrides, clock, and version (see
|
|
7993
|
+
* {@link PanelOptions}).
|
|
7727
7994
|
* @returns The renderer mounted on `state.render` and driven by the API + hooks.
|
|
7728
7995
|
* @example
|
|
7729
|
-
* const render = createPanelRenderer();
|
|
7996
|
+
* const render = createPanelRenderer({ version: "0.1.0-alpha" });
|
|
7730
7997
|
* render.header("build");
|
|
7731
7998
|
*/
|
|
7732
7999
|
function createPanelRenderer(options = {}) {
|
|
@@ -7737,26 +8004,24 @@ function createPanelRenderer(options = {}) {
|
|
|
7737
8004
|
});
|
|
7738
8005
|
const now = options.now ?? Date.now;
|
|
7739
8006
|
const color = options.color ?? supportsColor();
|
|
7740
|
-
const palette = makePalette(color);
|
|
8007
|
+
const palette = makePalette(color, options.truecolor ?? (color && supportsTruecolor()));
|
|
8008
|
+
const version = options.version ?? "dev";
|
|
8009
|
+
const coreVersion = options.coreVersion;
|
|
8010
|
+
const g = glyphSet(color);
|
|
7741
8011
|
let phaseRows = [];
|
|
7742
8012
|
let phaseDrawn = 0;
|
|
7743
8013
|
let phaseOpen = false;
|
|
8014
|
+
let blockStartedAt = 0;
|
|
7744
8015
|
let rebuilding = false;
|
|
7745
8016
|
let rebuildLabel = "";
|
|
7746
8017
|
let rebuildStartedAt = 0;
|
|
7747
|
-
let
|
|
8018
|
+
let idle = false;
|
|
8019
|
+
let idleStartedAt = 0;
|
|
8020
|
+
let serveMode = false;
|
|
7748
8021
|
let ticker;
|
|
7749
8022
|
/**
|
|
7750
|
-
*
|
|
7751
|
-
*
|
|
7752
|
-
* @returns The active braille spinner frame.
|
|
7753
|
-
* @example
|
|
7754
|
-
* frameGlyph(); // "⠙"
|
|
7755
|
-
*/
|
|
7756
|
-
const frameGlyph = () => SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length] ?? "⠋";
|
|
7757
|
-
/**
|
|
7758
|
-
* Render one phase row: a green `✓ name · time` when done, else a spinning cyan glyph
|
|
7759
|
-
* before the dim name.
|
|
8023
|
+
* Render one phase-tree row: a spinning cyan glyph + dim name while running, or a green
|
|
8024
|
+
* `✓` + name with the duration right-aligned in the dim timing column once done.
|
|
7760
8025
|
*
|
|
7761
8026
|
* @param row - The phase row to render.
|
|
7762
8027
|
* @returns The rendered row line (no trailing newline).
|
|
@@ -7764,12 +8029,34 @@ function createPanelRenderer(options = {}) {
|
|
|
7764
8029
|
* renderPhaseRow({ name: "pages", done: true, durationMs: 12 });
|
|
7765
8030
|
*/
|
|
7766
8031
|
const renderPhaseRow = (row) => {
|
|
7767
|
-
|
|
7768
|
-
return ` ${palette.
|
|
8032
|
+
const branch = palette.dim(g.tree);
|
|
8033
|
+
if (row.done) return railLine(` ${branch} ${palette.green("✓")} ${row.name}`, palette.dim(`· ${row.durationMs}ms`));
|
|
8034
|
+
return ` ${branch} ${palette.cyan(spinnerFrameAt(now() - blockStartedAt, SPIN_MS))} ${palette.dim(row.name)}`;
|
|
7769
8035
|
};
|
|
7770
8036
|
/**
|
|
7771
|
-
*
|
|
7772
|
-
*
|
|
8037
|
+
* Render the indeterminate "comet" build bar — a short pink fill window sweeping across
|
|
8038
|
+
* a dim track — for the given elapsed time. Animated purely from wall-clock elapsed so
|
|
8039
|
+
* it never needs a known phase total.
|
|
8040
|
+
*
|
|
8041
|
+
* @param elapsedMs - Milliseconds since the phase block opened.
|
|
8042
|
+
* @returns The rendered bar row (no trailing newline).
|
|
8043
|
+
* @example
|
|
8044
|
+
* renderBuildBar(300);
|
|
8045
|
+
*/
|
|
8046
|
+
const renderBuildBar = (elapsedMs) => {
|
|
8047
|
+
const length = 28;
|
|
8048
|
+
const window = 6;
|
|
8049
|
+
const head = Math.floor(elapsedMs / 28) % 34;
|
|
8050
|
+
let bar = "";
|
|
8051
|
+
for (let index = 0; index < length; index++) {
|
|
8052
|
+
const lit = index <= head && index > head - window;
|
|
8053
|
+
bar += lit ? palette.pink(g.barFill) : palette.dim(g.barTrack);
|
|
8054
|
+
}
|
|
8055
|
+
return ` ${bar}`;
|
|
8056
|
+
};
|
|
8057
|
+
/**
|
|
8058
|
+
* Repaint the live phase block in place (tree rows + animated build bar): move up over
|
|
8059
|
+
* the prior draw, rewrite each row, then the bar, clearing any stale trailing lines.
|
|
7773
8060
|
*
|
|
7774
8061
|
* @example
|
|
7775
8062
|
* paintPhaseBlock();
|
|
@@ -7777,8 +8064,9 @@ function createPanelRenderer(options = {}) {
|
|
|
7777
8064
|
const paintPhaseBlock = () => {
|
|
7778
8065
|
let frame = cursorUp(phaseDrawn);
|
|
7779
8066
|
for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
|
|
8067
|
+
frame += `${CLEAR_LINE}${renderBuildBar(now() - blockStartedAt)}\n`;
|
|
7780
8068
|
writeRaw(frame + CLEAR_BELOW);
|
|
7781
|
-
phaseDrawn = phaseRows.length;
|
|
8069
|
+
phaseDrawn = phaseRows.length + 1;
|
|
7782
8070
|
};
|
|
7783
8071
|
/**
|
|
7784
8072
|
* Repaint the single in-place rebuild line (spinner + label + live elapsed seconds).
|
|
@@ -7787,20 +8075,31 @@ function createPanelRenderer(options = {}) {
|
|
|
7787
8075
|
* paintRebuildLine();
|
|
7788
8076
|
*/
|
|
7789
8077
|
const paintRebuildLine = () => {
|
|
7790
|
-
const
|
|
7791
|
-
const
|
|
7792
|
-
writeRaw(`\r${CLEAR_LINE} ${
|
|
8078
|
+
const spinner = palette.cyan(spinnerFrameAt(now() - rebuildStartedAt, SPIN_MS));
|
|
8079
|
+
const elapsed = palette.dim(`· ${((now() - rebuildStartedAt) / 1e3).toFixed(1)}s`);
|
|
8080
|
+
writeRaw(`\r${CLEAR_LINE} ${spinner} rebuilding ${rebuildLabel} ${elapsed}`);
|
|
8081
|
+
};
|
|
8082
|
+
/**
|
|
8083
|
+
* Repaint the persistent in-place `◍ live` idle pulse beneath the serve panel — the
|
|
8084
|
+
* dot breathes (pink → dim) on a calm ~0.6s cycle so a quiet dev session always reads
|
|
8085
|
+
* as alive without strobing.
|
|
8086
|
+
*
|
|
8087
|
+
* @example
|
|
8088
|
+
* paintIdleLine();
|
|
8089
|
+
*/
|
|
8090
|
+
const paintIdleLine = () => {
|
|
8091
|
+
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…")}`);
|
|
7793
8092
|
};
|
|
7794
8093
|
/**
|
|
7795
|
-
* Advance
|
|
8094
|
+
* Advance whichever live region is active by one frame (driven by the shared ticker).
|
|
7796
8095
|
*
|
|
7797
8096
|
* @example
|
|
7798
8097
|
* onTick();
|
|
7799
8098
|
*/
|
|
7800
8099
|
const onTick = () => {
|
|
7801
|
-
spinnerFrame += 1;
|
|
7802
8100
|
if (rebuilding) paintRebuildLine();
|
|
7803
8101
|
else if (phaseOpen) paintPhaseBlock();
|
|
8102
|
+
else if (idle) paintIdleLine();
|
|
7804
8103
|
};
|
|
7805
8104
|
/**
|
|
7806
8105
|
* Start the animation ticker (TTY only; idempotent; `unref`'d so it never blocks exit).
|
|
@@ -7810,7 +8109,7 @@ function createPanelRenderer(options = {}) {
|
|
|
7810
8109
|
*/
|
|
7811
8110
|
const startTicker = () => {
|
|
7812
8111
|
if (!color || ticker) return;
|
|
7813
|
-
ticker = setInterval(onTick,
|
|
8112
|
+
ticker = setInterval(onTick, TICK_MS);
|
|
7814
8113
|
ticker.unref?.();
|
|
7815
8114
|
};
|
|
7816
8115
|
/**
|
|
@@ -7833,22 +8132,47 @@ function createPanelRenderer(options = {}) {
|
|
|
7833
8132
|
const writeBlock = (lines) => {
|
|
7834
8133
|
for (const line of lines) write(line);
|
|
7835
8134
|
};
|
|
8135
|
+
/**
|
|
8136
|
+
* Resume the serve idle pulse on a fresh bottom line (TTY serve sessions only). A no-op
|
|
8137
|
+
* outside serve so standalone rebuild/error calls in unit tests never leave a ticker
|
|
8138
|
+
* running.
|
|
8139
|
+
*
|
|
8140
|
+
* @example
|
|
8141
|
+
* resumeIdle();
|
|
8142
|
+
*/
|
|
8143
|
+
const resumeIdle = () => {
|
|
8144
|
+
if (!(color && serveMode)) {
|
|
8145
|
+
stopTicker();
|
|
8146
|
+
return;
|
|
8147
|
+
}
|
|
8148
|
+
idle = true;
|
|
8149
|
+
idleStartedAt = now();
|
|
8150
|
+
paintIdleLine();
|
|
8151
|
+
startTicker();
|
|
8152
|
+
};
|
|
7836
8153
|
return {
|
|
7837
8154
|
/**
|
|
7838
|
-
* Render the
|
|
8155
|
+
* Render the `▟▙ moku web` lockup + per-command label, a dim rule, and the runtime
|
|
8156
|
+
* facts line (live Node/Bun versions + platform). Called once per command (one
|
|
8157
|
+
* command = one process), so it never repeats within a run.
|
|
7839
8158
|
*
|
|
7840
|
-
* @param command - The command being run, shown beside the
|
|
8159
|
+
* @param command - The command being run, shown beside the wordmark.
|
|
7841
8160
|
* @example
|
|
7842
8161
|
* render.header("serve");
|
|
7843
8162
|
*/
|
|
7844
8163
|
header(command) {
|
|
7845
|
-
writeBlock(
|
|
8164
|
+
writeBlock([
|
|
8165
|
+
railLine(` ${palette.pink(g.cube)} ${palette.pink(palette.bold("moku web"))} ${palette.dim(COMMAND_LABEL[command])}`, palette.dim(version)),
|
|
8166
|
+
` ${palette.dim(g.rule.repeat(RAIL_WIDTH - 1))}`,
|
|
8167
|
+
` ${palette.dim(runtimeFacts(coreVersion))}`
|
|
8168
|
+
]);
|
|
7846
8169
|
},
|
|
7847
8170
|
/**
|
|
7848
|
-
* Render a per-phase row from a `build:phase` event. On a TTY each phase is ONE
|
|
7849
|
-
* that updates in place (spinning glyph while running → green ✓ + duration
|
|
7850
|
-
*
|
|
7851
|
-
* no-op while a serve()
|
|
8171
|
+
* Render a live per-phase row from a `build:phase` event. On a TTY each phase is ONE
|
|
8172
|
+
* tree row that updates in place (spinning glyph while running → green ✓ + duration
|
|
8173
|
+
* when done) beneath an animated indeterminate build bar; off a TTY one line is
|
|
8174
|
+
* printed per completed phase (no start/done duplication). A no-op while a serve()
|
|
8175
|
+
* rebuild is in flight — those show the compact rebuild line.
|
|
7852
8176
|
*
|
|
7853
8177
|
* @param phase - The `build:phase` payload.
|
|
7854
8178
|
* @example
|
|
@@ -7864,6 +8188,7 @@ function createPanelRenderer(options = {}) {
|
|
|
7864
8188
|
phaseRows = [];
|
|
7865
8189
|
phaseDrawn = 0;
|
|
7866
8190
|
phaseOpen = true;
|
|
8191
|
+
blockStartedAt = now();
|
|
7867
8192
|
}
|
|
7868
8193
|
const done = phase.status === "done";
|
|
7869
8194
|
const existing = phaseRows.find((row) => row.name === phase.phase);
|
|
@@ -7879,7 +8204,9 @@ function createPanelRenderer(options = {}) {
|
|
|
7879
8204
|
startTicker();
|
|
7880
8205
|
},
|
|
7881
8206
|
/**
|
|
7882
|
-
* Render the BUILD summary
|
|
8207
|
+
* Render the BUILD summary line + a one-shot throughput sparkline from a
|
|
8208
|
+
* `build:complete` event, finalizing the live phase tree (dropping its animated bar)
|
|
8209
|
+
* first.
|
|
7883
8210
|
*
|
|
7884
8211
|
* @param summary - The `build:complete` payload.
|
|
7885
8212
|
* @example
|
|
@@ -7887,33 +8214,52 @@ function createPanelRenderer(options = {}) {
|
|
|
7887
8214
|
*/
|
|
7888
8215
|
built(summary) {
|
|
7889
8216
|
if (rebuilding) return;
|
|
8217
|
+
if (color && phaseOpen) {
|
|
8218
|
+
let frame = cursorUp(phaseDrawn);
|
|
8219
|
+
for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
|
|
8220
|
+
writeRaw(frame + CLEAR_BELOW);
|
|
8221
|
+
}
|
|
8222
|
+
const phaseDurations = phaseRows.map((row) => row.durationMs).filter((value) => value !== void 0);
|
|
7890
8223
|
phaseOpen = false;
|
|
7891
8224
|
phaseDrawn = 0;
|
|
7892
8225
|
stopTicker();
|
|
7893
8226
|
const pages = palette.bold(String(summary.pageCount));
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
8227
|
+
const dot = palette.dim("·");
|
|
8228
|
+
const lines = [railLine(`${palette.green("✓")} ${palette.bold("BUILD")} ${dot} ${pages} pages`, `${summary.durationMs}ms ${dot} ${summary.outDir}/`, BOX_INNER)];
|
|
8229
|
+
if (color && summary.durationMs > 0) {
|
|
8230
|
+
const rate = Math.max(1, Math.round(summary.pageCount / (summary.durationMs / 1e3)));
|
|
8231
|
+
const spark = phaseDurations.length > 0 ? palette.pink(sparkline(phaseDurations)) : "";
|
|
8232
|
+
const rateLabel = palette.dim(`${rate} pages/s`);
|
|
8233
|
+
lines.push(railLine(spark, rateLabel, BOX_INNER));
|
|
8234
|
+
}
|
|
8235
|
+
writeBlock(box(lines, color, BOX_INNER));
|
|
7900
8236
|
},
|
|
7901
8237
|
/**
|
|
7902
|
-
* Render the
|
|
8238
|
+
* Render the server-ready rail (Local / Network URLs + watched dirs) and, on a TTY,
|
|
8239
|
+
* begin the persistent breathing `◍ live` idle pulse beneath it.
|
|
7903
8240
|
*
|
|
7904
8241
|
* @param info - Local/Network URLs and optionally the watched directories.
|
|
7905
8242
|
* @example
|
|
7906
8243
|
* render.serverReady({ local: "http://localhost:4173", network: null });
|
|
7907
8244
|
*/
|
|
7908
8245
|
serverReady(info) {
|
|
7909
|
-
const
|
|
7910
|
-
|
|
7911
|
-
|
|
8246
|
+
const network = info.network ? palette.cyan(info.network) : palette.dim("unavailable");
|
|
8247
|
+
const lines = [`${palette.green("➜")} ${palette.bold("Local")} ${palette.cyan(info.local)}`, `${palette.green("➜")} ${palette.bold("Network")} ${network}`];
|
|
8248
|
+
if (info.watching && info.watching.length > 0) lines.push(`${palette.dim("watching")} ${palette.dim(info.watching.join(", "))}`);
|
|
8249
|
+
writeBlock(box(lines, color, BOX_INNER));
|
|
8250
|
+
if (color) {
|
|
8251
|
+
serveMode = true;
|
|
8252
|
+
idle = true;
|
|
8253
|
+
idleStartedAt = now();
|
|
8254
|
+
paintIdleLine();
|
|
8255
|
+
startTicker();
|
|
8256
|
+
}
|
|
7912
8257
|
},
|
|
7913
8258
|
/**
|
|
7914
8259
|
* Begin a serve() rebuild: show ONE compact "rebuilding {label}" line (an animated
|
|
7915
|
-
* spinner with live elapsed on a TTY; a plain "~ {label}" line otherwise)
|
|
7916
|
-
* the verbose phase
|
|
8260
|
+
* spinner with live elapsed on a TTY; a plain "~ {label}" line otherwise), taking over
|
|
8261
|
+
* the idle-pulse line, and mute the verbose phase tree + BUILD summary until
|
|
8262
|
+
* {@link reload}/{@link error} settles it.
|
|
7917
8263
|
*
|
|
7918
8264
|
* @param label - The changed watch target shown in the line.
|
|
7919
8265
|
* @example
|
|
@@ -7921,9 +8267,9 @@ function createPanelRenderer(options = {}) {
|
|
|
7921
8267
|
*/
|
|
7922
8268
|
rebuildStart(label) {
|
|
7923
8269
|
rebuilding = true;
|
|
8270
|
+
idle = false;
|
|
7924
8271
|
rebuildLabel = label;
|
|
7925
8272
|
rebuildStartedAt = now();
|
|
7926
|
-
spinnerFrame = 0;
|
|
7927
8273
|
if (!color) {
|
|
7928
8274
|
write(` ${palette.yellow("~")} ${label}`);
|
|
7929
8275
|
return;
|
|
@@ -7933,9 +8279,9 @@ function createPanelRenderer(options = {}) {
|
|
|
7933
8279
|
},
|
|
7934
8280
|
/**
|
|
7935
8281
|
* Settle the current rebuild: replace the in-place "rebuilding…" line with a compact
|
|
7936
|
-
* "✓ rebuilt N pages · Xms · reloaded" (
|
|
7937
|
-
* Called standalone (no preceding {@link rebuildStart}) it also
|
|
7938
|
-
* line so the changed target stays visible.
|
|
8282
|
+
* "✓ rebuilt N pages · Xms · reloaded", then (in a serve session) resume the idle pulse
|
|
8283
|
+
* on a fresh bottom line. Called standalone (no preceding {@link rebuildStart}) it also
|
|
8284
|
+
* prints the "~ file" line so the changed target stays visible.
|
|
7939
8285
|
*
|
|
7940
8286
|
* @param info - The changed file plus the rebuild's page count and duration.
|
|
7941
8287
|
* @example
|
|
@@ -7944,30 +8290,26 @@ function createPanelRenderer(options = {}) {
|
|
|
7944
8290
|
reload(info) {
|
|
7945
8291
|
const settledRebuild = rebuilding;
|
|
7946
8292
|
rebuilding = false;
|
|
7947
|
-
stopTicker();
|
|
7948
8293
|
const line = ` ${palette.green("✓")} rebuilt ${palette.bold(String(info.pageCount))} pages ${palette.dim(`· ${info.durationMs}ms · reloaded`)}`;
|
|
7949
8294
|
if (settledRebuild && color) {
|
|
7950
8295
|
writeRaw(`\r${CLEAR_LINE}${line}\n`);
|
|
8296
|
+
resumeIdle();
|
|
7951
8297
|
return;
|
|
7952
8298
|
}
|
|
7953
8299
|
if (!settledRebuild) write(` ${palette.yellow("~")} ${info.file}`);
|
|
7954
8300
|
write(line);
|
|
7955
8301
|
},
|
|
7956
8302
|
/**
|
|
7957
|
-
* Render the deploy result
|
|
8303
|
+
* Render the deploy result from a `deploy:complete` event: a `✓ DEPLOYED → url` line
|
|
8304
|
+
* with the URL the hero value, then a dim `branch · id · time` line beneath it.
|
|
7958
8305
|
*
|
|
7959
8306
|
* @param result - The `deploy:complete` payload.
|
|
7960
8307
|
* @example
|
|
7961
8308
|
* render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
|
|
7962
8309
|
*/
|
|
7963
8310
|
deployed(result) {
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
`${palette.dim("url")} ${palette.cyan(result.url)}`,
|
|
7967
|
-
`${palette.dim("branch")} ${result.branch}`,
|
|
7968
|
-
`${palette.dim("id")} ${result.deploymentId}`,
|
|
7969
|
-
`${palette.dim("time")} ${result.durationMs}ms`
|
|
7970
|
-
], color));
|
|
8311
|
+
const meta = palette.dim(`branch ${result.branch} · ${result.deploymentId} · ${result.durationMs}ms`);
|
|
8312
|
+
writeBlock([` ${palette.green("✓")} ${palette.bold("DEPLOYED")} ${palette.dim("→")} ${palette.cyan(result.url)}`, ` ${meta}`]);
|
|
7971
8313
|
},
|
|
7972
8314
|
/**
|
|
7973
8315
|
* Render a neutral informational line.
|
|
@@ -7992,7 +8334,8 @@ function createPanelRenderer(options = {}) {
|
|
|
7992
8334
|
writeError(` ${palette.yellow("⚠")} ${message}`);
|
|
7993
8335
|
},
|
|
7994
8336
|
/**
|
|
7995
|
-
* Render an error line (to stderr), optionally with a cause.
|
|
8337
|
+
* Render an error line (to stderr), optionally with a cause. A failing rebuild settles
|
|
8338
|
+
* its in-place spinner line first; in a serve session the idle pulse then resumes.
|
|
7996
8339
|
*
|
|
7997
8340
|
* @param message - The error summary to print.
|
|
7998
8341
|
* @param cause - Optional underlying error/value to print beneath the summary.
|
|
@@ -8000,16 +8343,18 @@ function createPanelRenderer(options = {}) {
|
|
|
8000
8343
|
* render.error("build failed", err);
|
|
8001
8344
|
*/
|
|
8002
8345
|
error(message, cause) {
|
|
8346
|
+
const wasRebuilding = rebuilding;
|
|
8003
8347
|
if (rebuilding) {
|
|
8004
8348
|
rebuilding = false;
|
|
8005
|
-
stopTicker();
|
|
8006
8349
|
if (color) writeRaw(`\r${CLEAR_LINE}`);
|
|
8007
8350
|
}
|
|
8008
8351
|
writeError(` ${palette.red("✗")} ${message}`);
|
|
8009
8352
|
if (cause !== void 0) writeError(String(cause));
|
|
8353
|
+
if (wasRebuilding) resumeIdle();
|
|
8354
|
+
else stopTicker();
|
|
8010
8355
|
},
|
|
8011
8356
|
/**
|
|
8012
|
-
* Render a section heading (a blank line + a bold
|
|
8357
|
+
* Render a section heading (a blank line + a bold pink label) for a multi-step flow.
|
|
8013
8358
|
*
|
|
8014
8359
|
* @param text - The heading label.
|
|
8015
8360
|
* @example
|
|
@@ -8017,7 +8362,7 @@ function createPanelRenderer(options = {}) {
|
|
|
8017
8362
|
*/
|
|
8018
8363
|
heading(text) {
|
|
8019
8364
|
write("");
|
|
8020
|
-
write(` ${palette.bold(palette.
|
|
8365
|
+
write(` ${palette.bold(palette.pink(text))}`);
|
|
8021
8366
|
},
|
|
8022
8367
|
/**
|
|
8023
8368
|
* Render a diagnostic line: green `✓` (pass) or red `✗` (fail) + label, with optional
|
|
@@ -8032,6 +8377,19 @@ function createPanelRenderer(options = {}) {
|
|
|
8032
8377
|
check(ok, label, detail) {
|
|
8033
8378
|
write(` ${ok ? palette.green("✓") : palette.red("✗")} ${label}`);
|
|
8034
8379
|
if (detail !== void 0) for (const line of detail.split("\n")) write(` ${palette.dim(line)}`);
|
|
8380
|
+
},
|
|
8381
|
+
/**
|
|
8382
|
+
* Stop every animation and release the interval timer (serve()'s teardown calls this).
|
|
8383
|
+
*
|
|
8384
|
+
* @example
|
|
8385
|
+
* render.dispose();
|
|
8386
|
+
*/
|
|
8387
|
+
dispose() {
|
|
8388
|
+
stopTicker();
|
|
8389
|
+
idle = false;
|
|
8390
|
+
rebuilding = false;
|
|
8391
|
+
phaseOpen = false;
|
|
8392
|
+
serveMode = false;
|
|
8035
8393
|
}
|
|
8036
8394
|
};
|
|
8037
8395
|
}
|
|
@@ -8188,6 +8546,101 @@ function defaultFileMtime(filePath) {
|
|
|
8188
8546
|
function defaultNetworkUrl(port) {
|
|
8189
8547
|
return networkUrl(port);
|
|
8190
8548
|
}
|
|
8549
|
+
/** Memoized banner facts — resolution touches the filesystem + git once, then caches. */
|
|
8550
|
+
let cachedBanner;
|
|
8551
|
+
/**
|
|
8552
|
+
* Run a read-only `git` command in `dir`, returning its trimmed stdout (`undefined` on
|
|
8553
|
+
* any failure — not a checkout, git missing, etc.). A thin wrapper so the version
|
|
8554
|
+
* resolver can issue a couple of git queries without repeating the spawn boilerplate.
|
|
8555
|
+
*
|
|
8556
|
+
* @param dir - The working directory to run git in.
|
|
8557
|
+
* @param args - The git arguments (no user input is ever interpolated).
|
|
8558
|
+
* @returns The trimmed command output, or `undefined` on failure.
|
|
8559
|
+
* @example
|
|
8560
|
+
* git("/Users/me/moku/web", ["tag", "--list", "v*"]);
|
|
8561
|
+
*/
|
|
8562
|
+
function git(dir, args) {
|
|
8563
|
+
try {
|
|
8564
|
+
return execFileSync("git", args, {
|
|
8565
|
+
cwd: dir,
|
|
8566
|
+
encoding: "utf8",
|
|
8567
|
+
stdio: [
|
|
8568
|
+
"ignore",
|
|
8569
|
+
"pipe",
|
|
8570
|
+
"ignore"
|
|
8571
|
+
]
|
|
8572
|
+
}).trim();
|
|
8573
|
+
} catch {
|
|
8574
|
+
return;
|
|
8575
|
+
}
|
|
8576
|
+
}
|
|
8577
|
+
/**
|
|
8578
|
+
* The framework's source/dev version, derived the SAME way the publish workflow computes
|
|
8579
|
+
* a release: the highest semver `v*` tag is the source of truth (`@moku-labs/web` is
|
|
8580
|
+
* released tag-only — the working-tree `package.json` deliberately carries no `version`).
|
|
8581
|
+
* A `-dev` suffix marks it as a local build off that release line (e.g. `v1.1.0-dev`), so
|
|
8582
|
+
* it never masquerades as the published release. Falls back to the short commit (then
|
|
8583
|
+
* `undefined`) only when no tags exist. `undefined` when `dir` is not a git checkout (a
|
|
8584
|
+
* published npm install — which carries its real `version` instead).
|
|
8585
|
+
*
|
|
8586
|
+
* @param dir - A directory inside the framework's own repository (the realpath of the
|
|
8587
|
+
* package root, so a symlinked local checkout reports the framework's tag — not the
|
|
8588
|
+
* consumer's).
|
|
8589
|
+
* @returns The dev version (e.g. `v1.1.0-dev`), or `undefined`.
|
|
8590
|
+
* @example
|
|
8591
|
+
* devVersion("/Users/me/moku/web"); // "v1.1.0-dev"
|
|
8592
|
+
*/
|
|
8593
|
+
function devVersion(dir) {
|
|
8594
|
+
const latestTag = git(dir, [
|
|
8595
|
+
"tag",
|
|
8596
|
+
"--list",
|
|
8597
|
+
"v*",
|
|
8598
|
+
"--sort=-v:refname"
|
|
8599
|
+
])?.split("\n")[0]?.trim();
|
|
8600
|
+
if (latestTag) return `${latestTag}-dev`;
|
|
8601
|
+
const sha = git(dir, [
|
|
8602
|
+
"rev-parse",
|
|
8603
|
+
"--short",
|
|
8604
|
+
"HEAD"
|
|
8605
|
+
]);
|
|
8606
|
+
return sha ? `${sha}-dev` : void 0;
|
|
8607
|
+
}
|
|
8608
|
+
/**
|
|
8609
|
+
* Resolve the real version/runtime facts shown in the Panel banner (memoized). Reads the
|
|
8610
|
+
* `package.json` shipped beside the built bundle (`dist/../package.json`): a PUBLISHED
|
|
8611
|
+
* release carries a `version` and reports `v{version}`; a source/dev build (no `version`
|
|
8612
|
+
* field — `@moku-labs/web` is released tag-only) reports the latest semver tag + `-dev`
|
|
8613
|
+
* (e.g. `v1.1.0-dev`, the same tag the publish workflow treats as the version source), or
|
|
8614
|
+
* `"dev"` when git is unavailable. The pinned `@moku-labs/core` version comes from the
|
|
8615
|
+
* same file's `dependencies`.
|
|
8616
|
+
*
|
|
8617
|
+
* @returns The resolved {@link BannerFacts}.
|
|
8618
|
+
* @example
|
|
8619
|
+
* resolveBanner(); // { version: "v1.1.0-dev", coreVersion: "0.1.0-alpha.6" }
|
|
8620
|
+
*/
|
|
8621
|
+
function resolveBanner() {
|
|
8622
|
+
if (cachedBanner) return cachedBanner;
|
|
8623
|
+
let pkg = {};
|
|
8624
|
+
let pkgDir;
|
|
8625
|
+
try {
|
|
8626
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
8627
|
+
pkgDir = realpathSync(path.dirname(fileURLToPath(pkgUrl)));
|
|
8628
|
+
pkg = JSON.parse(readFileSync(pkgUrl, "utf8"));
|
|
8629
|
+
} catch {}
|
|
8630
|
+
const coreVersion = (pkg.dependencies?.["@moku-labs/core"] ?? "").replace(/^\D*/, "") || "unknown";
|
|
8631
|
+
const released = pkg.version;
|
|
8632
|
+
let version = "dev";
|
|
8633
|
+
if (released) version = `v${released}`;
|
|
8634
|
+
else {
|
|
8635
|
+
const dev = devVersion(pkgDir ?? process.cwd());
|
|
8636
|
+
if (dev) version = dev;
|
|
8637
|
+
}
|
|
8638
|
+
cachedBanner = {
|
|
8639
|
+
version,
|
|
8640
|
+
coreVersion
|
|
8641
|
+
};
|
|
8642
|
+
return cachedBanner;
|
|
8643
|
+
}
|
|
8191
8644
|
/**
|
|
8192
8645
|
* Create the initial cli plugin state with the production seams wired. Every field is
|
|
8193
8646
|
* an injectable seam (`render`, `confirm`, `clock`, `watch`, the server factories,
|
|
@@ -8201,8 +8654,12 @@ function defaultNetworkUrl(port) {
|
|
|
8201
8654
|
* const state = createState({ global: {}, config });
|
|
8202
8655
|
*/
|
|
8203
8656
|
function createState$1(_ctx) {
|
|
8657
|
+
const banner = resolveBanner();
|
|
8204
8658
|
return {
|
|
8205
|
-
render: createPanelRenderer(
|
|
8659
|
+
render: createPanelRenderer({
|
|
8660
|
+
version: banner.version,
|
|
8661
|
+
coreVersion: banner.coreVersion
|
|
8662
|
+
}),
|
|
8206
8663
|
confirm: defaultConfirm,
|
|
8207
8664
|
select: defaultSelect,
|
|
8208
8665
|
clock: Date.now,
|