@moku-labs/web 1.3.1 → 1.4.1
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 +29 -3
- package/dist/browser.mjs +92 -21
- package/dist/index.cjs +182 -50
- package/dist/index.d.cts +40 -4
- package/dist/index.d.mts +40 -4
- package/dist/index.mjs +182 -50
- package/package.json +1 -1
package/dist/browser.d.mts
CHANGED
|
@@ -557,6 +557,32 @@ type Api$3 = {
|
|
|
557
557
|
*/
|
|
558
558
|
t(locale: string, key: string): string;
|
|
559
559
|
};
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/plugins/router/iso-match.d.ts
|
|
562
|
+
/**
|
|
563
|
+
* A compiled, engine-agnostic path matcher: the same `.exec({ pathname })` shape the
|
|
564
|
+
* router consumed from `URLPattern`, but backed by a native `RegExp` with named
|
|
565
|
+
* groups. Dropping `URLPattern` keeps route matching alive in every browser engine —
|
|
566
|
+
* Safari < 18.4 and Firefox < ~142 have no `URLPattern` global and would otherwise
|
|
567
|
+
* throw `ReferenceError` the instant the router compiles its table on boot.
|
|
568
|
+
*/
|
|
569
|
+
interface PathMatcher {
|
|
570
|
+
/**
|
|
571
|
+
* Match a pathname, mirroring `URLPattern.exec`: the named-group bag (under
|
|
572
|
+
* `pathname.groups`) on a hit, or `null` on a miss.
|
|
573
|
+
*
|
|
574
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
575
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
576
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
577
|
+
*/
|
|
578
|
+
exec(input: {
|
|
579
|
+
readonly pathname: string;
|
|
580
|
+
}): {
|
|
581
|
+
readonly pathname: {
|
|
582
|
+
readonly groups: Record<string, string | undefined>;
|
|
583
|
+
};
|
|
584
|
+
} | null;
|
|
585
|
+
}
|
|
560
586
|
declare namespace types_d_exports$5 {
|
|
561
587
|
export { Api$2 as Api, ClientRoute, CompileInput, CompiledRoute, Config$2 as Config, ExtractApi$1 as ExtractApi, ExtractRouteParams, ExtractSegmentParameter, GenerateContext, HeadConfig$1 as HeadConfig, LayoutContext, LoadContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteRequire, RouteState, RouterApi, RouterConfig, RouterState, State$2 as State, TypedRoute, Urls };
|
|
562
588
|
}
|
|
@@ -836,10 +862,10 @@ interface CompiledRoute {
|
|
|
836
862
|
readonly pattern: string;
|
|
837
863
|
/** Dynamic-segment count (lower = more specific = matched first). */
|
|
838
864
|
readonly dynamicSegmentCount: number;
|
|
839
|
-
/** Pre-built
|
|
865
|
+
/** Pre-built path matchers (lang-aware + bare fallback). */
|
|
840
866
|
readonly matchers: {
|
|
841
|
-
readonly withLang:
|
|
842
|
-
readonly bare:
|
|
867
|
+
readonly withLang: PathMatcher;
|
|
868
|
+
readonly bare: PathMatcher;
|
|
843
869
|
};
|
|
844
870
|
/** Resolve pathname into params (withLang first, then bare with defaultLocale injected). */
|
|
845
871
|
readonly matchFn: (pathname: string) => Record<string, string> | null;
|
package/dist/browser.mjs
CHANGED
|
@@ -1331,22 +1331,101 @@ function extractGroups(groups) {
|
|
|
1331
1331
|
}
|
|
1332
1332
|
return params;
|
|
1333
1333
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1334
|
+
/** Regex metacharacters escaped when a static path segment is inlined into a compiled pattern. */
|
|
1335
|
+
const REGEX_METACHARS = /[.*+?^${}()|[\]\\]/g;
|
|
1336
|
+
/** Matches a `:name` or `:name(regex)` URLPattern group occupying one whole segment. */
|
|
1337
|
+
const NAMED_GROUP = /^:([A-Za-z_]\w*)(?:\((.+)\))?$/;
|
|
1336
1338
|
/**
|
|
1337
|
-
*
|
|
1339
|
+
* Escape a static path segment so its literal text matches verbatim inside the
|
|
1340
|
+
* compiled `RegExp` (a segment like `c++` must not be read as regex syntax).
|
|
1338
1341
|
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
+
* @param text - The static segment text.
|
|
1343
|
+
* @returns The regex-escaped segment.
|
|
1344
|
+
* @example
|
|
1345
|
+
* ```ts
|
|
1346
|
+
* escapeStaticSegment("about"); // "about"
|
|
1347
|
+
* ```
|
|
1342
1348
|
*/
|
|
1349
|
+
function escapeStaticSegment(text) {
|
|
1350
|
+
return text.replaceAll(REGEX_METACHARS, String.raw`\$&`);
|
|
1351
|
+
}
|
|
1343
1352
|
/**
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1353
|
+
* Compile one URLPattern source segment (no surrounding slash) into a regex fragment
|
|
1354
|
+
* that captures a single path segment: `:name` → a named `[^/]+` group, `:name(re)` →
|
|
1355
|
+
* a named group constrained by `re`, and static text → its escaped literal.
|
|
1346
1356
|
*
|
|
1347
|
-
* @param
|
|
1348
|
-
* @
|
|
1349
|
-
* @
|
|
1357
|
+
* @param segment - One source segment, e.g. `:slug`, `:lang(en|uk)`, or `archive`.
|
|
1358
|
+
* @returns The regex fragment for that segment.
|
|
1359
|
+
* @example
|
|
1360
|
+
* ```ts
|
|
1361
|
+
* segmentToRegex(":lang(en|uk)"); // "(?<lang>en|uk)"
|
|
1362
|
+
* ```
|
|
1363
|
+
*/
|
|
1364
|
+
function segmentToRegex(segment) {
|
|
1365
|
+
const named = NAMED_GROUP.exec(segment);
|
|
1366
|
+
if (named) {
|
|
1367
|
+
const [, name, constraint] = named;
|
|
1368
|
+
return `(?<${name}>${constraint ?? "[^/]+"})`;
|
|
1369
|
+
}
|
|
1370
|
+
return escapeStaticSegment(segment);
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Compile a URLPattern pathname source string into a {@link PathMatcher} backed by a
|
|
1374
|
+
* native `RegExp` — a drop-in replacement for `new URLPattern({ pathname })` over the
|
|
1375
|
+
* subset the router emits: `:name`, `:name(regex)`, the optional `:name?` segment
|
|
1376
|
+
* (whose leading `/` is absorbed, so `/:lang?` matches `/en` or nothing), static
|
|
1377
|
+
* segments, and a required trailing slash. Anchored full-match, like `URLPattern`.
|
|
1378
|
+
*
|
|
1379
|
+
* @param source - The URLPattern pathname source, e.g. `/:lang?/:slug/`.
|
|
1380
|
+
* @returns A matcher whose `.exec({ pathname })` yields named groups or `null`.
|
|
1381
|
+
* @example
|
|
1382
|
+
* ```ts
|
|
1383
|
+
* const m = createPathMatcher("/:lang?/:slug/");
|
|
1384
|
+
* m.exec({ pathname: "/en/hello/" }); // { pathname: { groups: { lang: "en", slug: "hello" } } }
|
|
1385
|
+
* ```
|
|
1386
|
+
*/
|
|
1387
|
+
function createPathMatcher(source) {
|
|
1388
|
+
const segments = source.split("/");
|
|
1389
|
+
let pattern = "^";
|
|
1390
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
1391
|
+
const segment = segments[index] ?? "";
|
|
1392
|
+
if (segment === "") {
|
|
1393
|
+
pattern += "/";
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
const optional = segment.endsWith("?");
|
|
1397
|
+
const fragment = segmentToRegex(optional ? segment.slice(0, -1) : segment);
|
|
1398
|
+
pattern += optional ? `(?:/${fragment})?` : `/${fragment}`;
|
|
1399
|
+
}
|
|
1400
|
+
pattern += "$";
|
|
1401
|
+
const regexp = new RegExp(pattern);
|
|
1402
|
+
return {
|
|
1403
|
+
/**
|
|
1404
|
+
* Run the compiled regex over a pathname (the {@link PathMatcher.exec} contract).
|
|
1405
|
+
*
|
|
1406
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
1407
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
1408
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
1409
|
+
* @example
|
|
1410
|
+
* ```ts
|
|
1411
|
+
* matcher.exec({ pathname: "/en/hello/" });
|
|
1412
|
+
* ```
|
|
1413
|
+
*/
|
|
1414
|
+
exec(input) {
|
|
1415
|
+
const result = regexp.exec(input.pathname);
|
|
1416
|
+
if (!result) return null;
|
|
1417
|
+
return { pathname: { groups: result.groups ?? {} } };
|
|
1418
|
+
} };
|
|
1419
|
+
}
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/plugins/router/builders/match.ts
|
|
1422
|
+
/**
|
|
1423
|
+
* Build a pathname matcher for a single route: tries the `withLang` matcher,
|
|
1424
|
+
* then the `bare` matcher injecting `defaultLocale` on miss.
|
|
1425
|
+
*
|
|
1426
|
+
* @param matchers - The pre-built `withLang` and `bare` matcher pair.
|
|
1427
|
+
* @param matchers.withLang - The locale-aware matcher variant.
|
|
1428
|
+
* @param matchers.bare - The bare matcher variant (no leading locale segment).
|
|
1350
1429
|
* @param defaultLocale - Locale injected when the bare fallback matches.
|
|
1351
1430
|
* @returns A function resolving a pathname into params, or `null` on no match.
|
|
1352
1431
|
* @example
|
|
@@ -1390,14 +1469,6 @@ function matchRoute(compiled, pathname) {
|
|
|
1390
1469
|
}
|
|
1391
1470
|
//#endregion
|
|
1392
1471
|
//#region src/plugins/router/builders/compile.ts
|
|
1393
|
-
/**
|
|
1394
|
-
* @file router plugin — compilation + validation domain.
|
|
1395
|
-
*
|
|
1396
|
-
* Pure functions invoked from `onInit`: validate the route map, then compile each
|
|
1397
|
-
* route into URLPattern matchers + URL/file builders, count dynamic segments,
|
|
1398
|
-
* sort by specificity, and assemble the immutable `MatcherTable`. Receives DATA
|
|
1399
|
-
* only (`CompileInput`) — never the plugin ctx.
|
|
1400
|
-
*/
|
|
1401
1472
|
/** Shared `[web]` error prefix for router validation failures. */
|
|
1402
1473
|
const ERROR_PREFIX$6 = "[web] router";
|
|
1403
1474
|
/** Maximum number of optional `{lang:?}` segments a single pattern may declare. */
|
|
@@ -1569,8 +1640,8 @@ function buildFilePath(pattern, params) {
|
|
|
1569
1640
|
function buildMatchers(pattern, locales) {
|
|
1570
1641
|
const langRegex = `(${locales.join("|")})`;
|
|
1571
1642
|
return {
|
|
1572
|
-
withLang:
|
|
1573
|
-
bare:
|
|
1643
|
+
withLang: createPathMatcher(patternToUrlPattern(pattern, "withLang", langRegex)),
|
|
1644
|
+
bare: createPathMatcher(patternToUrlPattern(pattern, "bare", langRegex))
|
|
1574
1645
|
};
|
|
1575
1646
|
}
|
|
1576
1647
|
/**
|
package/dist/index.cjs
CHANGED
|
@@ -1889,22 +1889,101 @@ function extractGroups(groups) {
|
|
|
1889
1889
|
}
|
|
1890
1890
|
return params;
|
|
1891
1891
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1892
|
+
/** Regex metacharacters escaped when a static path segment is inlined into a compiled pattern. */
|
|
1893
|
+
const REGEX_METACHARS = /[.*+?^${}()|[\]\\]/g;
|
|
1894
|
+
/** Matches a `:name` or `:name(regex)` URLPattern group occupying one whole segment. */
|
|
1895
|
+
const NAMED_GROUP = /^:([A-Za-z_]\w*)(?:\((.+)\))?$/;
|
|
1896
|
+
/**
|
|
1897
|
+
* Escape a static path segment so its literal text matches verbatim inside the
|
|
1898
|
+
* compiled `RegExp` (a segment like `c++` must not be read as regex syntax).
|
|
1899
|
+
*
|
|
1900
|
+
* @param text - The static segment text.
|
|
1901
|
+
* @returns The regex-escaped segment.
|
|
1902
|
+
* @example
|
|
1903
|
+
* ```ts
|
|
1904
|
+
* escapeStaticSegment("about"); // "about"
|
|
1905
|
+
* ```
|
|
1906
|
+
*/
|
|
1907
|
+
function escapeStaticSegment(text) {
|
|
1908
|
+
return text.replaceAll(REGEX_METACHARS, String.raw`\$&`);
|
|
1909
|
+
}
|
|
1894
1910
|
/**
|
|
1895
|
-
*
|
|
1911
|
+
* Compile one URLPattern source segment (no surrounding slash) into a regex fragment
|
|
1912
|
+
* that captures a single path segment: `:name` → a named `[^/]+` group, `:name(re)` →
|
|
1913
|
+
* a named group constrained by `re`, and static text → its escaped literal.
|
|
1896
1914
|
*
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1915
|
+
* @param segment - One source segment, e.g. `:slug`, `:lang(en|uk)`, or `archive`.
|
|
1916
|
+
* @returns The regex fragment for that segment.
|
|
1917
|
+
* @example
|
|
1918
|
+
* ```ts
|
|
1919
|
+
* segmentToRegex(":lang(en|uk)"); // "(?<lang>en|uk)"
|
|
1920
|
+
* ```
|
|
1900
1921
|
*/
|
|
1922
|
+
function segmentToRegex(segment) {
|
|
1923
|
+
const named = NAMED_GROUP.exec(segment);
|
|
1924
|
+
if (named) {
|
|
1925
|
+
const [, name, constraint] = named;
|
|
1926
|
+
return `(?<${name}>${constraint ?? "[^/]+"})`;
|
|
1927
|
+
}
|
|
1928
|
+
return escapeStaticSegment(segment);
|
|
1929
|
+
}
|
|
1901
1930
|
/**
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1931
|
+
* Compile a URLPattern pathname source string into a {@link PathMatcher} backed by a
|
|
1932
|
+
* native `RegExp` — a drop-in replacement for `new URLPattern({ pathname })` over the
|
|
1933
|
+
* subset the router emits: `:name`, `:name(regex)`, the optional `:name?` segment
|
|
1934
|
+
* (whose leading `/` is absorbed, so `/:lang?` matches `/en` or nothing), static
|
|
1935
|
+
* segments, and a required trailing slash. Anchored full-match, like `URLPattern`.
|
|
1904
1936
|
*
|
|
1905
|
-
* @param
|
|
1906
|
-
* @
|
|
1907
|
-
* @
|
|
1937
|
+
* @param source - The URLPattern pathname source, e.g. `/:lang?/:slug/`.
|
|
1938
|
+
* @returns A matcher whose `.exec({ pathname })` yields named groups or `null`.
|
|
1939
|
+
* @example
|
|
1940
|
+
* ```ts
|
|
1941
|
+
* const m = createPathMatcher("/:lang?/:slug/");
|
|
1942
|
+
* m.exec({ pathname: "/en/hello/" }); // { pathname: { groups: { lang: "en", slug: "hello" } } }
|
|
1943
|
+
* ```
|
|
1944
|
+
*/
|
|
1945
|
+
function createPathMatcher(source) {
|
|
1946
|
+
const segments = source.split("/");
|
|
1947
|
+
let pattern = "^";
|
|
1948
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
1949
|
+
const segment = segments[index] ?? "";
|
|
1950
|
+
if (segment === "") {
|
|
1951
|
+
pattern += "/";
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
const optional = segment.endsWith("?");
|
|
1955
|
+
const fragment = segmentToRegex(optional ? segment.slice(0, -1) : segment);
|
|
1956
|
+
pattern += optional ? `(?:/${fragment})?` : `/${fragment}`;
|
|
1957
|
+
}
|
|
1958
|
+
pattern += "$";
|
|
1959
|
+
const regexp = new RegExp(pattern);
|
|
1960
|
+
return {
|
|
1961
|
+
/**
|
|
1962
|
+
* Run the compiled regex over a pathname (the {@link PathMatcher.exec} contract).
|
|
1963
|
+
*
|
|
1964
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
1965
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
1966
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
1967
|
+
* @example
|
|
1968
|
+
* ```ts
|
|
1969
|
+
* matcher.exec({ pathname: "/en/hello/" });
|
|
1970
|
+
* ```
|
|
1971
|
+
*/
|
|
1972
|
+
exec(input) {
|
|
1973
|
+
const result = regexp.exec(input.pathname);
|
|
1974
|
+
if (!result) return null;
|
|
1975
|
+
return { pathname: { groups: result.groups ?? {} } };
|
|
1976
|
+
} };
|
|
1977
|
+
}
|
|
1978
|
+
//#endregion
|
|
1979
|
+
//#region src/plugins/router/builders/match.ts
|
|
1980
|
+
/**
|
|
1981
|
+
* Build a pathname matcher for a single route: tries the `withLang` matcher,
|
|
1982
|
+
* then the `bare` matcher injecting `defaultLocale` on miss.
|
|
1983
|
+
*
|
|
1984
|
+
* @param matchers - The pre-built `withLang` and `bare` matcher pair.
|
|
1985
|
+
* @param matchers.withLang - The locale-aware matcher variant.
|
|
1986
|
+
* @param matchers.bare - The bare matcher variant (no leading locale segment).
|
|
1908
1987
|
* @param defaultLocale - Locale injected when the bare fallback matches.
|
|
1909
1988
|
* @returns A function resolving a pathname into params, or `null` on no match.
|
|
1910
1989
|
* @example
|
|
@@ -1948,14 +2027,6 @@ function matchRoute(compiled, pathname) {
|
|
|
1948
2027
|
}
|
|
1949
2028
|
//#endregion
|
|
1950
2029
|
//#region src/plugins/router/builders/compile.ts
|
|
1951
|
-
/**
|
|
1952
|
-
* @file router plugin — compilation + validation domain.
|
|
1953
|
-
*
|
|
1954
|
-
* Pure functions invoked from `onInit`: validate the route map, then compile each
|
|
1955
|
-
* route into URLPattern matchers + URL/file builders, count dynamic segments,
|
|
1956
|
-
* sort by specificity, and assemble the immutable `MatcherTable`. Receives DATA
|
|
1957
|
-
* only (`CompileInput`) — never the plugin ctx.
|
|
1958
|
-
*/
|
|
1959
2030
|
/** Shared `[web]` error prefix for router validation failures. */
|
|
1960
2031
|
const ERROR_PREFIX$11 = "[web] router";
|
|
1961
2032
|
/** Maximum number of optional `{lang:?}` segments a single pattern may declare. */
|
|
@@ -2127,8 +2198,8 @@ function buildFilePath(pattern, params) {
|
|
|
2127
2198
|
function buildMatchers(pattern, locales) {
|
|
2128
2199
|
const langRegex = `(${locales.join("|")})`;
|
|
2129
2200
|
return {
|
|
2130
|
-
withLang:
|
|
2131
|
-
bare:
|
|
2201
|
+
withLang: createPathMatcher(patternToUrlPattern(pattern, "withLang", langRegex)),
|
|
2202
|
+
bare: createPathMatcher(patternToUrlPattern(pattern, "bare", langRegex))
|
|
2132
2203
|
};
|
|
2133
2204
|
}
|
|
2134
2205
|
/**
|
|
@@ -3842,7 +3913,7 @@ const DEFAULT_BODY = "<h1>404</h1><p>The page you requested could not be found.<
|
|
|
3842
3913
|
* ```
|
|
3843
3914
|
*/
|
|
3844
3915
|
function wrap(body) {
|
|
3845
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3916
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3846
3917
|
}
|
|
3847
3918
|
/**
|
|
3848
3919
|
* Emits `outDir/404.html`. When `config.notFound` is `true`, writes the built-in
|
|
@@ -4526,6 +4597,8 @@ const HEAD_PLACEHOLDER = "<!--moku:head-->";
|
|
|
4526
4597
|
const BODY_PLACEHOLDER = "<!--moku:body-->";
|
|
4527
4598
|
/** Template placeholder for the injected asset `<link>`/`<script>` tags. */
|
|
4528
4599
|
const ASSETS_PLACEHOLDER = "<!--moku:assets-->";
|
|
4600
|
+
/** Template placeholder for the page's locale (`<html lang>`). */
|
|
4601
|
+
const LANG_PLACEHOLDER = "<!--moku:lang-->";
|
|
4529
4602
|
/**
|
|
4530
4603
|
* Read the bundle phase's hashed asset manifest for one kind from `state.buildCache`
|
|
4531
4604
|
* as a typed {@link BuildCacheEntry} (no `Map<string, unknown>` reads).
|
|
@@ -4573,14 +4646,16 @@ function buildAssetTags(ctx) {
|
|
|
4573
4646
|
* ```
|
|
4574
4647
|
*/
|
|
4575
4648
|
function renderDocument(parts) {
|
|
4576
|
-
return `<!DOCTYPE html><html lang="${parts.locale}"><head>${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
|
|
4649
|
+
return `<!DOCTYPE html><html lang="${parts.locale}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
|
|
4577
4650
|
}
|
|
4578
4651
|
/**
|
|
4579
|
-
* Fill a shell template's `<!--moku:
|
|
4580
|
-
* `<!--moku:assets-->` placeholders deterministically at build
|
|
4652
|
+
* Fill a shell template's `<!--moku:lang-->` / `<!--moku:head-->` /
|
|
4653
|
+
* `<!--moku:body-->` / `<!--moku:assets-->` placeholders deterministically at build
|
|
4654
|
+
* time. `<!--moku:lang-->` carries the page locale (for `<html lang>`), so a single
|
|
4655
|
+
* shared template stays locale-correct across every locale.
|
|
4581
4656
|
*
|
|
4582
4657
|
* @param template - The raw shell template HTML.
|
|
4583
|
-
* @param parts - The composed head/body/assets pieces.
|
|
4658
|
+
* @param parts - The composed head/body/assets/locale pieces.
|
|
4584
4659
|
* @returns The filled document string.
|
|
4585
4660
|
* @example
|
|
4586
4661
|
* ```ts
|
|
@@ -4588,7 +4663,7 @@ function renderDocument(parts) {
|
|
|
4588
4663
|
* ```
|
|
4589
4664
|
*/
|
|
4590
4665
|
function fillTemplate(template, parts) {
|
|
4591
|
-
return template.replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
|
|
4666
|
+
return template.replaceAll(LANG_PLACEHOLDER, parts.locale).replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
|
|
4592
4667
|
}
|
|
4593
4668
|
/**
|
|
4594
4669
|
* Resolve the compiled entry for a manifest definition, asserting the router
|
|
@@ -5840,7 +5915,8 @@ function buildWranglerArgs(input) {
|
|
|
5840
5915
|
"--project-name",
|
|
5841
5916
|
input.slug,
|
|
5842
5917
|
"--branch",
|
|
5843
|
-
branch
|
|
5918
|
+
branch,
|
|
5919
|
+
"--commit-dirty=true"
|
|
5844
5920
|
];
|
|
5845
5921
|
}
|
|
5846
5922
|
/**
|
|
@@ -6449,17 +6525,18 @@ function validateConfig$1(ctx) {
|
|
|
6449
6525
|
ctx.require(sitePlugin);
|
|
6450
6526
|
}
|
|
6451
6527
|
/**
|
|
6452
|
-
* Run wrangler for the prepared argv and
|
|
6453
|
-
*
|
|
6454
|
-
*
|
|
6455
|
-
*
|
|
6528
|
+
* Run wrangler for the prepared argv and return its stdout, translating a non-zero
|
|
6529
|
+
* exit into the classified deploy error. The API token is read from env here so it
|
|
6530
|
+
* never crosses a logging boundary; the scrubbed stderr is used only to classify a
|
|
6531
|
+
* failure — it is never logged (that was console noise), so nothing leaks. Shared by
|
|
6532
|
+
* `run()` (deploy) and `createProject()` (project create).
|
|
6456
6533
|
*
|
|
6457
6534
|
* @param ctx - Plugin context (provides `state.spawn`, `config`, `env`).
|
|
6458
6535
|
* @param args - The fully-built, pre-validated wrangler argv.
|
|
6459
|
-
* @returns The wrangler `stdout`
|
|
6536
|
+
* @returns The wrangler `stdout` (for URL/id parsing on a deploy).
|
|
6460
6537
|
* @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
|
|
6461
6538
|
* @example
|
|
6462
|
-
* const
|
|
6539
|
+
* const stdout = await executeWrangler(ctx, args);
|
|
6463
6540
|
*/
|
|
6464
6541
|
async function executeWrangler(ctx, args) {
|
|
6465
6542
|
const token = ctx.env.require("CLOUDFLARE_API_TOKEN");
|
|
@@ -6473,10 +6550,7 @@ async function executeWrangler(ctx, args) {
|
|
|
6473
6550
|
const { code, message } = classifyWranglerError(exitCode, scrubbedStderr);
|
|
6474
6551
|
throw deployError(code, message);
|
|
6475
6552
|
}
|
|
6476
|
-
return
|
|
6477
|
-
stdout,
|
|
6478
|
-
scrubbedStderr
|
|
6479
|
-
};
|
|
6553
|
+
return stdout;
|
|
6480
6554
|
}
|
|
6481
6555
|
/**
|
|
6482
6556
|
* Assemble the public {@link DeployResult} from wrangler's stdout, parsing the
|
|
@@ -6533,9 +6607,7 @@ function createApi$2(ctx) {
|
|
|
6533
6607
|
root
|
|
6534
6608
|
});
|
|
6535
6609
|
const start = Date.now();
|
|
6536
|
-
const
|
|
6537
|
-
ctx.log.info(scrubbedStderr);
|
|
6538
|
-
const result = buildDeployResult(stdout, branch, start);
|
|
6610
|
+
const result = buildDeployResult(await executeWrangler(ctx, args), branch, start);
|
|
6539
6611
|
ctx.state.lastDeployment = result;
|
|
6540
6612
|
ctx.emit("deploy:complete", {
|
|
6541
6613
|
url: result.url,
|
|
@@ -6595,11 +6667,10 @@ function createApi$2(ctx) {
|
|
|
6595
6667
|
async createProject() {
|
|
6596
6668
|
const name = toSlug(ctx.require(sitePlugin).name());
|
|
6597
6669
|
const branch = ctx.config.productionBranch ?? "main";
|
|
6598
|
-
|
|
6670
|
+
await executeWrangler(ctx, buildProjectCreateArgs({
|
|
6599
6671
|
slug: name,
|
|
6600
6672
|
branch
|
|
6601
6673
|
}));
|
|
6602
|
-
ctx.log.info(scrubbedStderr);
|
|
6603
6674
|
return {
|
|
6604
6675
|
name,
|
|
6605
6676
|
branch
|
|
@@ -8766,16 +8837,24 @@ function createPanelRenderer(options = {}) {
|
|
|
8766
8837
|
write(line);
|
|
8767
8838
|
},
|
|
8768
8839
|
/**
|
|
8769
|
-
* Render the deploy result from a `deploy:complete` event
|
|
8770
|
-
*
|
|
8840
|
+
* Render the deploy result from a `deploy:complete` event as a full-width box (matching
|
|
8841
|
+
* the BUILD panel): a `✓ DEPLOYED · branch` header with the elapsed time right-aligned,
|
|
8842
|
+
* then a `→ url · id` row. The url/id row is omitted entirely when wrangler returned no
|
|
8843
|
+
* URL, so a first-deploy with nothing to parse never renders a dangling `→` or `· ·`.
|
|
8771
8844
|
*
|
|
8772
8845
|
* @param result - The `deploy:complete` payload.
|
|
8773
8846
|
* @example
|
|
8774
8847
|
* render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
|
|
8775
8848
|
*/
|
|
8776
8849
|
deployed(result) {
|
|
8777
|
-
const
|
|
8778
|
-
|
|
8850
|
+
const dot = palette.dim("·");
|
|
8851
|
+
const time = result.durationMs >= 1e3 ? `${(result.durationMs / 1e3).toFixed(1)}s` : `${result.durationMs}ms`;
|
|
8852
|
+
const lines = [railLine(`${palette.green("✓")} ${palette.bold("DEPLOYED")} ${dot} ${result.branch}`, palette.dim(time), BOX_INNER)];
|
|
8853
|
+
if (result.url) {
|
|
8854
|
+
const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
|
|
8855
|
+
lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
|
|
8856
|
+
} else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
|
|
8857
|
+
writeBlock(box(lines, color, BOX_INNER));
|
|
8779
8858
|
},
|
|
8780
8859
|
/**
|
|
8781
8860
|
* Render a neutral informational line.
|
|
@@ -8870,6 +8949,29 @@ function createPanelRenderer(options = {}) {
|
|
|
8870
8949
|
*/
|
|
8871
8950
|
/** Matches an explicit affirmative answer (`y`/`yes`, case-insensitive). */
|
|
8872
8951
|
const YES_PATTERN = /^y(es)?$/i;
|
|
8952
|
+
/** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
|
|
8953
|
+
const PROMPT_WIDTH = 66;
|
|
8954
|
+
/** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
|
|
8955
|
+
const PROMPT_COLOR = supportsColor();
|
|
8956
|
+
/** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
|
|
8957
|
+
const PROMPT_PALETTE = makePalette(PROMPT_COLOR, PROMPT_COLOR && supportsTruecolor());
|
|
8958
|
+
/**
|
|
8959
|
+
* Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
|
|
8960
|
+
* a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
|
|
8961
|
+
* to the plain `question [y/N] ` form off a color TTY (CI/pipes), where prompts rarely run.
|
|
8962
|
+
*
|
|
8963
|
+
* @param question - The yes/no question to display.
|
|
8964
|
+
* @returns The readline prompt string (the typed answer follows the caret).
|
|
8965
|
+
* @example
|
|
8966
|
+
* confirmPrompt("Deploy dist/ to Cloudflare Pages?");
|
|
8967
|
+
*/
|
|
8968
|
+
function confirmPrompt(question) {
|
|
8969
|
+
if (!PROMPT_COLOR) return `${question} [y/N] `;
|
|
8970
|
+
const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
|
|
8971
|
+
const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
|
|
8972
|
+
const gap = Math.max(1, PROMPT_WIDTH - visibleWidth(left) - visibleWidth(right));
|
|
8973
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
8974
|
+
}
|
|
8873
8975
|
/**
|
|
8874
8976
|
* Resolve the `Bun` runtime global, or `undefined` when not running under Bun.
|
|
8875
8977
|
*
|
|
@@ -8927,7 +9029,7 @@ function defaultConfirm(question) {
|
|
|
8927
9029
|
input: process.stdin,
|
|
8928
9030
|
output: process.stdout
|
|
8929
9031
|
});
|
|
8930
|
-
readline.question(
|
|
9032
|
+
readline.question(confirmPrompt(question), (answer) => {
|
|
8931
9033
|
readline.close();
|
|
8932
9034
|
resolve(YES_PATTERN.test(answer.trim()));
|
|
8933
9035
|
});
|
|
@@ -8950,8 +9052,8 @@ function defaultSelect(question, choices) {
|
|
|
8950
9052
|
input: process.stdin,
|
|
8951
9053
|
output: process.stdout
|
|
8952
9054
|
});
|
|
8953
|
-
|
|
8954
|
-
readline.question(
|
|
9055
|
+
console.log(selectChoicesBlock(question, choices));
|
|
9056
|
+
readline.question(selectPrompt(question, choices.length), (answer) => {
|
|
8955
9057
|
readline.close();
|
|
8956
9058
|
const picked = Number.parseInt(answer.trim(), 10);
|
|
8957
9059
|
resolve(Number.isInteger(picked) && picked >= 1 && picked <= choices.length ? picked - 1 : 0);
|
|
@@ -8959,6 +9061,36 @@ function defaultSelect(question, choices) {
|
|
|
8959
9061
|
});
|
|
8960
9062
|
}
|
|
8961
9063
|
/**
|
|
9064
|
+
* Render the select block: a brand `◆` marker + the question, then each choice as an
|
|
9065
|
+
* indented dim number + label. Off a color TTY, falls back to the plain ` N) label`
|
|
9066
|
+
* list (the question rides the prompt instead).
|
|
9067
|
+
*
|
|
9068
|
+
* @param question - The prompt shown above the choices (styled mode only).
|
|
9069
|
+
* @param choices - The selectable option labels.
|
|
9070
|
+
* @returns The multi-line choices block.
|
|
9071
|
+
* @example
|
|
9072
|
+
* selectChoicesBlock("Set up a workflow?", ["Auto", "Manual", "Skip"]);
|
|
9073
|
+
*/
|
|
9074
|
+
function selectChoicesBlock(question, choices) {
|
|
9075
|
+
if (!PROMPT_COLOR) return choices.map((choice, index) => ` ${index + 1}) ${choice}`).join("\n");
|
|
9076
|
+
return [` ${PROMPT_PALETTE.pink("◆")} ${question}`, ...choices.map((choice, index) => ` ${PROMPT_PALETTE.dim(String(index + 1))} ${choice}`)].join("\n");
|
|
9077
|
+
}
|
|
9078
|
+
/**
|
|
9079
|
+
* Build the select input prompt: a dim `pick 1–N` hint + cyan `›` caret in styled mode,
|
|
9080
|
+
* or the plain `question [1-N] ` form off a color TTY (where the question is not printed
|
|
9081
|
+
* separately).
|
|
9082
|
+
*
|
|
9083
|
+
* @param question - The prompt (used only by the plain fallback).
|
|
9084
|
+
* @param count - The number of choices.
|
|
9085
|
+
* @returns The readline prompt string.
|
|
9086
|
+
* @example
|
|
9087
|
+
* selectPrompt("Set up a workflow?", 3);
|
|
9088
|
+
*/
|
|
9089
|
+
function selectPrompt(question, count) {
|
|
9090
|
+
if (!PROMPT_COLOR) return `${question} [1-${count}] `;
|
|
9091
|
+
return ` ${PROMPT_PALETTE.dim(`pick 1–${count}`)} ${PROMPT_PALETTE.cyan("›")} `;
|
|
9092
|
+
}
|
|
9093
|
+
/**
|
|
8962
9094
|
* Default recursive directory watcher — wraps `node:fs.watch` with `{ recursive: true }`
|
|
8963
9095
|
* and adapts its handle to {@link WatchHandle}. Tests inject a fake emitter so no real
|
|
8964
9096
|
* FS watch is registered.
|
package/dist/index.d.cts
CHANGED
|
@@ -557,6 +557,32 @@ type Api$6 = {
|
|
|
557
557
|
*/
|
|
558
558
|
t(locale: string, key: string): string;
|
|
559
559
|
};
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/plugins/router/iso-match.d.ts
|
|
562
|
+
/**
|
|
563
|
+
* A compiled, engine-agnostic path matcher: the same `.exec({ pathname })` shape the
|
|
564
|
+
* router consumed from `URLPattern`, but backed by a native `RegExp` with named
|
|
565
|
+
* groups. Dropping `URLPattern` keeps route matching alive in every browser engine —
|
|
566
|
+
* Safari < 18.4 and Firefox < ~142 have no `URLPattern` global and would otherwise
|
|
567
|
+
* throw `ReferenceError` the instant the router compiles its table on boot.
|
|
568
|
+
*/
|
|
569
|
+
interface PathMatcher {
|
|
570
|
+
/**
|
|
571
|
+
* Match a pathname, mirroring `URLPattern.exec`: the named-group bag (under
|
|
572
|
+
* `pathname.groups`) on a hit, or `null` on a miss.
|
|
573
|
+
*
|
|
574
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
575
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
576
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
577
|
+
*/
|
|
578
|
+
exec(input: {
|
|
579
|
+
readonly pathname: string;
|
|
580
|
+
}): {
|
|
581
|
+
readonly pathname: {
|
|
582
|
+
readonly groups: Record<string, string | undefined>;
|
|
583
|
+
};
|
|
584
|
+
} | null;
|
|
585
|
+
}
|
|
560
586
|
declare namespace types_d_exports$8 {
|
|
561
587
|
export { Api$5 as Api, ClientRoute, CompileInput, CompiledRoute, Config$5 as Config, ExtractApi$2 as ExtractApi, ExtractRouteParams, ExtractSegmentParameter, GenerateContext, HeadConfig$1 as HeadConfig, LayoutContext, LoadContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteRequire, RouteState, RouterApi, RouterConfig, RouterState, State$5 as State, TypedRoute, Urls };
|
|
562
588
|
}
|
|
@@ -836,10 +862,10 @@ interface CompiledRoute {
|
|
|
836
862
|
readonly pattern: string;
|
|
837
863
|
/** Dynamic-segment count (lower = more specific = matched first). */
|
|
838
864
|
readonly dynamicSegmentCount: number;
|
|
839
|
-
/** Pre-built
|
|
865
|
+
/** Pre-built path matchers (lang-aware + bare fallback). */
|
|
840
866
|
readonly matchers: {
|
|
841
|
-
readonly withLang:
|
|
842
|
-
readonly bare:
|
|
867
|
+
readonly withLang: PathMatcher;
|
|
868
|
+
readonly bare: PathMatcher;
|
|
843
869
|
};
|
|
844
870
|
/** Resolve pathname into params (withLang first, then bare with defaultLocale injected). */
|
|
845
871
|
readonly matchFn: (pathname: string) => Record<string, string> | null;
|
|
@@ -1615,7 +1641,17 @@ type Config$3 = {
|
|
|
1615
1641
|
body?: string;
|
|
1616
1642
|
}; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
|
|
1617
1643
|
localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
|
|
1618
|
-
clientEntry?: string;
|
|
1644
|
+
clientEntry?: string;
|
|
1645
|
+
/**
|
|
1646
|
+
* Path to a custom HTML document shell, giving the app full control over the
|
|
1647
|
+
* scaffold (charset, viewport, `<html lang>`, body attributes, wrapper markup).
|
|
1648
|
+
* Placeholders, substituted per page at build time:
|
|
1649
|
+
* `<!--moku:lang-->` (page locale for `<html lang>`),
|
|
1650
|
+
* `<!--moku:head-->` (composed `<head>` inner HTML),
|
|
1651
|
+
* `<!--moku:assets-->` (injected `<link>`/`<script>` tags),
|
|
1652
|
+
* `<!--moku:body-->` (SSR body HTML).
|
|
1653
|
+
* When unset, the built-in shell is used (it emits charset + viewport by default).
|
|
1654
|
+
*/
|
|
1619
1655
|
template?: string;
|
|
1620
1656
|
};
|
|
1621
1657
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -557,6 +557,32 @@ type Api$6 = {
|
|
|
557
557
|
*/
|
|
558
558
|
t(locale: string, key: string): string;
|
|
559
559
|
};
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/plugins/router/iso-match.d.ts
|
|
562
|
+
/**
|
|
563
|
+
* A compiled, engine-agnostic path matcher: the same `.exec({ pathname })` shape the
|
|
564
|
+
* router consumed from `URLPattern`, but backed by a native `RegExp` with named
|
|
565
|
+
* groups. Dropping `URLPattern` keeps route matching alive in every browser engine —
|
|
566
|
+
* Safari < 18.4 and Firefox < ~142 have no `URLPattern` global and would otherwise
|
|
567
|
+
* throw `ReferenceError` the instant the router compiles its table on boot.
|
|
568
|
+
*/
|
|
569
|
+
interface PathMatcher {
|
|
570
|
+
/**
|
|
571
|
+
* Match a pathname, mirroring `URLPattern.exec`: the named-group bag (under
|
|
572
|
+
* `pathname.groups`) on a hit, or `null` on a miss.
|
|
573
|
+
*
|
|
574
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
575
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
576
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
577
|
+
*/
|
|
578
|
+
exec(input: {
|
|
579
|
+
readonly pathname: string;
|
|
580
|
+
}): {
|
|
581
|
+
readonly pathname: {
|
|
582
|
+
readonly groups: Record<string, string | undefined>;
|
|
583
|
+
};
|
|
584
|
+
} | null;
|
|
585
|
+
}
|
|
560
586
|
declare namespace types_d_exports$8 {
|
|
561
587
|
export { Api$5 as Api, ClientRoute, CompileInput, CompiledRoute, Config$5 as Config, ExtractApi$2 as ExtractApi, ExtractRouteParams, ExtractSegmentParameter, GenerateContext, HeadConfig$1 as HeadConfig, LayoutContext, LoadContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteRequire, RouteState, RouterApi, RouterConfig, RouterState, State$5 as State, TypedRoute, Urls };
|
|
562
588
|
}
|
|
@@ -836,10 +862,10 @@ interface CompiledRoute {
|
|
|
836
862
|
readonly pattern: string;
|
|
837
863
|
/** Dynamic-segment count (lower = more specific = matched first). */
|
|
838
864
|
readonly dynamicSegmentCount: number;
|
|
839
|
-
/** Pre-built
|
|
865
|
+
/** Pre-built path matchers (lang-aware + bare fallback). */
|
|
840
866
|
readonly matchers: {
|
|
841
|
-
readonly withLang:
|
|
842
|
-
readonly bare:
|
|
867
|
+
readonly withLang: PathMatcher;
|
|
868
|
+
readonly bare: PathMatcher;
|
|
843
869
|
};
|
|
844
870
|
/** Resolve pathname into params (withLang first, then bare with defaultLocale injected). */
|
|
845
871
|
readonly matchFn: (pathname: string) => Record<string, string> | null;
|
|
@@ -1615,7 +1641,17 @@ type Config$3 = {
|
|
|
1615
1641
|
body?: string;
|
|
1616
1642
|
}; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
|
|
1617
1643
|
localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
|
|
1618
|
-
clientEntry?: string;
|
|
1644
|
+
clientEntry?: string;
|
|
1645
|
+
/**
|
|
1646
|
+
* Path to a custom HTML document shell, giving the app full control over the
|
|
1647
|
+
* scaffold (charset, viewport, `<html lang>`, body attributes, wrapper markup).
|
|
1648
|
+
* Placeholders, substituted per page at build time:
|
|
1649
|
+
* `<!--moku:lang-->` (page locale for `<html lang>`),
|
|
1650
|
+
* `<!--moku:head-->` (composed `<head>` inner HTML),
|
|
1651
|
+
* `<!--moku:assets-->` (injected `<link>`/`<script>` tags),
|
|
1652
|
+
* `<!--moku:body-->` (SSR body HTML).
|
|
1653
|
+
* When unset, the built-in shell is used (it emits charset + viewport by default).
|
|
1654
|
+
*/
|
|
1619
1655
|
template?: string;
|
|
1620
1656
|
};
|
|
1621
1657
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1876,22 +1876,101 @@ function extractGroups(groups) {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
return params;
|
|
1878
1878
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1879
|
+
/** Regex metacharacters escaped when a static path segment is inlined into a compiled pattern. */
|
|
1880
|
+
const REGEX_METACHARS = /[.*+?^${}()|[\]\\]/g;
|
|
1881
|
+
/** Matches a `:name` or `:name(regex)` URLPattern group occupying one whole segment. */
|
|
1882
|
+
const NAMED_GROUP = /^:([A-Za-z_]\w*)(?:\((.+)\))?$/;
|
|
1883
|
+
/**
|
|
1884
|
+
* Escape a static path segment so its literal text matches verbatim inside the
|
|
1885
|
+
* compiled `RegExp` (a segment like `c++` must not be read as regex syntax).
|
|
1886
|
+
*
|
|
1887
|
+
* @param text - The static segment text.
|
|
1888
|
+
* @returns The regex-escaped segment.
|
|
1889
|
+
* @example
|
|
1890
|
+
* ```ts
|
|
1891
|
+
* escapeStaticSegment("about"); // "about"
|
|
1892
|
+
* ```
|
|
1893
|
+
*/
|
|
1894
|
+
function escapeStaticSegment(text) {
|
|
1895
|
+
return text.replaceAll(REGEX_METACHARS, String.raw`\$&`);
|
|
1896
|
+
}
|
|
1881
1897
|
/**
|
|
1882
|
-
*
|
|
1898
|
+
* Compile one URLPattern source segment (no surrounding slash) into a regex fragment
|
|
1899
|
+
* that captures a single path segment: `:name` → a named `[^/]+` group, `:name(re)` →
|
|
1900
|
+
* a named group constrained by `re`, and static text → its escaped literal.
|
|
1883
1901
|
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1902
|
+
* @param segment - One source segment, e.g. `:slug`, `:lang(en|uk)`, or `archive`.
|
|
1903
|
+
* @returns The regex fragment for that segment.
|
|
1904
|
+
* @example
|
|
1905
|
+
* ```ts
|
|
1906
|
+
* segmentToRegex(":lang(en|uk)"); // "(?<lang>en|uk)"
|
|
1907
|
+
* ```
|
|
1887
1908
|
*/
|
|
1909
|
+
function segmentToRegex(segment) {
|
|
1910
|
+
const named = NAMED_GROUP.exec(segment);
|
|
1911
|
+
if (named) {
|
|
1912
|
+
const [, name, constraint] = named;
|
|
1913
|
+
return `(?<${name}>${constraint ?? "[^/]+"})`;
|
|
1914
|
+
}
|
|
1915
|
+
return escapeStaticSegment(segment);
|
|
1916
|
+
}
|
|
1888
1917
|
/**
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1918
|
+
* Compile a URLPattern pathname source string into a {@link PathMatcher} backed by a
|
|
1919
|
+
* native `RegExp` — a drop-in replacement for `new URLPattern({ pathname })` over the
|
|
1920
|
+
* subset the router emits: `:name`, `:name(regex)`, the optional `:name?` segment
|
|
1921
|
+
* (whose leading `/` is absorbed, so `/:lang?` matches `/en` or nothing), static
|
|
1922
|
+
* segments, and a required trailing slash. Anchored full-match, like `URLPattern`.
|
|
1891
1923
|
*
|
|
1892
|
-
* @param
|
|
1893
|
-
* @
|
|
1894
|
-
* @
|
|
1924
|
+
* @param source - The URLPattern pathname source, e.g. `/:lang?/:slug/`.
|
|
1925
|
+
* @returns A matcher whose `.exec({ pathname })` yields named groups or `null`.
|
|
1926
|
+
* @example
|
|
1927
|
+
* ```ts
|
|
1928
|
+
* const m = createPathMatcher("/:lang?/:slug/");
|
|
1929
|
+
* m.exec({ pathname: "/en/hello/" }); // { pathname: { groups: { lang: "en", slug: "hello" } } }
|
|
1930
|
+
* ```
|
|
1931
|
+
*/
|
|
1932
|
+
function createPathMatcher(source) {
|
|
1933
|
+
const segments = source.split("/");
|
|
1934
|
+
let pattern = "^";
|
|
1935
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
1936
|
+
const segment = segments[index] ?? "";
|
|
1937
|
+
if (segment === "") {
|
|
1938
|
+
pattern += "/";
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
const optional = segment.endsWith("?");
|
|
1942
|
+
const fragment = segmentToRegex(optional ? segment.slice(0, -1) : segment);
|
|
1943
|
+
pattern += optional ? `(?:/${fragment})?` : `/${fragment}`;
|
|
1944
|
+
}
|
|
1945
|
+
pattern += "$";
|
|
1946
|
+
const regexp = new RegExp(pattern);
|
|
1947
|
+
return {
|
|
1948
|
+
/**
|
|
1949
|
+
* Run the compiled regex over a pathname (the {@link PathMatcher.exec} contract).
|
|
1950
|
+
*
|
|
1951
|
+
* @param input - The match input carrying the `pathname` to test.
|
|
1952
|
+
* @param input.pathname - The URL pathname to match, e.g. `/en/hello/`.
|
|
1953
|
+
* @returns A `{ pathname: { groups } }` result on a match, or `null` on no match.
|
|
1954
|
+
* @example
|
|
1955
|
+
* ```ts
|
|
1956
|
+
* matcher.exec({ pathname: "/en/hello/" });
|
|
1957
|
+
* ```
|
|
1958
|
+
*/
|
|
1959
|
+
exec(input) {
|
|
1960
|
+
const result = regexp.exec(input.pathname);
|
|
1961
|
+
if (!result) return null;
|
|
1962
|
+
return { pathname: { groups: result.groups ?? {} } };
|
|
1963
|
+
} };
|
|
1964
|
+
}
|
|
1965
|
+
//#endregion
|
|
1966
|
+
//#region src/plugins/router/builders/match.ts
|
|
1967
|
+
/**
|
|
1968
|
+
* Build a pathname matcher for a single route: tries the `withLang` matcher,
|
|
1969
|
+
* then the `bare` matcher injecting `defaultLocale` on miss.
|
|
1970
|
+
*
|
|
1971
|
+
* @param matchers - The pre-built `withLang` and `bare` matcher pair.
|
|
1972
|
+
* @param matchers.withLang - The locale-aware matcher variant.
|
|
1973
|
+
* @param matchers.bare - The bare matcher variant (no leading locale segment).
|
|
1895
1974
|
* @param defaultLocale - Locale injected when the bare fallback matches.
|
|
1896
1975
|
* @returns A function resolving a pathname into params, or `null` on no match.
|
|
1897
1976
|
* @example
|
|
@@ -1935,14 +2014,6 @@ function matchRoute(compiled, pathname) {
|
|
|
1935
2014
|
}
|
|
1936
2015
|
//#endregion
|
|
1937
2016
|
//#region src/plugins/router/builders/compile.ts
|
|
1938
|
-
/**
|
|
1939
|
-
* @file router plugin — compilation + validation domain.
|
|
1940
|
-
*
|
|
1941
|
-
* Pure functions invoked from `onInit`: validate the route map, then compile each
|
|
1942
|
-
* route into URLPattern matchers + URL/file builders, count dynamic segments,
|
|
1943
|
-
* sort by specificity, and assemble the immutable `MatcherTable`. Receives DATA
|
|
1944
|
-
* only (`CompileInput`) — never the plugin ctx.
|
|
1945
|
-
*/
|
|
1946
2017
|
/** Shared `[web]` error prefix for router validation failures. */
|
|
1947
2018
|
const ERROR_PREFIX$11 = "[web] router";
|
|
1948
2019
|
/** Maximum number of optional `{lang:?}` segments a single pattern may declare. */
|
|
@@ -2114,8 +2185,8 @@ function buildFilePath(pattern, params) {
|
|
|
2114
2185
|
function buildMatchers(pattern, locales) {
|
|
2115
2186
|
const langRegex = `(${locales.join("|")})`;
|
|
2116
2187
|
return {
|
|
2117
|
-
withLang:
|
|
2118
|
-
bare:
|
|
2188
|
+
withLang: createPathMatcher(patternToUrlPattern(pattern, "withLang", langRegex)),
|
|
2189
|
+
bare: createPathMatcher(patternToUrlPattern(pattern, "bare", langRegex))
|
|
2119
2190
|
};
|
|
2120
2191
|
}
|
|
2121
2192
|
/**
|
|
@@ -3829,7 +3900,7 @@ const DEFAULT_BODY = "<h1>404</h1><p>The page you requested could not be found.<
|
|
|
3829
3900
|
* ```
|
|
3830
3901
|
*/
|
|
3831
3902
|
function wrap(body) {
|
|
3832
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3903
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 — Not Found</title></head><body>${body}</body></html>`;
|
|
3833
3904
|
}
|
|
3834
3905
|
/**
|
|
3835
3906
|
* Emits `outDir/404.html`. When `config.notFound` is `true`, writes the built-in
|
|
@@ -4513,6 +4584,8 @@ const HEAD_PLACEHOLDER = "<!--moku:head-->";
|
|
|
4513
4584
|
const BODY_PLACEHOLDER = "<!--moku:body-->";
|
|
4514
4585
|
/** Template placeholder for the injected asset `<link>`/`<script>` tags. */
|
|
4515
4586
|
const ASSETS_PLACEHOLDER = "<!--moku:assets-->";
|
|
4587
|
+
/** Template placeholder for the page's locale (`<html lang>`). */
|
|
4588
|
+
const LANG_PLACEHOLDER = "<!--moku:lang-->";
|
|
4516
4589
|
/**
|
|
4517
4590
|
* Read the bundle phase's hashed asset manifest for one kind from `state.buildCache`
|
|
4518
4591
|
* as a typed {@link BuildCacheEntry} (no `Map<string, unknown>` reads).
|
|
@@ -4560,14 +4633,16 @@ function buildAssetTags(ctx) {
|
|
|
4560
4633
|
* ```
|
|
4561
4634
|
*/
|
|
4562
4635
|
function renderDocument(parts) {
|
|
4563
|
-
return `<!DOCTYPE html><html lang="${parts.locale}"><head>${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
|
|
4636
|
+
return `<!DOCTYPE html><html lang="${parts.locale}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
|
|
4564
4637
|
}
|
|
4565
4638
|
/**
|
|
4566
|
-
* Fill a shell template's `<!--moku:
|
|
4567
|
-
* `<!--moku:assets-->` placeholders deterministically at build
|
|
4639
|
+
* Fill a shell template's `<!--moku:lang-->` / `<!--moku:head-->` /
|
|
4640
|
+
* `<!--moku:body-->` / `<!--moku:assets-->` placeholders deterministically at build
|
|
4641
|
+
* time. `<!--moku:lang-->` carries the page locale (for `<html lang>`), so a single
|
|
4642
|
+
* shared template stays locale-correct across every locale.
|
|
4568
4643
|
*
|
|
4569
4644
|
* @param template - The raw shell template HTML.
|
|
4570
|
-
* @param parts - The composed head/body/assets pieces.
|
|
4645
|
+
* @param parts - The composed head/body/assets/locale pieces.
|
|
4571
4646
|
* @returns The filled document string.
|
|
4572
4647
|
* @example
|
|
4573
4648
|
* ```ts
|
|
@@ -4575,7 +4650,7 @@ function renderDocument(parts) {
|
|
|
4575
4650
|
* ```
|
|
4576
4651
|
*/
|
|
4577
4652
|
function fillTemplate(template, parts) {
|
|
4578
|
-
return template.replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
|
|
4653
|
+
return template.replaceAll(LANG_PLACEHOLDER, parts.locale).replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
|
|
4579
4654
|
}
|
|
4580
4655
|
/**
|
|
4581
4656
|
* Resolve the compiled entry for a manifest definition, asserting the router
|
|
@@ -5827,7 +5902,8 @@ function buildWranglerArgs(input) {
|
|
|
5827
5902
|
"--project-name",
|
|
5828
5903
|
input.slug,
|
|
5829
5904
|
"--branch",
|
|
5830
|
-
branch
|
|
5905
|
+
branch,
|
|
5906
|
+
"--commit-dirty=true"
|
|
5831
5907
|
];
|
|
5832
5908
|
}
|
|
5833
5909
|
/**
|
|
@@ -6436,17 +6512,18 @@ function validateConfig$1(ctx) {
|
|
|
6436
6512
|
ctx.require(sitePlugin);
|
|
6437
6513
|
}
|
|
6438
6514
|
/**
|
|
6439
|
-
* Run wrangler for the prepared argv and
|
|
6440
|
-
*
|
|
6441
|
-
*
|
|
6442
|
-
*
|
|
6515
|
+
* Run wrangler for the prepared argv and return its stdout, translating a non-zero
|
|
6516
|
+
* exit into the classified deploy error. The API token is read from env here so it
|
|
6517
|
+
* never crosses a logging boundary; the scrubbed stderr is used only to classify a
|
|
6518
|
+
* failure — it is never logged (that was console noise), so nothing leaks. Shared by
|
|
6519
|
+
* `run()` (deploy) and `createProject()` (project create).
|
|
6443
6520
|
*
|
|
6444
6521
|
* @param ctx - Plugin context (provides `state.spawn`, `config`, `env`).
|
|
6445
6522
|
* @param args - The fully-built, pre-validated wrangler argv.
|
|
6446
|
-
* @returns The wrangler `stdout`
|
|
6523
|
+
* @returns The wrangler `stdout` (for URL/id parsing on a deploy).
|
|
6447
6524
|
* @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
|
|
6448
6525
|
* @example
|
|
6449
|
-
* const
|
|
6526
|
+
* const stdout = await executeWrangler(ctx, args);
|
|
6450
6527
|
*/
|
|
6451
6528
|
async function executeWrangler(ctx, args) {
|
|
6452
6529
|
const token = ctx.env.require("CLOUDFLARE_API_TOKEN");
|
|
@@ -6460,10 +6537,7 @@ async function executeWrangler(ctx, args) {
|
|
|
6460
6537
|
const { code, message } = classifyWranglerError(exitCode, scrubbedStderr);
|
|
6461
6538
|
throw deployError(code, message);
|
|
6462
6539
|
}
|
|
6463
|
-
return
|
|
6464
|
-
stdout,
|
|
6465
|
-
scrubbedStderr
|
|
6466
|
-
};
|
|
6540
|
+
return stdout;
|
|
6467
6541
|
}
|
|
6468
6542
|
/**
|
|
6469
6543
|
* Assemble the public {@link DeployResult} from wrangler's stdout, parsing the
|
|
@@ -6520,9 +6594,7 @@ function createApi$2(ctx) {
|
|
|
6520
6594
|
root
|
|
6521
6595
|
});
|
|
6522
6596
|
const start = Date.now();
|
|
6523
|
-
const
|
|
6524
|
-
ctx.log.info(scrubbedStderr);
|
|
6525
|
-
const result = buildDeployResult(stdout, branch, start);
|
|
6597
|
+
const result = buildDeployResult(await executeWrangler(ctx, args), branch, start);
|
|
6526
6598
|
ctx.state.lastDeployment = result;
|
|
6527
6599
|
ctx.emit("deploy:complete", {
|
|
6528
6600
|
url: result.url,
|
|
@@ -6582,11 +6654,10 @@ function createApi$2(ctx) {
|
|
|
6582
6654
|
async createProject() {
|
|
6583
6655
|
const name = toSlug(ctx.require(sitePlugin).name());
|
|
6584
6656
|
const branch = ctx.config.productionBranch ?? "main";
|
|
6585
|
-
|
|
6657
|
+
await executeWrangler(ctx, buildProjectCreateArgs({
|
|
6586
6658
|
slug: name,
|
|
6587
6659
|
branch
|
|
6588
6660
|
}));
|
|
6589
|
-
ctx.log.info(scrubbedStderr);
|
|
6590
6661
|
return {
|
|
6591
6662
|
name,
|
|
6592
6663
|
branch
|
|
@@ -8753,16 +8824,24 @@ function createPanelRenderer(options = {}) {
|
|
|
8753
8824
|
write(line);
|
|
8754
8825
|
},
|
|
8755
8826
|
/**
|
|
8756
|
-
* Render the deploy result from a `deploy:complete` event
|
|
8757
|
-
*
|
|
8827
|
+
* Render the deploy result from a `deploy:complete` event as a full-width box (matching
|
|
8828
|
+
* the BUILD panel): a `✓ DEPLOYED · branch` header with the elapsed time right-aligned,
|
|
8829
|
+
* then a `→ url · id` row. The url/id row is omitted entirely when wrangler returned no
|
|
8830
|
+
* URL, so a first-deploy with nothing to parse never renders a dangling `→` or `· ·`.
|
|
8758
8831
|
*
|
|
8759
8832
|
* @param result - The `deploy:complete` payload.
|
|
8760
8833
|
* @example
|
|
8761
8834
|
* render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
|
|
8762
8835
|
*/
|
|
8763
8836
|
deployed(result) {
|
|
8764
|
-
const
|
|
8765
|
-
|
|
8837
|
+
const dot = palette.dim("·");
|
|
8838
|
+
const time = result.durationMs >= 1e3 ? `${(result.durationMs / 1e3).toFixed(1)}s` : `${result.durationMs}ms`;
|
|
8839
|
+
const lines = [railLine(`${palette.green("✓")} ${palette.bold("DEPLOYED")} ${dot} ${result.branch}`, palette.dim(time), BOX_INNER)];
|
|
8840
|
+
if (result.url) {
|
|
8841
|
+
const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
|
|
8842
|
+
lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
|
|
8843
|
+
} else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
|
|
8844
|
+
writeBlock(box(lines, color, BOX_INNER));
|
|
8766
8845
|
},
|
|
8767
8846
|
/**
|
|
8768
8847
|
* Render a neutral informational line.
|
|
@@ -8857,6 +8936,29 @@ function createPanelRenderer(options = {}) {
|
|
|
8857
8936
|
*/
|
|
8858
8937
|
/** Matches an explicit affirmative answer (`y`/`yes`, case-insensitive). */
|
|
8859
8938
|
const YES_PATTERN = /^y(es)?$/i;
|
|
8939
|
+
/** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
|
|
8940
|
+
const PROMPT_WIDTH = 66;
|
|
8941
|
+
/** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
|
|
8942
|
+
const PROMPT_COLOR = supportsColor();
|
|
8943
|
+
/** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
|
|
8944
|
+
const PROMPT_PALETTE = makePalette(PROMPT_COLOR, PROMPT_COLOR && supportsTruecolor());
|
|
8945
|
+
/**
|
|
8946
|
+
* Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
|
|
8947
|
+
* a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
|
|
8948
|
+
* to the plain `question [y/N] ` form off a color TTY (CI/pipes), where prompts rarely run.
|
|
8949
|
+
*
|
|
8950
|
+
* @param question - The yes/no question to display.
|
|
8951
|
+
* @returns The readline prompt string (the typed answer follows the caret).
|
|
8952
|
+
* @example
|
|
8953
|
+
* confirmPrompt("Deploy dist/ to Cloudflare Pages?");
|
|
8954
|
+
*/
|
|
8955
|
+
function confirmPrompt(question) {
|
|
8956
|
+
if (!PROMPT_COLOR) return `${question} [y/N] `;
|
|
8957
|
+
const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
|
|
8958
|
+
const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
|
|
8959
|
+
const gap = Math.max(1, PROMPT_WIDTH - visibleWidth(left) - visibleWidth(right));
|
|
8960
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
8961
|
+
}
|
|
8860
8962
|
/**
|
|
8861
8963
|
* Resolve the `Bun` runtime global, or `undefined` when not running under Bun.
|
|
8862
8964
|
*
|
|
@@ -8914,7 +9016,7 @@ function defaultConfirm(question) {
|
|
|
8914
9016
|
input: process.stdin,
|
|
8915
9017
|
output: process.stdout
|
|
8916
9018
|
});
|
|
8917
|
-
readline.question(
|
|
9019
|
+
readline.question(confirmPrompt(question), (answer) => {
|
|
8918
9020
|
readline.close();
|
|
8919
9021
|
resolve(YES_PATTERN.test(answer.trim()));
|
|
8920
9022
|
});
|
|
@@ -8937,8 +9039,8 @@ function defaultSelect(question, choices) {
|
|
|
8937
9039
|
input: process.stdin,
|
|
8938
9040
|
output: process.stdout
|
|
8939
9041
|
});
|
|
8940
|
-
|
|
8941
|
-
readline.question(
|
|
9042
|
+
console.log(selectChoicesBlock(question, choices));
|
|
9043
|
+
readline.question(selectPrompt(question, choices.length), (answer) => {
|
|
8942
9044
|
readline.close();
|
|
8943
9045
|
const picked = Number.parseInt(answer.trim(), 10);
|
|
8944
9046
|
resolve(Number.isInteger(picked) && picked >= 1 && picked <= choices.length ? picked - 1 : 0);
|
|
@@ -8946,6 +9048,36 @@ function defaultSelect(question, choices) {
|
|
|
8946
9048
|
});
|
|
8947
9049
|
}
|
|
8948
9050
|
/**
|
|
9051
|
+
* Render the select block: a brand `◆` marker + the question, then each choice as an
|
|
9052
|
+
* indented dim number + label. Off a color TTY, falls back to the plain ` N) label`
|
|
9053
|
+
* list (the question rides the prompt instead).
|
|
9054
|
+
*
|
|
9055
|
+
* @param question - The prompt shown above the choices (styled mode only).
|
|
9056
|
+
* @param choices - The selectable option labels.
|
|
9057
|
+
* @returns The multi-line choices block.
|
|
9058
|
+
* @example
|
|
9059
|
+
* selectChoicesBlock("Set up a workflow?", ["Auto", "Manual", "Skip"]);
|
|
9060
|
+
*/
|
|
9061
|
+
function selectChoicesBlock(question, choices) {
|
|
9062
|
+
if (!PROMPT_COLOR) return choices.map((choice, index) => ` ${index + 1}) ${choice}`).join("\n");
|
|
9063
|
+
return [` ${PROMPT_PALETTE.pink("◆")} ${question}`, ...choices.map((choice, index) => ` ${PROMPT_PALETTE.dim(String(index + 1))} ${choice}`)].join("\n");
|
|
9064
|
+
}
|
|
9065
|
+
/**
|
|
9066
|
+
* Build the select input prompt: a dim `pick 1–N` hint + cyan `›` caret in styled mode,
|
|
9067
|
+
* or the plain `question [1-N] ` form off a color TTY (where the question is not printed
|
|
9068
|
+
* separately).
|
|
9069
|
+
*
|
|
9070
|
+
* @param question - The prompt (used only by the plain fallback).
|
|
9071
|
+
* @param count - The number of choices.
|
|
9072
|
+
* @returns The readline prompt string.
|
|
9073
|
+
* @example
|
|
9074
|
+
* selectPrompt("Set up a workflow?", 3);
|
|
9075
|
+
*/
|
|
9076
|
+
function selectPrompt(question, count) {
|
|
9077
|
+
if (!PROMPT_COLOR) return `${question} [1-${count}] `;
|
|
9078
|
+
return ` ${PROMPT_PALETTE.dim(`pick 1–${count}`)} ${PROMPT_PALETTE.cyan("›")} `;
|
|
9079
|
+
}
|
|
9080
|
+
/**
|
|
8949
9081
|
* Default recursive directory watcher — wraps `node:fs.watch` with `{ recursive: true }`
|
|
8950
9082
|
* and adapts its handle to {@link WatchHandle}. Tests inject a fake emitter so no real
|
|
8951
9083
|
* FS watch is registered.
|
package/package.json
CHANGED