@moku-labs/web 1.13.2 → 1.15.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 +58 -39
- package/dist/browser.mjs +292 -202
- package/dist/index.cjs +2517 -2395
- package/dist/index.d.cts +61 -41
- package/dist/index.d.mts +61 -41
- package/dist/index.mjs +2517 -2395
- package/package.json +1 -1
package/dist/browser.mjs
CHANGED
|
@@ -49,166 +49,9 @@ const createPlugin$1 = coreConfig.createPlugin;
|
|
|
49
49
|
*/
|
|
50
50
|
const createCore = coreConfig.createCore;
|
|
51
51
|
//#endregion
|
|
52
|
-
//#region src/plugins/i18n/api.ts
|
|
53
|
-
/** Error prefix for all i18n lifecycle failures. */
|
|
54
|
-
const ERROR_PREFIX$8 = "[web]";
|
|
55
|
-
/**
|
|
56
|
-
* Validates the resolved i18n config (fail-fast at `createApp`). Throws when
|
|
57
|
-
* `locales` is empty or when `defaultLocale` is not a member of `locales`.
|
|
58
|
-
* Errors use the `[web]` prefix with an actionable remediation line.
|
|
59
|
-
*
|
|
60
|
-
* @param ctx - Plugin context carrying the resolved {@link Config}.
|
|
61
|
-
* @param ctx.config - The resolved i18n {@link Config}.
|
|
62
|
-
* @throws {Error} If `locales` is empty or `defaultLocale` is not in `locales`.
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* validateI18nConfig({ config: { locales: ["en"], defaultLocale: "en" } });
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
|
-
function validateI18nConfig(ctx) {
|
|
69
|
-
const { locales, defaultLocale } = ctx.config;
|
|
70
|
-
if (locales.length === 0) throw new Error(`${ERROR_PREFIX$8} i18n.locales must contain at least one locale.\n Set pluginConfigs.i18n.locales to a non-empty array, e.g. ["en"].`);
|
|
71
|
-
if (!locales.includes(defaultLocale)) throw new Error(`${ERROR_PREFIX$8} 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.`);
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Creates the i18n plugin API surface — locale registry accessors plus the
|
|
75
|
-
* `t()` translator with default-locale fallback. Every method is a pure read
|
|
76
|
-
* of `ctx.config`; none mutate, and `t()` always returns a string.
|
|
77
|
-
*
|
|
78
|
-
* @param ctx - Plugin context carrying the resolved {@link Config}.
|
|
79
|
-
* @param ctx.config - The resolved i18n {@link Config}.
|
|
80
|
-
* @returns The {@link Api} accessor surface mounted at `app.i18n`.
|
|
81
|
-
* @example
|
|
82
|
-
* ```ts
|
|
83
|
-
* const api = createI18nApi({ config: { locales: ["en"], defaultLocale: "en" } });
|
|
84
|
-
* api.t("en", "nav.home");
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
function createI18nApi(ctx) {
|
|
88
|
-
const { config } = ctx;
|
|
89
|
-
return {
|
|
90
|
-
/**
|
|
91
|
-
* Returns the configured supported locales in declared order.
|
|
92
|
-
*
|
|
93
|
-
* @returns The configured `locales` list (priority/display order).
|
|
94
|
-
* @example
|
|
95
|
-
* ```ts
|
|
96
|
-
* api.locales(); // ["en", "uk"]
|
|
97
|
-
* ```
|
|
98
|
-
*/
|
|
99
|
-
locales() {
|
|
100
|
-
return config.locales;
|
|
101
|
-
},
|
|
102
|
-
/**
|
|
103
|
-
* Returns the fallback locale used when a requested locale is absent.
|
|
104
|
-
*
|
|
105
|
-
* @returns The configured `defaultLocale`.
|
|
106
|
-
* @example
|
|
107
|
-
* ```ts
|
|
108
|
-
* api.defaultLocale(); // "en"
|
|
109
|
-
* ```
|
|
110
|
-
*/
|
|
111
|
-
defaultLocale() {
|
|
112
|
-
return config.defaultLocale;
|
|
113
|
-
},
|
|
114
|
-
/**
|
|
115
|
-
* Membership guard: whether `x` is one of the supported locales
|
|
116
|
-
* (case-sensitive).
|
|
117
|
-
*
|
|
118
|
-
* @param x - Candidate locale code.
|
|
119
|
-
* @returns `true` if `x ∈ locales`, else `false`.
|
|
120
|
-
* @example
|
|
121
|
-
* ```ts
|
|
122
|
-
* api.isLocale("uk"); // true
|
|
123
|
-
* ```
|
|
124
|
-
*/
|
|
125
|
-
isLocale(x) {
|
|
126
|
-
return config.locales.includes(x);
|
|
127
|
-
},
|
|
128
|
-
/**
|
|
129
|
-
* Human-readable display name for a locale.
|
|
130
|
-
*
|
|
131
|
-
* @param locale - Locale code to look up.
|
|
132
|
-
* @returns The display name, or `undefined` if unmapped.
|
|
133
|
-
* @example
|
|
134
|
-
* ```ts
|
|
135
|
-
* api.localeName("uk"); // "Українська"
|
|
136
|
-
* ```
|
|
137
|
-
*/
|
|
138
|
-
localeName(locale) {
|
|
139
|
-
return config.localeNames?.[locale];
|
|
140
|
-
},
|
|
141
|
-
/**
|
|
142
|
-
* Open Graph `og:locale` value for a locale.
|
|
143
|
-
*
|
|
144
|
-
* @param locale - Locale code to look up.
|
|
145
|
-
* @returns The `og:locale` value (e.g. `"en_US"`), or `undefined` if unmapped.
|
|
146
|
-
* @example
|
|
147
|
-
* ```ts
|
|
148
|
-
* api.ogLocale("en"); // "en_US"
|
|
149
|
-
* ```
|
|
150
|
-
*/
|
|
151
|
-
ogLocale(locale) {
|
|
152
|
-
return config.ogLocaleMap?.[locale];
|
|
153
|
-
},
|
|
154
|
-
/**
|
|
155
|
-
* Translate `key` for `locale` with a deterministic fallback chain
|
|
156
|
-
* (requested locale → default locale → the key itself). The default-locale
|
|
157
|
-
* lookup is skipped when `locale === defaultLocale`.
|
|
158
|
-
*
|
|
159
|
-
* @param locale - Requested locale code.
|
|
160
|
-
* @param key - Translation key (e.g. `"nav.home"`).
|
|
161
|
-
* @returns The translated value, the default-locale value, or `key`.
|
|
162
|
-
* @example
|
|
163
|
-
* ```ts
|
|
164
|
-
* api.t("uk", "nav.home"); // "Головна"
|
|
165
|
-
* ```
|
|
166
|
-
*/
|
|
167
|
-
t(locale, key) {
|
|
168
|
-
const exact = config.translations?.[locale]?.[key];
|
|
169
|
-
if (exact !== void 0) return exact;
|
|
170
|
-
if (locale !== config.defaultLocale) {
|
|
171
|
-
const fallback = config.translations?.[config.defaultLocale]?.[key];
|
|
172
|
-
if (fallback !== void 0) return fallback;
|
|
173
|
-
}
|
|
174
|
-
return key;
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Internationalization plugin — locale registry plus a flat translation helper
|
|
180
|
-
* with default-locale fallback. Pure config-as-data (no state or events);
|
|
181
|
-
* consumed read-only by content, router, head, and build.
|
|
182
|
-
*
|
|
183
|
-
* @example Register locales and translations
|
|
184
|
-
* ```ts
|
|
185
|
-
* const app = createApp({
|
|
186
|
-
* pluginConfigs: {
|
|
187
|
-
* i18n: {
|
|
188
|
-
* locales: ["en", "uk"],
|
|
189
|
-
* defaultLocale: "en",
|
|
190
|
-
* localeNames: { en: "English", uk: "Українська" },
|
|
191
|
-
* translations: { uk: { "nav.home": "Головна" } }
|
|
192
|
-
* }
|
|
193
|
-
* }
|
|
194
|
-
* });
|
|
195
|
-
* ```
|
|
196
|
-
*/
|
|
197
|
-
const i18nPlugin = createPlugin$1("i18n", {
|
|
198
|
-
config: {
|
|
199
|
-
locales: ["en"],
|
|
200
|
-
defaultLocale: "en",
|
|
201
|
-
localeNames: {},
|
|
202
|
-
ogLocaleMap: {},
|
|
203
|
-
translations: {}
|
|
204
|
-
},
|
|
205
|
-
onInit: validateI18nConfig,
|
|
206
|
-
api: createI18nApi
|
|
207
|
-
});
|
|
208
|
-
//#endregion
|
|
209
52
|
//#region src/plugins/site/api.ts
|
|
210
53
|
/** Error prefix for all site lifecycle/validation failures. */
|
|
211
|
-
const ERROR_PREFIX$
|
|
54
|
+
const ERROR_PREFIX$8 = "[web]";
|
|
212
55
|
/** URL protocols that qualify a parsed URL as an absolute http/https URL. */
|
|
213
56
|
const HTTP_PROTOCOLS = new Set(["http:", "https:"]);
|
|
214
57
|
/**
|
|
@@ -307,8 +150,8 @@ function isAbsoluteUrl(value) {
|
|
|
307
150
|
* ```
|
|
308
151
|
*/
|
|
309
152
|
function validateSiteConfig(ctx) {
|
|
310
|
-
if (!isNonEmpty(ctx.config.name)) throw new Error(`${ERROR_PREFIX$
|
|
311
|
-
if (!isAbsoluteUrl(ctx.config.url)) throw new Error(`${ERROR_PREFIX$
|
|
153
|
+
if (!isNonEmpty(ctx.config.name)) throw new Error(`${ERROR_PREFIX$8} site.name is required.\n Provide a non-empty site name in pluginConfigs.site.name.`);
|
|
154
|
+
if (!isAbsoluteUrl(ctx.config.url)) throw new Error(`${ERROR_PREFIX$8} 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".`);
|
|
312
155
|
}
|
|
313
156
|
/**
|
|
314
157
|
* Creates the site plugin API surface — read-only accessors over frozen config
|
|
@@ -421,6 +264,202 @@ const sitePlugin = createPlugin$1("site", {
|
|
|
421
264
|
api: createSiteApi
|
|
422
265
|
});
|
|
423
266
|
//#endregion
|
|
267
|
+
//#region src/plugins/i18n/api.ts
|
|
268
|
+
/** Error prefix for all i18n lifecycle failures. */
|
|
269
|
+
const ERROR_PREFIX$7 = "[web]";
|
|
270
|
+
/**
|
|
271
|
+
* The framework's default i18n config — a single `"en"` locale with empty lookup
|
|
272
|
+
* maps. Used both as the i18n plugin's `config` default and as the source for
|
|
273
|
+
* {@link fallbackI18n}, so "no i18n config" and "no i18n plugin" resolve identically.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* createI18nApi({ config: DEFAULT_I18N_CONFIG }).defaultLocale(); // "en"
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
const DEFAULT_I18N_CONFIG = {
|
|
281
|
+
locales: ["en"],
|
|
282
|
+
defaultLocale: "en",
|
|
283
|
+
localeNames: {},
|
|
284
|
+
ogLocaleMap: {},
|
|
285
|
+
translations: {}
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Validates the resolved i18n config (fail-fast at `createApp`). Throws when
|
|
289
|
+
* `locales` is empty or when `defaultLocale` is not a member of `locales`.
|
|
290
|
+
* Errors use the `[web]` prefix with an actionable remediation line.
|
|
291
|
+
*
|
|
292
|
+
* @param ctx - Plugin context carrying the resolved {@link Config}.
|
|
293
|
+
* @param ctx.config - The resolved i18n {@link Config}.
|
|
294
|
+
* @throws {Error} If `locales` is empty or `defaultLocale` is not in `locales`.
|
|
295
|
+
* @example
|
|
296
|
+
* ```ts
|
|
297
|
+
* validateI18nConfig({ config: { locales: ["en"], defaultLocale: "en" } });
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
function validateI18nConfig(ctx) {
|
|
301
|
+
const { locales, defaultLocale } = ctx.config;
|
|
302
|
+
if (locales.length === 0) throw new Error(`${ERROR_PREFIX$7} i18n.locales must contain at least one locale.\n Set pluginConfigs.i18n.locales to a non-empty array, e.g. ["en"].`);
|
|
303
|
+
if (!locales.includes(defaultLocale)) throw new Error(`${ERROR_PREFIX$7} 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.`);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Creates the i18n plugin API surface — locale registry accessors plus the
|
|
307
|
+
* `t()` translator with default-locale fallback. Every method is a pure read
|
|
308
|
+
* of `ctx.config`; none mutate, and `t()` always returns a string.
|
|
309
|
+
*
|
|
310
|
+
* @param ctx - Plugin context carrying the resolved {@link Config}.
|
|
311
|
+
* @param ctx.config - The resolved i18n {@link Config}.
|
|
312
|
+
* @returns The {@link Api} accessor surface mounted at `app.i18n`.
|
|
313
|
+
* @example
|
|
314
|
+
* ```ts
|
|
315
|
+
* const api = createI18nApi({ config: { locales: ["en"], defaultLocale: "en" } });
|
|
316
|
+
* api.t("en", "nav.home");
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
function createI18nApi(ctx) {
|
|
320
|
+
const { config } = ctx;
|
|
321
|
+
return {
|
|
322
|
+
/**
|
|
323
|
+
* Returns the configured supported locales in declared order.
|
|
324
|
+
*
|
|
325
|
+
* @returns The configured `locales` list (priority/display order).
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* api.locales(); // ["en", "uk"]
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
locales() {
|
|
332
|
+
return config.locales;
|
|
333
|
+
},
|
|
334
|
+
/**
|
|
335
|
+
* Returns the fallback locale used when a requested locale is absent.
|
|
336
|
+
*
|
|
337
|
+
* @returns The configured `defaultLocale`.
|
|
338
|
+
* @example
|
|
339
|
+
* ```ts
|
|
340
|
+
* api.defaultLocale(); // "en"
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
defaultLocale() {
|
|
344
|
+
return config.defaultLocale;
|
|
345
|
+
},
|
|
346
|
+
/**
|
|
347
|
+
* Membership guard: whether `x` is one of the supported locales
|
|
348
|
+
* (case-sensitive).
|
|
349
|
+
*
|
|
350
|
+
* @param x - Candidate locale code.
|
|
351
|
+
* @returns `true` if `x ∈ locales`, else `false`.
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* api.isLocale("uk"); // true
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
isLocale(x) {
|
|
358
|
+
return config.locales.includes(x);
|
|
359
|
+
},
|
|
360
|
+
/**
|
|
361
|
+
* Human-readable display name for a locale.
|
|
362
|
+
*
|
|
363
|
+
* @param locale - Locale code to look up.
|
|
364
|
+
* @returns The display name, or `undefined` if unmapped.
|
|
365
|
+
* @example
|
|
366
|
+
* ```ts
|
|
367
|
+
* api.localeName("uk"); // "Українська"
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
localeName(locale) {
|
|
371
|
+
return config.localeNames?.[locale];
|
|
372
|
+
},
|
|
373
|
+
/**
|
|
374
|
+
* Open Graph `og:locale` value for a locale.
|
|
375
|
+
*
|
|
376
|
+
* @param locale - Locale code to look up.
|
|
377
|
+
* @returns The `og:locale` value (e.g. `"en_US"`), or `undefined` if unmapped.
|
|
378
|
+
* @example
|
|
379
|
+
* ```ts
|
|
380
|
+
* api.ogLocale("en"); // "en_US"
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
ogLocale(locale) {
|
|
384
|
+
return config.ogLocaleMap?.[locale];
|
|
385
|
+
},
|
|
386
|
+
/**
|
|
387
|
+
* Translate `key` for `locale` with a deterministic fallback chain
|
|
388
|
+
* (requested locale → default locale → the key itself). The default-locale
|
|
389
|
+
* lookup is skipped when `locale === defaultLocale`.
|
|
390
|
+
*
|
|
391
|
+
* @param locale - Requested locale code.
|
|
392
|
+
* @param key - Translation key (e.g. `"nav.home"`).
|
|
393
|
+
* @returns The translated value, the default-locale value, or `key`.
|
|
394
|
+
* @example
|
|
395
|
+
* ```ts
|
|
396
|
+
* api.t("uk", "nav.home"); // "Головна"
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
t(locale, key) {
|
|
400
|
+
const exact = config.translations?.[locale]?.[key];
|
|
401
|
+
if (exact !== void 0) return exact;
|
|
402
|
+
if (locale !== config.defaultLocale) {
|
|
403
|
+
const fallback = config.translations?.[config.defaultLocale]?.[key];
|
|
404
|
+
if (fallback !== void 0) return fallback;
|
|
405
|
+
}
|
|
406
|
+
return key;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The i18n API a consumer sees when the i18n plugin is NOT composed: a single
|
|
412
|
+
* default locale (`"en"`) with empty maps. `locales()` is `["en"]`,
|
|
413
|
+
* `defaultLocale()` is `"en"`, and every map lookup misses (`undefined`, or the
|
|
414
|
+
* key for `t()`). Identical to composing the i18n plugin with its defaults — which
|
|
415
|
+
* is what makes i18n optional: `router`/`head`/`content`/`build` fall back to this
|
|
416
|
+
* when `ctx.has("i18n")` is false, leaving every downstream call unchanged.
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```ts
|
|
420
|
+
* const i18n = ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n;
|
|
421
|
+
* i18n.locales(); // ["en"]
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
const fallbackI18n = createI18nApi({ config: DEFAULT_I18N_CONFIG });
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/plugins/i18n/index.ts
|
|
427
|
+
/**
|
|
428
|
+
* i18n — Micro tier. Multi-file layout (index wiring + api.ts + types.ts) so
|
|
429
|
+
* index.ts stays within the ≤30-line wiring-only hook; logic lives in api.ts.
|
|
430
|
+
*
|
|
431
|
+
* Locale registry + flat translation helper with default-locale fallback.
|
|
432
|
+
* Pure config-as-data: no state, no events, no lifecycle resources.
|
|
433
|
+
* Consumed read-only by content/router/head/build via `ctx.require(i18nPlugin)`.
|
|
434
|
+
*
|
|
435
|
+
* @file i18n plugin wiring harness.
|
|
436
|
+
* @see README.md
|
|
437
|
+
*/
|
|
438
|
+
/**
|
|
439
|
+
* Internationalization plugin — locale registry plus a flat translation helper
|
|
440
|
+
* with default-locale fallback. Pure config-as-data (no state or events);
|
|
441
|
+
* consumed read-only by content, router, head, and build.
|
|
442
|
+
*
|
|
443
|
+
* @example Register locales and translations
|
|
444
|
+
* ```ts
|
|
445
|
+
* const app = createApp({
|
|
446
|
+
* pluginConfigs: {
|
|
447
|
+
* i18n: {
|
|
448
|
+
* locales: ["en", "uk"],
|
|
449
|
+
* defaultLocale: "en",
|
|
450
|
+
* localeNames: { en: "English", uk: "Українська" },
|
|
451
|
+
* translations: { uk: { "nav.home": "Головна" } }
|
|
452
|
+
* }
|
|
453
|
+
* }
|
|
454
|
+
* });
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
const i18nPlugin = createPlugin$1("i18n", {
|
|
458
|
+
config: DEFAULT_I18N_CONFIG,
|
|
459
|
+
onInit: validateI18nConfig,
|
|
460
|
+
api: createI18nApi
|
|
461
|
+
});
|
|
462
|
+
//#endregion
|
|
424
463
|
//#region src/plugins/router/iso-match.ts
|
|
425
464
|
/**
|
|
426
465
|
* Parse a single path segment into its `{…}` placeholder, or `false` for a static
|
|
@@ -1004,8 +1043,9 @@ function compileRoutes(input) {
|
|
|
1004
1043
|
const ERROR_PREFIX$5 = "[web] router";
|
|
1005
1044
|
/**
|
|
1006
1045
|
* Validate a route map and compile it into the matcher table on `ctx.state`,
|
|
1007
|
-
* resolving the global render `mode` + site base URL + i18n locales
|
|
1008
|
-
*
|
|
1046
|
+
* resolving the global render `mode` + site base URL + i18n locales (or the single
|
|
1047
|
+
* default-locale fallback when i18n is not composed) at call time. Called by the
|
|
1048
|
+
* router's `onInit` to compile `config.routes`. Re-calling replaces the table.
|
|
1009
1049
|
*
|
|
1010
1050
|
* @param ctx - The router register context (state + global mode + require).
|
|
1011
1051
|
* @param routes - The route map to compile (an `import * as routes` namespace works).
|
|
@@ -1017,7 +1057,7 @@ const ERROR_PREFIX$5 = "[web] router";
|
|
|
1017
1057
|
*/
|
|
1018
1058
|
function registerRoutes(ctx, routes) {
|
|
1019
1059
|
validateRoutes(routes);
|
|
1020
|
-
const i18n = ctx.require(i18nPlugin);
|
|
1060
|
+
const i18n = ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n;
|
|
1021
1061
|
ctx.state.table = compileRoutes({
|
|
1022
1062
|
routes,
|
|
1023
1063
|
mode: ctx.global.mode,
|
|
@@ -1442,8 +1482,8 @@ function createState$2(_ctx) {
|
|
|
1442
1482
|
/**
|
|
1443
1483
|
* Router plugin — typed, named route definitions with locale-aware URL generation
|
|
1444
1484
|
* and matching. Author routes with {@link route}, then register them the normal config
|
|
1445
|
-
* way via `pluginConfigs.router.routes` (compiled at init). Depends on site (base URL)
|
|
1446
|
-
*
|
|
1485
|
+
* way via `pluginConfigs.router.routes` (compiled at init). Depends on site (base URL);
|
|
1486
|
+
* i18n (locales) is OPTIONAL — falls back to a single default locale ("en") when absent.
|
|
1447
1487
|
*
|
|
1448
1488
|
* @example Register routes via config, then start/build
|
|
1449
1489
|
* ```ts
|
|
@@ -1456,7 +1496,7 @@ function createState$2(_ctx) {
|
|
|
1456
1496
|
* ```
|
|
1457
1497
|
*/
|
|
1458
1498
|
const routerPlugin = createPlugin$1("router", {
|
|
1459
|
-
depends: [sitePlugin
|
|
1499
|
+
depends: [sitePlugin],
|
|
1460
1500
|
helpers: {
|
|
1461
1501
|
route,
|
|
1462
1502
|
defineRoutes,
|
|
@@ -1898,7 +1938,8 @@ function serializeHead(elements) {
|
|
|
1898
1938
|
*
|
|
1899
1939
|
* The `render` method pulls `site`/`i18n`/`router` via `ctx.require` at call time,
|
|
1900
1940
|
* composes the head element set via the shared `compose.ts` module, and serializes
|
|
1901
|
-
* it to a string. It holds no resource and caches no subscription.
|
|
1941
|
+
* it to a string. It holds no resource and caches no subscription. `i18n` is
|
|
1942
|
+
* OPTIONAL — a single default-locale fallback is used when it is not composed.
|
|
1902
1943
|
*/
|
|
1903
1944
|
/** Error prefix for head API invariant failures. */
|
|
1904
1945
|
const ERROR_PREFIX$4 = "[web] head";
|
|
@@ -1950,7 +1991,7 @@ function createApi$1(ctx) {
|
|
|
1950
1991
|
data,
|
|
1951
1992
|
defaults: readDefaults(ctx.state),
|
|
1952
1993
|
site: ctx.require(sitePlugin),
|
|
1953
|
-
i18n: ctx.require(i18nPlugin),
|
|
1994
|
+
i18n: ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n,
|
|
1954
1995
|
router: ctx.require(routerPlugin)
|
|
1955
1996
|
}));
|
|
1956
1997
|
},
|
|
@@ -1970,7 +2011,8 @@ function createApi$1(ctx) {
|
|
|
1970
2011
|
*/
|
|
1971
2012
|
siteHead(input) {
|
|
1972
2013
|
const site = ctx.require(sitePlugin);
|
|
1973
|
-
const
|
|
2014
|
+
const i18n = ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n;
|
|
2015
|
+
const ogLocale = input.locale === void 0 ? void 0 : i18n.ogLocale(input.locale);
|
|
1974
2016
|
return serializeHead(composeSiteHead({
|
|
1975
2017
|
site,
|
|
1976
2018
|
defaults: readDefaults(ctx.state),
|
|
@@ -2107,7 +2149,7 @@ function createState$1(_ctx) {
|
|
|
2107
2149
|
* Head plugin — composes per-route `<head>` metadata (title template, Open Graph,
|
|
2108
2150
|
* Twitter cards, canonical, hreflang). Use the re-exported SEO primitives
|
|
2109
2151
|
* ({@link meta}, {@link og}, {@link twitter}, …) inside a route's `.head()`.
|
|
2110
|
-
* Depends on site
|
|
2152
|
+
* Depends on site and router; i18n is OPTIONAL (single default-locale fallback when absent).
|
|
2111
2153
|
*
|
|
2112
2154
|
* @example Set global head defaults
|
|
2113
2155
|
* ```ts
|
|
@@ -2123,11 +2165,7 @@ function createState$1(_ctx) {
|
|
|
2123
2165
|
* ```
|
|
2124
2166
|
*/
|
|
2125
2167
|
const headPlugin = createPlugin$1("head", {
|
|
2126
|
-
depends: [
|
|
2127
|
-
sitePlugin,
|
|
2128
|
-
i18nPlugin,
|
|
2129
|
-
routerPlugin
|
|
2130
|
-
],
|
|
2168
|
+
depends: [sitePlugin, routerPlugin],
|
|
2131
2169
|
helpers: headHelpers,
|
|
2132
2170
|
config: defaultConfig,
|
|
2133
2171
|
createState: createState$1,
|
|
@@ -2473,6 +2511,23 @@ const ERROR_PREFIX$2 = "[web]";
|
|
|
2473
2511
|
/** The set of legal hook names, frozen for O(1) membership checks. */
|
|
2474
2512
|
const HOOK_NAME_SET = new Set(COMPONENT_HOOK_NAMES);
|
|
2475
2513
|
/**
|
|
2514
|
+
* No-op link builder for the {@link EMPTY_ROUTE} slice (used when no route matched).
|
|
2515
|
+
*
|
|
2516
|
+
* @returns An empty string.
|
|
2517
|
+
* @example
|
|
2518
|
+
* const href = noUrl();
|
|
2519
|
+
*/
|
|
2520
|
+
function noUrl() {
|
|
2521
|
+
return "";
|
|
2522
|
+
}
|
|
2523
|
+
/** Empty route slice — used for mounts with no matched route (headless, tests, public `scan()`). */
|
|
2524
|
+
const EMPTY_ROUTE = {
|
|
2525
|
+
params: {},
|
|
2526
|
+
meta: {},
|
|
2527
|
+
locale: "",
|
|
2528
|
+
url: noUrl
|
|
2529
|
+
};
|
|
2530
|
+
/**
|
|
2476
2531
|
* Validate a single hook entry: its key must be a known hook name and its value
|
|
2477
2532
|
* must be a function. Throws fail-fast on the first violation.
|
|
2478
2533
|
*
|
|
@@ -2560,18 +2615,25 @@ function runHook(instance, hook, ctx) {
|
|
|
2560
2615
|
instance.def.hooks[hook]?.(ctx);
|
|
2561
2616
|
}
|
|
2562
2617
|
/**
|
|
2563
|
-
* Builds the component context handed to a hook
|
|
2618
|
+
* Builds the component context handed to a hook: the bound element + page data, merged
|
|
2619
|
+
* with the matched route's slice (params/meta/locale/url). Defaults to {@link EMPTY_ROUTE}
|
|
2620
|
+
* when no route is supplied (headless, tests, public `scan()`).
|
|
2564
2621
|
*
|
|
2565
2622
|
* @param element - The element the instance is bound to.
|
|
2566
2623
|
* @param data - The current page data payload.
|
|
2624
|
+
* @param route - The matched-route slice for the current URL.
|
|
2567
2625
|
* @returns The hook context.
|
|
2568
2626
|
* @example
|
|
2569
|
-
* const ctx = makeContext(element, data);
|
|
2627
|
+
* const ctx = makeContext(element, data, route);
|
|
2570
2628
|
*/
|
|
2571
|
-
function makeContext(element, data) {
|
|
2629
|
+
function makeContext(element, data, route = EMPTY_ROUTE) {
|
|
2572
2630
|
return {
|
|
2573
2631
|
el: element,
|
|
2574
|
-
data
|
|
2632
|
+
data,
|
|
2633
|
+
params: route.params,
|
|
2634
|
+
meta: route.meta,
|
|
2635
|
+
locale: route.locale,
|
|
2636
|
+
url: route.url
|
|
2575
2637
|
};
|
|
2576
2638
|
}
|
|
2577
2639
|
/**
|
|
@@ -2585,17 +2647,18 @@ function makeContext(element, data) {
|
|
|
2585
2647
|
* @param swapArea - The swap-region element, or null when none was found.
|
|
2586
2648
|
* @param data - The current page data payload.
|
|
2587
2649
|
* @param element - The candidate element carrying a `data-component` attribute.
|
|
2650
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
2588
2651
|
* @example
|
|
2589
|
-
* mountElement(state, emit, swapArea, data, element);
|
|
2652
|
+
* mountElement(state, emit, swapArea, data, element, route);
|
|
2590
2653
|
*/
|
|
2591
|
-
function mountElement(state, emit, swapArea, data, element) {
|
|
2654
|
+
function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
|
|
2592
2655
|
if (state.instances.has(element)) return;
|
|
2593
2656
|
const name = element.dataset.component;
|
|
2594
2657
|
if (!name) return;
|
|
2595
2658
|
const definition = state.registeredComponents.get(name);
|
|
2596
2659
|
if (!definition) return;
|
|
2597
2660
|
const instance = createInstance(definition, element, swapArea ? !swapArea.contains(element) : true);
|
|
2598
|
-
const ctx = makeContext(element, data);
|
|
2661
|
+
const ctx = makeContext(element, data, route);
|
|
2599
2662
|
runHook(instance, "onCreate", ctx);
|
|
2600
2663
|
runHook(instance, "onMount", ctx);
|
|
2601
2664
|
state.instances.set(element, instance);
|
|
@@ -2613,14 +2676,15 @@ function mountElement(state, emit, swapArea, data, element) {
|
|
|
2613
2676
|
* @param state - The plugin state (registeredComponents + instances).
|
|
2614
2677
|
* @param emit - The event emitter for spa:component-mount.
|
|
2615
2678
|
* @param swapSelector - CSS selector bounding page-specific components.
|
|
2679
|
+
* @param route - The matched-route slice for the current URL (params/meta/locale/url).
|
|
2616
2680
|
* @example
|
|
2617
|
-
* scanAndMount(state, emit, "main > section");
|
|
2681
|
+
* scanAndMount(state, emit, "main > section", route);
|
|
2618
2682
|
*/
|
|
2619
|
-
function scanAndMount(state, emit, swapSelector) {
|
|
2683
|
+
function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
|
|
2620
2684
|
if (typeof document === "undefined") return;
|
|
2621
2685
|
const swapArea = document.querySelector(swapSelector);
|
|
2622
2686
|
const data = extractPageData(document);
|
|
2623
|
-
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element);
|
|
2687
|
+
for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element, route);
|
|
2624
2688
|
}
|
|
2625
2689
|
/**
|
|
2626
2690
|
* Unmounts page-specific instances inside the swap region (runs `onUnMount`
|
|
@@ -2686,12 +2750,13 @@ function notifyNavStart(state) {
|
|
|
2686
2750
|
* instances were already destroyed and re-created by the swap).
|
|
2687
2751
|
*
|
|
2688
2752
|
* @param state - The plugin state holding live instances.
|
|
2753
|
+
* @param route - The matched-route slice for the destination URL (params/meta/locale/url).
|
|
2689
2754
|
* @example
|
|
2690
|
-
* notifyNavEnd(state);
|
|
2755
|
+
* notifyNavEnd(state, route);
|
|
2691
2756
|
*/
|
|
2692
|
-
function notifyNavEnd(state) {
|
|
2757
|
+
function notifyNavEnd(state, route = EMPTY_ROUTE) {
|
|
2693
2758
|
const data = typeof document === "undefined" ? {} : extractPageData(document);
|
|
2694
|
-
for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data));
|
|
2759
|
+
for (const [element, instance] of state.instances) if (instance.persistent) runHook(instance, "onNavEnd", makeContext(element, data, route));
|
|
2695
2760
|
}
|
|
2696
2761
|
//#endregion
|
|
2697
2762
|
//#region src/plugins/spa/head.ts
|
|
@@ -3334,6 +3399,26 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3334
3399
|
});
|
|
3335
3400
|
};
|
|
3336
3401
|
/**
|
|
3402
|
+
* Build the matched-route slice (params/meta/locale/url) for the component context at `path`,
|
|
3403
|
+
* so islands read their route's params/meta directly. An unmatched path yields an empty slice.
|
|
3404
|
+
*
|
|
3405
|
+
* @param path - The URL (pathname + search) to match.
|
|
3406
|
+
* @returns The route slice for the matched route.
|
|
3407
|
+
* @example
|
|
3408
|
+
* scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(pathname));
|
|
3409
|
+
*/
|
|
3410
|
+
const componentRouteContext = (path) => {
|
|
3411
|
+
const matchPath = path.split("?")[0] ?? path;
|
|
3412
|
+
const hit = deps.router.match(matchPath);
|
|
3413
|
+
const locale = hit?.params.lang ?? (typeof document === "undefined" ? "" : document.documentElement.lang) ?? "";
|
|
3414
|
+
return {
|
|
3415
|
+
params: hit?.params ?? {},
|
|
3416
|
+
meta: hit?.route._meta ?? {},
|
|
3417
|
+
locale,
|
|
3418
|
+
url: (name, params = {}) => deps.router.toUrl(name, params)
|
|
3419
|
+
};
|
|
3420
|
+
};
|
|
3421
|
+
/**
|
|
3337
3422
|
* Process one navigation: head-sync, unmount, swap, re-mount, emit navigated.
|
|
3338
3423
|
* When the region cannot be swapped (either document lacks the swap selector)
|
|
3339
3424
|
* the SPA nav cannot complete — the head is already synced and the islands torn
|
|
@@ -3351,8 +3436,9 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3351
3436
|
syncHead(deps.head, doc);
|
|
3352
3437
|
unmountPageSpecific(state, emit);
|
|
3353
3438
|
if (!swapRegion(doc, resolved.swapSelector, resolved.viewTransitions, () => {
|
|
3354
|
-
|
|
3355
|
-
|
|
3439
|
+
const routeSlice = componentRouteContext(pathname);
|
|
3440
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3441
|
+
notifyNavEnd(state, routeSlice);
|
|
3356
3442
|
}, applyPendingScroll)) {
|
|
3357
3443
|
handleError();
|
|
3358
3444
|
location.href = pathname;
|
|
@@ -3421,6 +3507,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3421
3507
|
params: hit.params,
|
|
3422
3508
|
data,
|
|
3423
3509
|
locale,
|
|
3510
|
+
meta: hit.route._meta,
|
|
3424
3511
|
url: (routeName, routeParams = {}) => deps.router.toUrl(routeName, routeParams)
|
|
3425
3512
|
};
|
|
3426
3513
|
const vnode = hit.route._handlers.render(routeContext);
|
|
@@ -3452,6 +3539,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3452
3539
|
if (signal?.aborted) return;
|
|
3453
3540
|
syncDataHead(deps.head, route, routeContext);
|
|
3454
3541
|
unmountPageSpecific(state, emit);
|
|
3542
|
+
const routeSlice = componentRouteContext(pathname);
|
|
3455
3543
|
/**
|
|
3456
3544
|
* Render the VNode into the region and re-mount its islands in one paint — the
|
|
3457
3545
|
* swap body handed to `runSwap` (optionally wrapped in a View Transition).
|
|
@@ -3463,8 +3551,8 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3463
3551
|
*/
|
|
3464
3552
|
const renderAndMount = () => {
|
|
3465
3553
|
renderVNode(vnode, region);
|
|
3466
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
3467
|
-
notifyNavEnd(state);
|
|
3554
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3555
|
+
notifyNavEnd(state, routeSlice);
|
|
3468
3556
|
};
|
|
3469
3557
|
runSwap(renderAndMount, resolved.viewTransitions, applyPendingScroll);
|
|
3470
3558
|
state.currentUrl = pathname;
|
|
@@ -3509,15 +3597,16 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3509
3597
|
* await bootRender("/b/abc123");
|
|
3510
3598
|
*/
|
|
3511
3599
|
const bootRender = async (pathname) => {
|
|
3600
|
+
const routeSlice = componentRouteContext(pathname);
|
|
3512
3601
|
const resolvedRender = await resolveDataRender(pathname);
|
|
3513
3602
|
if (resolvedRender === false) {
|
|
3514
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
3603
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3515
3604
|
return;
|
|
3516
3605
|
}
|
|
3517
3606
|
const { vnode, region } = resolvedRender;
|
|
3518
3607
|
const { renderVNode } = await import("./render-BNe0s7fr.mjs");
|
|
3519
3608
|
renderVNode(vnode, region);
|
|
3520
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
3609
|
+
scanAndMount(state, emit, resolved.swapSelector, routeSlice);
|
|
3521
3610
|
};
|
|
3522
3611
|
/**
|
|
3523
3612
|
* Unified navigation: try the client DATA path first (only when the `data`
|
|
@@ -3567,7 +3656,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3567
3656
|
const matchPath = state.currentUrl.split("?")[0] ?? state.currentUrl;
|
|
3568
3657
|
const hit = deps.router.match(matchPath);
|
|
3569
3658
|
if (hit?.route._handlers.render && isClientOnlyRoute(deps.router.mode(), hit.route)) bootRender(state.currentUrl);
|
|
3570
|
-
else scanAndMount(state, emit, resolved.swapSelector);
|
|
3659
|
+
else scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
3571
3660
|
state.started = true;
|
|
3572
3661
|
},
|
|
3573
3662
|
/**
|
|
@@ -3598,7 +3687,7 @@ function createSpaKernel(state, config, emit, deps) {
|
|
|
3598
3687
|
* kernel.scan();
|
|
3599
3688
|
*/
|
|
3600
3689
|
scan() {
|
|
3601
|
-
scanAndMount(state, emit, resolved.swapSelector);
|
|
3690
|
+
scanAndMount(state, emit, resolved.swapSelector, componentRouteContext(state.currentUrl));
|
|
3602
3691
|
},
|
|
3603
3692
|
/**
|
|
3604
3693
|
* Tear down router listeners, dispose all instances, reset boot state.
|
|
@@ -3908,7 +3997,8 @@ function articleNotFound(slug, locale) {
|
|
|
3908
3997
|
return /* @__PURE__ */ new Error(`[web] content article "${slug}" not found for locale "${locale}".\n Looked for ${slug}/${locale}.md and the default-locale fallback.`);
|
|
3909
3998
|
}
|
|
3910
3999
|
/**
|
|
3911
|
-
* Plugin `api` factory: resolves i18n via `ctx.require
|
|
4000
|
+
* Plugin `api` factory: resolves i18n via `ctx.require` (or the single default-locale
|
|
4001
|
+
* fallback when i18n is not composed), merges `config.providers` into
|
|
3912
4002
|
* one source, assembles the kernel-free {@link ContentApiContext}, and delegates to
|
|
3913
4003
|
* {@link createContentApi}. Referenced directly as the plugin's `api` so index.ts stays
|
|
3914
4004
|
* wiring-only. Imports no node code (the provider owns it).
|
|
@@ -3921,7 +4011,7 @@ function articleNotFound(slug, locale) {
|
|
|
3921
4011
|
* ```
|
|
3922
4012
|
*/
|
|
3923
4013
|
function contentApi(ctx) {
|
|
3924
|
-
const i18nApi = ctx.require(i18nPlugin);
|
|
4014
|
+
const i18nApi = ctx.has("i18n") ? ctx.require(i18nPlugin) : fallbackI18n;
|
|
3925
4015
|
/**
|
|
3926
4016
|
* Active locale codes from i18n.
|
|
3927
4017
|
*
|
|
@@ -4326,8 +4416,8 @@ function validateContentConfig(config) {
|
|
|
4326
4416
|
* @file content — Complex Plugin skeleton (wiring-only).
|
|
4327
4417
|
*
|
|
4328
4418
|
* Markdown pipeline: discover, parse frontmatter, render to sanitized HTML, and
|
|
4329
|
-
* expose a locale-keyed Article model.
|
|
4330
|
-
* and `content:invalidated`.
|
|
4419
|
+
* expose a locale-keyed Article model. i18n is OPTIONAL (single default-locale
|
|
4420
|
+
* fallback when absent). Emits `content:ready` and `content:invalidated`.
|
|
4331
4421
|
* @see README.md
|
|
4332
4422
|
*/
|
|
4333
4423
|
/**
|
|
@@ -4335,7 +4425,8 @@ function validateContentConfig(config) {
|
|
|
4335
4425
|
* (locale fallback, draft filtering, sort, caching, events) lives here; source I/O +
|
|
4336
4426
|
* the Markdown pipeline live in a {@link ContentProvider} you compose (like `env`
|
|
4337
4427
|
* providers). The shell imports zero node code, so `contentPlugin` is browser-safe.
|
|
4338
|
-
*
|
|
4428
|
+
* i18n is OPTIONAL (single default-locale fallback when absent); emits `content:ready`
|
|
4429
|
+
* and `content:invalidated`.
|
|
4339
4430
|
*
|
|
4340
4431
|
* @example Compose the node filesystem provider with a content dir + Shiki theme
|
|
4341
4432
|
* ```ts
|
|
@@ -4351,7 +4442,6 @@ function validateContentConfig(config) {
|
|
|
4351
4442
|
* ```
|
|
4352
4443
|
*/
|
|
4353
4444
|
const contentPlugin = createPlugin$1("content", {
|
|
4354
|
-
depends: [i18nPlugin],
|
|
4355
4445
|
events: contentEvents,
|
|
4356
4446
|
config: defaultContentConfig,
|
|
4357
4447
|
createState: createContentState,
|