@moku-labs/web 1.12.3 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.mts +13 -351
- package/dist/browser.mjs +9 -838
- package/dist/index.cjs +73 -1338
- package/dist/index.d.cts +12 -391
- package/dist/index.d.mts +12 -393
- package/dist/index.mjs +17 -1300
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_convention = require("./convention-BpDfzX7e.cjs");
|
|
3
|
+
let _moku_labs_common = require("@moku-labs/common");
|
|
3
4
|
let _moku_labs_core = require("@moku-labs/core");
|
|
4
5
|
let node_fs = require("node:fs");
|
|
5
6
|
let node_crypto = require("node:crypto");
|
|
@@ -14,6 +15,7 @@ let preact_render_to_string = require("preact-render-to-string");
|
|
|
14
15
|
let node_child_process = require("node:child_process");
|
|
15
16
|
let node_readline = require("node:readline");
|
|
16
17
|
let node_url = require("node:url");
|
|
18
|
+
let _moku_labs_common_cli = require("@moku-labs/common/cli");
|
|
17
19
|
let gray_matter = require("gray-matter");
|
|
18
20
|
gray_matter = require_convention.__toESM(gray_matter, 1);
|
|
19
21
|
let _shikijs_rehype = require("@shikijs/rehype");
|
|
@@ -41,831 +43,6 @@ let hast_util_sanitize = require("hast-util-sanitize");
|
|
|
41
43
|
let reading_time = require("reading-time");
|
|
42
44
|
reading_time = require_convention.__toESM(reading_time, 1);
|
|
43
45
|
let node_process = require("node:process");
|
|
44
|
-
//#region src/plugins/env/api.ts
|
|
45
|
-
/** Error prefix for all env API failures. */
|
|
46
|
-
const ERROR_PREFIX$16 = "[web]";
|
|
47
|
-
/**
|
|
48
|
-
* Creates the env plugin API surface mounted at `ctx.env`. Closes over
|
|
49
|
-
* `ctx.state` ({@link EnvState}) and reads the frozen `resolved` / `publicMap`
|
|
50
|
-
* maps; closures never return a raw `ctx.state` reference.
|
|
51
|
-
*
|
|
52
|
-
* @param ctx - Core plugin context carrying the frozen env state.
|
|
53
|
-
* @param ctx.state - The resolved + public {@link EnvState} maps.
|
|
54
|
-
* @returns The {@link EnvApi} accessor surface mounted at `ctx.env`.
|
|
55
|
-
* @example
|
|
56
|
-
* ```ts
|
|
57
|
-
* const api = createEnvApi(ctx);
|
|
58
|
-
* api.get("PUBLIC_API_URL");
|
|
59
|
-
* ```
|
|
60
|
-
*/
|
|
61
|
-
function createEnvApi(ctx) {
|
|
62
|
-
const { resolved, publicMap } = ctx.state;
|
|
63
|
-
return {
|
|
64
|
-
/**
|
|
65
|
-
* Reads a resolved variable.
|
|
66
|
-
*
|
|
67
|
-
* @param key - Variable name.
|
|
68
|
-
* @returns The value, or `undefined` if not present.
|
|
69
|
-
* @example
|
|
70
|
-
* ```ts
|
|
71
|
-
* api.get("PUBLIC_API_URL");
|
|
72
|
-
* ```
|
|
73
|
-
*/
|
|
74
|
-
get(key) {
|
|
75
|
-
return resolved.get(key);
|
|
76
|
-
},
|
|
77
|
-
/**
|
|
78
|
-
* Reads a variable that must exist.
|
|
79
|
-
*
|
|
80
|
-
* @param key - Variable name.
|
|
81
|
-
* @returns The value.
|
|
82
|
-
* @throws {Error} If the variable is undefined.
|
|
83
|
-
* @example
|
|
84
|
-
* ```ts
|
|
85
|
-
* api.require("DEPLOY_TOKEN");
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
require(key) {
|
|
89
|
-
const value = resolved.get(key);
|
|
90
|
-
if (value === void 0) throw new Error(`${ERROR_PREFIX$16} env: required variable "${key}" is not defined.`);
|
|
91
|
-
return value;
|
|
92
|
-
},
|
|
93
|
-
/**
|
|
94
|
-
* Tests presence of a resolved variable.
|
|
95
|
-
*
|
|
96
|
-
* @param key - Variable name.
|
|
97
|
-
* @returns `true` if a value is present.
|
|
98
|
-
* @example
|
|
99
|
-
* ```ts
|
|
100
|
-
* api.has("PUBLIC_API_URL");
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
has(key) {
|
|
104
|
-
return resolved.has(key);
|
|
105
|
-
},
|
|
106
|
-
/**
|
|
107
|
-
* Returns all public variables as a frozen plain object — a fresh copy,
|
|
108
|
-
* never the raw state map.
|
|
109
|
-
*
|
|
110
|
-
* @returns A frozen `Record` of public variable names to values.
|
|
111
|
-
* @example
|
|
112
|
-
* ```ts
|
|
113
|
-
* const payload = { ...api.getPublic() };
|
|
114
|
-
* ```
|
|
115
|
-
*/
|
|
116
|
-
getPublic() {
|
|
117
|
-
return Object.freeze(Object.fromEntries(publicMap));
|
|
118
|
-
},
|
|
119
|
-
/**
|
|
120
|
-
* Returns the already-frozen map of public variables.
|
|
121
|
-
*
|
|
122
|
-
* @returns The frozen public map.
|
|
123
|
-
* @example
|
|
124
|
-
* ```ts
|
|
125
|
-
* [...api.getPublicMap()];
|
|
126
|
-
* ```
|
|
127
|
-
*/
|
|
128
|
-
getPublicMap() {
|
|
129
|
-
return publicMap;
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
//#endregion
|
|
134
|
-
//#region src/plugins/env/state.ts
|
|
135
|
-
/**
|
|
136
|
-
* Creates initial env plugin state: two empty, mutable maps that are populated
|
|
137
|
-
* and frozen by `validateSchema` (the `onInit`) at `createApp` time.
|
|
138
|
-
*
|
|
139
|
-
* @returns A fresh `EnvState` with empty `resolved` and `publicMap` maps.
|
|
140
|
-
* @example
|
|
141
|
-
* ```ts
|
|
142
|
-
* const state = createEnvState();
|
|
143
|
-
* state.resolved.size; // 0
|
|
144
|
-
* ```
|
|
145
|
-
*/
|
|
146
|
-
function createEnvState() {
|
|
147
|
-
return {
|
|
148
|
-
resolved: /* @__PURE__ */ new Map(),
|
|
149
|
-
publicMap: /* @__PURE__ */ new Map()
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
//#endregion
|
|
153
|
-
//#region src/plugins/env/validate.ts
|
|
154
|
-
/** Error message thrown by every frozen-map mutator. */
|
|
155
|
-
const FROZEN_MESSAGE = "env: map is frozen and cannot be mutated";
|
|
156
|
-
/** Error prefix for all resolution-pipeline failures. */
|
|
157
|
-
const ERROR_PREFIX$15 = "[web]";
|
|
158
|
-
/** The `Map` mutators redefined as throwers when a map is frozen. */
|
|
159
|
-
const FROZEN_METHODS = [
|
|
160
|
-
"set",
|
|
161
|
-
"clear",
|
|
162
|
-
"delete"
|
|
163
|
-
];
|
|
164
|
-
/**
|
|
165
|
-
* Throws the canonical frozen-map error; installed as a map's `set`/`clear`/`delete`.
|
|
166
|
-
*
|
|
167
|
-
* @throws {TypeError} Always, signalling the map is frozen.
|
|
168
|
-
* @example
|
|
169
|
-
* ```ts
|
|
170
|
-
* frozenThrower(); // throws TypeError
|
|
171
|
-
* ```
|
|
172
|
-
*/
|
|
173
|
-
function frozenThrower() {
|
|
174
|
-
throw new TypeError(FROZEN_MESSAGE);
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Coerces a raw provider value to its effective presence: an empty string counts
|
|
178
|
-
* as "absent" so a `KEY=""` falls through to later providers.
|
|
179
|
-
*
|
|
180
|
-
* @param raw - The raw value a provider supplied for a key (possibly `undefined`).
|
|
181
|
-
* @returns The value, or `undefined` when it is missing or an empty string.
|
|
182
|
-
* @example
|
|
183
|
-
* ```ts
|
|
184
|
-
* coerceEmpty(""); // => undefined
|
|
185
|
-
* coerceEmpty("3000"); // => "3000"
|
|
186
|
-
* ```
|
|
187
|
-
*/
|
|
188
|
-
function coerceEmpty(raw) {
|
|
189
|
-
return raw === "" ? void 0 : raw;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Merges providers in array order, coercing empty strings to `undefined` before
|
|
193
|
-
* precedence so a `KEY=""` falls through to later providers. First non-empty
|
|
194
|
-
* value wins.
|
|
195
|
-
*
|
|
196
|
-
* @param config - The resolved env config carrying the ordered providers.
|
|
197
|
-
* @returns A flat record of the first defined value found per key.
|
|
198
|
-
* @example
|
|
199
|
-
* ```ts
|
|
200
|
-
* mergeProviders({ providers: [a, b], schema: {}, publicPrefix: "PUBLIC_" });
|
|
201
|
-
* ```
|
|
202
|
-
*/
|
|
203
|
-
function mergeProviders$1(config) {
|
|
204
|
-
const merged = {};
|
|
205
|
-
for (const provider of config.providers) for (const [key, raw] of Object.entries(provider.load())) {
|
|
206
|
-
const value = coerceEmpty(raw);
|
|
207
|
-
if (value !== void 0 && merged[key] === void 0) merged[key] = value;
|
|
208
|
-
}
|
|
209
|
-
return merged;
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Bidirectionally enforces the `PUBLIC_` naming convention against each schema
|
|
213
|
-
* entry's `public` flag. Throws on either violation direction.
|
|
214
|
-
*
|
|
215
|
-
* @param config - The resolved env config carrying `schema` + `publicPrefix`.
|
|
216
|
-
* @throws {Error} If a public var lacks the prefix, or a prefixed var is not public.
|
|
217
|
-
* @example
|
|
218
|
-
* ```ts
|
|
219
|
-
* crossCheckPublicPrefix(config); // throws if PUBLIC_X is not public:true
|
|
220
|
-
* ```
|
|
221
|
-
*/
|
|
222
|
-
function crossCheckPublicPrefix(config) {
|
|
223
|
-
const { schema, publicPrefix } = config;
|
|
224
|
-
for (const [key, spec] of Object.entries(schema)) {
|
|
225
|
-
const hasPrefix = key.startsWith(publicPrefix);
|
|
226
|
-
if (spec.public === true && !hasPrefix) throw new Error(`${ERROR_PREFIX$15} env: "${key}" is marked public but does not start with "${publicPrefix}".`);
|
|
227
|
-
if (hasPrefix && spec.public !== true) throw new Error(`${ERROR_PREFIX$15} env: "${key}" starts with "${publicPrefix}" but is not marked public:true.`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Seals a map so `set`, `clear`, and `delete` throw, then `Object.freeze`s it
|
|
232
|
-
* for defense in depth. Closes the `Object.freeze`-on-`Map` mutability hole by
|
|
233
|
-
* redefining the mutators as non-writable, non-configurable throwers.
|
|
234
|
-
*
|
|
235
|
-
* @param map - The map to freeze in place.
|
|
236
|
-
* @example
|
|
237
|
-
* ```ts
|
|
238
|
-
* freezeMap(state.resolved); // resolved.set(...) now throws
|
|
239
|
-
* ```
|
|
240
|
-
*/
|
|
241
|
-
function freezeMap(map) {
|
|
242
|
-
for (const method of FROZEN_METHODS) Object.defineProperty(map, method, {
|
|
243
|
-
value: frozenThrower,
|
|
244
|
-
writable: false,
|
|
245
|
-
configurable: false,
|
|
246
|
-
enumerable: false
|
|
247
|
-
});
|
|
248
|
-
Object.freeze(map);
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Populates `state.publicMap` with the schema-driven public subset: every
|
|
252
|
-
* `public:true` schema key that resolved to a defined value. This map is the only
|
|
253
|
-
* sanctioned input to a browser-facing `define`, so it stays schema-scoped (never
|
|
254
|
-
* includes non-schema provider keys).
|
|
255
|
-
*
|
|
256
|
-
* @param schema - The per-variable schema from {@link EnvConfig}.
|
|
257
|
-
* @param merged - The merged provider values keyed by variable name.
|
|
258
|
-
* @param publicMap - The mutable public map to fill in place.
|
|
259
|
-
* @example
|
|
260
|
-
* ```ts
|
|
261
|
-
* populatePublicMap(config.schema, merged, state.publicMap);
|
|
262
|
-
* ```
|
|
263
|
-
*/
|
|
264
|
-
function populatePublicMap(schema, merged, publicMap) {
|
|
265
|
-
for (const [key, spec] of Object.entries(schema)) {
|
|
266
|
-
const value = merged[key];
|
|
267
|
-
if (spec.public === true && value !== void 0) publicMap.set(key, value);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Populates `state.resolved` with EVERY merged key that carries a defined value
|
|
272
|
-
* (spec/02 Lifecycle §5), including non-schema provider keys so
|
|
273
|
-
* `ctx.env.require()` works for dynamic keys.
|
|
274
|
-
*
|
|
275
|
-
* @param merged - The merged provider values keyed by variable name.
|
|
276
|
-
* @param resolved - The mutable resolved map to fill in place.
|
|
277
|
-
* @example
|
|
278
|
-
* ```ts
|
|
279
|
-
* populateResolved(merged, state.resolved);
|
|
280
|
-
* ```
|
|
281
|
-
*/
|
|
282
|
-
function populateResolved(merged, resolved) {
|
|
283
|
-
for (const [key, value] of Object.entries(merged)) resolved.set(key, value);
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Resolves, validates, and freezes the environment table at `onInit`.
|
|
287
|
-
*
|
|
288
|
-
* Pipeline order: merge providers (with empty-string → undefined coercion) →
|
|
289
|
-
* `PUBLIC_` bidirectional cross-check → apply defaults → assert required →
|
|
290
|
-
* populate `state.resolved` / `state.publicMap` → freeze both via
|
|
291
|
-
* {@link freezeMap}. Fail-fast: any violation throws at `createApp` time.
|
|
292
|
-
*
|
|
293
|
-
* @param ctx - Core plugin context (`{ config, state }`).
|
|
294
|
-
* @param ctx.config - The resolved {@link EnvConfig}.
|
|
295
|
-
* @param ctx.state - The mutable {@link EnvState} to populate and freeze.
|
|
296
|
-
* @throws {Error} On a `PUBLIC_` cross-check violation or a missing required variable.
|
|
297
|
-
* @example
|
|
298
|
-
* ```ts
|
|
299
|
-
* validateSchema(ctx); // throws on missing required / PUBLIC_ violation
|
|
300
|
-
* ```
|
|
301
|
-
*/
|
|
302
|
-
function validateSchema(ctx) {
|
|
303
|
-
const { config, state } = ctx;
|
|
304
|
-
const { schema } = config;
|
|
305
|
-
const merged = mergeProviders$1(config);
|
|
306
|
-
crossCheckPublicPrefix(config);
|
|
307
|
-
for (const [key, spec] of Object.entries(schema)) {
|
|
308
|
-
if (merged[key] === void 0 && spec.default !== void 0) merged[key] = spec.default;
|
|
309
|
-
if (merged[key] === void 0 && spec.required === true) throw new Error(`${ERROR_PREFIX$15} env: required variable "${key}" is not defined by any provider or default.`);
|
|
310
|
-
}
|
|
311
|
-
populatePublicMap(schema, merged, state.publicMap);
|
|
312
|
-
populateResolved(merged, state.resolved);
|
|
313
|
-
freezeMap(state.resolved);
|
|
314
|
-
freezeMap(state.publicMap);
|
|
315
|
-
}
|
|
316
|
-
//#endregion
|
|
317
|
-
//#region src/plugins/env/providers.browser.ts
|
|
318
|
-
/** Default `globalThis` property holding a runtime-injected public-env snapshot. */
|
|
319
|
-
const DEFAULT_GLOBAL_KEY = "__ENV__";
|
|
320
|
-
/**
|
|
321
|
-
* A browser-safe {@link EnvProvider} that reads `import.meta.env` and an optional
|
|
322
|
-
* `globalThis[globalKey]` snapshot, merging them with the runtime global winning.
|
|
323
|
-
* Contains zero `node:*` imports, so it is safe to include in the client bundle.
|
|
324
|
-
* Never throws on missing sources — each absent source resolves to `{}`.
|
|
325
|
-
*
|
|
326
|
-
* @param options - Optional settings.
|
|
327
|
-
* @param options.globalKey - `globalThis` key to read a public-env snapshot from. Defaults to `"__ENV__"`.
|
|
328
|
-
* @returns An {@link EnvProvider} named `browser-env`.
|
|
329
|
-
* @example
|
|
330
|
-
* ```ts
|
|
331
|
-
* const provider = browserEnv();
|
|
332
|
-
* provider.load(); // { PUBLIC_API_URL: "/api", ... }
|
|
333
|
-
* ```
|
|
334
|
-
*/
|
|
335
|
-
function browserEnv(options) {
|
|
336
|
-
const globalKey = options?.globalKey ?? DEFAULT_GLOBAL_KEY;
|
|
337
|
-
return {
|
|
338
|
-
name: "browser-env",
|
|
339
|
-
/**
|
|
340
|
-
* Merges `import.meta.env` with `globalThis[globalKey]`, the runtime global
|
|
341
|
-
* winning. Each absent source resolves to `{}`; never throws.
|
|
342
|
-
*
|
|
343
|
-
* @returns The merged environment record.
|
|
344
|
-
* @example
|
|
345
|
-
* ```ts
|
|
346
|
-
* browserEnv().load();
|
|
347
|
-
* ```
|
|
348
|
-
*/
|
|
349
|
-
load() {
|
|
350
|
-
const importEnv = {}.env ?? {};
|
|
351
|
-
const globalObject = globalThis[globalKey] ?? {};
|
|
352
|
-
return {
|
|
353
|
-
...importEnv,
|
|
354
|
-
...globalObject
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Core plugin that resolves, validates, and freezes the environment at `onInit`,
|
|
361
|
-
* exposing a read-only accessor at `ctx.env`. No `onStart`/`onStop` — holds no resource.
|
|
362
|
-
*
|
|
363
|
-
* @example
|
|
364
|
-
* ```ts
|
|
365
|
-
* createApp({ pluginConfigs: { env: { schema: { PUBLIC_API_URL: { public: true } } } } });
|
|
366
|
-
* ```
|
|
367
|
-
*/
|
|
368
|
-
const envPlugin = (0, _moku_labs_core.createCorePlugin)("env", {
|
|
369
|
-
config: {
|
|
370
|
-
schema: {},
|
|
371
|
-
providers: [],
|
|
372
|
-
publicPrefix: "PUBLIC_"
|
|
373
|
-
},
|
|
374
|
-
createState: createEnvState,
|
|
375
|
-
api: createEnvApi,
|
|
376
|
-
onInit: validateSchema
|
|
377
|
-
});
|
|
378
|
-
//#endregion
|
|
379
|
-
//#region src/plugins/log/expect.ts
|
|
380
|
-
/**
|
|
381
|
-
* Named error thrown by `expect()` assertions when a trace condition fails.
|
|
382
|
-
*
|
|
383
|
-
* @example
|
|
384
|
-
* ```ts
|
|
385
|
-
* throw new LogExpectAssertionError("missing event build:complete");
|
|
386
|
-
* ```
|
|
387
|
-
*/
|
|
388
|
-
var LogExpectAssertionError = class extends Error {
|
|
389
|
-
/**
|
|
390
|
-
* Construct a new assertion error with a descriptive failure message.
|
|
391
|
-
*
|
|
392
|
-
* @param message - Descriptive failure message (event name, partial, index).
|
|
393
|
-
* @example
|
|
394
|
-
* ```ts
|
|
395
|
-
* throw new LogExpectAssertionError("missing event build:complete");
|
|
396
|
-
* ```
|
|
397
|
-
*/
|
|
398
|
-
constructor(message) {
|
|
399
|
-
super(message);
|
|
400
|
-
this.name = "LogExpectAssertionError";
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
/**
|
|
404
|
-
* Tests whether a value is a non-null, non-array plain object.
|
|
405
|
-
*
|
|
406
|
-
* @param value - The value to test.
|
|
407
|
-
* @returns `true` when `value` is a non-null object that is not an array.
|
|
408
|
-
* @example
|
|
409
|
-
* ```ts
|
|
410
|
-
* isPlainObject({ a: 1 }); // true
|
|
411
|
-
* isPlainObject([1]); // false
|
|
412
|
-
* ```
|
|
413
|
-
*/
|
|
414
|
-
function isPlainObject$1(value) {
|
|
415
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Tests whether `actual` is an array that recursively matches every element of
|
|
419
|
-
* the `partial` array (element-wise, with equal length).
|
|
420
|
-
*
|
|
421
|
-
* @param actual - The value to test against (must be an array of equal length).
|
|
422
|
-
* @param partial - The expected partial array shape.
|
|
423
|
-
* @returns `true` when `actual` is an equal-length array matching `partial` element-wise.
|
|
424
|
-
* @example
|
|
425
|
-
* ```ts
|
|
426
|
-
* matchesPartialArray([1, 2], [1, 2]); // true
|
|
427
|
-
* matchesPartialArray([1], [1, 2]); // false (length mismatch)
|
|
428
|
-
* ```
|
|
429
|
-
*/
|
|
430
|
-
function matchesPartialArray(actual, partial) {
|
|
431
|
-
if (!Array.isArray(actual) || actual.length !== partial.length) return false;
|
|
432
|
-
return partial.every((value, index) => matchesPartial(actual[index], value));
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Tests whether `actual` is a plain object in which every `partial` key
|
|
436
|
-
* recursively matches (extra `actual` keys are ignored).
|
|
437
|
-
*
|
|
438
|
-
* @param actual - The value to test against (must be a plain object).
|
|
439
|
-
* @param partial - The expected partial object shape.
|
|
440
|
-
* @returns `true` when every `partial` key exists in `actual` and matches recursively.
|
|
441
|
-
* @example
|
|
442
|
-
* ```ts
|
|
443
|
-
* matchesPartialObject({ a: 1, b: 2 }, { a: 1 }); // true
|
|
444
|
-
* matchesPartialObject({ a: 1 }, { b: 1 }); // false (missing key)
|
|
445
|
-
* ```
|
|
446
|
-
*/
|
|
447
|
-
function matchesPartialObject(actual, partial) {
|
|
448
|
-
if (!isPlainObject$1(actual)) return false;
|
|
449
|
-
return Object.keys(partial).every((key) => key in actual && matchesPartial(actual[key], partial[key]));
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Subset-equality matcher: is `partial` a recursive subset of `actual`?
|
|
453
|
-
*
|
|
454
|
-
* Fast path via `Object.is` (covers identical primitives/references and
|
|
455
|
-
* `null`/`NaN`); primitives compare with `Object.is`; arrays match element-wise
|
|
456
|
-
* with equal length; plain objects require every `partial` key to recursively
|
|
457
|
-
* match (extra `actual` keys ignored).
|
|
458
|
-
*
|
|
459
|
-
* @param actual - The value to test against (typically `entry.data`).
|
|
460
|
-
* @param partial - The expected partial shape.
|
|
461
|
-
* @returns `true` when `partial` is a recursive subset of `actual`.
|
|
462
|
-
* @example
|
|
463
|
-
* ```ts
|
|
464
|
-
* matchesPartial({ a: 1, b: 2 }, { a: 1 }); // true
|
|
465
|
-
* matchesPartial([1, 2], [1]); // false (length mismatch)
|
|
466
|
-
* ```
|
|
467
|
-
*/
|
|
468
|
-
function matchesPartial(actual, partial) {
|
|
469
|
-
if (Object.is(actual, partial)) return true;
|
|
470
|
-
if (Array.isArray(partial)) return matchesPartialArray(actual, partial);
|
|
471
|
-
if (isPlainObject$1(partial)) return matchesPartialObject(actual, partial);
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Tests whether an entry matches `event` and (when provided) `partial`.
|
|
476
|
-
*
|
|
477
|
-
* @param entry - The candidate trace entry.
|
|
478
|
-
* @param event - Required event name.
|
|
479
|
-
* @param partial - Optional partial data shape (subset-matched against `entry.data`).
|
|
480
|
-
* @returns `true` when the entry matches the event and optional partial.
|
|
481
|
-
* @example
|
|
482
|
-
* ```ts
|
|
483
|
-
* entryMatches({ level: "info", event: "a", data: { x: 1 }, ts: 0 }, "a", { x: 1 }); // true
|
|
484
|
-
* ```
|
|
485
|
-
*/
|
|
486
|
-
function entryMatches(entry, event, partial) {
|
|
487
|
-
if (entry.event !== event) return false;
|
|
488
|
-
return partial === void 0 ? true : matchesPartial(entry.data, partial);
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Render a `partial` for an error message, prefixed with a space when present.
|
|
492
|
-
*
|
|
493
|
-
* @param partial - Optional partial data shape.
|
|
494
|
-
* @returns A ` matching <json>` suffix, or an empty string when absent.
|
|
495
|
-
* @example
|
|
496
|
-
* ```ts
|
|
497
|
-
* describePartial({ ok: true }); // ' matching {"ok":true}'
|
|
498
|
-
* ```
|
|
499
|
-
*/
|
|
500
|
-
function describePartial(partial) {
|
|
501
|
-
return partial === void 0 ? "" : ` matching ${JSON.stringify(partial)}`;
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Find the first entry with `event` at or after `startIndex`, scanning forward.
|
|
505
|
-
*
|
|
506
|
-
* @param entries - The trace array to scan.
|
|
507
|
-
* @param event - Event name to find.
|
|
508
|
-
* @param startIndex - Index to begin scanning from (inclusive).
|
|
509
|
-
* @returns The index of the first match, or `-1` when none exists from `startIndex` on.
|
|
510
|
-
* @example
|
|
511
|
-
* ```ts
|
|
512
|
-
* findEventAtOrAfter([{ event: "a" }, { event: "b" }] as LogEntry[], "b", 0); // 1
|
|
513
|
-
* ```
|
|
514
|
-
*/
|
|
515
|
-
function findEventAtOrAfter(entries, event, startIndex) {
|
|
516
|
-
for (let index = startIndex; index < entries.length; index++) if (entries[index]?.event === event) return index;
|
|
517
|
-
return -1;
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Create a fluent assertion chain bound to the live `entries` array. Each method
|
|
521
|
-
* reads `entries` at call time, so assertions reflect later logging.
|
|
522
|
-
*
|
|
523
|
-
* @param entries - The live trace array (read on each assertion call).
|
|
524
|
-
* @returns A fresh {@link ExpectChain} backed by `entries`.
|
|
525
|
-
* @example
|
|
526
|
-
* ```ts
|
|
527
|
-
* createExpectChain(state.entries).toHaveEvent("build:complete");
|
|
528
|
-
* ```
|
|
529
|
-
*/
|
|
530
|
-
function createExpectChain(entries) {
|
|
531
|
-
const chain = {
|
|
532
|
-
/**
|
|
533
|
-
* Assert at least one entry has `event`, optionally matching `partial`.
|
|
534
|
-
*
|
|
535
|
-
* @param event - Event name to find.
|
|
536
|
-
* @param partial - Optional partial data shape (subset-matched).
|
|
537
|
-
* @returns The same chain for chaining.
|
|
538
|
-
* @throws {LogExpectAssertionError} When no matching entry exists.
|
|
539
|
-
* @example
|
|
540
|
-
* ```ts
|
|
541
|
-
* chain.toHaveEvent("build:phase", { status: "start" });
|
|
542
|
-
* ```
|
|
543
|
-
*/
|
|
544
|
-
toHaveEvent(event, partial) {
|
|
545
|
-
if (!entries.some((entry) => entryMatches(entry, event, partial))) throw new LogExpectAssertionError(`Expected trace to contain event "${event}"${describePartial(partial)}, but none was found.`);
|
|
546
|
-
return chain;
|
|
547
|
-
},
|
|
548
|
-
/**
|
|
549
|
-
* Assert all of `events` appear in the trace in the given relative order.
|
|
550
|
-
*
|
|
551
|
-
* @param events - Ordered list of event names (gaps allowed).
|
|
552
|
-
* @returns The same chain for chaining.
|
|
553
|
-
* @throws {LogExpectAssertionError} When the ordering cannot be satisfied.
|
|
554
|
-
* @example
|
|
555
|
-
* ```ts
|
|
556
|
-
* chain.toHaveEventInOrder(["build:phase", "build:complete"]);
|
|
557
|
-
* ```
|
|
558
|
-
*/
|
|
559
|
-
toHaveEventInOrder(events) {
|
|
560
|
-
let cursor = 0;
|
|
561
|
-
for (const [position, event] of events.entries()) {
|
|
562
|
-
const matchIndex = findEventAtOrAfter(entries, event, cursor);
|
|
563
|
-
if (matchIndex === -1) throw new LogExpectAssertionError(`Expected events in order ${JSON.stringify(events)}, but "${event}" (index ${position}) was not found at or after position ${cursor}.`);
|
|
564
|
-
cursor = matchIndex + 1;
|
|
565
|
-
}
|
|
566
|
-
return chain;
|
|
567
|
-
},
|
|
568
|
-
/**
|
|
569
|
-
* Assert NO entry has `event` (optionally narrowed by `partial`).
|
|
570
|
-
*
|
|
571
|
-
* @param event - Event name that must be absent.
|
|
572
|
-
* @param partial - Optional partial data shape; only matching entries violate.
|
|
573
|
-
* @returns The same chain for chaining.
|
|
574
|
-
* @throws {LogExpectAssertionError} When a matching entry exists.
|
|
575
|
-
* @example
|
|
576
|
-
* ```ts
|
|
577
|
-
* chain.toNotHaveEvent("deploy:failed");
|
|
578
|
-
* ```
|
|
579
|
-
*/
|
|
580
|
-
toNotHaveEvent(event, partial) {
|
|
581
|
-
const offending = entries.findIndex((entry) => entryMatches(entry, event, partial));
|
|
582
|
-
if (offending !== -1) throw new LogExpectAssertionError(`Expected trace to NOT contain event "${event}"${describePartial(partial)}, but found one at index ${offending}.`);
|
|
583
|
-
return chain;
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
return chain;
|
|
587
|
-
}
|
|
588
|
-
//#endregion
|
|
589
|
-
//#region src/plugins/log/api.ts
|
|
590
|
-
/**
|
|
591
|
-
* @file log plugin — API factory.
|
|
592
|
-
*
|
|
593
|
-
* Builds the `LogApi` over the plugin's `{ config, state }` core context:
|
|
594
|
-
* the leveled loggers (via a shared `append`), the frozen `trace()` snapshot,
|
|
595
|
-
* the live `expect()` chain, `addSink`, and `reset`.
|
|
596
|
-
*/
|
|
597
|
-
/**
|
|
598
|
-
* Append a new entry to the trace and fan it out to every sink in order.
|
|
599
|
-
*
|
|
600
|
-
* @param state - The mutable log state to append to.
|
|
601
|
-
* @param level - Severity level for the entry.
|
|
602
|
-
* @param event - Event identifier.
|
|
603
|
-
* @param data - Optional structured payload.
|
|
604
|
-
* @example
|
|
605
|
-
* ```ts
|
|
606
|
-
* append(state, "info", "content:ready", { count: 12 });
|
|
607
|
-
* ```
|
|
608
|
-
*/
|
|
609
|
-
function append(state, level, event, data) {
|
|
610
|
-
const entry = {
|
|
611
|
-
level,
|
|
612
|
-
event,
|
|
613
|
-
data,
|
|
614
|
-
ts: Date.now()
|
|
615
|
-
};
|
|
616
|
-
state.entries.push(entry);
|
|
617
|
-
for (const sink of state.sinks) sink.write(entry);
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Tests whether a value is a non-null, non-array plain object.
|
|
621
|
-
*
|
|
622
|
-
* @param value - The value to test.
|
|
623
|
-
* @returns `true` when `value` is a non-null object that is not an array.
|
|
624
|
-
* @example
|
|
625
|
-
* ```ts
|
|
626
|
-
* isPlainObject({ a: 1 }); // true
|
|
627
|
-
* isPlainObject([1]); // false
|
|
628
|
-
* ```
|
|
629
|
-
*/
|
|
630
|
-
function isPlainObject(value) {
|
|
631
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Merge an `Error`'s `message`/`stack` into `data` under an `error` key. The
|
|
635
|
-
* `error` field is always preserved; only a plain object `data` contributes its
|
|
636
|
-
* keys. Non-plain-object `data` (arrays and primitives) is replaced by `{}` —
|
|
637
|
-
* its original value is not retained — so the merge target is always a record.
|
|
638
|
-
*
|
|
639
|
-
* @param data - Original payload (any shape).
|
|
640
|
-
* @param error - The originating error to merge.
|
|
641
|
-
* @returns A new object carrying any plain-object keys plus the `error` field.
|
|
642
|
-
* @example
|
|
643
|
-
* ```ts
|
|
644
|
-
* mergeError({ target: "cf" }, new Error("boom"));
|
|
645
|
-
* // { target: "cf", error: { message: "boom", stack: "..." } }
|
|
646
|
-
* ```
|
|
647
|
-
*/
|
|
648
|
-
function mergeError(data, error) {
|
|
649
|
-
return {
|
|
650
|
-
...isPlainObject(data) ? data : {},
|
|
651
|
-
error: {
|
|
652
|
-
message: error.message,
|
|
653
|
-
stack: error.stack
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Create the log plugin API surface injected as `ctx.log` / `app.log`.
|
|
659
|
-
*
|
|
660
|
-
* @param ctx - Core plugin context (`{ config, state }`).
|
|
661
|
-
* @returns The {@link LogApi} bound to `ctx.state`.
|
|
662
|
-
* @example
|
|
663
|
-
* ```ts
|
|
664
|
-
* const log = createLogApi(ctx);
|
|
665
|
-
* log.info("content:ready", { articleCount: 12 });
|
|
666
|
-
* ```
|
|
667
|
-
*/
|
|
668
|
-
function createLogApi(ctx) {
|
|
669
|
-
const { state } = ctx;
|
|
670
|
-
return {
|
|
671
|
-
/**
|
|
672
|
-
* Append an `info` entry and fan it out to every sink.
|
|
673
|
-
*
|
|
674
|
-
* @param event - Event identifier (convention: `domain:action`).
|
|
675
|
-
* @param data - Optional structured payload.
|
|
676
|
-
* @example
|
|
677
|
-
* ```ts
|
|
678
|
-
* log.info("content:ready", { count: 12 });
|
|
679
|
-
* ```
|
|
680
|
-
*/
|
|
681
|
-
info(event, data) {
|
|
682
|
-
append(state, "info", event, data);
|
|
683
|
-
},
|
|
684
|
-
/**
|
|
685
|
-
* Append a `debug` entry and fan it out to every sink.
|
|
686
|
-
*
|
|
687
|
-
* @param event - Event identifier (convention: `domain:action`).
|
|
688
|
-
* @param data - Optional structured payload.
|
|
689
|
-
* @example
|
|
690
|
-
* ```ts
|
|
691
|
-
* log.debug("router:match", { path: "/blog/" });
|
|
692
|
-
* ```
|
|
693
|
-
*/
|
|
694
|
-
debug(event, data) {
|
|
695
|
-
append(state, "debug", event, data);
|
|
696
|
-
},
|
|
697
|
-
/**
|
|
698
|
-
* Append a `warn` entry and fan it out to every sink.
|
|
699
|
-
*
|
|
700
|
-
* @param event - Event identifier (convention: `domain:action`).
|
|
701
|
-
* @param data - Optional structured payload.
|
|
702
|
-
* @example
|
|
703
|
-
* ```ts
|
|
704
|
-
* log.warn("build:skip", { reason: "no sitemap" });
|
|
705
|
-
* ```
|
|
706
|
-
*/
|
|
707
|
-
warn(event, data) {
|
|
708
|
-
append(state, "warn", event, data);
|
|
709
|
-
},
|
|
710
|
-
/**
|
|
711
|
-
* Append an `error` entry. When `error` is provided, its `message`/`stack`
|
|
712
|
-
* are merged into `data` under an `error` key (existing keys preserved);
|
|
713
|
-
* otherwise `data` is recorded as-is.
|
|
714
|
-
*
|
|
715
|
-
* @param event - Event identifier (convention: `domain:action`).
|
|
716
|
-
* @param data - Optional structured payload.
|
|
717
|
-
* @param error - Optional originating Error to merge into `data`.
|
|
718
|
-
* @example
|
|
719
|
-
* ```ts
|
|
720
|
-
* log.error("deploy:failed", { target: "cf" }, err);
|
|
721
|
-
* ```
|
|
722
|
-
*/
|
|
723
|
-
error(event, data, error) {
|
|
724
|
-
append(state, "error", event, error === void 0 ? data : mergeError(data, error));
|
|
725
|
-
},
|
|
726
|
-
/**
|
|
727
|
-
* Return a frozen snapshot (fresh copy) of the entries recorded so far.
|
|
728
|
-
*
|
|
729
|
-
* @returns A readonly, frozen copy of the recorded entries.
|
|
730
|
-
* @example
|
|
731
|
-
* ```ts
|
|
732
|
-
* const entries = log.trace();
|
|
733
|
-
* ```
|
|
734
|
-
*/
|
|
735
|
-
trace() {
|
|
736
|
-
return Object.freeze([...state.entries]);
|
|
737
|
-
},
|
|
738
|
-
/**
|
|
739
|
-
* Return a fluent assertion chain bound to the live entries array.
|
|
740
|
-
*
|
|
741
|
-
* @returns A fresh {@link ExpectChain} reading `state.entries` live.
|
|
742
|
-
* @example
|
|
743
|
-
* ```ts
|
|
744
|
-
* log.expect().toHaveEvent("build:complete");
|
|
745
|
-
* ```
|
|
746
|
-
*/
|
|
747
|
-
expect() {
|
|
748
|
-
return createExpectChain(state.entries);
|
|
749
|
-
},
|
|
750
|
-
/**
|
|
751
|
-
* Register an additional output sink at runtime.
|
|
752
|
-
*
|
|
753
|
-
* @param sink - The sink to add to the fan-out list.
|
|
754
|
-
* @example
|
|
755
|
-
* ```ts
|
|
756
|
-
* log.addSink({ write: (e) => stream.write(JSON.stringify(e)) });
|
|
757
|
-
* ```
|
|
758
|
-
*/
|
|
759
|
-
addSink(sink) {
|
|
760
|
-
state.sinks.push(sink);
|
|
761
|
-
},
|
|
762
|
-
/**
|
|
763
|
-
* Clear all recorded entries while keeping registered sinks.
|
|
764
|
-
*
|
|
765
|
-
* @example
|
|
766
|
-
* ```ts
|
|
767
|
-
* log.reset();
|
|
768
|
-
* ```
|
|
769
|
-
*/
|
|
770
|
-
reset() {
|
|
771
|
-
state.entries.length = 0;
|
|
772
|
-
}
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
//#endregion
|
|
776
|
-
//#region src/plugins/log/sinks.ts
|
|
777
|
-
/** Severity rank for threshold comparison (higher = more severe). */
|
|
778
|
-
const LEVEL_RANK = {
|
|
779
|
-
debug: 10,
|
|
780
|
-
info: 20,
|
|
781
|
-
warn: 30,
|
|
782
|
-
error: 40
|
|
783
|
-
};
|
|
784
|
-
/**
|
|
785
|
-
* Build the console sink: routes entries by channel — `error` → `console.error`,
|
|
786
|
-
* `warn` → `console.warn`, and `debug`/`info` → `console.log`. The full entry
|
|
787
|
-
* object is forwarded so the console serializes its `event` and `data`. Entries
|
|
788
|
-
* below `minLevel` are dropped (the in-memory trace still records everything).
|
|
789
|
-
*
|
|
790
|
-
* @param minLevel - Lowest severity to print. Defaults to `"debug"` (print all).
|
|
791
|
-
* @returns A {@link LogSink} that writes to the matching `console` channel.
|
|
792
|
-
* @example
|
|
793
|
-
* ```ts
|
|
794
|
-
* state.sinks.push(consoleSink("info")); // suppress debug spam
|
|
795
|
-
* ```
|
|
796
|
-
*/
|
|
797
|
-
function consoleSink(minLevel = "debug") {
|
|
798
|
-
const threshold = LEVEL_RANK[minLevel];
|
|
799
|
-
return {
|
|
800
|
-
/**
|
|
801
|
-
* Route a single entry to the console channel matching its level.
|
|
802
|
-
*
|
|
803
|
-
* @param entry - The entry to emit.
|
|
804
|
-
* @example
|
|
805
|
-
* ```ts
|
|
806
|
-
* sink.write({ level: "warn", event: "build:skip", ts: Date.now() });
|
|
807
|
-
* ```
|
|
808
|
-
*/
|
|
809
|
-
write(entry) {
|
|
810
|
-
if (LEVEL_RANK[entry.level] < threshold) return;
|
|
811
|
-
if (entry.level === "error") console.error(entry);
|
|
812
|
-
else if (entry.level === "warn") console.warn(entry);
|
|
813
|
-
else console.log(entry);
|
|
814
|
-
} };
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Install mode-selected default sinks at onInit. The in-memory trace is always
|
|
818
|
-
* on (`state.entries`); the console sink is added only in dev/production. `dev`
|
|
819
|
-
* prints everything (debug+); `production` prints `info`+ only, so the per-phase
|
|
820
|
-
* `debug` events (build:bundle, build:pages, …) don't spam a prod build. Both
|
|
821
|
-
* modes still record all levels in the in-memory trace.
|
|
822
|
-
*
|
|
823
|
-
* @param ctx - Core plugin context (`{ config, state }`).
|
|
824
|
-
* @param ctx.config - Resolved log config (`{ mode }`).
|
|
825
|
-
* @param ctx.state - Mutable log state (`{ entries, sinks }`).
|
|
826
|
-
* @example
|
|
827
|
-
* ```ts
|
|
828
|
-
* // "dev" -> [consoleSink("debug")]; "production" -> [consoleSink("info")]; "test"/"silent" -> []
|
|
829
|
-
* ```
|
|
830
|
-
*/
|
|
831
|
-
function installDefaultSinks(ctx) {
|
|
832
|
-
if (ctx.config.mode === "dev") ctx.state.sinks.push(consoleSink("debug"));
|
|
833
|
-
else if (ctx.config.mode === "production") ctx.state.sinks.push(consoleSink("info"));
|
|
834
|
-
}
|
|
835
|
-
//#endregion
|
|
836
|
-
//#region src/plugins/log/state.ts
|
|
837
|
-
/**
|
|
838
|
-
* Create fresh log state: an empty append-only trace and an empty sink list.
|
|
839
|
-
* No module-level singletons — guarantees per-`createApp` isolation (two
|
|
840
|
-
* `createApp` calls never share `entries` or `sinks`).
|
|
841
|
-
*
|
|
842
|
-
* @param _ctx - Core plugin context (`{ config }`); unused at construction.
|
|
843
|
-
* @returns A fresh `LogState` with empty `entries` and `sinks` arrays.
|
|
844
|
-
* @example
|
|
845
|
-
* ```ts
|
|
846
|
-
* const state = createLogState({ config: { mode: "test" } }); // { entries: [], sinks: [] }
|
|
847
|
-
* ```
|
|
848
|
-
*/
|
|
849
|
-
function createLogState(_ctx) {
|
|
850
|
-
return {
|
|
851
|
-
entries: [],
|
|
852
|
-
sinks: []
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* Core logging plugin — always-on in-memory trace + `expect()` event-trace DSL.
|
|
857
|
-
* API injected as `ctx.log` on every regular plugin and surfaced as `app.log`.
|
|
858
|
-
* No depends / events / hooks (core plugin per spec/03 §5).
|
|
859
|
-
*
|
|
860
|
-
* @see README.md
|
|
861
|
-
*/
|
|
862
|
-
const logPlugin = (0, _moku_labs_core.createCorePlugin)("log", {
|
|
863
|
-
config: { mode: "production" },
|
|
864
|
-
createState: createLogState,
|
|
865
|
-
api: createLogApi,
|
|
866
|
-
onInit: installDefaultSinks
|
|
867
|
-
});
|
|
868
|
-
//#endregion
|
|
869
46
|
//#region src/config.ts
|
|
870
47
|
/**
|
|
871
48
|
* @file Framework configuration — Config + Events types, core plugin registration.
|
|
@@ -887,7 +64,7 @@ const coreConfig = (0, _moku_labs_core.createCoreConfig)("web", {
|
|
|
887
64
|
stage: "production",
|
|
888
65
|
mode: "hybrid"
|
|
889
66
|
},
|
|
890
|
-
plugins: [logPlugin, envPlugin],
|
|
67
|
+
plugins: [_moku_labs_common.logPlugin, _moku_labs_common.envPlugin],
|
|
891
68
|
pluginConfigs: { log: { mode: "production" } }
|
|
892
69
|
});
|
|
893
70
|
/**
|
|
@@ -8859,312 +8036,17 @@ function networkUrl(port, source = node_os.networkInterfaces) {
|
|
|
8859
8036
|
return ip === null ? null : `http://${ip}:${port}`;
|
|
8860
8037
|
}
|
|
8861
8038
|
//#endregion
|
|
8862
|
-
//#region src/plugins/cli/render/
|
|
8863
|
-
/**
|
|
8864
|
-
* @file cli plugin — TTY/NO_COLOR-aware ANSI color + box-drawing helpers shared by
|
|
8865
|
-
* the Panel renderer. Modeled on the legacy `scripts/_log.ts`: color and box glyphs
|
|
8866
|
-
* are emitted only on a real TTY with `NO_COLOR` unset; otherwise plain ASCII so
|
|
8867
|
-
* CI logs and pipes stay readable.
|
|
8868
|
-
*/
|
|
8869
|
-
/** The ANSI escape byte (ESC, `0x1b`), built so no literal control char is in source. */
|
|
8870
|
-
const ESC = String.fromCodePoint(27);
|
|
8871
|
-
/** ANSI SGR codes used by the Panel renderer (each prefixed with the ESC byte). */
|
|
8872
|
-
const ANSI = {
|
|
8873
|
-
reset: `${ESC}[0m`,
|
|
8874
|
-
bold: `${ESC}[1m`,
|
|
8875
|
-
dim: `${ESC}[2m`,
|
|
8876
|
-
red: `${ESC}[31m`,
|
|
8877
|
-
green: `${ESC}[32m`,
|
|
8878
|
-
yellow: `${ESC}[33m`,
|
|
8879
|
-
blue: `${ESC}[34m`,
|
|
8880
|
-
magenta: `${ESC}[35m`,
|
|
8881
|
-
cyan: `${ESC}[36m`,
|
|
8882
|
-
gray: `${ESC}[90m`
|
|
8883
|
-
};
|
|
8884
|
-
/**
|
|
8885
|
-
* The Moku brand pink (`#FF1E6F`) as an RGB triple, used for 24-bit truecolor output.
|
|
8886
|
-
* Degrades to {@link ANSI.magenta} on a 16-color TTY and to plain text off a TTY.
|
|
8887
|
-
*/
|
|
8888
|
-
const BRAND_PINK = {
|
|
8889
|
-
r: 255,
|
|
8890
|
-
g: 30,
|
|
8891
|
-
b: 111
|
|
8892
|
-
};
|
|
8893
|
-
/**
|
|
8894
|
-
* Build a 24-bit (truecolor) SGR foreground escape for the given RGB triple.
|
|
8895
|
-
*
|
|
8896
|
-
* @param r - Red channel (0–255).
|
|
8897
|
-
* @param g - Green channel (0–255).
|
|
8898
|
-
* @param b - Blue channel (0–255).
|
|
8899
|
-
* @returns The `ESC[38;2;r;g;bm` foreground sequence.
|
|
8900
|
-
* @example
|
|
8901
|
-
* fg24(255, 30, 111); // "\x1b[38;2;255;30;111m"
|
|
8902
|
-
*/
|
|
8903
|
-
function fg24(r, g, b) {
|
|
8904
|
-
return `${ESC}[38;2;${r};${g};${b}m`;
|
|
8905
|
-
}
|
|
8906
|
-
/** ANSI: erase the entire current line, leaving the cursor where it is. */
|
|
8907
|
-
const CLEAR_LINE = `${ESC}[2K`;
|
|
8908
|
-
/** ANSI: erase from the cursor to the end of the screen (drops stale trailing rows). */
|
|
8909
|
-
const CLEAR_BELOW = `${ESC}[0J`;
|
|
8910
|
-
/**
|
|
8911
|
-
* Braille spinner frames for live "working…" indicators on a TTY (advance one per tick).
|
|
8912
|
-
* Off a TTY the Panel never animates, so this is unused in plain/CI output.
|
|
8913
|
-
*/
|
|
8914
|
-
const SPINNER_FRAMES = [
|
|
8915
|
-
"⠋",
|
|
8916
|
-
"⠙",
|
|
8917
|
-
"⠹",
|
|
8918
|
-
"⠸",
|
|
8919
|
-
"⠼",
|
|
8920
|
-
"⠴",
|
|
8921
|
-
"⠦",
|
|
8922
|
-
"⠧",
|
|
8923
|
-
"⠇",
|
|
8924
|
-
"⠏"
|
|
8925
|
-
];
|
|
8926
|
-
/**
|
|
8927
|
-
* The ANSI sequence to move the cursor up `n` lines (empty string for `n <= 0`). The
|
|
8928
|
-
* Panel uses it to repaint a live block in place — move up over the previous draw, then
|
|
8929
|
-
* rewrite each row — so progress updates a fixed region instead of scrolling new lines.
|
|
8930
|
-
*
|
|
8931
|
-
* @param n - Number of lines to move the cursor up.
|
|
8932
|
-
* @returns The cursor-up escape sequence, or `""` when `n <= 0`.
|
|
8933
|
-
* @example
|
|
8934
|
-
* cursorUp(3); // "\x1b[3A"
|
|
8935
|
-
*/
|
|
8936
|
-
function cursorUp(n) {
|
|
8937
|
-
return n > 0 ? `${ESC}[${n}A` : "";
|
|
8938
|
-
}
|
|
8939
|
-
/** Unicode rounded box glyphs used when output is a color-capable TTY. */
|
|
8940
|
-
const UNICODE_BOX = {
|
|
8941
|
-
topLeft: "╭",
|
|
8942
|
-
topRight: "╮",
|
|
8943
|
-
bottomLeft: "╰",
|
|
8944
|
-
bottomRight: "╯",
|
|
8945
|
-
horizontal: "─",
|
|
8946
|
-
vertical: "│"
|
|
8947
|
-
};
|
|
8948
|
-
/** ASCII box glyphs used when output is piped/CI (plain mode). */
|
|
8949
|
-
const ASCII_BOX = {
|
|
8950
|
-
topLeft: "+",
|
|
8951
|
-
topRight: "+",
|
|
8952
|
-
bottomLeft: "+",
|
|
8953
|
-
bottomRight: "+",
|
|
8954
|
-
horizontal: "-",
|
|
8955
|
-
vertical: "|"
|
|
8956
|
-
};
|
|
8957
|
-
/**
|
|
8958
|
-
* Matches every ANSI SGR escape sequence (used to measure visible width). Built from
|
|
8959
|
-
* the {@link ESC} byte so no literal control character appears in the source regex.
|
|
8960
|
-
*/
|
|
8961
|
-
const ANSI_PATTERN = new RegExp(String.raw`${ESC}\[[0-9;]*m`, "g");
|
|
8962
|
-
/**
|
|
8963
|
-
* Whether ANSI color/box glyphs should be emitted: a TTY stream with `NO_COLOR`
|
|
8964
|
-
* unset. Reads `process.stdout.isTTY` and `process.env.NO_COLOR` by default so the
|
|
8965
|
-
* renderer auto-degrades in CI and pipes, exactly like the legacy logger.
|
|
8966
|
-
*
|
|
8967
|
-
* @param stream - Stream to probe for `isTTY` (defaults to `process.stdout`).
|
|
8968
|
-
* @param noColor - The `NO_COLOR` value (defaults to `process.env.NO_COLOR`).
|
|
8969
|
-
* @returns `true` when color should be used.
|
|
8970
|
-
* @example
|
|
8971
|
-
* supportsColor(); // true in an interactive terminal
|
|
8972
|
-
*/
|
|
8973
|
-
function supportsColor(stream = process.stdout, noColor = process.env.NO_COLOR) {
|
|
8974
|
-
return stream.isTTY === true && noColor === void 0;
|
|
8975
|
-
}
|
|
8976
|
-
/**
|
|
8977
|
-
* Whether the terminal advertises 24-bit (truecolor) support via `COLORTERM`, so the
|
|
8978
|
-
* renderer may emit the exact brand pink ({@link BRAND_PINK}) instead of the 16-color
|
|
8979
|
-
* `magenta` approximation. Always layered on top of {@link supportsColor} — truecolor
|
|
8980
|
-
* is never used when color itself is disabled.
|
|
8981
|
-
*
|
|
8982
|
-
* @param colorTerm - The `COLORTERM` value (defaults to `process.env.COLORTERM`).
|
|
8983
|
-
* @returns `true` when `COLORTERM` is `truecolor` or `24bit`.
|
|
8984
|
-
* @example
|
|
8985
|
-
* supportsTruecolor("truecolor"); // true
|
|
8986
|
-
*/
|
|
8987
|
-
function supportsTruecolor(colorTerm = process.env.COLORTERM) {
|
|
8988
|
-
return colorTerm === "truecolor" || colorTerm === "24bit";
|
|
8989
|
-
}
|
|
8990
|
-
/**
|
|
8991
|
-
* The braille spinner glyph for a given elapsed time, advancing one frame per
|
|
8992
|
-
* `frameMs`. Deriving the frame from wall-clock elapsed (rather than a tick counter)
|
|
8993
|
-
* keeps the spinner correct even when the animation ticker is briefly starved by a
|
|
8994
|
-
* synchronous build phase and several ticks coalesce — the glyph still reflects real
|
|
8995
|
-
* elapsed time instead of freezing on a stale frame.
|
|
8996
|
-
*
|
|
8997
|
-
* @param elapsedMs - Milliseconds since the live region opened.
|
|
8998
|
-
* @param frameMs - Milliseconds per frame (defaults to `80`).
|
|
8999
|
-
* @returns The active spinner glyph.
|
|
9000
|
-
* @example
|
|
9001
|
-
* spinnerFrameAt(240); // "⠹" (the 4th frame at 80ms/frame)
|
|
9002
|
-
*/
|
|
9003
|
-
function spinnerFrameAt(elapsedMs, frameMs = 80) {
|
|
9004
|
-
return SPINNER_FRAMES[Math.floor(Math.max(0, elapsedMs) / frameMs) % SPINNER_FRAMES.length] ?? "⠋";
|
|
9005
|
-
}
|
|
9006
|
-
/**
|
|
9007
|
-
* Select the box glyph set for the given color mode (Unicode on a TTY, ASCII off it).
|
|
9008
|
-
*
|
|
9009
|
-
* @param color - Whether color/Unicode output is enabled.
|
|
9010
|
-
* @returns The matching {@link BoxGlyphs} set.
|
|
9011
|
-
* @example
|
|
9012
|
-
* const glyphs = boxGlyphs(supportsColor());
|
|
9013
|
-
*/
|
|
9014
|
-
function boxGlyphs(color) {
|
|
9015
|
-
return color ? UNICODE_BOX : ASCII_BOX;
|
|
9016
|
-
}
|
|
9017
|
-
/**
|
|
9018
|
-
* The visible width of a string, ignoring any ANSI escape sequences it contains.
|
|
9019
|
-
*
|
|
9020
|
-
* @param text - The (possibly colorized) text to measure.
|
|
9021
|
-
* @returns The number of visible characters.
|
|
9022
|
-
* @example
|
|
9023
|
-
* visibleWidth(`${ANSI.red}hi${ANSI.reset}`); // 2
|
|
9024
|
-
*/
|
|
9025
|
-
function visibleWidth(text) {
|
|
9026
|
-
return text.replaceAll(ANSI_PATTERN, "").length;
|
|
9027
|
-
}
|
|
9028
|
-
/**
|
|
9029
|
-
* Build a {@link Palette} bound to a fixed color mode. When `color` is `false` every
|
|
9030
|
-
* helper returns its input unchanged, so the same render code path produces plain
|
|
9031
|
-
* output in CI/pipes.
|
|
9032
|
-
*
|
|
9033
|
-
* @param color - Whether color is enabled (typically `supportsColor()`).
|
|
9034
|
-
* @param truecolor - Whether 24-bit output is enabled (typically `supportsTruecolor()`);
|
|
9035
|
-
* only consulted by {@link Palette.pink}. Defaults to `false` (16-color magenta).
|
|
9036
|
-
* @returns The bound color palette.
|
|
9037
|
-
* @example
|
|
9038
|
-
* const palette = makePalette(supportsColor(), supportsTruecolor());
|
|
9039
|
-
* const line = palette.green("done");
|
|
9040
|
-
*/
|
|
9041
|
-
function makePalette(color, truecolor = false) {
|
|
9042
|
-
return {
|
|
9043
|
-
enabled: color,
|
|
9044
|
-
/**
|
|
9045
|
-
* Wrap text in the given ANSI code (returns it unchanged when color is off).
|
|
9046
|
-
*
|
|
9047
|
-
* @param code - The ANSI SGR code to apply.
|
|
9048
|
-
* @param text - The text to colorize.
|
|
9049
|
-
* @returns The colorized (or unchanged) text.
|
|
9050
|
-
* @example
|
|
9051
|
-
* palette.paint(ANSI.green, "ok");
|
|
9052
|
-
*/
|
|
9053
|
-
paint(code, text) {
|
|
9054
|
-
return color ? `${code}${text}${ANSI.reset}` : text;
|
|
9055
|
-
},
|
|
9056
|
-
/**
|
|
9057
|
-
* Bold the given text (no-op in plain mode).
|
|
9058
|
-
*
|
|
9059
|
-
* @param text - The text to embolden.
|
|
9060
|
-
* @returns The bold (or unchanged) text.
|
|
9061
|
-
* @example
|
|
9062
|
-
* palette.bold("title");
|
|
9063
|
-
*/
|
|
9064
|
-
bold(text) {
|
|
9065
|
-
return this.paint(ANSI.bold, text);
|
|
9066
|
-
},
|
|
9067
|
-
/**
|
|
9068
|
-
* Dim the given text (no-op in plain mode).
|
|
9069
|
-
*
|
|
9070
|
-
* @param text - The text to dim.
|
|
9071
|
-
* @returns The dim (or unchanged) text.
|
|
9072
|
-
* @example
|
|
9073
|
-
* palette.dim("· 84ms");
|
|
9074
|
-
*/
|
|
9075
|
-
dim(text) {
|
|
9076
|
-
return this.paint(ANSI.dim, text);
|
|
9077
|
-
},
|
|
9078
|
-
/**
|
|
9079
|
-
* Color the given text green (no-op in plain mode).
|
|
9080
|
-
*
|
|
9081
|
-
* @param text - The text to colorize.
|
|
9082
|
-
* @returns The green (or unchanged) text.
|
|
9083
|
-
* @example
|
|
9084
|
-
* palette.green("✓");
|
|
9085
|
-
*/
|
|
9086
|
-
green(text) {
|
|
9087
|
-
return this.paint(ANSI.green, text);
|
|
9088
|
-
},
|
|
9089
|
-
/**
|
|
9090
|
-
* Color the given text yellow (no-op in plain mode).
|
|
9091
|
-
*
|
|
9092
|
-
* @param text - The text to colorize.
|
|
9093
|
-
* @returns The yellow (or unchanged) text.
|
|
9094
|
-
* @example
|
|
9095
|
-
* palette.yellow("~");
|
|
9096
|
-
*/
|
|
9097
|
-
yellow(text) {
|
|
9098
|
-
return this.paint(ANSI.yellow, text);
|
|
9099
|
-
},
|
|
9100
|
-
/**
|
|
9101
|
-
* Color the given text red (no-op in plain mode).
|
|
9102
|
-
*
|
|
9103
|
-
* @param text - The text to colorize.
|
|
9104
|
-
* @returns The red (or unchanged) text.
|
|
9105
|
-
* @example
|
|
9106
|
-
* palette.red("✗");
|
|
9107
|
-
*/
|
|
9108
|
-
red(text) {
|
|
9109
|
-
return this.paint(ANSI.red, text);
|
|
9110
|
-
},
|
|
9111
|
-
/**
|
|
9112
|
-
* Color the given text cyan (no-op in plain mode).
|
|
9113
|
-
*
|
|
9114
|
-
* @param text - The text to colorize.
|
|
9115
|
-
* @returns The cyan (or unchanged) text.
|
|
9116
|
-
* @example
|
|
9117
|
-
* palette.cyan("http://localhost:4173");
|
|
9118
|
-
*/
|
|
9119
|
-
cyan(text) {
|
|
9120
|
-
return this.paint(ANSI.cyan, text);
|
|
9121
|
-
},
|
|
9122
|
-
/**
|
|
9123
|
-
* Color the given text the Moku brand pink: exact `#FF1E6F` (24-bit) when truecolor
|
|
9124
|
-
* is enabled, the 16-color `magenta` approximation otherwise, unchanged in plain mode.
|
|
9125
|
-
*
|
|
9126
|
-
* @param text - The text to colorize.
|
|
9127
|
-
* @returns The pink (or unchanged) text.
|
|
9128
|
-
* @example
|
|
9129
|
-
* palette.pink("▟▙ moku web");
|
|
9130
|
-
*/
|
|
9131
|
-
pink(text) {
|
|
9132
|
-
if (!color) return text;
|
|
9133
|
-
if (truecolor) return `${fg24(BRAND_PINK.r, BRAND_PINK.g, BRAND_PINK.b)}${text}${ANSI.reset}`;
|
|
9134
|
-
return this.paint(ANSI.magenta, text);
|
|
9135
|
-
}
|
|
9136
|
-
};
|
|
9137
|
-
}
|
|
8039
|
+
//#region src/plugins/cli/render/panel.ts
|
|
9138
8040
|
/**
|
|
9139
|
-
*
|
|
9140
|
-
*
|
|
9141
|
-
*
|
|
9142
|
-
*
|
|
9143
|
-
*
|
|
9144
|
-
* @
|
|
9145
|
-
*
|
|
9146
|
-
*
|
|
9147
|
-
* @returns The boxed lines (top border, content rows, bottom border).
|
|
9148
|
-
* @example
|
|
9149
|
-
* box(["Local: http://localhost:4173"], true, 62);
|
|
8041
|
+
* @file cli plugin — the Panel renderer (the "Velocity Lockup" CLI identity). Produces
|
|
8042
|
+
* the `▟▙ moku web` lockup + version/runtime banner, the live phase tree with an
|
|
8043
|
+
* animated indeterminate build bar, the BUILD summary + throughput sparkline, the
|
|
8044
|
+
* server-ready rail with a persistent breathing `◍ live` idle pulse, the compact
|
|
8045
|
+
* rebuild line, the deploy result, and diagnostic heading/check rows. TTY/`NO_COLOR`-
|
|
8046
|
+
* aware via {@link makePalette} (24-bit brand pink when truecolor is available, the
|
|
8047
|
+
* 16-color magenta approximation otherwise, plain text off a TTY); every line is
|
|
8048
|
+
* written through an injectable sink so tests can capture it.
|
|
9150
8049
|
*/
|
|
9151
|
-
function box(lines, color, minInnerWidth = 0) {
|
|
9152
|
-
const glyphs = boxGlyphs(color);
|
|
9153
|
-
const inner = Math.max(0, minInnerWidth, ...lines.map((line) => visibleWidth(line)));
|
|
9154
|
-
const horizontal = glyphs.horizontal.repeat(inner + 2);
|
|
9155
|
-
const top = `${glyphs.topLeft}${horizontal}${glyphs.topRight}`;
|
|
9156
|
-
const bottom = `${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`;
|
|
9157
|
-
return [
|
|
9158
|
-
top,
|
|
9159
|
-
...lines.map((line) => {
|
|
9160
|
-
const pad = " ".repeat(inner - visibleWidth(line));
|
|
9161
|
-
return `${glyphs.vertical} ${line}${pad} ${glyphs.vertical}`;
|
|
9162
|
-
}),
|
|
9163
|
-
bottom
|
|
9164
|
-
];
|
|
9165
|
-
}
|
|
9166
|
-
//#endregion
|
|
9167
|
-
//#region src/plugins/cli/render/panel.ts
|
|
9168
8050
|
/** Per-command label shown beside the lockup wordmark. */
|
|
9169
8051
|
const COMMAND_LABEL = {
|
|
9170
8052
|
build: "build",
|
|
@@ -9254,7 +8136,7 @@ function durationSuffix(palette, durationMs) {
|
|
|
9254
8136
|
* railLine(" ├─ ✓ pages", "· 12ms");
|
|
9255
8137
|
*/
|
|
9256
8138
|
function railLine(left, right, width = RAIL_WIDTH) {
|
|
9257
|
-
const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
|
|
8139
|
+
const gap = Math.max(1, width - (0, _moku_labs_common_cli.visibleWidth)(left) - (0, _moku_labs_common_cli.visibleWidth)(right));
|
|
9258
8140
|
return `${left}${" ".repeat(gap)}${right}`;
|
|
9259
8141
|
}
|
|
9260
8142
|
/**
|
|
@@ -9295,8 +8177,8 @@ function createPanelRenderer(options = {}) {
|
|
|
9295
8177
|
process.stdout.write(chunk);
|
|
9296
8178
|
});
|
|
9297
8179
|
const now = options.now ?? Date.now;
|
|
9298
|
-
const color = options.color ?? supportsColor();
|
|
9299
|
-
const palette = makePalette(color, options.truecolor ?? (color && supportsTruecolor()));
|
|
8180
|
+
const color = options.color ?? (0, _moku_labs_common_cli.supportsColor)();
|
|
8181
|
+
const palette = (0, _moku_labs_common_cli.makePalette)(color, options.truecolor ?? (color && (0, _moku_labs_common_cli.supportsTruecolor)()));
|
|
9300
8182
|
const version = options.version ?? "dev";
|
|
9301
8183
|
const coreVersion = options.coreVersion;
|
|
9302
8184
|
const g = glyphSet(color);
|
|
@@ -9323,7 +8205,7 @@ function createPanelRenderer(options = {}) {
|
|
|
9323
8205
|
const renderPhaseRow = (row) => {
|
|
9324
8206
|
const branch = palette.dim(g.tree);
|
|
9325
8207
|
if (row.done) return railLine(` ${branch} ${palette.green("✓")} ${row.name}`, palette.dim(`· ${row.durationMs}ms`));
|
|
9326
|
-
return ` ${branch} ${palette.cyan(spinnerFrameAt(now() - blockStartedAt, SPIN_MS))} ${palette.dim(row.name)}`;
|
|
8208
|
+
return ` ${branch} ${palette.cyan((0, _moku_labs_common_cli.spinnerFrameAt)(now() - blockStartedAt, SPIN_MS))} ${palette.dim(row.name)}`;
|
|
9327
8209
|
};
|
|
9328
8210
|
/**
|
|
9329
8211
|
* Render the indeterminate "comet" build bar — a short pink fill window sweeping across
|
|
@@ -9354,10 +8236,10 @@ function createPanelRenderer(options = {}) {
|
|
|
9354
8236
|
* paintPhaseBlock();
|
|
9355
8237
|
*/
|
|
9356
8238
|
const paintPhaseBlock = () => {
|
|
9357
|
-
let frame = cursorUp(phaseDrawn);
|
|
9358
|
-
for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
|
|
9359
|
-
frame += `${CLEAR_LINE}${renderBuildBar(now() - blockStartedAt)}\n`;
|
|
9360
|
-
writeRaw(frame + CLEAR_BELOW);
|
|
8239
|
+
let frame = (0, _moku_labs_common_cli.cursorUp)(phaseDrawn);
|
|
8240
|
+
for (const row of phaseRows) frame += `${_moku_labs_common_cli.CLEAR_LINE}${renderPhaseRow(row)}\n`;
|
|
8241
|
+
frame += `${_moku_labs_common_cli.CLEAR_LINE}${renderBuildBar(now() - blockStartedAt)}\n`;
|
|
8242
|
+
writeRaw(frame + _moku_labs_common_cli.CLEAR_BELOW);
|
|
9361
8243
|
phaseDrawn = phaseRows.length + 1;
|
|
9362
8244
|
};
|
|
9363
8245
|
/**
|
|
@@ -9367,9 +8249,9 @@ function createPanelRenderer(options = {}) {
|
|
|
9367
8249
|
* paintRebuildLine();
|
|
9368
8250
|
*/
|
|
9369
8251
|
const paintRebuildLine = () => {
|
|
9370
|
-
const spinner = palette.cyan(spinnerFrameAt(now() - rebuildStartedAt, SPIN_MS));
|
|
8252
|
+
const spinner = palette.cyan((0, _moku_labs_common_cli.spinnerFrameAt)(now() - rebuildStartedAt, SPIN_MS));
|
|
9371
8253
|
const elapsed = palette.dim(`· ${((now() - rebuildStartedAt) / 1e3).toFixed(1)}s`);
|
|
9372
|
-
writeRaw(`\r${CLEAR_LINE} ${spinner} rebuilding ${rebuildLabel} ${elapsed}`);
|
|
8254
|
+
writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE} ${spinner} rebuilding ${rebuildLabel} ${elapsed}`);
|
|
9373
8255
|
};
|
|
9374
8256
|
/**
|
|
9375
8257
|
* Repaint the persistent in-place `◍ live` idle pulse beneath the serve panel — the
|
|
@@ -9380,7 +8262,7 @@ function createPanelRenderer(options = {}) {
|
|
|
9380
8262
|
* paintIdleLine();
|
|
9381
8263
|
*/
|
|
9382
8264
|
const paintIdleLine = () => {
|
|
9383
|
-
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…")}`);
|
|
8265
|
+
writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE} ${Math.floor((now() - idleStartedAt) / 450) % 2 === 0 ? palette.pink(g.liveOn) : palette.dim(g.liveOff)} ${palette.dim("live · waiting for changes…")}`);
|
|
9384
8266
|
};
|
|
9385
8267
|
/**
|
|
9386
8268
|
* Advance whichever live region is active by one frame (driven by the shared ticker).
|
|
@@ -9507,9 +8389,9 @@ function createPanelRenderer(options = {}) {
|
|
|
9507
8389
|
built(summary) {
|
|
9508
8390
|
if (rebuilding) return;
|
|
9509
8391
|
if (color && phaseOpen) {
|
|
9510
|
-
let frame = cursorUp(phaseDrawn);
|
|
9511
|
-
for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
|
|
9512
|
-
writeRaw(frame + CLEAR_BELOW);
|
|
8392
|
+
let frame = (0, _moku_labs_common_cli.cursorUp)(phaseDrawn);
|
|
8393
|
+
for (const row of phaseRows) frame += `${_moku_labs_common_cli.CLEAR_LINE}${renderPhaseRow(row)}\n`;
|
|
8394
|
+
writeRaw(frame + _moku_labs_common_cli.CLEAR_BELOW);
|
|
9513
8395
|
}
|
|
9514
8396
|
const phaseDurations = phaseRows.map((row) => row.durationMs).filter((value) => value !== void 0);
|
|
9515
8397
|
phaseOpen = false;
|
|
@@ -9524,7 +8406,7 @@ function createPanelRenderer(options = {}) {
|
|
|
9524
8406
|
const rateLabel = palette.dim(`${rate} pages/s`);
|
|
9525
8407
|
lines.push(railLine(spark, rateLabel, BOX_INNER));
|
|
9526
8408
|
}
|
|
9527
|
-
writeBlock(box(lines, color, BOX_INNER));
|
|
8409
|
+
writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
|
|
9528
8410
|
},
|
|
9529
8411
|
/**
|
|
9530
8412
|
* Render the server-ready rail (Local / Network URLs + watched dirs) and, on a TTY,
|
|
@@ -9538,7 +8420,7 @@ function createPanelRenderer(options = {}) {
|
|
|
9538
8420
|
const network = info.network ? palette.cyan(info.network) : palette.dim("unavailable");
|
|
9539
8421
|
const lines = [`${palette.green("➜")} ${palette.bold("Local")} ${palette.cyan(info.local)}`, `${palette.green("➜")} ${palette.bold("Network")} ${network}`];
|
|
9540
8422
|
if (info.watching && info.watching.length > 0) lines.push(`${palette.dim("watching")} ${palette.dim(info.watching.join(", "))}`);
|
|
9541
|
-
writeBlock(box(lines, color, BOX_INNER));
|
|
8423
|
+
writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
|
|
9542
8424
|
if (color) {
|
|
9543
8425
|
serveMode = true;
|
|
9544
8426
|
idle = true;
|
|
@@ -9584,7 +8466,7 @@ function createPanelRenderer(options = {}) {
|
|
|
9584
8466
|
rebuilding = false;
|
|
9585
8467
|
const line = ` ${palette.green("✓")} rebuilt ${palette.bold(String(info.pageCount))} pages ${palette.dim(`· ${info.durationMs}ms · reloaded`)}`;
|
|
9586
8468
|
if (settledRebuild && color) {
|
|
9587
|
-
writeRaw(`\r${CLEAR_LINE}${line}\n`);
|
|
8469
|
+
writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE}${line}\n`);
|
|
9588
8470
|
resumeIdle();
|
|
9589
8471
|
return;
|
|
9590
8472
|
}
|
|
@@ -9609,7 +8491,7 @@ function createPanelRenderer(options = {}) {
|
|
|
9609
8491
|
const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
|
|
9610
8492
|
lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
|
|
9611
8493
|
} else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
|
|
9612
|
-
writeBlock(box(lines, color, BOX_INNER));
|
|
8494
|
+
writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
|
|
9613
8495
|
},
|
|
9614
8496
|
/**
|
|
9615
8497
|
* Render a neutral informational line.
|
|
@@ -9646,7 +8528,7 @@ function createPanelRenderer(options = {}) {
|
|
|
9646
8528
|
const wasRebuilding = rebuilding;
|
|
9647
8529
|
if (rebuilding) {
|
|
9648
8530
|
rebuilding = false;
|
|
9649
|
-
if (color) writeRaw(`\r${CLEAR_LINE}`);
|
|
8531
|
+
if (color) writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE}`);
|
|
9650
8532
|
}
|
|
9651
8533
|
writeError(` ${palette.red("✗")} ${message}`);
|
|
9652
8534
|
if (cause !== void 0) writeError(String(cause));
|
|
@@ -9707,9 +8589,9 @@ const YES_PATTERN = /^y(es)?$/i;
|
|
|
9707
8589
|
/** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
|
|
9708
8590
|
const PROMPT_WIDTH = 66;
|
|
9709
8591
|
/** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
|
|
9710
|
-
const PROMPT_COLOR = supportsColor();
|
|
8592
|
+
const PROMPT_COLOR = (0, _moku_labs_common_cli.supportsColor)();
|
|
9711
8593
|
/** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
|
|
9712
|
-
const PROMPT_PALETTE = makePalette(PROMPT_COLOR, PROMPT_COLOR && supportsTruecolor());
|
|
8594
|
+
const PROMPT_PALETTE = (0, _moku_labs_common_cli.makePalette)(PROMPT_COLOR, PROMPT_COLOR && (0, _moku_labs_common_cli.supportsTruecolor)());
|
|
9713
8595
|
/**
|
|
9714
8596
|
* Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
|
|
9715
8597
|
* a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
|
|
@@ -9724,7 +8606,7 @@ function confirmPrompt(question) {
|
|
|
9724
8606
|
if (!PROMPT_COLOR) return `${question} [y/N] `;
|
|
9725
8607
|
const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
|
|
9726
8608
|
const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
|
|
9727
|
-
const gap = Math.max(1, PROMPT_WIDTH - visibleWidth(left) - visibleWidth(right));
|
|
8609
|
+
const gap = Math.max(1, PROMPT_WIDTH - (0, _moku_labs_common_cli.visibleWidth)(left) - (0, _moku_labs_common_cli.visibleWidth)(right));
|
|
9728
8610
|
return `${left}${" ".repeat(gap)}${right}`;
|
|
9729
8611
|
}
|
|
9730
8612
|
/**
|
|
@@ -10148,7 +9030,7 @@ function spaEvents(register) {
|
|
|
10148
9030
|
}
|
|
10149
9031
|
//#endregion
|
|
10150
9032
|
//#region src/plugins/spa/types.ts
|
|
10151
|
-
var types_exports$
|
|
9033
|
+
var types_exports$7 = /* @__PURE__ */ require_convention.__exportAll({ COMPONENT_HOOK_NAMES: () => COMPONENT_HOOK_NAMES });
|
|
10152
9034
|
/** Allowed hook names — single source of truth for fail-fast validation. */
|
|
10153
9035
|
const COMPONENT_HOOK_NAMES = [
|
|
10154
9036
|
"onCreate",
|
|
@@ -11484,176 +10366,11 @@ var types_exports$3 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
|
11484
10366
|
//#region src/plugins/deploy/types.ts
|
|
11485
10367
|
var types_exports$4 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
11486
10368
|
//#endregion
|
|
11487
|
-
//#region src/plugins/env/types.ts
|
|
11488
|
-
var types_exports$5 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
11489
|
-
//#endregion
|
|
11490
10369
|
//#region src/plugins/head/types.ts
|
|
11491
|
-
var types_exports$
|
|
11492
|
-
//#endregion
|
|
11493
|
-
//#region src/plugins/log/types.ts
|
|
11494
|
-
var types_exports$7 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
10370
|
+
var types_exports$5 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
11495
10371
|
//#endregion
|
|
11496
10372
|
//#region src/plugins/router/types.ts
|
|
11497
|
-
var types_exports$
|
|
11498
|
-
//#endregion
|
|
11499
|
-
//#region src/plugins/env/providers.ts
|
|
11500
|
-
/**
|
|
11501
|
-
* @file env plugin — built-in providers: dotenv, processEnv, cloudflareBindings.
|
|
11502
|
-
*/
|
|
11503
|
-
/** Default dotenv file path: optional local overrides. */
|
|
11504
|
-
const DEFAULT_DOTENV_PATH = ".env.local";
|
|
11505
|
-
/** Property on `globalThis` that the consumer sets per Cloudflare request. */
|
|
11506
|
-
const CLOUDFLARE_GLOBAL = "__CLOUDFLARE_ENV__";
|
|
11507
|
-
/** `String.indexOf` sentinel meaning "no `=` separator on this line". */
|
|
11508
|
-
const NO_SEPARATOR = -1;
|
|
11509
|
-
/**
|
|
11510
|
-
* Strips a single matching pair of surrounding double or single quotes from a
|
|
11511
|
-
* value. Leaves unquoted values (and trailing inline comments) untouched.
|
|
11512
|
-
*
|
|
11513
|
-
* @param value - The already-trimmed raw value.
|
|
11514
|
-
* @returns The value with one outer quote pair removed, if present.
|
|
11515
|
-
* @example
|
|
11516
|
-
* ```ts
|
|
11517
|
-
* stripQuotes('"a"'); // "a"
|
|
11518
|
-
* stripQuotes("plain # c"); // "plain # c"
|
|
11519
|
-
* ```
|
|
11520
|
-
*/
|
|
11521
|
-
function stripQuotes(value) {
|
|
11522
|
-
if (value.length < 2) return value;
|
|
11523
|
-
const first = value[0];
|
|
11524
|
-
const last = value.at(-1);
|
|
11525
|
-
if ((first === "\"" || first === "'") && first === last) return value.slice(1, -1);
|
|
11526
|
-
return value;
|
|
11527
|
-
}
|
|
11528
|
-
/**
|
|
11529
|
-
* Reports whether a trimmed line carries no assignment — a blank line or a
|
|
11530
|
-
* full-line `#` comment — and should be skipped by the parser.
|
|
11531
|
-
*
|
|
11532
|
-
* @param trimmed - A whitespace-trimmed line from the dotenv text.
|
|
11533
|
-
* @returns `true` when the line is empty or a comment.
|
|
11534
|
-
* @example
|
|
11535
|
-
* ```ts
|
|
11536
|
-
* isIgnoredLine(""); // true
|
|
11537
|
-
* isIgnoredLine("# note"); // true
|
|
11538
|
-
* isIgnoredLine("A=1"); // false
|
|
11539
|
-
* ```
|
|
11540
|
-
*/
|
|
11541
|
-
function isIgnoredLine(trimmed) {
|
|
11542
|
-
return trimmed === "" || trimmed.startsWith("#");
|
|
11543
|
-
}
|
|
11544
|
-
/**
|
|
11545
|
-
* Parses `.env`-style text into a flat record. Handles CRLF/LF, blank lines,
|
|
11546
|
-
* full-line `#` comments, first-`=` splitting, key/value trimming, and a single
|
|
11547
|
-
* outer quote pair. Does not strip trailing inline comments on unquoted values.
|
|
11548
|
-
*
|
|
11549
|
-
* @param text - The raw file contents.
|
|
11550
|
-
* @returns A flat record of parsed key/value pairs.
|
|
11551
|
-
* @example
|
|
11552
|
-
* ```ts
|
|
11553
|
-
* parseDotenv('A=1\nB="two"'); // { A: "1", B: "two" }
|
|
11554
|
-
* ```
|
|
11555
|
-
*/
|
|
11556
|
-
function parseDotenv(text) {
|
|
11557
|
-
const out = {};
|
|
11558
|
-
for (const line of text.split(/\r?\n/)) {
|
|
11559
|
-
const trimmed = line.trim();
|
|
11560
|
-
if (isIgnoredLine(trimmed)) continue;
|
|
11561
|
-
const eq = trimmed.indexOf("=");
|
|
11562
|
-
if (eq === NO_SEPARATOR) continue;
|
|
11563
|
-
const key = trimmed.slice(0, eq).trim();
|
|
11564
|
-
out[key] = stripQuotes(trimmed.slice(eq + 1).trim());
|
|
11565
|
-
}
|
|
11566
|
-
return out;
|
|
11567
|
-
}
|
|
11568
|
-
/**
|
|
11569
|
-
* A zero-dependency `.env`-style provider that re-reads and re-parses the file
|
|
11570
|
-
* from disk on every `load()`. Missing file resolves to `{}` (optional
|
|
11571
|
-
* overrides). Strips a single outer quote pair; does not strip trailing inline
|
|
11572
|
-
* comments on unquoted values.
|
|
11573
|
-
*
|
|
11574
|
-
* @param path - Path to the dotenv file. Defaults to `.env.local`.
|
|
11575
|
-
* @returns An {@link EnvProvider} named `dotenv:<path>` that reads fresh per call.
|
|
11576
|
-
* @example
|
|
11577
|
-
* ```ts
|
|
11578
|
-
* const provider = dotenv(".env.local");
|
|
11579
|
-
* provider.load(); // { PUBLIC_API_URL: "/api", ... }
|
|
11580
|
-
* ```
|
|
11581
|
-
*/
|
|
11582
|
-
function dotenv(path = DEFAULT_DOTENV_PATH) {
|
|
11583
|
-
return {
|
|
11584
|
-
name: `dotenv:${path}`,
|
|
11585
|
-
/**
|
|
11586
|
-
* Reads and parses the dotenv file fresh from disk; `{}` if it is missing.
|
|
11587
|
-
*
|
|
11588
|
-
* @returns The parsed environment record, or `{}` when the file is absent.
|
|
11589
|
-
* @example
|
|
11590
|
-
* ```ts
|
|
11591
|
-
* dotenv(".env.local").load();
|
|
11592
|
-
* ```
|
|
11593
|
-
*/
|
|
11594
|
-
load() {
|
|
11595
|
-
if (!(0, node_fs.existsSync)(path)) return {};
|
|
11596
|
-
return parseDotenv((0, node_fs.readFileSync)(path, "utf8"));
|
|
11597
|
-
}
|
|
11598
|
-
};
|
|
11599
|
-
}
|
|
11600
|
-
/**
|
|
11601
|
-
* A provider that returns a shallow copy of `process.env` at `load()` time.
|
|
11602
|
-
*
|
|
11603
|
-
* @returns An {@link EnvProvider} named `process-env`.
|
|
11604
|
-
* @example
|
|
11605
|
-
* ```ts
|
|
11606
|
-
* const provider = processEnv();
|
|
11607
|
-
* provider.load().HOME; // current process value
|
|
11608
|
-
* ```
|
|
11609
|
-
*/
|
|
11610
|
-
function processEnv() {
|
|
11611
|
-
return {
|
|
11612
|
-
name: "process-env",
|
|
11613
|
-
/**
|
|
11614
|
-
* Returns a shallow copy of `process.env` at call time.
|
|
11615
|
-
*
|
|
11616
|
-
* @returns A fresh shallow copy of `process.env`.
|
|
11617
|
-
* @example
|
|
11618
|
-
* ```ts
|
|
11619
|
-
* processEnv().load();
|
|
11620
|
-
* ```
|
|
11621
|
-
*/
|
|
11622
|
-
load() {
|
|
11623
|
-
return { ...process.env };
|
|
11624
|
-
}
|
|
11625
|
-
};
|
|
11626
|
-
}
|
|
11627
|
-
/**
|
|
11628
|
-
* A provider that reads live, per-request Cloudflare bindings from
|
|
11629
|
-
* `globalThis.__CLOUDFLARE_ENV__` at `load()` time (`?? {}` when absent). Never
|
|
11630
|
-
* caches the binding object; the consumer owns the global's request lifecycle.
|
|
11631
|
-
*
|
|
11632
|
-
* @returns An {@link EnvProvider} named `cloudflare`.
|
|
11633
|
-
* @example
|
|
11634
|
-
* ```ts
|
|
11635
|
-
* globalThis.__CLOUDFLARE_ENV__ = env; // set by the request handler
|
|
11636
|
-
* const provider = cloudflareBindings();
|
|
11637
|
-
* provider.load(); // reads the current request's bindings
|
|
11638
|
-
* ```
|
|
11639
|
-
*/
|
|
11640
|
-
function cloudflareBindings() {
|
|
11641
|
-
return {
|
|
11642
|
-
name: "cloudflare",
|
|
11643
|
-
/**
|
|
11644
|
-
* Reads `globalThis.__CLOUDFLARE_ENV__` fresh, never caching the bindings.
|
|
11645
|
-
*
|
|
11646
|
-
* @returns The current Cloudflare bindings, or `{}` when the global is unset.
|
|
11647
|
-
* @example
|
|
11648
|
-
* ```ts
|
|
11649
|
-
* cloudflareBindings().load();
|
|
11650
|
-
* ```
|
|
11651
|
-
*/
|
|
11652
|
-
load() {
|
|
11653
|
-
return globalThis[CLOUDFLARE_GLOBAL] ?? {};
|
|
11654
|
-
}
|
|
11655
|
-
};
|
|
11656
|
-
}
|
|
10373
|
+
var types_exports$6 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
11657
10374
|
//#endregion
|
|
11658
10375
|
//#region node_modules/unist-util-stringify-position/lib/index.js
|
|
11659
10376
|
/**
|
|
@@ -13884,43 +12601,41 @@ Object.defineProperty(exports, "Deploy", {
|
|
|
13884
12601
|
}
|
|
13885
12602
|
});
|
|
13886
12603
|
exports.EmbedFacadeButton = EmbedFacadeButton;
|
|
13887
|
-
|
|
12604
|
+
exports.GalleryTrack = GalleryTrack;
|
|
12605
|
+
Object.defineProperty(exports, "Head", {
|
|
13888
12606
|
enumerable: true,
|
|
13889
12607
|
get: function() {
|
|
13890
12608
|
return types_exports$5;
|
|
13891
12609
|
}
|
|
13892
12610
|
});
|
|
13893
|
-
exports
|
|
13894
|
-
Object.defineProperty(exports, "Head", {
|
|
12611
|
+
Object.defineProperty(exports, "Router", {
|
|
13895
12612
|
enumerable: true,
|
|
13896
12613
|
get: function() {
|
|
13897
12614
|
return types_exports$6;
|
|
13898
12615
|
}
|
|
13899
12616
|
});
|
|
13900
|
-
Object.defineProperty(exports, "
|
|
12617
|
+
Object.defineProperty(exports, "Spa", {
|
|
13901
12618
|
enumerable: true,
|
|
13902
12619
|
get: function() {
|
|
13903
12620
|
return types_exports$7;
|
|
13904
12621
|
}
|
|
13905
12622
|
});
|
|
13906
|
-
Object.defineProperty(exports, "
|
|
13907
|
-
enumerable: true,
|
|
13908
|
-
get: function() {
|
|
13909
|
-
return types_exports$8;
|
|
13910
|
-
}
|
|
13911
|
-
});
|
|
13912
|
-
Object.defineProperty(exports, "Spa", {
|
|
12623
|
+
Object.defineProperty(exports, "browserEnv", {
|
|
13913
12624
|
enumerable: true,
|
|
13914
12625
|
get: function() {
|
|
13915
|
-
return
|
|
12626
|
+
return _moku_labs_common.browserEnv;
|
|
13916
12627
|
}
|
|
13917
12628
|
});
|
|
13918
|
-
exports.browserEnv = browserEnv;
|
|
13919
12629
|
exports.buildArticleHead = buildArticleHead;
|
|
13920
12630
|
exports.buildPlugin = buildPlugin;
|
|
13921
12631
|
exports.canonical = canonical;
|
|
13922
12632
|
exports.cliPlugin = cliPlugin;
|
|
13923
|
-
exports
|
|
12633
|
+
Object.defineProperty(exports, "cloudflareBindings", {
|
|
12634
|
+
enumerable: true,
|
|
12635
|
+
get: function() {
|
|
12636
|
+
return _moku_labs_common.cloudflareBindings;
|
|
12637
|
+
}
|
|
12638
|
+
});
|
|
13924
12639
|
exports.contentPlugin = contentPlugin;
|
|
13925
12640
|
exports.createApp = createApp;
|
|
13926
12641
|
exports.createComponent = createComponent;
|
|
@@ -13929,8 +12644,18 @@ exports.createUrls = createUrls;
|
|
|
13929
12644
|
exports.dataPlugin = dataPlugin;
|
|
13930
12645
|
exports.defineRoutes = defineRoutes;
|
|
13931
12646
|
exports.deployPlugin = deployPlugin;
|
|
13932
|
-
exports
|
|
13933
|
-
|
|
12647
|
+
Object.defineProperty(exports, "dotenv", {
|
|
12648
|
+
enumerable: true,
|
|
12649
|
+
get: function() {
|
|
12650
|
+
return _moku_labs_common.dotenv;
|
|
12651
|
+
}
|
|
12652
|
+
});
|
|
12653
|
+
Object.defineProperty(exports, "envPlugin", {
|
|
12654
|
+
enumerable: true,
|
|
12655
|
+
get: function() {
|
|
12656
|
+
return _moku_labs_common.envPlugin;
|
|
12657
|
+
}
|
|
12658
|
+
});
|
|
13934
12659
|
exports.feedLink = feedLink;
|
|
13935
12660
|
exports.fileSystemContent = fileSystemContent;
|
|
13936
12661
|
exports.headPlugin = headPlugin;
|
|
@@ -13938,10 +12663,20 @@ exports.hreflang = hreflang;
|
|
|
13938
12663
|
exports.i18nPlugin = i18nPlugin;
|
|
13939
12664
|
exports.jsonLd = jsonLd;
|
|
13940
12665
|
exports.lazyEmbed = lazyEmbed;
|
|
13941
|
-
exports
|
|
12666
|
+
Object.defineProperty(exports, "logPlugin", {
|
|
12667
|
+
enumerable: true,
|
|
12668
|
+
get: function() {
|
|
12669
|
+
return _moku_labs_common.logPlugin;
|
|
12670
|
+
}
|
|
12671
|
+
});
|
|
13942
12672
|
exports.meta = meta;
|
|
13943
12673
|
exports.og = og;
|
|
13944
|
-
exports
|
|
12674
|
+
Object.defineProperty(exports, "processEnv", {
|
|
12675
|
+
enumerable: true,
|
|
12676
|
+
get: function() {
|
|
12677
|
+
return _moku_labs_common.processEnv;
|
|
12678
|
+
}
|
|
12679
|
+
});
|
|
13945
12680
|
exports.route = route;
|
|
13946
12681
|
exports.routerPlugin = routerPlugin;
|
|
13947
12682
|
exports.sitePlugin = sitePlugin;
|