@moku-labs/web 1.4.0 → 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 +92 -21
- package/dist/index.d.cts +29 -3
- package/dist/index.d.mts +29 -3
- package/dist/index.mjs +92 -21
- 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*)(?:\((.+)\))?$/;
|
|
1894
1896
|
/**
|
|
1895
|
-
*
|
|
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).
|
|
1896
1899
|
*
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1900
|
+
* @param text - The static segment text.
|
|
1901
|
+
* @returns The regex-escaped segment.
|
|
1902
|
+
* @example
|
|
1903
|
+
* ```ts
|
|
1904
|
+
* escapeStaticSegment("about"); // "about"
|
|
1905
|
+
* ```
|
|
1900
1906
|
*/
|
|
1907
|
+
function escapeStaticSegment(text) {
|
|
1908
|
+
return text.replaceAll(REGEX_METACHARS, String.raw`\$&`);
|
|
1909
|
+
}
|
|
1901
1910
|
/**
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
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.
|
|
1904
1914
|
*
|
|
1905
|
-
* @param
|
|
1906
|
-
* @
|
|
1907
|
-
* @
|
|
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
|
+
* ```
|
|
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
|
+
}
|
|
1930
|
+
/**
|
|
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`.
|
|
1936
|
+
*
|
|
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
|
/**
|
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;
|
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;
|
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*)(?:\((.+)\))?$/;
|
|
1881
1883
|
/**
|
|
1882
|
-
*
|
|
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).
|
|
1883
1886
|
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1887
|
+
* @param text - The static segment text.
|
|
1888
|
+
* @returns The regex-escaped segment.
|
|
1889
|
+
* @example
|
|
1890
|
+
* ```ts
|
|
1891
|
+
* escapeStaticSegment("about"); // "about"
|
|
1892
|
+
* ```
|
|
1887
1893
|
*/
|
|
1894
|
+
function escapeStaticSegment(text) {
|
|
1895
|
+
return text.replaceAll(REGEX_METACHARS, String.raw`\$&`);
|
|
1896
|
+
}
|
|
1888
1897
|
/**
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
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.
|
|
1891
1901
|
*
|
|
1892
|
-
* @param
|
|
1893
|
-
* @
|
|
1894
|
-
* @
|
|
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
|
+
* ```
|
|
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
|
+
}
|
|
1917
|
+
/**
|
|
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`.
|
|
1923
|
+
*
|
|
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
|
/**
|
package/package.json
CHANGED