@moku-labs/web 0.5.6 → 0.6.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 -0
- package/dist/index.cjs +1262 -89
- package/dist/index.d.cts +819 -403
- package/dist/index.d.mts +819 -403
- package/dist/index.mjs +1249 -83
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -35,9 +35,11 @@ let node_crypto = require("node:crypto");
|
|
|
35
35
|
let feed = require("feed");
|
|
36
36
|
let preact = require("preact");
|
|
37
37
|
let preact_render_to_string = require("preact-render-to-string");
|
|
38
|
+
let node_readline = require("node:readline");
|
|
39
|
+
let node_os = require("node:os");
|
|
38
40
|
//#region src/plugins/env/api.ts
|
|
39
41
|
/** Error prefix for all env API failures. */
|
|
40
|
-
const ERROR_PREFIX$
|
|
42
|
+
const ERROR_PREFIX$15 = "[web]";
|
|
41
43
|
/**
|
|
42
44
|
* Creates the env plugin API surface mounted at `ctx.env`. Closes over
|
|
43
45
|
* `ctx.state` ({@link EnvState}) and reads the frozen `resolved` / `publicMap`
|
|
@@ -81,7 +83,7 @@ function createEnvApi(ctx) {
|
|
|
81
83
|
*/
|
|
82
84
|
require(key) {
|
|
83
85
|
const value = resolved.get(key);
|
|
84
|
-
if (value === void 0) throw new Error(`${ERROR_PREFIX$
|
|
86
|
+
if (value === void 0) throw new Error(`${ERROR_PREFIX$15} env: required variable "${key}" is not defined.`);
|
|
85
87
|
return value;
|
|
86
88
|
},
|
|
87
89
|
/**
|
|
@@ -148,7 +150,7 @@ function createEnvState() {
|
|
|
148
150
|
/** Error message thrown by every frozen-map mutator. */
|
|
149
151
|
const FROZEN_MESSAGE = "env: map is frozen and cannot be mutated";
|
|
150
152
|
/** Error prefix for all resolution-pipeline failures. */
|
|
151
|
-
const ERROR_PREFIX$
|
|
153
|
+
const ERROR_PREFIX$14 = "[web]";
|
|
152
154
|
/**
|
|
153
155
|
* Throws the canonical frozen-map error; installed as a map's `set`/`clear`/`delete`.
|
|
154
156
|
*
|
|
@@ -199,8 +201,8 @@ function crossCheckPublicPrefix(config) {
|
|
|
199
201
|
const { schema, publicPrefix } = config;
|
|
200
202
|
for (const [key, spec] of Object.entries(schema)) {
|
|
201
203
|
const hasPrefix = key.startsWith(publicPrefix);
|
|
202
|
-
if (spec.public === true && !hasPrefix) throw new Error(`${ERROR_PREFIX$
|
|
203
|
-
if (hasPrefix && spec.public !== true) throw new Error(`${ERROR_PREFIX$
|
|
204
|
+
if (spec.public === true && !hasPrefix) throw new Error(`${ERROR_PREFIX$14} env: "${key}" is marked public but does not start with "${publicPrefix}".`);
|
|
205
|
+
if (hasPrefix && spec.public !== true) throw new Error(`${ERROR_PREFIX$14} env: "${key}" starts with "${publicPrefix}" but is not marked public:true.`);
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
208
|
/**
|
|
@@ -251,7 +253,7 @@ function validateSchema(ctx) {
|
|
|
251
253
|
crossCheckPublicPrefix(config);
|
|
252
254
|
for (const [key, spec] of Object.entries(schema)) {
|
|
253
255
|
if (merged[key] === void 0 && spec.default !== void 0) merged[key] = spec.default;
|
|
254
|
-
if (merged[key] === void 0 && spec.required === true) throw new Error(`${ERROR_PREFIX$
|
|
256
|
+
if (merged[key] === void 0 && spec.required === true) throw new Error(`${ERROR_PREFIX$14} env: required variable "${key}" is not defined by any provider or default.`);
|
|
255
257
|
}
|
|
256
258
|
for (const [key, spec] of Object.entries(schema)) {
|
|
257
259
|
const value = merged[key];
|
|
@@ -793,7 +795,7 @@ const createCore = coreConfig.createCore;
|
|
|
793
795
|
//#endregion
|
|
794
796
|
//#region src/plugins/i18n/api.ts
|
|
795
797
|
/** Error prefix for all i18n lifecycle failures. */
|
|
796
|
-
const ERROR_PREFIX$
|
|
798
|
+
const ERROR_PREFIX$13 = "[web]";
|
|
797
799
|
/**
|
|
798
800
|
* Validates the resolved i18n config (fail-fast at `createApp`). Throws when
|
|
799
801
|
* `locales` is empty or when `defaultLocale` is not a member of `locales`.
|
|
@@ -809,8 +811,8 @@ const ERROR_PREFIX$12 = "[web]";
|
|
|
809
811
|
*/
|
|
810
812
|
function validateI18nConfig(ctx) {
|
|
811
813
|
const { locales, defaultLocale } = ctx.config;
|
|
812
|
-
if (locales.length === 0) throw new Error(`${ERROR_PREFIX$
|
|
813
|
-
if (!locales.includes(defaultLocale)) throw new Error(`${ERROR_PREFIX$
|
|
814
|
+
if (locales.length === 0) throw new Error(`${ERROR_PREFIX$13} i18n.locales must contain at least one locale.\n Set pluginConfigs.i18n.locales to a non-empty array, e.g. ["en"].`);
|
|
815
|
+
if (!locales.includes(defaultLocale)) throw new Error(`${ERROR_PREFIX$13} i18n.defaultLocale "${defaultLocale}" is not in i18n.locales [${locales.join(", ")}].\n Set pluginConfigs.i18n.defaultLocale to one of the configured locales, or add "${defaultLocale}" to i18n.locales.`);
|
|
814
816
|
}
|
|
815
817
|
/**
|
|
816
818
|
* Creates the i18n plugin API surface — locale registry accessors plus the
|
|
@@ -1814,7 +1816,7 @@ const contentPlugin = createPlugin$1("content", {
|
|
|
1814
1816
|
//#endregion
|
|
1815
1817
|
//#region src/plugins/site/api.ts
|
|
1816
1818
|
/** Error prefix for all site lifecycle/validation failures. */
|
|
1817
|
-
const ERROR_PREFIX$
|
|
1819
|
+
const ERROR_PREFIX$12 = "[web]";
|
|
1818
1820
|
/**
|
|
1819
1821
|
* Joins a relative path against an absolute base URL, normalizing the slash
|
|
1820
1822
|
* boundary to exactly one "/". Returns the base unchanged for an empty or
|
|
@@ -1882,8 +1884,8 @@ function isAbsoluteUrl(value) {
|
|
|
1882
1884
|
* ```
|
|
1883
1885
|
*/
|
|
1884
1886
|
function validateSiteConfig(ctx) {
|
|
1885
|
-
if (!isNonEmpty(ctx.config.name)) throw new Error(`${ERROR_PREFIX$
|
|
1886
|
-
if (!isAbsoluteUrl(ctx.config.url)) throw new Error(`${ERROR_PREFIX$
|
|
1887
|
+
if (!isNonEmpty(ctx.config.name)) throw new Error(`${ERROR_PREFIX$12} site.name is required.\n Provide a non-empty site name in pluginConfigs.site.name.`);
|
|
1888
|
+
if (!isAbsoluteUrl(ctx.config.url)) throw new Error(`${ERROR_PREFIX$12} site.url must be a valid absolute URL (http/https), received ${JSON.stringify(ctx.config.url)}.\n Provide an absolute URL in pluginConfigs.site.url, e.g. "https://blog.dev".`);
|
|
1887
1889
|
}
|
|
1888
1890
|
/**
|
|
1889
1891
|
* Creates the site plugin API surface — read-only accessors over frozen config
|
|
@@ -2073,7 +2075,7 @@ function matchRoute(compiled, pathname) {
|
|
|
2073
2075
|
* `manifest`. Returns values/copies, never the raw `ctx.state` reference (spec/11 §2.4).
|
|
2074
2076
|
*/
|
|
2075
2077
|
/** Error prefix for router API failures. */
|
|
2076
|
-
const ERROR_PREFIX$
|
|
2078
|
+
const ERROR_PREFIX$11 = "[web] router";
|
|
2077
2079
|
/**
|
|
2078
2080
|
* Read the compiled matcher table, throwing if `onInit` has not run yet. This
|
|
2079
2081
|
* `null` cannot occur in practice post-`onInit`; the guard documents the invariant.
|
|
@@ -2087,7 +2089,7 @@ const ERROR_PREFIX$10 = "[web] router";
|
|
|
2087
2089
|
* ```
|
|
2088
2090
|
*/
|
|
2089
2091
|
function readTable(state) {
|
|
2090
|
-
if (state.table === null) throw new Error(`${ERROR_PREFIX$
|
|
2092
|
+
if (state.table === null) throw new Error(`${ERROR_PREFIX$11}: matcher table accessed before onInit compiled it.`);
|
|
2091
2093
|
return state.table;
|
|
2092
2094
|
}
|
|
2093
2095
|
/**
|
|
@@ -2141,7 +2143,7 @@ function toClientRoute(entry) {
|
|
|
2141
2143
|
* api.match("/en/hello/");
|
|
2142
2144
|
* ```
|
|
2143
2145
|
*/
|
|
2144
|
-
function createApi$
|
|
2146
|
+
function createApi$5(ctx) {
|
|
2145
2147
|
const { state } = ctx;
|
|
2146
2148
|
return {
|
|
2147
2149
|
/**
|
|
@@ -2171,7 +2173,7 @@ function createApi$4(ctx) {
|
|
|
2171
2173
|
*/
|
|
2172
2174
|
toUrl(routeName, params) {
|
|
2173
2175
|
const entry = readTable(state).byName.get(routeName);
|
|
2174
|
-
if (!entry) throw new Error(`${ERROR_PREFIX$
|
|
2176
|
+
if (!entry) throw new Error(`${ERROR_PREFIX$11}: unknown route name "${routeName}".`);
|
|
2175
2177
|
return entry.toUrl(params);
|
|
2176
2178
|
},
|
|
2177
2179
|
/**
|
|
@@ -2306,7 +2308,7 @@ function bySpecificity(a, b) {
|
|
|
2306
2308
|
* only (`CompileInput`) — never the plugin ctx.
|
|
2307
2309
|
*/
|
|
2308
2310
|
/** Shared `[web]` error prefix for router validation failures. */
|
|
2309
|
-
const ERROR_PREFIX$
|
|
2311
|
+
const ERROR_PREFIX$10 = "[web] router";
|
|
2310
2312
|
/**
|
|
2311
2313
|
* Validate the route map (fail-fast in `onInit`). Throws with the `[web]` prefix
|
|
2312
2314
|
* naming the offending route/pattern on any failure: empty map, a pattern not
|
|
@@ -2321,12 +2323,12 @@ const ERROR_PREFIX$9 = "[web] router";
|
|
|
2321
2323
|
*/
|
|
2322
2324
|
function validateRoutes(routes) {
|
|
2323
2325
|
const names = Object.keys(routes);
|
|
2324
|
-
if (names.length === 0) throw new Error(`${ERROR_PREFIX$
|
|
2326
|
+
if (names.length === 0) throw new Error(`${ERROR_PREFIX$10}: route map is empty — provide at least one route via pluginConfigs.router.routes.`);
|
|
2325
2327
|
for (const name of names) {
|
|
2326
2328
|
const pattern = routes[name]?.pattern ?? "";
|
|
2327
|
-
if (!pattern.startsWith("/")) throw new Error(`${ERROR_PREFIX$
|
|
2328
|
-
if ((pattern.match(/\{/g) ?? []).length !== (pattern.match(/\}/g) ?? []).length) throw new Error(`${ERROR_PREFIX$
|
|
2329
|
-
if ((pattern.match(/\{lang:\?\}/g) ?? []).length > 1) throw new Error(`${ERROR_PREFIX$
|
|
2329
|
+
if (!pattern.startsWith("/")) throw new Error(`${ERROR_PREFIX$10}: route "${name}" pattern must start with "/" (got "${pattern}").`);
|
|
2330
|
+
if ((pattern.match(/\{/g) ?? []).length !== (pattern.match(/\}/g) ?? []).length) throw new Error(`${ERROR_PREFIX$10}: route "${name}" pattern has unbalanced braces ("${pattern}").`);
|
|
2331
|
+
if ((pattern.match(/\{lang:\?\}/g) ?? []).length > 1) throw new Error(`${ERROR_PREFIX$10}: route "${name}" pattern has more than one {lang:?} segment ("${pattern}").`);
|
|
2330
2332
|
}
|
|
2331
2333
|
}
|
|
2332
2334
|
/**
|
|
@@ -2731,7 +2733,7 @@ function defineRoutes(routes) {
|
|
|
2731
2733
|
* const state = createState({ global: {}, config: { routes: {} } });
|
|
2732
2734
|
* ```
|
|
2733
2735
|
*/
|
|
2734
|
-
function createState$
|
|
2736
|
+
function createState$5(_ctx) {
|
|
2735
2737
|
return {
|
|
2736
2738
|
table: null,
|
|
2737
2739
|
mode: _ctx.config.mode ?? "hybrid"
|
|
@@ -2767,8 +2769,8 @@ const routerPlugin = createPlugin$1("router", {
|
|
|
2767
2769
|
routes: {},
|
|
2768
2770
|
mode: "hybrid"
|
|
2769
2771
|
},
|
|
2770
|
-
createState: createState$
|
|
2771
|
-
api: createApi$
|
|
2772
|
+
createState: createState$5,
|
|
2773
|
+
api: createApi$5,
|
|
2772
2774
|
onInit(ctx) {
|
|
2773
2775
|
const i18n = ctx.require(i18nPlugin);
|
|
2774
2776
|
const baseUrl = ctx.require(sitePlugin).url();
|
|
@@ -3108,7 +3110,7 @@ function serializeHead(elements) {
|
|
|
3108
3110
|
* it to a string. It holds no resource and caches no subscription.
|
|
3109
3111
|
*/
|
|
3110
3112
|
/** Error prefix for head API invariant failures. */
|
|
3111
|
-
const ERROR_PREFIX$
|
|
3113
|
+
const ERROR_PREFIX$9 = "[head]";
|
|
3112
3114
|
/**
|
|
3113
3115
|
* Read the normalized defaults, asserting the post-`onInit` invariant (the slot is
|
|
3114
3116
|
* `null` only before `onInit` assigns it, which cannot occur at render time).
|
|
@@ -3122,7 +3124,7 @@ const ERROR_PREFIX$8 = "[head]";
|
|
|
3122
3124
|
* ```
|
|
3123
3125
|
*/
|
|
3124
3126
|
function readDefaults(state) {
|
|
3125
|
-
if (state.defaults === null) throw new Error(`${ERROR_PREFIX$
|
|
3127
|
+
if (state.defaults === null) throw new Error(`${ERROR_PREFIX$9}: defaults accessed before onInit normalized them.`);
|
|
3126
3128
|
return state.defaults;
|
|
3127
3129
|
}
|
|
3128
3130
|
/**
|
|
@@ -3138,7 +3140,7 @@ function readDefaults(state) {
|
|
|
3138
3140
|
* api.render(route, data);
|
|
3139
3141
|
* ```
|
|
3140
3142
|
*/
|
|
3141
|
-
function createApi$
|
|
3143
|
+
function createApi$4(ctx) {
|
|
3142
3144
|
return {
|
|
3143
3145
|
/**
|
|
3144
3146
|
* Compose the final `<head>` inner HTML for a route (pulled by `build`).
|
|
@@ -3165,7 +3167,7 @@ render(route, data) {
|
|
|
3165
3167
|
//#endregion
|
|
3166
3168
|
//#region src/plugins/head/config.ts
|
|
3167
3169
|
/** Error prefix for all head config-validation failures. */
|
|
3168
|
-
const ERROR_PREFIX$
|
|
3170
|
+
const ERROR_PREFIX$8 = "[head] config:";
|
|
3169
3171
|
/** The allowed `twitterCard` literals (also the runtime guard set). */
|
|
3170
3172
|
const VALID_TWITTER_CARDS = ["summary", "summary_large_image"];
|
|
3171
3173
|
/**
|
|
@@ -3178,7 +3180,7 @@ const VALID_TWITTER_CARDS = ["summary", "summary_large_image"];
|
|
|
3178
3180
|
* createPlugin("head", { config: defaultConfig });
|
|
3179
3181
|
* ```
|
|
3180
3182
|
*/
|
|
3181
|
-
const defaultConfig$
|
|
3183
|
+
const defaultConfig$3 = { twitterCard: "summary_large_image" };
|
|
3182
3184
|
/**
|
|
3183
3185
|
* Structurally validate the resolved head config (no I/O). Throws a standard
|
|
3184
3186
|
* `[head] config: …` error when `titleTemplate` is provided without the `%s`
|
|
@@ -3192,8 +3194,8 @@ const defaultConfig$2 = { twitterCard: "summary_large_image" };
|
|
|
3192
3194
|
* ```
|
|
3193
3195
|
*/
|
|
3194
3196
|
function validateHeadConfig(config) {
|
|
3195
|
-
if (config.titleTemplate !== void 0 && !config.titleTemplate.includes("%s")) throw new Error(`${ERROR_PREFIX$
|
|
3196
|
-
if (config.twitterCard !== void 0 && !VALID_TWITTER_CARDS.includes(config.twitterCard)) throw new Error(`${ERROR_PREFIX$
|
|
3197
|
+
if (config.titleTemplate !== void 0 && !config.titleTemplate.includes("%s")) throw new Error(`${ERROR_PREFIX$8} titleTemplate must contain the "%s" token (replaced by the route title), received ${JSON.stringify(config.titleTemplate)}.`);
|
|
3198
|
+
if (config.twitterCard !== void 0 && !VALID_TWITTER_CARDS.includes(config.twitterCard)) throw new Error(`${ERROR_PREFIX$8} twitterCard must be one of [${VALID_TWITTER_CARDS.join(", ")}], received ${JSON.stringify(config.twitterCard)}.`);
|
|
3197
3199
|
}
|
|
3198
3200
|
/**
|
|
3199
3201
|
* Validate then build the frozen, normalized {@link HeadDefaults} snapshot read by
|
|
@@ -3261,7 +3263,7 @@ const headHelpers = {
|
|
|
3261
3263
|
* const state = createState({ global: {}, config: {} });
|
|
3262
3264
|
* ```
|
|
3263
3265
|
*/
|
|
3264
|
-
function createState$
|
|
3266
|
+
function createState$4(_ctx) {
|
|
3265
3267
|
return { defaults: null };
|
|
3266
3268
|
}
|
|
3267
3269
|
//#endregion
|
|
@@ -3296,9 +3298,9 @@ const headPlugin = createPlugin$1("head", {
|
|
|
3296
3298
|
routerPlugin
|
|
3297
3299
|
],
|
|
3298
3300
|
helpers: headHelpers,
|
|
3299
|
-
config: defaultConfig$
|
|
3300
|
-
createState: createState$
|
|
3301
|
-
api: createApi$
|
|
3301
|
+
config: defaultConfig$3,
|
|
3302
|
+
createState: createState$4,
|
|
3303
|
+
api: createApi$4,
|
|
3302
3304
|
onInit(ctx) {
|
|
3303
3305
|
ctx.state.defaults = normalizeHeadConfig(ctx.config);
|
|
3304
3306
|
}
|
|
@@ -4891,7 +4893,7 @@ async function runPipeline(ctx, options) {
|
|
|
4891
4893
|
* @file build plugin — API factory (run + phases), cross-plugin wiring, and onInit config validation.
|
|
4892
4894
|
*/
|
|
4893
4895
|
/** Error prefix for build config/validation failures (spec/11 Part-3). */
|
|
4894
|
-
const ERROR_PREFIX$
|
|
4896
|
+
const ERROR_PREFIX$7 = "[web] build";
|
|
4895
4897
|
/** Recognized font file extensions for OG-image validation. */
|
|
4896
4898
|
const FONT_EXTENSIONS = [
|
|
4897
4899
|
".ttf",
|
|
@@ -4899,7 +4901,7 @@ const FONT_EXTENSIONS = [
|
|
|
4899
4901
|
".woff"
|
|
4900
4902
|
];
|
|
4901
4903
|
/** Typed default `build` config (R6: no inline `as`). `ogImage: false` disables OG generation. */
|
|
4902
|
-
const defaultConfig$
|
|
4904
|
+
const defaultConfig$2 = {
|
|
4903
4905
|
outDir: "./dist",
|
|
4904
4906
|
minify: true,
|
|
4905
4907
|
feeds: true,
|
|
@@ -4921,7 +4923,7 @@ const defaultConfig$1 = {
|
|
|
4921
4923
|
* await api.run({ outDir: "./preview" });
|
|
4922
4924
|
* ```
|
|
4923
4925
|
*/
|
|
4924
|
-
function createApi$
|
|
4926
|
+
function createApi$3(ctx) {
|
|
4925
4927
|
return {
|
|
4926
4928
|
/**
|
|
4927
4929
|
* Run the full SSG pipeline and write the site to disk.
|
|
@@ -4962,8 +4964,8 @@ function createApi$2(ctx) {
|
|
|
4962
4964
|
* ```
|
|
4963
4965
|
*/
|
|
4964
4966
|
function validateFonts(og) {
|
|
4965
|
-
if (typeof og.fontDir !== "string" || og.fontDir.length === 0 || !(0, node_fs.existsSync)(og.fontDir)) throw new Error(`${ERROR_PREFIX$
|
|
4966
|
-
if (!(0, node_fs.readdirSync)(og.fontDir).some((name) => FONT_EXTENSIONS.some((extension) => name.endsWith(extension)))) throw new Error(`${ERROR_PREFIX$
|
|
4967
|
+
if (typeof og.fontDir !== "string" || og.fontDir.length === 0 || !(0, node_fs.existsSync)(og.fontDir)) throw new Error(`${ERROR_PREFIX$7}.ogImage: fontDir "${og.fontDir}" does not exist — provide a directory with at least one font.`);
|
|
4968
|
+
if (!(0, node_fs.readdirSync)(og.fontDir).some((name) => FONT_EXTENSIONS.some((extension) => name.endsWith(extension)))) throw new Error(`${ERROR_PREFIX$7}.ogImage: fontDir "${og.fontDir}" contains no .ttf/.otf/.woff font files.`);
|
|
4967
4969
|
}
|
|
4968
4970
|
/**
|
|
4969
4971
|
* Validates `build` config synchronously in `onInit` (return value discarded).
|
|
@@ -4976,11 +4978,11 @@ function validateFonts(og) {
|
|
|
4976
4978
|
* validateConfig(ctx.config);
|
|
4977
4979
|
* ```
|
|
4978
4980
|
*/
|
|
4979
|
-
function validateConfig$
|
|
4980
|
-
if (typeof config.outDir !== "string" || config.outDir.trim().length === 0) throw new Error(`${ERROR_PREFIX$
|
|
4981
|
-
if (config.publicDir !== void 0 && typeof config.publicDir !== "string") throw new Error(`${ERROR_PREFIX$
|
|
4982
|
-
if (config.template !== void 0 && typeof config.template !== "string") throw new Error(`${ERROR_PREFIX$
|
|
4983
|
-
if (config.clientEntry !== void 0 && typeof config.clientEntry !== "string") throw new Error(`${ERROR_PREFIX$
|
|
4981
|
+
function validateConfig$2(config) {
|
|
4982
|
+
if (typeof config.outDir !== "string" || config.outDir.trim().length === 0) throw new Error(`${ERROR_PREFIX$7}.outDir: must be a non-empty string.`);
|
|
4983
|
+
if (config.publicDir !== void 0 && typeof config.publicDir !== "string") throw new Error(`${ERROR_PREFIX$7}.publicDir: must be a string when set.`);
|
|
4984
|
+
if (config.template !== void 0 && typeof config.template !== "string") throw new Error(`${ERROR_PREFIX$7}.template: must be a string path when set.`);
|
|
4985
|
+
if (config.clientEntry !== void 0 && typeof config.clientEntry !== "string") throw new Error(`${ERROR_PREFIX$7}.clientEntry: must be a string path when set.`);
|
|
4984
4986
|
if (config.ogImage) validateFonts(config.ogImage);
|
|
4985
4987
|
}
|
|
4986
4988
|
//#endregion
|
|
@@ -5019,7 +5021,7 @@ function createEvents(register) {
|
|
|
5019
5021
|
* const state = createState({ global: {}, config });
|
|
5020
5022
|
* ```
|
|
5021
5023
|
*/
|
|
5022
|
-
function createState$
|
|
5024
|
+
function createState$3(ctx) {
|
|
5023
5025
|
return {
|
|
5024
5026
|
config: ctx.config,
|
|
5025
5027
|
manifest: null,
|
|
@@ -5063,11 +5065,11 @@ const buildPlugin = createPlugin$1("build", {
|
|
|
5063
5065
|
routerPlugin,
|
|
5064
5066
|
headPlugin
|
|
5065
5067
|
],
|
|
5066
|
-
config: defaultConfig$
|
|
5067
|
-
createState: createState$
|
|
5068
|
+
config: defaultConfig$2,
|
|
5069
|
+
createState: createState$3,
|
|
5068
5070
|
events: createEvents,
|
|
5069
|
-
api: createApi$
|
|
5070
|
-
onInit: (ctx) => validateConfig$
|
|
5071
|
+
api: createApi$3,
|
|
5072
|
+
onInit: (ctx) => validateConfig$2(ctx.config)
|
|
5071
5073
|
});
|
|
5072
5074
|
//#endregion
|
|
5073
5075
|
//#region src/plugins/deploy/wrangler.ts
|
|
@@ -5082,7 +5084,7 @@ const buildPlugin = createPlugin$1("build", {
|
|
|
5082
5084
|
*/
|
|
5083
5085
|
const MOKU_WRANGLER_VERSION = "4.34.0";
|
|
5084
5086
|
/** Error prefix for deploy runtime failures (spec/11 Part-3). */
|
|
5085
|
-
const ERROR_PREFIX$
|
|
5087
|
+
const ERROR_PREFIX$6 = "[web] deploy";
|
|
5086
5088
|
/** Mask substituted for a detected secret-like token. */
|
|
5087
5089
|
const MASK = "***";
|
|
5088
5090
|
/** Minimum token length eligible for entropy-gated scrubbing. */
|
|
@@ -5160,7 +5162,7 @@ function scrubSecrets(text, allowlist) {
|
|
|
5160
5162
|
* guardBranch("preview/landing"); // "preview/landing"
|
|
5161
5163
|
*/
|
|
5162
5164
|
function guardBranch(branch) {
|
|
5163
|
-
if (!BRANCH_REGEX.test(branch) || branch.startsWith("-")) throw deployError("ERR_DEPLOY_INVALID_BRANCH", `${ERROR_PREFIX$
|
|
5165
|
+
if (!BRANCH_REGEX.test(branch) || branch.startsWith("-")) throw deployError("ERR_DEPLOY_INVALID_BRANCH", `${ERROR_PREFIX$6}: branch ${JSON.stringify(branch)} is invalid.\n Branches must match /^[a-zA-Z0-9/_.-]+$/ so they cannot inject wrangler flags.`);
|
|
5164
5166
|
return branch;
|
|
5165
5167
|
}
|
|
5166
5168
|
/**
|
|
@@ -5177,7 +5179,7 @@ function guardBranch(branch) {
|
|
|
5177
5179
|
function assertWithinRoot(outDir, root) {
|
|
5178
5180
|
const resolved = node_path$1.default.isAbsolute(outDir) ? node_path$1.default.resolve(outDir) : node_path$1.default.resolve(root, outDir);
|
|
5179
5181
|
const rootResolved = node_path$1.default.resolve(root);
|
|
5180
|
-
if (resolved !== rootResolved && !resolved.startsWith(rootResolved + node_path$1.default.sep)) throw deployError("ERR_DEPLOY_PATH_TRAVERSAL", `${ERROR_PREFIX$
|
|
5182
|
+
if (resolved !== rootResolved && !resolved.startsWith(rootResolved + node_path$1.default.sep)) throw deployError("ERR_DEPLOY_PATH_TRAVERSAL", `${ERROR_PREFIX$6}: outDir ${JSON.stringify(outDir)} resolves outside the project root.\n Point outDir at a directory inside ${JSON.stringify(rootResolved)}.`);
|
|
5181
5183
|
return resolved;
|
|
5182
5184
|
}
|
|
5183
5185
|
/**
|
|
@@ -5262,11 +5264,11 @@ function classifyWranglerError(exitCode, scrubbedStderr) {
|
|
|
5262
5264
|
const haystack = scrubbedStderr.toLowerCase();
|
|
5263
5265
|
for (const signature of ERROR_SIGNATURES) if (signature.match.some((needle) => haystack.includes(needle))) return {
|
|
5264
5266
|
code: signature.kind,
|
|
5265
|
-
message: `${ERROR_PREFIX$
|
|
5267
|
+
message: `${ERROR_PREFIX$6}: wrangler failed (exit ${exitCode}).\n ${signature.advice}`
|
|
5266
5268
|
};
|
|
5267
5269
|
return {
|
|
5268
5270
|
code: "ERR_DEPLOY_WRANGLER_FAILED",
|
|
5269
|
-
message: `${ERROR_PREFIX$
|
|
5271
|
+
message: `${ERROR_PREFIX$6}: wrangler failed (exit ${exitCode}).\n ${scrubbedStderr.trim().slice(-500)}`
|
|
5270
5272
|
};
|
|
5271
5273
|
}
|
|
5272
5274
|
/**
|
|
@@ -5532,7 +5534,7 @@ async function reconcile(input) {
|
|
|
5532
5534
|
* and short-circuiting on the first failure.
|
|
5533
5535
|
*/
|
|
5534
5536
|
/** Error prefix for deploy preflight failures (spec/11 Part-3). */
|
|
5535
|
-
const ERROR_PREFIX$
|
|
5537
|
+
const ERROR_PREFIX$5 = "[web] deploy";
|
|
5536
5538
|
/** Cloudflare Pages free-tier file-count limit. */
|
|
5537
5539
|
const FREE_TIER_FILE_LIMIT = 2e4;
|
|
5538
5540
|
/** Cloudflare Pages paid-tier file-count limit (env override target). */
|
|
@@ -5608,15 +5610,15 @@ async function runPreflight(config, root, env = process.env) {
|
|
|
5608
5610
|
try {
|
|
5609
5611
|
await (0, node_fs_promises.stat)(wranglerPath);
|
|
5610
5612
|
} catch {
|
|
5611
|
-
throw deployError("ERR_DEPLOY_NO_WRANGLER_CONFIG", `${ERROR_PREFIX$
|
|
5613
|
+
throw deployError("ERR_DEPLOY_NO_WRANGLER_CONFIG", `${ERROR_PREFIX$5}: wrangler.jsonc not found.\n Run \`app.deploy.init()\` to scaffold it, then retry.`);
|
|
5612
5614
|
}
|
|
5613
5615
|
const stats = await inspectOutdir(node_path$1.default.isAbsolute(config.outDir) ? node_path$1.default.resolve(config.outDir) : node_path$1.default.resolve(root, config.outDir)).catch(() => {
|
|
5614
|
-
throw deployError("ERR_DEPLOY_EMPTY_OUTDIR", `${ERROR_PREFIX$
|
|
5616
|
+
throw deployError("ERR_DEPLOY_EMPTY_OUTDIR", `${ERROR_PREFIX$5}: outDir ${JSON.stringify(config.outDir)} is missing.\n Run your build first, then retry.`);
|
|
5615
5617
|
});
|
|
5616
|
-
if (stats.fileCount === 0) throw deployError("ERR_DEPLOY_EMPTY_OUTDIR", `${ERROR_PREFIX$
|
|
5618
|
+
if (stats.fileCount === 0) throw deployError("ERR_DEPLOY_EMPTY_OUTDIR", `${ERROR_PREFIX$5}: outDir ${JSON.stringify(config.outDir)} is empty — nothing to deploy.`);
|
|
5617
5619
|
const limit = resolveFileLimit(env);
|
|
5618
|
-
if (stats.fileCount > limit) throw deployError("ERR_DEPLOY_TOO_MANY_FILES", `${ERROR_PREFIX$
|
|
5619
|
-
if (stats.oversizePath !== null) throw deployError("ERR_DEPLOY_FILE_TOO_LARGE", `${ERROR_PREFIX$
|
|
5620
|
+
if (stats.fileCount > limit) throw deployError("ERR_DEPLOY_TOO_MANY_FILES", `${ERROR_PREFIX$5}: outDir contains ${stats.fileCount} files; the limit is ${limit}.\n Raise it with ${MAX_FILES_ENV} (paid tier) or reduce the output.`);
|
|
5621
|
+
if (stats.oversizePath !== null) throw deployError("ERR_DEPLOY_FILE_TOO_LARGE", `${ERROR_PREFIX$5}: file ${JSON.stringify(stats.oversizePath)} exceeds the 25 MiB per-file limit.`);
|
|
5620
5622
|
}
|
|
5621
5623
|
//#endregion
|
|
5622
5624
|
//#region src/plugins/deploy/slug.ts
|
|
@@ -5661,7 +5663,7 @@ function toSlug(name) {
|
|
|
5661
5663
|
//#endregion
|
|
5662
5664
|
//#region src/plugins/deploy/api.ts
|
|
5663
5665
|
/** Error prefix for deploy config/validation failures (spec/11 Part-3). */
|
|
5664
|
-
const ERROR_PREFIX$
|
|
5666
|
+
const ERROR_PREFIX$4 = "[web] deploy";
|
|
5665
5667
|
/** `YYYY-MM-DD` validator for the compatibility date config field. */
|
|
5666
5668
|
const COMPAT_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
5667
5669
|
/**
|
|
@@ -5674,12 +5676,12 @@ const COMPAT_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
5674
5676
|
* @example
|
|
5675
5677
|
* createPlugin("deploy", { onInit: validateConfig });
|
|
5676
5678
|
*/
|
|
5677
|
-
function validateConfig(ctx) {
|
|
5679
|
+
function validateConfig$1(ctx) {
|
|
5678
5680
|
const { config } = ctx;
|
|
5679
|
-
if (config.target !== "cloudflare-pages") throw deployError("ERR_DEPLOY_CONFIG", `${ERROR_PREFIX$
|
|
5680
|
-
if (typeof config.outDir !== "string" || config.outDir.length === 0) throw deployError("ERR_DEPLOY_CONFIG", `${ERROR_PREFIX$
|
|
5681
|
-
if (!Array.isArray(config.scrubAllowlist) || !config.scrubAllowlist.every((item) => typeof item === "string")) throw deployError("ERR_DEPLOY_CONFIG", `${ERROR_PREFIX$
|
|
5682
|
-
if (config.compatibilityDate !== void 0 && !COMPAT_DATE_REGEX.test(config.compatibilityDate)) throw deployError("ERR_DEPLOY_CONFIG", `${ERROR_PREFIX$
|
|
5681
|
+
if (config.target !== "cloudflare-pages") throw deployError("ERR_DEPLOY_CONFIG", `${ERROR_PREFIX$4}: target ${JSON.stringify(config.target)} is unsupported.\n Only "cloudflare-pages" is supported in this version.`);
|
|
5682
|
+
if (typeof config.outDir !== "string" || config.outDir.length === 0) throw deployError("ERR_DEPLOY_CONFIG", `${ERROR_PREFIX$4}: outDir must be a non-empty string.\n Set pluginConfigs.deploy.outDir to your build output directory (e.g. "dist").`);
|
|
5683
|
+
if (!Array.isArray(config.scrubAllowlist) || !config.scrubAllowlist.every((item) => typeof item === "string")) throw deployError("ERR_DEPLOY_CONFIG", `${ERROR_PREFIX$4}: scrubAllowlist must be an array of strings.`);
|
|
5684
|
+
if (config.compatibilityDate !== void 0 && !COMPAT_DATE_REGEX.test(config.compatibilityDate)) throw deployError("ERR_DEPLOY_CONFIG", `${ERROR_PREFIX$4}: compatibilityDate ${JSON.stringify(config.compatibilityDate)} must be in YYYY-MM-DD form.`);
|
|
5683
5685
|
ctx.require(sitePlugin);
|
|
5684
5686
|
}
|
|
5685
5687
|
/**
|
|
@@ -5695,7 +5697,7 @@ function validateConfig(ctx) {
|
|
|
5695
5697
|
* const api = createApi(ctx);
|
|
5696
5698
|
* await api.run({ branch: "preview/landing" });
|
|
5697
5699
|
*/
|
|
5698
|
-
function createApi$
|
|
5700
|
+
function createApi$2(ctx) {
|
|
5699
5701
|
return {
|
|
5700
5702
|
/**
|
|
5701
5703
|
* Deploy the built outDir to Cloudflare Pages via the wrangler subprocess.
|
|
@@ -5786,7 +5788,7 @@ function createApi$1(ctx) {
|
|
|
5786
5788
|
* createPlugin("deploy", { config: defaultConfig });
|
|
5787
5789
|
* ```
|
|
5788
5790
|
*/
|
|
5789
|
-
const defaultConfig = {
|
|
5791
|
+
const defaultConfig$1 = {
|
|
5790
5792
|
target: "cloudflare-pages",
|
|
5791
5793
|
outDir: "dist",
|
|
5792
5794
|
productionBranch: "main",
|
|
@@ -5839,7 +5841,7 @@ const defaultSpawn = (cmd, options) => {
|
|
|
5839
5841
|
* @example
|
|
5840
5842
|
* const state = createState({ global: {}, config });
|
|
5841
5843
|
*/
|
|
5842
|
-
function createState$
|
|
5844
|
+
function createState$2(_ctx) {
|
|
5843
5845
|
return {
|
|
5844
5846
|
lastDeployment: null,
|
|
5845
5847
|
spawn: defaultSpawn
|
|
@@ -5873,11 +5875,1172 @@ function createState$1(_ctx) {
|
|
|
5873
5875
|
* ```
|
|
5874
5876
|
*/
|
|
5875
5877
|
const deployPlugin = createPlugin$1("deploy", {
|
|
5876
|
-
config: defaultConfig,
|
|
5878
|
+
config: defaultConfig$1,
|
|
5877
5879
|
depends: [sitePlugin],
|
|
5878
|
-
createState: createState$
|
|
5880
|
+
createState: createState$2,
|
|
5879
5881
|
events: deployEvents,
|
|
5880
|
-
onInit: validateConfig,
|
|
5882
|
+
onInit: validateConfig$1,
|
|
5883
|
+
api: createApi$2
|
|
5884
|
+
});
|
|
5885
|
+
//#endregion
|
|
5886
|
+
//#region src/plugins/cli/errors.ts
|
|
5887
|
+
/** Error prefix for cli config/validation/runtime failures (spec/11 Part-3). */
|
|
5888
|
+
const ERROR_PREFIX$3 = "[web] cli";
|
|
5889
|
+
/**
|
|
5890
|
+
* Construct a cli `Error` carrying a taxonomy `code` property. Centralizes the
|
|
5891
|
+
* `Object.assign(new Error(message), { code })` pattern so the `code` is always
|
|
5892
|
+
* preserved on the thrown value (the message is expected to already be prefixed).
|
|
5893
|
+
*
|
|
5894
|
+
* @param code - The cli error `code` from the taxonomy.
|
|
5895
|
+
* @param message - The actionable error message.
|
|
5896
|
+
* @returns An `Error` whose `code` property is set.
|
|
5897
|
+
* @example
|
|
5898
|
+
* throw cliError("ERR_CLI_CONFIG", "[web] cli: port must be 1–65535.");
|
|
5899
|
+
*/
|
|
5900
|
+
function cliError(code, message) {
|
|
5901
|
+
return Object.assign(new Error(message), { code });
|
|
5902
|
+
}
|
|
5903
|
+
/** The live-reload client snippet injected before `</body>` in served HTML. */
|
|
5904
|
+
const RELOAD_CLIENT = `<script>(()=>{try{const s=new EventSource("/__moku_reload");s.addEventListener("reload",()=>location.reload());}catch{}})();<\/script>`;
|
|
5905
|
+
/**
|
|
5906
|
+
* Inject the live-reload SSE client immediately before the closing `</body>` (or
|
|
5907
|
+
* append it when there is no `</body>`). Pure — unit-testable without a server.
|
|
5908
|
+
*
|
|
5909
|
+
* @param html - The page HTML to augment.
|
|
5910
|
+
* @returns The HTML with the reload client injected.
|
|
5911
|
+
* @example
|
|
5912
|
+
* injectReloadClient("<body>hi</body>"); // "<body>hi<script>…<\/script></body>"
|
|
5913
|
+
*/
|
|
5914
|
+
function injectReloadClient(html) {
|
|
5915
|
+
const index = html.lastIndexOf("</body>");
|
|
5916
|
+
return index === -1 ? html + RELOAD_CLIENT : html.slice(0, index) + RELOAD_CLIENT + html.slice(index);
|
|
5917
|
+
}
|
|
5918
|
+
/**
|
|
5919
|
+
* Run one rebuild and report the result. Skips re-entrancy via the shared `building`
|
|
5920
|
+
* flag and routes success to `onReloaded`, failure to `onError`.
|
|
5921
|
+
*
|
|
5922
|
+
* @param input - The rebuild dependencies + the changed file.
|
|
5923
|
+
* @param input.runBuild - Runs one build and resolves with its summary.
|
|
5924
|
+
* @param input.onReloaded - Called with the changed file + summary after a rebuild.
|
|
5925
|
+
* @param input.onError - Called when a rebuild throws.
|
|
5926
|
+
* @param input.file - The changed file to report alongside the summary.
|
|
5927
|
+
* @returns Resolves once the rebuild settles (always — errors are routed, not thrown).
|
|
5928
|
+
* @example
|
|
5929
|
+
* await runOneRebuild({ runBuild, onReloaded, onError, file: "a.md" });
|
|
5930
|
+
*/
|
|
5931
|
+
async function runOneRebuild(input) {
|
|
5932
|
+
try {
|
|
5933
|
+
const summary = await input.runBuild();
|
|
5934
|
+
input.onReloaded({
|
|
5935
|
+
file: input.file,
|
|
5936
|
+
pageCount: summary.pageCount,
|
|
5937
|
+
durationMs: summary.durationMs
|
|
5938
|
+
});
|
|
5939
|
+
} catch (error) {
|
|
5940
|
+
input.onError(error);
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
/**
|
|
5944
|
+
* Create a {@link Rebuilder}. The latest changed file within the debounce window
|
|
5945
|
+
* wins; while a rebuild is in flight further notifications are dropped (the watcher
|
|
5946
|
+
* keeps firing, but only one build runs at a time, matching the blog `dev.ts`).
|
|
5947
|
+
*
|
|
5948
|
+
* @param input - The rebuild dependencies.
|
|
5949
|
+
* @param input.debounceMs - Debounce window in milliseconds.
|
|
5950
|
+
* @param input.runBuild - Runs one build and resolves with its summary.
|
|
5951
|
+
* @param input.onReloaded - Called with the changed file + summary after a rebuild.
|
|
5952
|
+
* @param input.onError - Called when a rebuild throws.
|
|
5953
|
+
* @returns The debounced rebuild driver.
|
|
5954
|
+
* @example
|
|
5955
|
+
* createRebuilder({ debounceMs: 150, runBuild, onReloaded, onError });
|
|
5956
|
+
*/
|
|
5957
|
+
function createRebuilder(input) {
|
|
5958
|
+
let timer;
|
|
5959
|
+
let pendingFile = "";
|
|
5960
|
+
let building = false;
|
|
5961
|
+
/**
|
|
5962
|
+
* Run the queued rebuild once (skips when one is already in flight), resetting the
|
|
5963
|
+
* in-flight flag when it settles.
|
|
5964
|
+
*
|
|
5965
|
+
* @returns Resolves once the rebuild settles (errors are routed, never thrown).
|
|
5966
|
+
* @example
|
|
5967
|
+
* await fire();
|
|
5968
|
+
*/
|
|
5969
|
+
const fire = async () => {
|
|
5970
|
+
timer = void 0;
|
|
5971
|
+
if (building) return;
|
|
5972
|
+
building = true;
|
|
5973
|
+
await runOneRebuild({
|
|
5974
|
+
runBuild: input.runBuild,
|
|
5975
|
+
onReloaded: input.onReloaded,
|
|
5976
|
+
onError: input.onError,
|
|
5977
|
+
file: pendingFile
|
|
5978
|
+
});
|
|
5979
|
+
building = false;
|
|
5980
|
+
};
|
|
5981
|
+
return {
|
|
5982
|
+
/**
|
|
5983
|
+
* Queue a rebuild for the changed file (debounced + coalesced).
|
|
5984
|
+
*
|
|
5985
|
+
* @param file - The changed path that triggered the rebuild.
|
|
5986
|
+
* @example
|
|
5987
|
+
* rebuilder.schedule("a.md");
|
|
5988
|
+
*/
|
|
5989
|
+
schedule(file) {
|
|
5990
|
+
pendingFile = file;
|
|
5991
|
+
if (timer) clearTimeout(timer);
|
|
5992
|
+
timer = setTimeout(fire, input.debounceMs);
|
|
5993
|
+
},
|
|
5994
|
+
/**
|
|
5995
|
+
* Cancel any pending (not-yet-fired) rebuild timer.
|
|
5996
|
+
*
|
|
5997
|
+
* @example
|
|
5998
|
+
* rebuilder.cancel();
|
|
5999
|
+
*/
|
|
6000
|
+
cancel() {
|
|
6001
|
+
if (timer) clearTimeout(timer);
|
|
6002
|
+
timer = void 0;
|
|
6003
|
+
}
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
6006
|
+
/**
|
|
6007
|
+
* Install SIGINT/SIGTERM handlers that run `teardown()` and resolve the returned
|
|
6008
|
+
* promise, so a long-running command (`serve`/`preview`) unblocks its `await` on
|
|
6009
|
+
* Ctrl-C / termination and detaches its own listeners. Used by both servers.
|
|
6010
|
+
*
|
|
6011
|
+
* @param teardown - Cleanup to run on the first termination signal.
|
|
6012
|
+
* @returns A promise that resolves once a termination signal has been handled.
|
|
6013
|
+
* @example
|
|
6014
|
+
* await installSignalTeardown(() => server.stop());
|
|
6015
|
+
*/
|
|
6016
|
+
function installSignalTeardown(teardown) {
|
|
6017
|
+
return new Promise((resolve) => {
|
|
6018
|
+
/**
|
|
6019
|
+
* Detach both signal listeners, run teardown, and resolve the wait (once).
|
|
6020
|
+
*
|
|
6021
|
+
* @example
|
|
6022
|
+
* onSignal();
|
|
6023
|
+
*/
|
|
6024
|
+
const onSignal = () => {
|
|
6025
|
+
process.off("SIGINT", onSignal);
|
|
6026
|
+
process.off("SIGTERM", onSignal);
|
|
6027
|
+
teardown();
|
|
6028
|
+
resolve();
|
|
6029
|
+
};
|
|
6030
|
+
process.on("SIGINT", onSignal);
|
|
6031
|
+
process.on("SIGTERM", onSignal);
|
|
6032
|
+
});
|
|
6033
|
+
}
|
|
6034
|
+
/** The SSE comment line sent on connect to open the stream. */
|
|
6035
|
+
const SSE_OPEN = ": connected\n\n";
|
|
6036
|
+
/** The SSE frame pushed to reload a connected browser. */
|
|
6037
|
+
const SSE_RELOAD = "event: reload\ndata: 1\n\n";
|
|
6038
|
+
/**
|
|
6039
|
+
* Create a {@link ReloadHub} backed by `ReadableStream` controllers. Each `connect()`
|
|
6040
|
+
* enqueues into a new stream; `reloadAll()` writes the reload frame to every live
|
|
6041
|
+
* controller (dropping any that have closed).
|
|
6042
|
+
*
|
|
6043
|
+
* @returns The reload hub.
|
|
6044
|
+
* @example
|
|
6045
|
+
* const hub = createReloadHub();
|
|
6046
|
+
*/
|
|
6047
|
+
function createReloadHub() {
|
|
6048
|
+
const encoder = new TextEncoder();
|
|
6049
|
+
const clients = /* @__PURE__ */ new Set();
|
|
6050
|
+
return {
|
|
6051
|
+
/**
|
|
6052
|
+
* Open one SSE connection, register its controller, and return the streaming
|
|
6053
|
+
* `Response` (a connect comment is sent immediately to open the stream).
|
|
6054
|
+
*
|
|
6055
|
+
* @returns A `text/event-stream` response wired to this hub.
|
|
6056
|
+
* @example
|
|
6057
|
+
* return hub.connect();
|
|
6058
|
+
*/
|
|
6059
|
+
connect() {
|
|
6060
|
+
let owned;
|
|
6061
|
+
const stream = new ReadableStream({
|
|
6062
|
+
/**
|
|
6063
|
+
* Register the stream's controller and send the opening comment.
|
|
6064
|
+
*
|
|
6065
|
+
* @param controller - The new stream's controller.
|
|
6066
|
+
* @example
|
|
6067
|
+
* start(controller);
|
|
6068
|
+
*/
|
|
6069
|
+
start(controller) {
|
|
6070
|
+
owned = controller;
|
|
6071
|
+
clients.add(controller);
|
|
6072
|
+
controller.enqueue(encoder.encode(SSE_OPEN));
|
|
6073
|
+
},
|
|
6074
|
+
/**
|
|
6075
|
+
* Drop this client's controller when the browser disconnects.
|
|
6076
|
+
*
|
|
6077
|
+
* @example
|
|
6078
|
+
* cancel();
|
|
6079
|
+
*/
|
|
6080
|
+
cancel() {
|
|
6081
|
+
if (owned) clients.delete(owned);
|
|
6082
|
+
}
|
|
6083
|
+
});
|
|
6084
|
+
return new Response(stream, { headers: {
|
|
6085
|
+
"content-type": "text/event-stream",
|
|
6086
|
+
"cache-control": "no-cache",
|
|
6087
|
+
connection: "keep-alive"
|
|
6088
|
+
} });
|
|
6089
|
+
},
|
|
6090
|
+
/**
|
|
6091
|
+
* Push a `reload` frame to every connected client, dropping any closed ones.
|
|
6092
|
+
*
|
|
6093
|
+
* @example
|
|
6094
|
+
* hub.reloadAll();
|
|
6095
|
+
*/
|
|
6096
|
+
reloadAll() {
|
|
6097
|
+
for (const controller of clients) try {
|
|
6098
|
+
controller.enqueue(encoder.encode(SSE_RELOAD));
|
|
6099
|
+
} catch {
|
|
6100
|
+
clients.delete(controller);
|
|
6101
|
+
}
|
|
6102
|
+
},
|
|
6103
|
+
/**
|
|
6104
|
+
* The number of currently-connected clients.
|
|
6105
|
+
*
|
|
6106
|
+
* @returns The live client count.
|
|
6107
|
+
* @example
|
|
6108
|
+
* hub.size();
|
|
6109
|
+
*/
|
|
6110
|
+
size() {
|
|
6111
|
+
return clients.size;
|
|
6112
|
+
}
|
|
6113
|
+
};
|
|
6114
|
+
}
|
|
6115
|
+
/**
|
|
6116
|
+
* Build the live-reload-aware request handler for the dev server: serves the SSE
|
|
6117
|
+
* stream at {@link RELOAD_PATH}, injects the reload client into HTML responses (when
|
|
6118
|
+
* `liveReload`), and falls through to the {@link resolveCleanUrl} static resolver for
|
|
6119
|
+
* everything else.
|
|
6120
|
+
*
|
|
6121
|
+
* @param ctx - The cli plugin context (provides `config` + `state.fileResponse`).
|
|
6122
|
+
* @param hub - The reload hub the SSE endpoint connects to.
|
|
6123
|
+
* @returns The `fetch` handler passed to the static server.
|
|
6124
|
+
* @example
|
|
6125
|
+
* const handler = createDevHandler(ctx, hub);
|
|
6126
|
+
*/
|
|
6127
|
+
function createDevHandler(ctx, hub) {
|
|
6128
|
+
return async (request) => {
|
|
6129
|
+
const pathname = decodeURIComponent(new URL(request.url).pathname);
|
|
6130
|
+
if (pathname === "/__moku_reload") return hub.connect();
|
|
6131
|
+
const resolved = resolveCleanUrl(ctx.config.outDir, pathname);
|
|
6132
|
+
if (resolved.file === null) return new Response("Not Found", { status: 404 });
|
|
6133
|
+
const response = ctx.state.fileResponse(resolved.file, resolved.status);
|
|
6134
|
+
if (ctx.config.liveReload && resolved.file.endsWith(".html")) {
|
|
6135
|
+
const html = injectReloadClient(await response.text());
|
|
6136
|
+
return new Response(html, {
|
|
6137
|
+
status: resolved.status,
|
|
6138
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
6139
|
+
});
|
|
6140
|
+
}
|
|
6141
|
+
return response;
|
|
6142
|
+
};
|
|
6143
|
+
}
|
|
6144
|
+
/**
|
|
6145
|
+
* Run the dev loop: an initial build, an in-process static server that injects the
|
|
6146
|
+
* live-reload client, a recursive watcher over `config.watchDirs`, and a debounced
|
|
6147
|
+
* rebuild that re-renders and pushes a browser reload. Resolves on SIGINT/SIGTERM,
|
|
6148
|
+
* which stops the server, closes the watchers, and cancels any pending rebuild.
|
|
6149
|
+
*
|
|
6150
|
+
* @param ctx - The cli plugin context (config, state seams, `require`).
|
|
6151
|
+
* @param port - The port to bind the dev server to.
|
|
6152
|
+
* @returns Resolves once the server has been torn down by a termination signal.
|
|
6153
|
+
* @example
|
|
6154
|
+
* await runDevServer(ctx, 4173);
|
|
6155
|
+
*/
|
|
6156
|
+
async function runDevServer(ctx, port) {
|
|
6157
|
+
await ctx.require(buildPlugin).run();
|
|
6158
|
+
const hub = createReloadHub();
|
|
6159
|
+
const server = ctx.state.serveStatic({
|
|
6160
|
+
port,
|
|
6161
|
+
fetch: createDevHandler(ctx, hub)
|
|
6162
|
+
});
|
|
6163
|
+
const rebuilder = createRebuilder({
|
|
6164
|
+
debounceMs: ctx.config.debounceMs,
|
|
6165
|
+
/**
|
|
6166
|
+
* Re-run the SSG build for a rebuild.
|
|
6167
|
+
*
|
|
6168
|
+
* @returns The rebuild summary.
|
|
6169
|
+
* @example
|
|
6170
|
+
* await runBuild();
|
|
6171
|
+
*/
|
|
6172
|
+
runBuild() {
|
|
6173
|
+
return ctx.require(buildPlugin).run();
|
|
6174
|
+
},
|
|
6175
|
+
/**
|
|
6176
|
+
* Render the reload line and push a browser reload after a rebuild.
|
|
6177
|
+
*
|
|
6178
|
+
* @param info - The changed file plus the rebuild's page count and duration.
|
|
6179
|
+
* @example
|
|
6180
|
+
* onReloaded({ file: "a.md", pageCount: 1, durationMs: 10 });
|
|
6181
|
+
*/
|
|
6182
|
+
onReloaded(info) {
|
|
6183
|
+
ctx.state.render.reload(info);
|
|
6184
|
+
hub.reloadAll();
|
|
6185
|
+
},
|
|
6186
|
+
/**
|
|
6187
|
+
* Render a rebuild failure (the dev loop keeps running).
|
|
6188
|
+
*
|
|
6189
|
+
* @param error - The thrown rebuild error.
|
|
6190
|
+
* @example
|
|
6191
|
+
* onError(new Error("boom"));
|
|
6192
|
+
*/
|
|
6193
|
+
onError(error) {
|
|
6194
|
+
ctx.state.render.error("rebuild failed", error);
|
|
6195
|
+
}
|
|
6196
|
+
});
|
|
6197
|
+
const watchers = ctx.config.watchDirs.map((dir) => ctx.state.watch(dir, () => rebuilder.schedule(dir)));
|
|
6198
|
+
ctx.state.render.serverReady({
|
|
6199
|
+
local: `http://localhost:${port}`,
|
|
6200
|
+
network: ctx.state.networkUrl(port),
|
|
6201
|
+
watching: ctx.config.watchDirs
|
|
6202
|
+
});
|
|
6203
|
+
return installSignalTeardown(() => {
|
|
6204
|
+
rebuilder.cancel();
|
|
6205
|
+
for (const watcher of watchers) watcher.close();
|
|
6206
|
+
server.stop();
|
|
6207
|
+
});
|
|
6208
|
+
}
|
|
6209
|
+
//#endregion
|
|
6210
|
+
//#region src/plugins/cli/preview.ts
|
|
6211
|
+
/**
|
|
6212
|
+
* @file cli plugin — static preview server for the built `dist/`. Exposes a PURE,
|
|
6213
|
+
* server-agnostic clean-URL resolver (`resolveCleanUrl`) — unit-tested without a
|
|
6214
|
+
* socket — plus `runPreviewServer`, which serves the resolved files via `Bun.serve`
|
|
6215
|
+
* the way Cloudflare Pages does (trailing slash → `index.html`, extensionless →
|
|
6216
|
+
* `<path>/index.html`, a miss → the nearest `404.html`). No reload injection.
|
|
6217
|
+
*/
|
|
6218
|
+
/**
|
|
6219
|
+
* Strip leading `../` segments (after `normalize`) so a request can never escape the
|
|
6220
|
+
* served root via path traversal.
|
|
6221
|
+
*
|
|
6222
|
+
* @param pathname - The decoded request pathname.
|
|
6223
|
+
* @returns The traversal-safe relative path.
|
|
6224
|
+
* @example
|
|
6225
|
+
* safePath("../../etc/passwd"); // "etc/passwd"
|
|
6226
|
+
*/
|
|
6227
|
+
function safePath(pathname) {
|
|
6228
|
+
return node_path$1.default.normalize(pathname).replace(/^(\.\.(?:[/\\]|$))+/, "");
|
|
6229
|
+
}
|
|
6230
|
+
/**
|
|
6231
|
+
* The default {@link FileProbe} backed by `node:fs.statSync` — a single stat (no
|
|
6232
|
+
* exists+stat race) that swallows the ENOENT thrown for a missing path.
|
|
6233
|
+
*
|
|
6234
|
+
* @param filePath - Candidate on-disk path.
|
|
6235
|
+
* @returns Whether it resolves to a regular file.
|
|
6236
|
+
* @example
|
|
6237
|
+
* statIsFile("/dist/index.html");
|
|
6238
|
+
*/
|
|
6239
|
+
function statIsFile(filePath) {
|
|
6240
|
+
try {
|
|
6241
|
+
return (0, node_fs.statSync)(filePath).isFile();
|
|
6242
|
+
} catch {
|
|
6243
|
+
return false;
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
/**
|
|
6247
|
+
* Resolve a request pathname to a real file under `rootDir`, mirroring Cloudflare
|
|
6248
|
+
* Pages clean URLs. Pure and server-agnostic: it touches the filesystem only through
|
|
6249
|
+
* the injected {@link FileProbe}, so it is unit-tested without a server. A trailing
|
|
6250
|
+
* slash maps to `index.html`; an extensionless path tries the file then
|
|
6251
|
+
* `<path>/index.html`; a miss climbs toward the root for the nearest `404.html`
|
|
6252
|
+
* (served with status `404`).
|
|
6253
|
+
*
|
|
6254
|
+
* @param rootDir - The absolute (or cwd-relative) build output directory.
|
|
6255
|
+
* @param pathname - The decoded request pathname (always starts with `/`).
|
|
6256
|
+
* @param isFile - File-existence probe (defaults to {@link statIsFile}).
|
|
6257
|
+
* @returns The resolved file + status (file `null` when not even a `404.html` exists).
|
|
6258
|
+
* @example
|
|
6259
|
+
* resolveCleanUrl("dist", "/about/", path => set.has(path));
|
|
6260
|
+
*/
|
|
6261
|
+
function resolveCleanUrl(rootDir, pathname, isFile = statIsFile) {
|
|
6262
|
+
const relative = safePath(pathname);
|
|
6263
|
+
const base = node_path$1.default.join(rootDir, relative);
|
|
6264
|
+
const candidates = pathname.endsWith("/") ? [node_path$1.default.join(base, "index.html")] : [base, node_path$1.default.join(base, "index.html")];
|
|
6265
|
+
for (const candidate of candidates) if (isFile(candidate)) return {
|
|
6266
|
+
file: candidate,
|
|
6267
|
+
status: 200
|
|
6268
|
+
};
|
|
6269
|
+
const segments = node_path$1.default.join(rootDir, relative).split(node_path$1.default.sep).filter(Boolean);
|
|
6270
|
+
for (let depth = segments.length; depth >= 1; depth--) {
|
|
6271
|
+
const candidate = node_path$1.default.join(segments.slice(0, depth).join(node_path$1.default.sep), "404.html");
|
|
6272
|
+
if (isFile(candidate)) return {
|
|
6273
|
+
file: candidate,
|
|
6274
|
+
status: 404
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
6277
|
+
const root = node_path$1.default.join(rootDir, "404.html");
|
|
6278
|
+
return isFile(root) ? {
|
|
6279
|
+
file: root,
|
|
6280
|
+
status: 404
|
|
6281
|
+
} : {
|
|
6282
|
+
file: null,
|
|
6283
|
+
status: 404
|
|
6284
|
+
};
|
|
6285
|
+
}
|
|
6286
|
+
/**
|
|
6287
|
+
* Build the request handler for the preview server: resolves each request via
|
|
6288
|
+
* {@link resolveCleanUrl} and serves the file (no reload injection, mirroring prod).
|
|
6289
|
+
*
|
|
6290
|
+
* @param ctx - The cli plugin context (provides `config` + `state.fileResponse`).
|
|
6291
|
+
* @returns The `fetch` handler passed to the static server.
|
|
6292
|
+
* @example
|
|
6293
|
+
* const handler = createPreviewHandler(ctx);
|
|
6294
|
+
*/
|
|
6295
|
+
function createPreviewHandler(ctx) {
|
|
6296
|
+
return (request) => {
|
|
6297
|
+
const pathname = decodeURIComponent(new URL(request.url).pathname);
|
|
6298
|
+
const resolved = resolveCleanUrl(ctx.config.outDir, pathname);
|
|
6299
|
+
if (resolved.file === null) return new Response("Not Found", { status: 404 });
|
|
6300
|
+
return ctx.state.fileResponse(resolved.file, resolved.status);
|
|
6301
|
+
};
|
|
6302
|
+
}
|
|
6303
|
+
/**
|
|
6304
|
+
* Run the static preview server for the built `dist/`. Serves files resolved by
|
|
6305
|
+
* {@link resolveCleanUrl} via the injectable static-server seam — with no live-reload
|
|
6306
|
+
* injection, mirroring production. Renders the server-ready panel and resolves on
|
|
6307
|
+
* SIGINT/SIGTERM.
|
|
6308
|
+
*
|
|
6309
|
+
* @param ctx - The cli plugin context (provides `config` + `state` seams).
|
|
6310
|
+
* @param port - The port to bind to.
|
|
6311
|
+
* @returns Resolves once the server has been torn down by a termination signal.
|
|
6312
|
+
* @example
|
|
6313
|
+
* await runPreviewServer(ctx, 4173);
|
|
6314
|
+
*/
|
|
6315
|
+
function runPreviewServer(ctx, port) {
|
|
6316
|
+
const server = ctx.state.serveStatic({
|
|
6317
|
+
port,
|
|
6318
|
+
fetch: createPreviewHandler(ctx)
|
|
6319
|
+
});
|
|
6320
|
+
ctx.state.render.serverReady({
|
|
6321
|
+
local: `http://localhost:${port}`,
|
|
6322
|
+
network: ctx.state.networkUrl(port)
|
|
6323
|
+
});
|
|
6324
|
+
return installSignalTeardown(() => server.stop());
|
|
6325
|
+
}
|
|
6326
|
+
//#endregion
|
|
6327
|
+
//#region src/plugins/cli/api.ts
|
|
6328
|
+
/**
|
|
6329
|
+
* @file cli plugin — API factory (build · serve · preview · deploy), the cli plugin
|
|
6330
|
+
* context type, and config-only `validateConfig`. The four closures are wiring-thin:
|
|
6331
|
+
* each renders the Panel header, then delegates to `build`/`deploy` (via `require`)
|
|
6332
|
+
* or to the server modules. Live build/deploy progress arrives through hooks (in
|
|
6333
|
+
* `index.ts`), so the methods' return values come from the awaited `run()` results.
|
|
6334
|
+
*/
|
|
6335
|
+
/** Lowest valid TCP port. */
|
|
6336
|
+
const MIN_PORT = 1;
|
|
6337
|
+
/** Highest valid TCP port. */
|
|
6338
|
+
const MAX_PORT = 65535;
|
|
6339
|
+
/**
|
|
6340
|
+
* Validate the resolved cli config during `onInit` (config-only — no resource
|
|
6341
|
+
* allocation, per spec/06 §2). Throws `ERR_CLI_CONFIG` (`[web] cli: …`) when `port`
|
|
6342
|
+
* is not an integer in 1–65535, `outDir`/`notFoundFile` are not non-empty strings,
|
|
6343
|
+
* `watchDirs` is not a non-empty string array, or `debounceMs` is negative.
|
|
6344
|
+
*
|
|
6345
|
+
* @param config - The resolved cli configuration to validate.
|
|
6346
|
+
* @throws {Error} `ERR_CLI_CONFIG` when any field is invalid.
|
|
6347
|
+
* @example
|
|
6348
|
+
* validateConfig({ outDir: "dist", port: 4173, watchDirs: ["content"], debounceMs: 150, notFoundFile: "404.html", liveReload: true });
|
|
6349
|
+
*/
|
|
6350
|
+
function validateConfig(config) {
|
|
6351
|
+
if (!Number.isInteger(config.port) || config.port < MIN_PORT || config.port > MAX_PORT) throw cliError("ERR_CLI_CONFIG", `${ERROR_PREFIX$3}: port must be an integer in ${MIN_PORT}–${MAX_PORT}.\n Set pluginConfigs.cli.port to a valid TCP port (e.g. 4173).`);
|
|
6352
|
+
if (typeof config.outDir !== "string" || config.outDir.length === 0) throw cliError("ERR_CLI_CONFIG", `${ERROR_PREFIX$3}: outDir must be a non-empty string.\n Set pluginConfigs.cli.outDir to your build output directory (e.g. "dist").`);
|
|
6353
|
+
if (typeof config.notFoundFile !== "string" || config.notFoundFile.length === 0) throw cliError("ERR_CLI_CONFIG", `${ERROR_PREFIX$3}: notFoundFile must be a non-empty string.\n Set pluginConfigs.cli.notFoundFile to the not-found page filename (e.g. "404.html").`);
|
|
6354
|
+
if (!Array.isArray(config.watchDirs) || config.watchDirs.length === 0 || !config.watchDirs.every((dir) => typeof dir === "string" && dir.length > 0)) throw cliError("ERR_CLI_CONFIG", `${ERROR_PREFIX$3}: watchDirs must be a non-empty array of non-empty strings.\n Set pluginConfigs.cli.watchDirs to the directories serve() should watch (e.g. ["content", "src"]).`);
|
|
6355
|
+
if (typeof config.debounceMs !== "number" || config.debounceMs < 0) throw cliError("ERR_CLI_CONFIG", `${ERROR_PREFIX$3}: debounceMs must be a number >= 0.\n Set pluginConfigs.cli.debounceMs to the rebuild debounce window in milliseconds (e.g. 150).`);
|
|
6356
|
+
}
|
|
6357
|
+
/**
|
|
6358
|
+
* Create the cli plugin API surface — exactly `build`, `serve`, `preview`, `deploy`.
|
|
6359
|
+
* Each method renders `state.render.header(<command>)` first, then does its work;
|
|
6360
|
+
* live progress is rendered by the hooks wired in `index.ts`, so each method's
|
|
6361
|
+
* return value comes from the awaited `build.run()` / `deploy.run()` result.
|
|
6362
|
+
*
|
|
6363
|
+
* @param ctx - Plugin context (provides `require`, `state`, `config`).
|
|
6364
|
+
* @returns The {@link Api} surface mounted at `app.cli`.
|
|
6365
|
+
* @example
|
|
6366
|
+
* const api = createApi(ctx);
|
|
6367
|
+
* await api.build();
|
|
6368
|
+
*/
|
|
6369
|
+
function createApi$1(ctx) {
|
|
6370
|
+
return {
|
|
6371
|
+
/**
|
|
6372
|
+
* Run the SSG build and (by default) assert the not-found page exists.
|
|
6373
|
+
*
|
|
6374
|
+
* @param options - Optional `assertNotFound` toggle (default `true`).
|
|
6375
|
+
* @returns The build summary (`outDir`, `pageCount`, `durationMs`).
|
|
6376
|
+
* @throws {Error} `ERR_CLI_NOT_FOUND` when the not-found page is missing and asserted.
|
|
6377
|
+
* @example
|
|
6378
|
+
* await api.build();
|
|
6379
|
+
*/
|
|
6380
|
+
async build(options = {}) {
|
|
6381
|
+
const { assertNotFound = true } = options;
|
|
6382
|
+
ctx.state.render.header("build");
|
|
6383
|
+
const result = await ctx.require(buildPlugin).run();
|
|
6384
|
+
const page = node_path$1.default.join(ctx.config.outDir, ctx.config.notFoundFile);
|
|
6385
|
+
if (assertNotFound && !(0, node_fs.existsSync)(page)) {
|
|
6386
|
+
ctx.state.render.error(`${page} missing — set build.notFound (CF Pages would flip to SPA mode)`);
|
|
6387
|
+
throw cliError("ERR_CLI_NOT_FOUND", `${ERROR_PREFIX$3}: ${page} missing after build.\n Set build.notFound so the SSG emits it (CF Pages flips to SPA mode without a top-level 404), or pass { assertNotFound: false } to skip this check.`);
|
|
6388
|
+
}
|
|
6389
|
+
return result;
|
|
6390
|
+
},
|
|
6391
|
+
/**
|
|
6392
|
+
* Dev loop: build once, serve `dist/` in-process (live-reload injected), watch
|
|
6393
|
+
* `watchDirs`, debounced rebuild + reload. Resolves on SIGINT/SIGTERM.
|
|
6394
|
+
*
|
|
6395
|
+
* @param options - Optional port override (defaults to `config.port`).
|
|
6396
|
+
* @returns Resolves once the server has been torn down.
|
|
6397
|
+
* @example
|
|
6398
|
+
* await api.serve({ port: 3000 });
|
|
6399
|
+
*/
|
|
6400
|
+
serve(options = {}) {
|
|
6401
|
+
const { port = ctx.config.port } = options;
|
|
6402
|
+
ctx.state.render.header("serve");
|
|
6403
|
+
return runDevServer(ctx, port);
|
|
6404
|
+
},
|
|
6405
|
+
/**
|
|
6406
|
+
* Static preview of the built `dist/` with CF-Pages clean-URL resolution.
|
|
6407
|
+
*
|
|
6408
|
+
* @param options - Optional port override (defaults to `config.port`).
|
|
6409
|
+
* @returns Resolves once the server has been torn down.
|
|
6410
|
+
* @example
|
|
6411
|
+
* await api.preview();
|
|
6412
|
+
*/
|
|
6413
|
+
preview(options = {}) {
|
|
6414
|
+
const { port = ctx.config.port } = options;
|
|
6415
|
+
ctx.state.render.header("preview");
|
|
6416
|
+
return runPreviewServer(ctx, port);
|
|
6417
|
+
},
|
|
6418
|
+
/**
|
|
6419
|
+
* Scaffold, then deploy. A y/N confirm is shown only when a human is present (an
|
|
6420
|
+
* interactive TTY, with `CI` unset). Non-interactive runs (CI, or any non-TTY)
|
|
6421
|
+
* skip the prompt and deploy, so the consumer scripts never hang a pipeline.
|
|
6422
|
+
* `options.yes` forces the skip anywhere. An interactive "no" returns
|
|
6423
|
+
* `{ deployed: false, reason: "declined" }`.
|
|
6424
|
+
*
|
|
6425
|
+
* @param options - Optional branch override and `yes` flag.
|
|
6426
|
+
* @returns The deploy outcome (completed details, or `declined` if a TTY user says no).
|
|
6427
|
+
* @example
|
|
6428
|
+
* await api.deploy({ branch: "preview/x", yes: true });
|
|
6429
|
+
*/
|
|
6430
|
+
async deploy(options = {}) {
|
|
6431
|
+
const { branch, yes = false } = options;
|
|
6432
|
+
ctx.state.render.header("deploy");
|
|
6433
|
+
await ctx.require(deployPlugin).init({ ci: true });
|
|
6434
|
+
if (process.stdout.isTTY === true && process.env.CI === void 0 && !yes) {
|
|
6435
|
+
if (!await ctx.state.confirm(`Deploy ${ctx.config.outDir}/ to cloudflare-pages?`)) {
|
|
6436
|
+
ctx.state.render.warn("deploy skipped");
|
|
6437
|
+
return {
|
|
6438
|
+
deployed: false,
|
|
6439
|
+
reason: "declined"
|
|
6440
|
+
};
|
|
6441
|
+
}
|
|
6442
|
+
} else if (!yes) ctx.state.render.info("non-interactive — skipping deploy confirmation");
|
|
6443
|
+
return {
|
|
6444
|
+
deployed: true,
|
|
6445
|
+
...await ctx.require(deployPlugin).run(branch === void 0 ? {} : { branch })
|
|
6446
|
+
};
|
|
6447
|
+
}
|
|
6448
|
+
};
|
|
6449
|
+
}
|
|
6450
|
+
//#endregion
|
|
6451
|
+
//#region src/plugins/cli/defaults.ts
|
|
6452
|
+
/**
|
|
6453
|
+
* Default cli configuration. Consumers override individual fields via
|
|
6454
|
+
* `pluginConfigs.cli`. Declared as a typed const (no inline `as` assertion).
|
|
6455
|
+
*
|
|
6456
|
+
* @example
|
|
6457
|
+
* ```ts
|
|
6458
|
+
* createPlugin("cli", { config: defaultConfig });
|
|
6459
|
+
* ```
|
|
6460
|
+
*/
|
|
6461
|
+
const defaultConfig = {
|
|
6462
|
+
outDir: "dist",
|
|
6463
|
+
port: 4173,
|
|
6464
|
+
watchDirs: ["content", "src"],
|
|
6465
|
+
debounceMs: 150,
|
|
6466
|
+
notFoundFile: "404.html",
|
|
6467
|
+
liveReload: true
|
|
6468
|
+
};
|
|
6469
|
+
//#endregion
|
|
6470
|
+
//#region src/plugins/cli/network.ts
|
|
6471
|
+
/**
|
|
6472
|
+
* @file cli plugin — LAN network-URL derivation. Picks the first non-internal IPv4
|
|
6473
|
+
* from `node:os` `networkInterfaces()` to render the "Network" URL in the
|
|
6474
|
+
* server-ready panel. The interface source is injectable so it is unit-testable.
|
|
6475
|
+
*/
|
|
6476
|
+
/**
|
|
6477
|
+
* Whether an interface entry is a usable, non-internal IPv4 address.
|
|
6478
|
+
*
|
|
6479
|
+
* @param entry - One interface address entry.
|
|
6480
|
+
* @returns `true` when it is an external IPv4 address.
|
|
6481
|
+
* @example
|
|
6482
|
+
* isExternalIPv4({ address: "10.0.0.2", family: "IPv4", internal: false }); // true
|
|
6483
|
+
*/
|
|
6484
|
+
function isExternalIPv4(entry) {
|
|
6485
|
+
return !entry.internal && (entry.family === "IPv4" || entry.family === 4);
|
|
6486
|
+
}
|
|
6487
|
+
/**
|
|
6488
|
+
* Pick the first non-internal IPv4 address from the interface source, or `null` when
|
|
6489
|
+
* none exists (offline / loopback-only).
|
|
6490
|
+
*
|
|
6491
|
+
* @param source - Interface source (defaults to `node:os` `networkInterfaces`).
|
|
6492
|
+
* @returns The first external IPv4 address string, or `null`.
|
|
6493
|
+
* @example
|
|
6494
|
+
* const ip = lanAddress();
|
|
6495
|
+
*/
|
|
6496
|
+
function lanAddress(source = node_os.networkInterfaces) {
|
|
6497
|
+
for (const entries of Object.values(source())) for (const entry of entries ?? []) if (isExternalIPv4(entry)) return entry.address;
|
|
6498
|
+
return null;
|
|
6499
|
+
}
|
|
6500
|
+
/**
|
|
6501
|
+
* Build the LAN URL (`http://<ip>:<port>`) for the server-ready panel, or `null`
|
|
6502
|
+
* when no non-internal IPv4 is available.
|
|
6503
|
+
*
|
|
6504
|
+
* @param port - The port the server is bound to.
|
|
6505
|
+
* @param source - Interface source (defaults to `node:os` `networkInterfaces`).
|
|
6506
|
+
* @returns The `http://<ip>:<port>` URL, or `null` when offline.
|
|
6507
|
+
* @example
|
|
6508
|
+
* networkUrl(4173); // "http://192.168.1.10:4173" or null
|
|
6509
|
+
*/
|
|
6510
|
+
function networkUrl(port, source = node_os.networkInterfaces) {
|
|
6511
|
+
const ip = lanAddress(source);
|
|
6512
|
+
return ip === null ? null : `http://${ip}:${port}`;
|
|
6513
|
+
}
|
|
6514
|
+
//#endregion
|
|
6515
|
+
//#region src/plugins/cli/render/ansi.ts
|
|
6516
|
+
/**
|
|
6517
|
+
* @file cli plugin — TTY/NO_COLOR-aware ANSI color + box-drawing helpers shared by
|
|
6518
|
+
* the Panel renderer. Modeled on the legacy `scripts/_log.ts`: color and box glyphs
|
|
6519
|
+
* are emitted only on a real TTY with `NO_COLOR` unset; otherwise plain ASCII so
|
|
6520
|
+
* CI logs and pipes stay readable.
|
|
6521
|
+
*/
|
|
6522
|
+
/** The ANSI escape byte (ESC, `0x1b`), built so no literal control char is in source. */
|
|
6523
|
+
const ESC = String.fromCodePoint(27);
|
|
6524
|
+
/** ANSI SGR codes used by the Panel renderer (each prefixed with the ESC byte). */
|
|
6525
|
+
const ANSI = {
|
|
6526
|
+
reset: `${ESC}[0m`,
|
|
6527
|
+
bold: `${ESC}[1m`,
|
|
6528
|
+
dim: `${ESC}[2m`,
|
|
6529
|
+
red: `${ESC}[31m`,
|
|
6530
|
+
green: `${ESC}[32m`,
|
|
6531
|
+
yellow: `${ESC}[33m`,
|
|
6532
|
+
blue: `${ESC}[34m`,
|
|
6533
|
+
magenta: `${ESC}[35m`,
|
|
6534
|
+
cyan: `${ESC}[36m`,
|
|
6535
|
+
gray: `${ESC}[90m`
|
|
6536
|
+
};
|
|
6537
|
+
/** Unicode rounded box glyphs used when output is a color-capable TTY. */
|
|
6538
|
+
const UNICODE_BOX = {
|
|
6539
|
+
topLeft: "╭",
|
|
6540
|
+
topRight: "╮",
|
|
6541
|
+
bottomLeft: "╰",
|
|
6542
|
+
bottomRight: "╯",
|
|
6543
|
+
horizontal: "─",
|
|
6544
|
+
vertical: "│"
|
|
6545
|
+
};
|
|
6546
|
+
/** ASCII box glyphs used when output is piped/CI (plain mode). */
|
|
6547
|
+
const ASCII_BOX = {
|
|
6548
|
+
topLeft: "+",
|
|
6549
|
+
topRight: "+",
|
|
6550
|
+
bottomLeft: "+",
|
|
6551
|
+
bottomRight: "+",
|
|
6552
|
+
horizontal: "-",
|
|
6553
|
+
vertical: "|"
|
|
6554
|
+
};
|
|
6555
|
+
/**
|
|
6556
|
+
* Matches every ANSI SGR escape sequence (used to measure visible width). Built from
|
|
6557
|
+
* the {@link ESC} byte so no literal control character appears in the source regex.
|
|
6558
|
+
*/
|
|
6559
|
+
const ANSI_PATTERN = new RegExp(String.raw`${ESC}\[[0-9;]*m`, "g");
|
|
6560
|
+
/**
|
|
6561
|
+
* Whether ANSI color/box glyphs should be emitted: a TTY stream with `NO_COLOR`
|
|
6562
|
+
* unset. Reads `process.stdout.isTTY` and `process.env.NO_COLOR` by default so the
|
|
6563
|
+
* renderer auto-degrades in CI and pipes, exactly like the legacy logger.
|
|
6564
|
+
*
|
|
6565
|
+
* @param stream - Stream to probe for `isTTY` (defaults to `process.stdout`).
|
|
6566
|
+
* @param noColor - The `NO_COLOR` value (defaults to `process.env.NO_COLOR`).
|
|
6567
|
+
* @returns `true` when color should be used.
|
|
6568
|
+
* @example
|
|
6569
|
+
* supportsColor(); // true in an interactive terminal
|
|
6570
|
+
*/
|
|
6571
|
+
function supportsColor(stream = process.stdout, noColor = process.env.NO_COLOR) {
|
|
6572
|
+
return stream.isTTY === true && noColor === void 0;
|
|
6573
|
+
}
|
|
6574
|
+
/**
|
|
6575
|
+
* Select the box glyph set for the given color mode (Unicode on a TTY, ASCII off it).
|
|
6576
|
+
*
|
|
6577
|
+
* @param color - Whether color/Unicode output is enabled.
|
|
6578
|
+
* @returns The matching {@link BoxGlyphs} set.
|
|
6579
|
+
* @example
|
|
6580
|
+
* const glyphs = boxGlyphs(supportsColor());
|
|
6581
|
+
*/
|
|
6582
|
+
function boxGlyphs(color) {
|
|
6583
|
+
return color ? UNICODE_BOX : ASCII_BOX;
|
|
6584
|
+
}
|
|
6585
|
+
/**
|
|
6586
|
+
* The visible width of a string, ignoring any ANSI escape sequences it contains.
|
|
6587
|
+
*
|
|
6588
|
+
* @param text - The (possibly colorized) text to measure.
|
|
6589
|
+
* @returns The number of visible characters.
|
|
6590
|
+
* @example
|
|
6591
|
+
* visibleWidth(`${ANSI.red}hi${ANSI.reset}`); // 2
|
|
6592
|
+
*/
|
|
6593
|
+
function visibleWidth(text) {
|
|
6594
|
+
return text.replaceAll(ANSI_PATTERN, "").length;
|
|
6595
|
+
}
|
|
6596
|
+
/**
|
|
6597
|
+
* Build a {@link Palette} bound to a fixed color mode. When `color` is `false` every
|
|
6598
|
+
* helper returns its input unchanged, so the same render code path produces plain
|
|
6599
|
+
* output in CI/pipes.
|
|
6600
|
+
*
|
|
6601
|
+
* @param color - Whether color is enabled (typically `supportsColor()`).
|
|
6602
|
+
* @returns The bound color palette.
|
|
6603
|
+
* @example
|
|
6604
|
+
* const palette = makePalette(supportsColor());
|
|
6605
|
+
* const line = palette.green("done");
|
|
6606
|
+
*/
|
|
6607
|
+
function makePalette(color) {
|
|
6608
|
+
return {
|
|
6609
|
+
enabled: color,
|
|
6610
|
+
/**
|
|
6611
|
+
* Wrap text in the given ANSI code (returns it unchanged when color is off).
|
|
6612
|
+
*
|
|
6613
|
+
* @param code - The ANSI SGR code to apply.
|
|
6614
|
+
* @param text - The text to colorize.
|
|
6615
|
+
* @returns The colorized (or unchanged) text.
|
|
6616
|
+
* @example
|
|
6617
|
+
* palette.paint(ANSI.green, "ok");
|
|
6618
|
+
*/
|
|
6619
|
+
paint(code, text) {
|
|
6620
|
+
return color ? `${code}${text}${ANSI.reset}` : text;
|
|
6621
|
+
},
|
|
6622
|
+
/**
|
|
6623
|
+
* Bold the given text (no-op in plain mode).
|
|
6624
|
+
*
|
|
6625
|
+
* @param text - The text to embolden.
|
|
6626
|
+
* @returns The bold (or unchanged) text.
|
|
6627
|
+
* @example
|
|
6628
|
+
* palette.bold("title");
|
|
6629
|
+
*/
|
|
6630
|
+
bold(text) {
|
|
6631
|
+
return this.paint(ANSI.bold, text);
|
|
6632
|
+
},
|
|
6633
|
+
/**
|
|
6634
|
+
* Dim the given text (no-op in plain mode).
|
|
6635
|
+
*
|
|
6636
|
+
* @param text - The text to dim.
|
|
6637
|
+
* @returns The dim (or unchanged) text.
|
|
6638
|
+
* @example
|
|
6639
|
+
* palette.dim("· 84ms");
|
|
6640
|
+
*/
|
|
6641
|
+
dim(text) {
|
|
6642
|
+
return this.paint(ANSI.dim, text);
|
|
6643
|
+
},
|
|
6644
|
+
/**
|
|
6645
|
+
* Color the given text green (no-op in plain mode).
|
|
6646
|
+
*
|
|
6647
|
+
* @param text - The text to colorize.
|
|
6648
|
+
* @returns The green (or unchanged) text.
|
|
6649
|
+
* @example
|
|
6650
|
+
* palette.green("✓");
|
|
6651
|
+
*/
|
|
6652
|
+
green(text) {
|
|
6653
|
+
return this.paint(ANSI.green, text);
|
|
6654
|
+
},
|
|
6655
|
+
/**
|
|
6656
|
+
* Color the given text yellow (no-op in plain mode).
|
|
6657
|
+
*
|
|
6658
|
+
* @param text - The text to colorize.
|
|
6659
|
+
* @returns The yellow (or unchanged) text.
|
|
6660
|
+
* @example
|
|
6661
|
+
* palette.yellow("~");
|
|
6662
|
+
*/
|
|
6663
|
+
yellow(text) {
|
|
6664
|
+
return this.paint(ANSI.yellow, text);
|
|
6665
|
+
},
|
|
6666
|
+
/**
|
|
6667
|
+
* Color the given text red (no-op in plain mode).
|
|
6668
|
+
*
|
|
6669
|
+
* @param text - The text to colorize.
|
|
6670
|
+
* @returns The red (or unchanged) text.
|
|
6671
|
+
* @example
|
|
6672
|
+
* palette.red("✗");
|
|
6673
|
+
*/
|
|
6674
|
+
red(text) {
|
|
6675
|
+
return this.paint(ANSI.red, text);
|
|
6676
|
+
},
|
|
6677
|
+
/**
|
|
6678
|
+
* Color the given text cyan (no-op in plain mode).
|
|
6679
|
+
*
|
|
6680
|
+
* @param text - The text to colorize.
|
|
6681
|
+
* @returns The cyan (or unchanged) text.
|
|
6682
|
+
* @example
|
|
6683
|
+
* palette.cyan("http://localhost:4173");
|
|
6684
|
+
*/
|
|
6685
|
+
cyan(text) {
|
|
6686
|
+
return this.paint(ANSI.cyan, text);
|
|
6687
|
+
}
|
|
6688
|
+
};
|
|
6689
|
+
}
|
|
6690
|
+
/**
|
|
6691
|
+
* Frame a list of already-rendered content lines in a box, padding each line to the
|
|
6692
|
+
* width of the widest visible line. Uses Unicode borders when `color` is enabled and
|
|
6693
|
+
* ASCII otherwise. Visible width ignores embedded ANSI so colored lines align.
|
|
6694
|
+
*
|
|
6695
|
+
* @param lines - The content lines (may contain ANSI color codes).
|
|
6696
|
+
* @param color - Whether to use Unicode borders (and assume color-capable output).
|
|
6697
|
+
* @returns The boxed lines (top border, content rows, bottom border).
|
|
6698
|
+
* @example
|
|
6699
|
+
* box(["Local: http://localhost:4173"], true);
|
|
6700
|
+
*/
|
|
6701
|
+
function box(lines, color) {
|
|
6702
|
+
const glyphs = boxGlyphs(color);
|
|
6703
|
+
const inner = Math.max(0, ...lines.map((line) => visibleWidth(line)));
|
|
6704
|
+
const horizontal = glyphs.horizontal.repeat(inner + 2);
|
|
6705
|
+
const top = `${glyphs.topLeft}${horizontal}${glyphs.topRight}`;
|
|
6706
|
+
const bottom = `${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`;
|
|
6707
|
+
return [
|
|
6708
|
+
top,
|
|
6709
|
+
...lines.map((line) => {
|
|
6710
|
+
const pad = " ".repeat(inner - visibleWidth(line));
|
|
6711
|
+
return `${glyphs.vertical} ${line}${pad} ${glyphs.vertical}`;
|
|
6712
|
+
}),
|
|
6713
|
+
bottom
|
|
6714
|
+
];
|
|
6715
|
+
}
|
|
6716
|
+
//#endregion
|
|
6717
|
+
//#region src/plugins/cli/render/panel.ts
|
|
6718
|
+
/** Per-command label shown in the header badge beside the logo. */
|
|
6719
|
+
const COMMAND_LABEL = {
|
|
6720
|
+
build: "build",
|
|
6721
|
+
serve: "serve · dev",
|
|
6722
|
+
preview: "preview",
|
|
6723
|
+
deploy: "deploy"
|
|
6724
|
+
};
|
|
6725
|
+
/**
|
|
6726
|
+
* Render one human-readable duration suffix (e.g. `· 84ms`).
|
|
6727
|
+
*
|
|
6728
|
+
* @param palette - The active color palette.
|
|
6729
|
+
* @param durationMs - Duration in milliseconds (omitted → empty string).
|
|
6730
|
+
* @returns The dim `· Nms` suffix, or `""` when no duration is given.
|
|
6731
|
+
* @example
|
|
6732
|
+
* durationSuffix(palette, 84); // " · 84ms" (dim)
|
|
6733
|
+
*/
|
|
6734
|
+
function durationSuffix(palette, durationMs) {
|
|
6735
|
+
if (durationMs === void 0) return "";
|
|
6736
|
+
return ` ${palette.dim(`· ${durationMs}ms`)}`;
|
|
6737
|
+
}
|
|
6738
|
+
/**
|
|
6739
|
+
* Create the Panel {@link CliRenderer}. Output is written through the injected sink
|
|
6740
|
+
* (default `console.log`/`console.error`) and colorized only when color is enabled,
|
|
6741
|
+
* so the identical render path yields box-drawn color panels on a TTY and plain
|
|
6742
|
+
* ASCII lines in CI/pipes.
|
|
6743
|
+
*
|
|
6744
|
+
* @param options - Optional sinks + a color override (see {@link PanelOptions}).
|
|
6745
|
+
* @returns The renderer mounted on `state.render` and driven by the API + hooks.
|
|
6746
|
+
* @example
|
|
6747
|
+
* const render = createPanelRenderer();
|
|
6748
|
+
* render.header("build");
|
|
6749
|
+
*/
|
|
6750
|
+
function createPanelRenderer(options = {}) {
|
|
6751
|
+
const write = options.write ?? ((line) => console.log(line));
|
|
6752
|
+
const writeError = options.writeError ?? ((line) => console.error(line));
|
|
6753
|
+
const color = options.color ?? supportsColor();
|
|
6754
|
+
const palette = makePalette(color);
|
|
6755
|
+
/**
|
|
6756
|
+
* Write each line of a multi-line block through the stdout sink.
|
|
6757
|
+
*
|
|
6758
|
+
* @param lines - The rendered lines to write in order.
|
|
6759
|
+
* @example
|
|
6760
|
+
* writeBlock(["a", "b"]);
|
|
6761
|
+
*/
|
|
6762
|
+
const writeBlock = (lines) => {
|
|
6763
|
+
for (const line of lines) write(line);
|
|
6764
|
+
};
|
|
6765
|
+
return {
|
|
6766
|
+
/**
|
|
6767
|
+
* Render the boxed `MOKU WEB` logo + command label.
|
|
6768
|
+
*
|
|
6769
|
+
* @param command - The command being run, shown beside the logo.
|
|
6770
|
+
* @example
|
|
6771
|
+
* render.header("serve");
|
|
6772
|
+
*/
|
|
6773
|
+
header(command) {
|
|
6774
|
+
writeBlock(box([`${palette.bold(palette.cyan("MOKU WEB"))} ${palette.dim(COMMAND_LABEL[command])}`], color));
|
|
6775
|
+
},
|
|
6776
|
+
/**
|
|
6777
|
+
* Render a live per-phase row from a `build:phase` event.
|
|
6778
|
+
*
|
|
6779
|
+
* @param phase - The `build:phase` payload.
|
|
6780
|
+
* @example
|
|
6781
|
+
* render.phase({ phase: "pages", status: "done", durationMs: 12 });
|
|
6782
|
+
*/
|
|
6783
|
+
phase(phase) {
|
|
6784
|
+
const done = phase.status === "done";
|
|
6785
|
+
write(` ${done ? palette.green("✓") : palette.dim("•")} ${done ? phase.phase : palette.dim(phase.phase)}${durationSuffix(palette, phase.durationMs)}`);
|
|
6786
|
+
},
|
|
6787
|
+
/**
|
|
6788
|
+
* Render the BUILD summary block from a `build:complete` event.
|
|
6789
|
+
*
|
|
6790
|
+
* @param summary - The `build:complete` payload.
|
|
6791
|
+
* @example
|
|
6792
|
+
* render.built({ outDir: "dist", pageCount: 12, durationMs: 840 });
|
|
6793
|
+
*/
|
|
6794
|
+
built(summary) {
|
|
6795
|
+
const pages = palette.bold(String(summary.pageCount));
|
|
6796
|
+
writeBlock(box([
|
|
6797
|
+
`${palette.green("✓")} ${palette.bold("BUILD")} complete`,
|
|
6798
|
+
`${palette.dim("pages")} ${pages}`,
|
|
6799
|
+
`${palette.dim("time")} ${summary.durationMs}ms`,
|
|
6800
|
+
`${palette.dim("out")} ${summary.outDir}/`
|
|
6801
|
+
], color));
|
|
6802
|
+
},
|
|
6803
|
+
/**
|
|
6804
|
+
* Render the bordered server-ready panel (Local / Network URLs + watched dirs).
|
|
6805
|
+
*
|
|
6806
|
+
* @param info - Local/Network URLs and optionally the watched directories.
|
|
6807
|
+
* @example
|
|
6808
|
+
* render.serverReady({ local: "http://localhost:4173", network: null });
|
|
6809
|
+
*/
|
|
6810
|
+
serverReady(info) {
|
|
6811
|
+
const lines = [`${palette.green("➜")} ${palette.bold("Local")} ${palette.cyan(info.local)}`, `${palette.green("➜")} ${palette.bold("Network")} ${info.network ? palette.cyan(info.network) : palette.dim("unavailable")}`];
|
|
6812
|
+
if (info.watching && info.watching.length > 0) lines.push(`${palette.dim("watching")} ${palette.dim(info.watching.join(", "))}`);
|
|
6813
|
+
writeBlock(box(lines, color));
|
|
6814
|
+
},
|
|
6815
|
+
/**
|
|
6816
|
+
* Render the post-rebuild line ("~ file" + "✓ rebuilt N pages · Xms · reloaded").
|
|
6817
|
+
*
|
|
6818
|
+
* @param info - The changed file plus the rebuild's page count and duration.
|
|
6819
|
+
* @example
|
|
6820
|
+
* render.reload({ file: "content/a.md", pageCount: 12, durationMs: 84 });
|
|
6821
|
+
*/
|
|
6822
|
+
reload(info) {
|
|
6823
|
+
write(` ${palette.yellow("~")} ${info.file}`);
|
|
6824
|
+
write(` ${palette.green("✓")} rebuilt ${palette.bold(String(info.pageCount))} pages ${palette.dim(`· ${info.durationMs}ms · browser reloaded`)}`);
|
|
6825
|
+
},
|
|
6826
|
+
/**
|
|
6827
|
+
* Render the deploy result panel from a `deploy:complete` event.
|
|
6828
|
+
*
|
|
6829
|
+
* @param result - The `deploy:complete` payload.
|
|
6830
|
+
* @example
|
|
6831
|
+
* render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
|
|
6832
|
+
*/
|
|
6833
|
+
deployed(result) {
|
|
6834
|
+
writeBlock(box([
|
|
6835
|
+
`${palette.green("✓")} ${palette.bold("DEPLOYED")}`,
|
|
6836
|
+
`${palette.dim("url")} ${palette.cyan(result.url)}`,
|
|
6837
|
+
`${palette.dim("branch")} ${result.branch}`,
|
|
6838
|
+
`${palette.dim("id")} ${result.deploymentId}`,
|
|
6839
|
+
`${palette.dim("time")} ${result.durationMs}ms`
|
|
6840
|
+
], color));
|
|
6841
|
+
},
|
|
6842
|
+
/**
|
|
6843
|
+
* Render a neutral informational line.
|
|
6844
|
+
*
|
|
6845
|
+
* @param message - The line to print.
|
|
6846
|
+
* @example
|
|
6847
|
+
* render.info("watching for changes…");
|
|
6848
|
+
*/
|
|
6849
|
+
info(message) {
|
|
6850
|
+
write(` ${palette.cyan("›")} ${message}`);
|
|
6851
|
+
},
|
|
6852
|
+
/**
|
|
6853
|
+
* Render a warning line (to stderr).
|
|
6854
|
+
*
|
|
6855
|
+
* @param message - The warning to print.
|
|
6856
|
+
* @example
|
|
6857
|
+
* render.warn("deploy skipped");
|
|
6858
|
+
*/
|
|
6859
|
+
warn(message) {
|
|
6860
|
+
writeError(` ${palette.yellow("⚠")} ${message}`);
|
|
6861
|
+
},
|
|
6862
|
+
/**
|
|
6863
|
+
* Render an error line (to stderr), optionally with a cause.
|
|
6864
|
+
*
|
|
6865
|
+
* @param message - The error summary to print.
|
|
6866
|
+
* @param cause - Optional underlying error/value to print beneath the summary.
|
|
6867
|
+
* @example
|
|
6868
|
+
* render.error("build failed", err);
|
|
6869
|
+
*/
|
|
6870
|
+
error(message, cause) {
|
|
6871
|
+
writeError(` ${palette.red("✗")} ${message}`);
|
|
6872
|
+
if (cause !== void 0) writeError(String(cause));
|
|
6873
|
+
}
|
|
6874
|
+
};
|
|
6875
|
+
}
|
|
6876
|
+
//#endregion
|
|
6877
|
+
//#region src/plugins/cli/state.ts
|
|
6878
|
+
/**
|
|
6879
|
+
* @file cli plugin — state factory. Wires the default injectable seams: the Panel
|
|
6880
|
+
* renderer, a stdin y/N `confirm`, the `Date.now` clock, a recursive `node:fs.watch`
|
|
6881
|
+
* wrapper, and the `Bun.serve`/`Bun.file` static-server seams (resolved lazily, like
|
|
6882
|
+
* deploy's `defaultSpawn`, so a non-Bun runtime fails coded rather than as a raw
|
|
6883
|
+
* `TypeError` and tests can inject fakes). Unit tests swap any of these.
|
|
6884
|
+
*/
|
|
6885
|
+
/**
|
|
6886
|
+
* Resolve the `Bun` runtime global, or `undefined` when not running under Bun.
|
|
6887
|
+
*
|
|
6888
|
+
* @returns The Bun runtime, or `undefined`.
|
|
6889
|
+
* @example
|
|
6890
|
+
* const bun = bunRuntime();
|
|
6891
|
+
*/
|
|
6892
|
+
function bunRuntime() {
|
|
6893
|
+
return globalThis.Bun;
|
|
6894
|
+
}
|
|
6895
|
+
/**
|
|
6896
|
+
* Default static-server factory — resolves `Bun.serve` lazily at call time so the
|
|
6897
|
+
* server is only required when a long-running command actually starts one.
|
|
6898
|
+
*
|
|
6899
|
+
* @param options - Port + `fetch` handler (see {@link ServeStaticFunction}).
|
|
6900
|
+
* @returns The running server handle.
|
|
6901
|
+
* @throws {Error} When no Bun runtime is available to serve.
|
|
6902
|
+
* @example
|
|
6903
|
+
* defaultServeStatic({ port: 4173, fetch: () => new Response("ok") });
|
|
6904
|
+
*/
|
|
6905
|
+
const defaultServeStatic = (options) => {
|
|
6906
|
+
const runtime = bunRuntime();
|
|
6907
|
+
if (runtime === void 0) throw new Error("[web] cli: no Bun runtime available to start the server.\n Run serve()/preview() under Bun, or inject state.serveStatic in tests.");
|
|
6908
|
+
return runtime.serve(options);
|
|
6909
|
+
};
|
|
6910
|
+
/**
|
|
6911
|
+
* Default file-response factory — `new Response(Bun.file(path), { status })`. Resolves
|
|
6912
|
+
* `Bun.file` lazily so it is only required when a request is actually served.
|
|
6913
|
+
*
|
|
6914
|
+
* @param path - Absolute on-disk path to stream.
|
|
6915
|
+
* @param status - HTTP status for the response.
|
|
6916
|
+
* @returns The file `Response`.
|
|
6917
|
+
* @throws {Error} When no Bun runtime is available to read the file.
|
|
6918
|
+
* @example
|
|
6919
|
+
* defaultFileResponse("/dist/index.html", 200);
|
|
6920
|
+
*/
|
|
6921
|
+
const defaultFileResponse = (path, status) => {
|
|
6922
|
+
const runtime = bunRuntime();
|
|
6923
|
+
if (runtime === void 0) throw new Error("[web] cli: no Bun runtime available to read files.\n Run serve()/preview() under Bun, or inject state.fileResponse in tests.");
|
|
6924
|
+
return new Response(runtime.file(path), { status });
|
|
6925
|
+
};
|
|
6926
|
+
/**
|
|
6927
|
+
* Default stdin y/N prompt. Reads a single line from `process.stdin` via
|
|
6928
|
+
* `node:readline` and resolves `true` only on an explicit `y`/`yes` (default `No`).
|
|
6929
|
+
* Tests inject a canned answer so no real TTY interaction occurs.
|
|
6930
|
+
*
|
|
6931
|
+
* @param question - The yes/no question to display.
|
|
6932
|
+
* @returns Resolves `true` when the user answered yes.
|
|
6933
|
+
* @example
|
|
6934
|
+
* await defaultConfirm("Deploy dist/?");
|
|
6935
|
+
*/
|
|
6936
|
+
function defaultConfirm(question) {
|
|
6937
|
+
return new Promise((resolve) => {
|
|
6938
|
+
const readline = (0, node_readline.createInterface)({
|
|
6939
|
+
input: process.stdin,
|
|
6940
|
+
output: process.stdout
|
|
6941
|
+
});
|
|
6942
|
+
readline.question(`${question} [y/N] `, (answer) => {
|
|
6943
|
+
readline.close();
|
|
6944
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
6945
|
+
});
|
|
6946
|
+
});
|
|
6947
|
+
}
|
|
6948
|
+
/**
|
|
6949
|
+
* Default recursive directory watcher — wraps `node:fs.watch` with `{ recursive: true }`
|
|
6950
|
+
* and adapts its handle to {@link WatchHandle}. Tests inject a fake emitter so no real
|
|
6951
|
+
* FS watch is registered.
|
|
6952
|
+
*
|
|
6953
|
+
* @param dir - The directory to watch recursively.
|
|
6954
|
+
* @param onChange - Invoked on any change beneath `dir`.
|
|
6955
|
+
* @returns A handle whose `close()` detaches the watcher.
|
|
6956
|
+
* @example
|
|
6957
|
+
* const handle = defaultWatch("content", () => rebuild());
|
|
6958
|
+
*/
|
|
6959
|
+
function defaultWatch(dir, onChange) {
|
|
6960
|
+
const watcher = (0, node_fs.watch)(dir, { recursive: true }, () => onChange());
|
|
6961
|
+
return {
|
|
6962
|
+
/**
|
|
6963
|
+
* Detach the underlying `node:fs.watch` listener.
|
|
6964
|
+
*
|
|
6965
|
+
* @example
|
|
6966
|
+
* handle.close();
|
|
6967
|
+
*/
|
|
6968
|
+
close() {
|
|
6969
|
+
watcher.close();
|
|
6970
|
+
} };
|
|
6971
|
+
}
|
|
6972
|
+
/**
|
|
6973
|
+
* Default LAN network-URL deriver — wraps {@link networkUrl} so the production seam
|
|
6974
|
+
* reads `node:os` interfaces while tests can inject a deterministic value.
|
|
6975
|
+
*
|
|
6976
|
+
* @param port - The port the server is bound to.
|
|
6977
|
+
* @returns The `http://<ip>:<port>` URL, or `null` when offline.
|
|
6978
|
+
* @example
|
|
6979
|
+
* defaultNetworkUrl(4173);
|
|
6980
|
+
*/
|
|
6981
|
+
function defaultNetworkUrl(port) {
|
|
6982
|
+
return networkUrl(port);
|
|
6983
|
+
}
|
|
6984
|
+
/**
|
|
6985
|
+
* Create the initial cli plugin state with the production seams wired. Every field is
|
|
6986
|
+
* an injectable seam (`render`, `confirm`, `clock`, `watch`, the server factories,
|
|
6987
|
+
* and `networkUrl`) so commands run under unit tests without real sockets/FS/TTY.
|
|
6988
|
+
*
|
|
6989
|
+
* @param _ctx - Minimal context with global + config (unused — state is static).
|
|
6990
|
+
* @param _ctx.global - Global plugin registry.
|
|
6991
|
+
* @param _ctx.config - Resolved plugin configuration.
|
|
6992
|
+
* @returns The initial cli state.
|
|
6993
|
+
* @example
|
|
6994
|
+
* const state = createState({ global: {}, config });
|
|
6995
|
+
*/
|
|
6996
|
+
function createState$1(_ctx) {
|
|
6997
|
+
return {
|
|
6998
|
+
render: createPanelRenderer(),
|
|
6999
|
+
confirm: defaultConfirm,
|
|
7000
|
+
clock: Date.now,
|
|
7001
|
+
watch: defaultWatch,
|
|
7002
|
+
serveStatic: defaultServeStatic,
|
|
7003
|
+
fileResponse: defaultFileResponse,
|
|
7004
|
+
networkUrl: defaultNetworkUrl
|
|
7005
|
+
};
|
|
7006
|
+
}
|
|
7007
|
+
//#endregion
|
|
7008
|
+
//#region src/plugins/cli/index.ts
|
|
7009
|
+
/**
|
|
7010
|
+
* @file cli — Complex plugin (wiring harness only). Developer CLI:
|
|
7011
|
+
* build · serve · preview · deploy, with the boxed Panel renderer.
|
|
7012
|
+
* Depends: build, deploy. Listens: build:phase, build:complete, deploy:complete.
|
|
7013
|
+
* @see README.md
|
|
7014
|
+
*/
|
|
7015
|
+
/**
|
|
7016
|
+
* cli plugin — the node-only developer CLI for `@moku-labs/web`. Mounts exactly four
|
|
7017
|
+
* methods at `app.cli` (`build`/`serve`/`preview`/`deploy`), each rendering through
|
|
7018
|
+
* the boxed Panel UI. Live build/deploy progress rides on hooks over the `build` and
|
|
7019
|
+
* `deploy` plugins' events; there is no argv parser and no `run()` dispatcher — the
|
|
7020
|
+
* consumer drives it from one thin script per command.
|
|
7021
|
+
*
|
|
7022
|
+
* @example Compose the CLI in a consumer app (node-only)
|
|
7023
|
+
* ```ts
|
|
7024
|
+
* import { buildPlugin, cliPlugin, createApp, deployPlugin } from "@moku-labs/web";
|
|
7025
|
+
*
|
|
7026
|
+
* const app = createApp({
|
|
7027
|
+
* plugins: [buildPlugin, deployPlugin, cliPlugin],
|
|
7028
|
+
* pluginConfigs: { cli: { outDir: "dist", port: 4173, watchDirs: ["content", "src"] } }
|
|
7029
|
+
* });
|
|
7030
|
+
* await app.start();
|
|
7031
|
+
* await app.cli.build();
|
|
7032
|
+
* ```
|
|
7033
|
+
*/
|
|
7034
|
+
const cliPlugin = createPlugin$1("cli", {
|
|
7035
|
+
config: defaultConfig,
|
|
7036
|
+
depends: [buildPlugin, deployPlugin],
|
|
7037
|
+
createState: createState$1,
|
|
7038
|
+
onInit: (ctx) => validateConfig(ctx.config),
|
|
7039
|
+
hooks: (ctx) => ({
|
|
7040
|
+
"build:phase": (p) => ctx.state.render.phase(p),
|
|
7041
|
+
"build:complete": (p) => ctx.state.render.built(p),
|
|
7042
|
+
"deploy:complete": (p) => ctx.state.render.deployed(p)
|
|
7043
|
+
}),
|
|
5881
7044
|
api: createApi$1
|
|
5882
7045
|
});
|
|
5883
7046
|
//#endregion
|
|
@@ -5948,7 +7111,7 @@ function spaEvents(register) {
|
|
|
5948
7111
|
}
|
|
5949
7112
|
//#endregion
|
|
5950
7113
|
//#region src/plugins/spa/types.ts
|
|
5951
|
-
var types_exports$
|
|
7114
|
+
var types_exports$9 = /* @__PURE__ */ require_convention.__exportAll({ COMPONENT_HOOK_NAMES: () => COMPONENT_HOOK_NAMES });
|
|
5952
7115
|
/** Allowed hook names — single source of truth for fail-fast validation. */
|
|
5953
7116
|
const COMPONENT_HOOK_NAMES = [
|
|
5954
7117
|
"onCreate",
|
|
@@ -7014,27 +8177,30 @@ const spaPlugin = createPlugin$1("spa", {
|
|
|
7014
8177
|
//#region src/plugins/build/types.ts
|
|
7015
8178
|
var types_exports = /* @__PURE__ */ require_convention.__exportAll({});
|
|
7016
8179
|
//#endregion
|
|
7017
|
-
//#region src/plugins/
|
|
8180
|
+
//#region src/plugins/cli/types.ts
|
|
7018
8181
|
var types_exports$1 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
7019
8182
|
//#endregion
|
|
7020
|
-
//#region src/plugins/
|
|
8183
|
+
//#region src/plugins/content/types.ts
|
|
7021
8184
|
var types_exports$2 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
7022
8185
|
//#endregion
|
|
7023
|
-
//#region src/plugins/
|
|
8186
|
+
//#region src/plugins/data/types.ts
|
|
7024
8187
|
var types_exports$3 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
7025
8188
|
//#endregion
|
|
7026
|
-
//#region src/plugins/
|
|
8189
|
+
//#region src/plugins/deploy/types.ts
|
|
7027
8190
|
var types_exports$4 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
7028
8191
|
//#endregion
|
|
7029
|
-
//#region src/plugins/
|
|
8192
|
+
//#region src/plugins/env/types.ts
|
|
7030
8193
|
var types_exports$5 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
7031
8194
|
//#endregion
|
|
7032
|
-
//#region src/plugins/
|
|
8195
|
+
//#region src/plugins/head/types.ts
|
|
7033
8196
|
var types_exports$6 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
7034
8197
|
//#endregion
|
|
7035
|
-
//#region src/plugins/
|
|
8198
|
+
//#region src/plugins/log/types.ts
|
|
7036
8199
|
var types_exports$7 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
7037
8200
|
//#endregion
|
|
8201
|
+
//#region src/plugins/router/types.ts
|
|
8202
|
+
var types_exports$8 = /* @__PURE__ */ require_convention.__exportAll({});
|
|
8203
|
+
//#endregion
|
|
7038
8204
|
//#region src/plugins/env/providers.ts
|
|
7039
8205
|
/**
|
|
7040
8206
|
* @file env plugin — built-in providers: dotenv, processEnv, cloudflareBindings.
|
|
@@ -7257,58 +8423,65 @@ Object.defineProperty(exports, "Build", {
|
|
|
7257
8423
|
return types_exports;
|
|
7258
8424
|
}
|
|
7259
8425
|
});
|
|
7260
|
-
Object.defineProperty(exports, "
|
|
8426
|
+
Object.defineProperty(exports, "Cli", {
|
|
7261
8427
|
enumerable: true,
|
|
7262
8428
|
get: function() {
|
|
7263
8429
|
return types_exports$1;
|
|
7264
8430
|
}
|
|
7265
8431
|
});
|
|
7266
|
-
Object.defineProperty(exports, "
|
|
8432
|
+
Object.defineProperty(exports, "Content", {
|
|
7267
8433
|
enumerable: true,
|
|
7268
8434
|
get: function() {
|
|
7269
8435
|
return types_exports$2;
|
|
7270
8436
|
}
|
|
7271
8437
|
});
|
|
7272
|
-
Object.defineProperty(exports, "
|
|
8438
|
+
Object.defineProperty(exports, "Data", {
|
|
7273
8439
|
enumerable: true,
|
|
7274
8440
|
get: function() {
|
|
7275
8441
|
return types_exports$3;
|
|
7276
8442
|
}
|
|
7277
8443
|
});
|
|
7278
|
-
Object.defineProperty(exports, "
|
|
8444
|
+
Object.defineProperty(exports, "Deploy", {
|
|
7279
8445
|
enumerable: true,
|
|
7280
8446
|
get: function() {
|
|
7281
8447
|
return types_exports$4;
|
|
7282
8448
|
}
|
|
7283
8449
|
});
|
|
7284
|
-
Object.defineProperty(exports, "
|
|
8450
|
+
Object.defineProperty(exports, "Env", {
|
|
7285
8451
|
enumerable: true,
|
|
7286
8452
|
get: function() {
|
|
7287
8453
|
return types_exports$5;
|
|
7288
8454
|
}
|
|
7289
8455
|
});
|
|
7290
|
-
Object.defineProperty(exports, "
|
|
8456
|
+
Object.defineProperty(exports, "Head", {
|
|
7291
8457
|
enumerable: true,
|
|
7292
8458
|
get: function() {
|
|
7293
8459
|
return types_exports$6;
|
|
7294
8460
|
}
|
|
7295
8461
|
});
|
|
7296
|
-
Object.defineProperty(exports, "
|
|
8462
|
+
Object.defineProperty(exports, "Log", {
|
|
7297
8463
|
enumerable: true,
|
|
7298
8464
|
get: function() {
|
|
7299
8465
|
return types_exports$7;
|
|
7300
8466
|
}
|
|
7301
8467
|
});
|
|
7302
|
-
Object.defineProperty(exports, "
|
|
8468
|
+
Object.defineProperty(exports, "Router", {
|
|
7303
8469
|
enumerable: true,
|
|
7304
8470
|
get: function() {
|
|
7305
8471
|
return types_exports$8;
|
|
7306
8472
|
}
|
|
7307
8473
|
});
|
|
8474
|
+
Object.defineProperty(exports, "Spa", {
|
|
8475
|
+
enumerable: true,
|
|
8476
|
+
get: function() {
|
|
8477
|
+
return types_exports$9;
|
|
8478
|
+
}
|
|
8479
|
+
});
|
|
7308
8480
|
exports.browserEnv = browserEnv;
|
|
7309
8481
|
exports.buildArticleHead = buildArticleHead;
|
|
7310
8482
|
exports.buildPlugin = buildPlugin;
|
|
7311
8483
|
exports.canonical = canonical;
|
|
8484
|
+
exports.cliPlugin = cliPlugin;
|
|
7312
8485
|
exports.cloudflareBindings = cloudflareBindings;
|
|
7313
8486
|
exports.contentPlugin = contentPlugin;
|
|
7314
8487
|
exports.createApp = createApp;
|