@tyndall/core 0.0.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/README.md +34 -0
- package/dist/client-router-bootstrap.d.ts +11 -0
- package/dist/client-router-bootstrap.d.ts.map +1 -0
- package/dist/client-router-bootstrap.js +319 -0
- package/dist/config.d.ts +197 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +552 -0
- package/dist/dynamic-manifest.d.ts +78 -0
- package/dist/dynamic-manifest.d.ts.map +1 -0
- package/dist/dynamic-manifest.js +189 -0
- package/dist/dynamic-page-api.d.ts +34 -0
- package/dist/dynamic-page-api.d.ts.map +1 -0
- package/dist/dynamic-page-api.js +136 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +31 -0
- package/dist/head.d.ts +3 -0
- package/dist/head.d.ts.map +1 -0
- package/dist/head.js +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/layouts.d.ts +14 -0
- package/dist/layouts.d.ts.map +1 -0
- package/dist/layouts.js +51 -0
- package/dist/manifest.d.ts +81 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +139 -0
- package/dist/page-api.d.ts +56 -0
- package/dist/page-api.d.ts.map +1 -0
- package/dist/page-api.js +100 -0
- package/dist/plugin.d.ts +35 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +133 -0
- package/dist/program-sandbox.d.ts +18 -0
- package/dist/program-sandbox.d.ts.map +1 -0
- package/dist/program-sandbox.js +84 -0
- package/dist/props.d.ts +2 -0
- package/dist/props.d.ts.map +1 -0
- package/dist/props.js +14 -0
- package/dist/render-policy.d.ts +8 -0
- package/dist/render-policy.d.ts.map +1 -0
- package/dist/render-policy.js +12 -0
- package/dist/resolver-fallback.d.ts +49 -0
- package/dist/resolver-fallback.d.ts.map +1 -0
- package/dist/resolver-fallback.js +99 -0
- package/dist/route-graph.d.ts +9 -0
- package/dist/route-graph.d.ts.map +1 -0
- package/dist/route-graph.js +157 -0
- package/dist/route-meta.d.ts +4 -0
- package/dist/route-meta.d.ts.map +1 -0
- package/dist/route-meta.js +48 -0
- package/dist/router.d.ts +27 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +83 -0
- package/dist/ui-adapter.d.ts +67 -0
- package/dist/ui-adapter.d.ts.map +1 -0
- package/dist/ui-adapter.js +27 -0
- package/package.json +23 -0
package/dist/props.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAO/D,CAAC"}
|
package/dist/props.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const escapeJson = (value) => value
|
|
2
|
+
.replace(/</g, "\\u003c")
|
|
3
|
+
.replace(/>/g, "\\u003e")
|
|
4
|
+
.replace(/&/g, "\\u0026")
|
|
5
|
+
.replace(/\u2028/g, "\\u2028")
|
|
6
|
+
.replace(/\u2029/g, "\\u2029");
|
|
7
|
+
export const serializeProps = (props) => {
|
|
8
|
+
const json = JSON.stringify(props);
|
|
9
|
+
if (json === undefined) {
|
|
10
|
+
return "null";
|
|
11
|
+
}
|
|
12
|
+
// Important: escape characters that can break out of a script tag.
|
|
13
|
+
return escapeJson(json);
|
|
14
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type AppBuildMode = "ssg" | "ssr";
|
|
2
|
+
export type PolicyRenderMode = "csr" | "ssr" | "auto";
|
|
3
|
+
export interface RenderPolicyDecision {
|
|
4
|
+
mode: "csr" | "ssr";
|
|
5
|
+
warnings: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const evaluateRenderPolicy: (appMode: AppBuildMode, policyRender?: PolicyRenderMode) => RenderPolicyDecision;
|
|
8
|
+
//# sourceMappingURL=render-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-policy.d.ts","sourceRoot":"","sources":["../src/render-policy.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,CAAC;AACzC,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,oBAAoB,GAC/B,SAAS,YAAY,EACrB,eAAc,gBAAyB,KACtC,oBAcF,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const evaluateRenderPolicy = (appMode, policyRender = "auto") => {
|
|
2
|
+
const warnings = [];
|
|
3
|
+
if (policyRender === "auto") {
|
|
4
|
+
return { mode: appMode === "ssg" ? "csr" : "ssr", warnings };
|
|
5
|
+
}
|
|
6
|
+
if (appMode === "ssg" && policyRender === "ssr") {
|
|
7
|
+
// Important: SSG builds cannot satisfy SSR policy; fall back to CSR and warn.
|
|
8
|
+
warnings.push("SSR policy requested for SSG build; falling back to CSR.");
|
|
9
|
+
return { mode: "csr", warnings };
|
|
10
|
+
}
|
|
11
|
+
return { mode: policyRender, warnings };
|
|
12
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type MatchResult, type RouteGraph } from "./router.js";
|
|
2
|
+
import { type AppBuildMode, type PolicyRenderMode } from "./render-policy.js";
|
|
3
|
+
export interface ResolverFallbackPolicy {
|
|
4
|
+
dynamicRoutePrefixes?: string[];
|
|
5
|
+
forceDynamic?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type DynamicManifestResolver = (pathname: string) => Promise<unknown | null>;
|
|
8
|
+
export type RouteFallbackResult = {
|
|
9
|
+
type: "file";
|
|
10
|
+
match: MatchResult;
|
|
11
|
+
} | {
|
|
12
|
+
type: "dynamic";
|
|
13
|
+
envelope: unknown;
|
|
14
|
+
} | {
|
|
15
|
+
type: "notfound";
|
|
16
|
+
};
|
|
17
|
+
export type DynamicPolicyRenderMode = PolicyRenderMode | "redirect";
|
|
18
|
+
export interface DynamicFallbackPolicy {
|
|
19
|
+
render?: DynamicPolicyRenderMode;
|
|
20
|
+
redirect?: string;
|
|
21
|
+
}
|
|
22
|
+
export type RoutePolicyFallbackResult = {
|
|
23
|
+
type: "file";
|
|
24
|
+
match: MatchResult;
|
|
25
|
+
} | {
|
|
26
|
+
type: "dynamic";
|
|
27
|
+
envelope: unknown;
|
|
28
|
+
mode: "csr" | "ssr";
|
|
29
|
+
warnings: string[];
|
|
30
|
+
} | {
|
|
31
|
+
type: "redirect";
|
|
32
|
+
envelope: unknown;
|
|
33
|
+
location: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: "notfound";
|
|
36
|
+
};
|
|
37
|
+
export interface ResolveRouteWithFallbackOptions {
|
|
38
|
+
pathname: string;
|
|
39
|
+
routeGraph?: RouteGraph;
|
|
40
|
+
dynamicResolver?: DynamicManifestResolver;
|
|
41
|
+
policy?: ResolverFallbackPolicy;
|
|
42
|
+
}
|
|
43
|
+
export interface ResolveRouteWithPolicyFallbackOptions extends ResolveRouteWithFallbackOptions {
|
|
44
|
+
appMode?: AppBuildMode;
|
|
45
|
+
}
|
|
46
|
+
export declare const shouldForceDynamicFallback: (pathname: string, policy?: ResolverFallbackPolicy) => boolean;
|
|
47
|
+
export declare const resolveRouteWithFallback: (options: ResolveRouteWithFallbackOptions) => Promise<RouteFallbackResult>;
|
|
48
|
+
export declare const resolveRouteWithPolicyFallback: (options: ResolveRouteWithPolicyFallbackOptions) => Promise<RoutePolicyFallbackResult>;
|
|
49
|
+
//# sourceMappingURL=resolver-fallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver-fallback.d.ts","sourceRoot":"","sources":["../src/resolver-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,sBAAsB;IACrC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,uBAAuB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;AAEpF,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzB,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,GAAG,UAAU,CAAC;AAEpE,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,yBAAyB,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/E;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzB,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,sBAAsB,CAAC;CACjC;AAED,MAAM,WAAW,qCACf,SAAQ,+BAA+B;IACvC,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAuBD,eAAO,MAAM,0BAA0B,GACrC,UAAU,MAAM,EAChB,SAAS,sBAAsB,KAC9B,OAUF,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,+BAA+B,KACvC,OAAO,CAAC,mBAAmB,CAkB7B,CAAC;AA4BF,eAAO,MAAM,8BAA8B,GACzC,SAAS,qCAAqC,KAC7C,OAAO,CAAC,yBAAyB,CA6BnC,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { matchRoute } from "./router.js";
|
|
2
|
+
import { evaluateRenderPolicy, } from "./render-policy.js";
|
|
3
|
+
const normalizePathname = (value) => {
|
|
4
|
+
const trimmed = value.split("?")[0].split("#")[0];
|
|
5
|
+
if (!trimmed.startsWith("/")) {
|
|
6
|
+
return `/${trimmed}`;
|
|
7
|
+
}
|
|
8
|
+
return trimmed;
|
|
9
|
+
};
|
|
10
|
+
const matchesPrefix = (pathname, prefix) => {
|
|
11
|
+
if (!prefix) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (prefix === "/") {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (!prefix.startsWith("/")) {
|
|
18
|
+
prefix = `/${prefix}`;
|
|
19
|
+
}
|
|
20
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
21
|
+
};
|
|
22
|
+
export const shouldForceDynamicFallback = (pathname, policy) => {
|
|
23
|
+
if (!policy) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (policy.forceDynamic) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const prefixes = policy.dynamicRoutePrefixes ?? [];
|
|
30
|
+
const normalized = normalizePathname(pathname);
|
|
31
|
+
return prefixes.some((prefix) => matchesPrefix(normalized, prefix));
|
|
32
|
+
};
|
|
33
|
+
export const resolveRouteWithFallback = async (options) => {
|
|
34
|
+
const pathname = normalizePathname(options.pathname);
|
|
35
|
+
const match = options.routeGraph ? matchRoute(pathname, options.routeGraph) : null;
|
|
36
|
+
const forceDynamic = shouldForceDynamicFallback(pathname, options.policy);
|
|
37
|
+
// Important: forced dynamic bypasses file matches to honor policy overrides.
|
|
38
|
+
if (match && !forceDynamic) {
|
|
39
|
+
return { type: "file", match };
|
|
40
|
+
}
|
|
41
|
+
if (options.dynamicResolver) {
|
|
42
|
+
const envelope = await options.dynamicResolver(pathname);
|
|
43
|
+
if (envelope) {
|
|
44
|
+
return { type: "dynamic", envelope };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { type: "notfound" };
|
|
48
|
+
};
|
|
49
|
+
const POLICY_RENDER_VALUES = new Set([
|
|
50
|
+
"auto",
|
|
51
|
+
"csr",
|
|
52
|
+
"ssr",
|
|
53
|
+
"redirect",
|
|
54
|
+
]);
|
|
55
|
+
const readDynamicFallbackPolicy = (envelope) => {
|
|
56
|
+
if (!envelope || typeof envelope !== "object") {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
const candidate = envelope;
|
|
60
|
+
if (!candidate.policy || typeof candidate.policy !== "object") {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
const policy = candidate.policy;
|
|
64
|
+
const normalized = {};
|
|
65
|
+
if (typeof policy.render === "string" && POLICY_RENDER_VALUES.has(policy.render)) {
|
|
66
|
+
normalized.render = policy.render;
|
|
67
|
+
}
|
|
68
|
+
if (typeof policy.redirect === "string" && policy.redirect.length > 0) {
|
|
69
|
+
normalized.redirect = policy.redirect;
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
};
|
|
73
|
+
export const resolveRouteWithPolicyFallback = async (options) => {
|
|
74
|
+
const resolved = await resolveRouteWithFallback(options);
|
|
75
|
+
if (resolved.type !== "dynamic") {
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
const policy = readDynamicFallbackPolicy(resolved.envelope);
|
|
79
|
+
const warnings = [];
|
|
80
|
+
if (policy.render === "redirect") {
|
|
81
|
+
if (policy.redirect) {
|
|
82
|
+
return {
|
|
83
|
+
type: "redirect",
|
|
84
|
+
envelope: resolved.envelope,
|
|
85
|
+
location: policy.redirect,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Mirror router semantics: redirect without target falls back to auto.
|
|
89
|
+
warnings.push("Redirect policy missing target; falling back to auto.");
|
|
90
|
+
}
|
|
91
|
+
const policyRender = policy.render && policy.render !== "redirect" ? policy.render : "auto";
|
|
92
|
+
const decision = evaluateRenderPolicy(options.appMode ?? "ssg", policyRender);
|
|
93
|
+
return {
|
|
94
|
+
type: "dynamic",
|
|
95
|
+
envelope: resolved.envelope,
|
|
96
|
+
mode: decision.mode,
|
|
97
|
+
warnings: [...warnings, ...decision.warnings],
|
|
98
|
+
};
|
|
99
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RouteGraph } from "./router.js";
|
|
2
|
+
export interface RouteGraphOptions {
|
|
3
|
+
routesDir: string;
|
|
4
|
+
extensions?: string[];
|
|
5
|
+
routeMeta?: boolean;
|
|
6
|
+
nestedLayouts?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const createRouteGraph: (options: RouteGraphOptions) => Promise<RouteGraph>;
|
|
9
|
+
//# sourceMappingURL=route-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-graph.d.ts","sourceRoot":"","sources":["../src/route-graph.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAA0C,MAAM,aAAa,CAAC;AAKtF,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAuED,eAAO,MAAM,gBAAgB,GAAU,SAAS,iBAAiB,KAAG,OAAO,CAAC,UAAU,CA0GrF,CAAC"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { extname, join, relative, sep } from "node:path";
|
|
3
|
+
import { HyperError } from "./errors.js";
|
|
4
|
+
import { loadRouteMeta, mergeRoutePolicies } from "./route-meta.js";
|
|
5
|
+
import { collectRouteLayouts } from "./layouts.js";
|
|
6
|
+
const defaultExtensions = [".tsx", ".ts", ".jsx", ".js"];
|
|
7
|
+
const isGroupSegment = (segment) => segment.startsWith("(") && segment.endsWith(")") && segment.length > 2;
|
|
8
|
+
const parseSegment = (segment) => {
|
|
9
|
+
if (isGroupSegment(segment)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (segment.startsWith("[...") && segment.endsWith("]")) {
|
|
13
|
+
const name = segment.slice(4, -1).trim();
|
|
14
|
+
if (!name) {
|
|
15
|
+
throw new HyperError("ROUTE_INVALID", "Catch-all segment name is required.", {
|
|
16
|
+
segment,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return { type: "catchAll", name };
|
|
20
|
+
}
|
|
21
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
22
|
+
const name = segment.slice(1, -1).trim();
|
|
23
|
+
if (!name) {
|
|
24
|
+
throw new HyperError("ROUTE_INVALID", "Dynamic segment name is required.", {
|
|
25
|
+
segment,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return { type: "dynamic", name };
|
|
29
|
+
}
|
|
30
|
+
return { type: "static", value: segment };
|
|
31
|
+
};
|
|
32
|
+
const segmentsToId = (segments) => {
|
|
33
|
+
if (segments.length === 0) {
|
|
34
|
+
return "/";
|
|
35
|
+
}
|
|
36
|
+
return ("/" +
|
|
37
|
+
segments
|
|
38
|
+
.map((segment) => {
|
|
39
|
+
if (segment.type === "static") {
|
|
40
|
+
return segment.value;
|
|
41
|
+
}
|
|
42
|
+
if (segment.type === "dynamic") {
|
|
43
|
+
return `:${segment.name}`;
|
|
44
|
+
}
|
|
45
|
+
return `*${segment.name}`;
|
|
46
|
+
})
|
|
47
|
+
.join("/"));
|
|
48
|
+
};
|
|
49
|
+
const walkFiles = async (dir) => {
|
|
50
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
51
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
const files = [];
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const fullPath = join(dir, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
files.push(...(await walkFiles(fullPath)));
|
|
57
|
+
}
|
|
58
|
+
else if (entry.isFile()) {
|
|
59
|
+
files.push(fullPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return files;
|
|
63
|
+
};
|
|
64
|
+
export const createRouteGraph = async (options) => {
|
|
65
|
+
const extensionList = options.extensions ?? defaultExtensions;
|
|
66
|
+
const extensions = new Set(extensionList);
|
|
67
|
+
const enableRouteMeta = options.routeMeta ?? true;
|
|
68
|
+
const enableNestedLayouts = options.nestedLayouts ?? false;
|
|
69
|
+
const files = await walkFiles(options.routesDir);
|
|
70
|
+
const routes = [];
|
|
71
|
+
const seenIds = new Set();
|
|
72
|
+
const routeMetaFiles = new Map();
|
|
73
|
+
const routeMetaCache = new Map();
|
|
74
|
+
const extensionRank = (extension) => {
|
|
75
|
+
const index = extensionList.indexOf(extension);
|
|
76
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
77
|
+
};
|
|
78
|
+
const loadCachedRouteMeta = async (filePath) => {
|
|
79
|
+
if (routeMetaCache.has(filePath)) {
|
|
80
|
+
return routeMetaCache.get(filePath) ?? null;
|
|
81
|
+
}
|
|
82
|
+
const policy = await loadRouteMeta(filePath);
|
|
83
|
+
routeMetaCache.set(filePath, policy);
|
|
84
|
+
return policy;
|
|
85
|
+
};
|
|
86
|
+
for (const filePath of files) {
|
|
87
|
+
const extension = extname(filePath);
|
|
88
|
+
if (!extensions.has(extension)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const relativePath = relative(options.routesDir, filePath);
|
|
92
|
+
const parts = relativePath.split(sep);
|
|
93
|
+
const fileName = parts.pop();
|
|
94
|
+
if (!fileName) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const baseName = fileName.slice(0, -extension.length);
|
|
98
|
+
if (baseName === "_route") {
|
|
99
|
+
const dirKey = parts.join(sep);
|
|
100
|
+
const rank = extensionRank(extension);
|
|
101
|
+
const existing = routeMetaFiles.get(dirKey);
|
|
102
|
+
if (!existing || rank < existing.rank) {
|
|
103
|
+
routeMetaFiles.set(dirKey, { filePath, rank });
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (baseName.startsWith("_")) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const segments = [];
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
const segment = parseSegment(part);
|
|
113
|
+
if (segment) {
|
|
114
|
+
segments.push(segment);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (baseName !== "index") {
|
|
118
|
+
const segment = parseSegment(baseName);
|
|
119
|
+
if (segment) {
|
|
120
|
+
segments.push(segment);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const id = segmentsToId(segments);
|
|
124
|
+
// Important: duplicate routes indicate ambiguous behavior and must be rejected.
|
|
125
|
+
if (seenIds.has(id)) {
|
|
126
|
+
throw new HyperError("ROUTE_DUPLICATE", `Duplicate route detected: ${id}`, { id });
|
|
127
|
+
}
|
|
128
|
+
seenIds.add(id);
|
|
129
|
+
let policy;
|
|
130
|
+
if (enableRouteMeta && routeMetaFiles.size > 0) {
|
|
131
|
+
const policies = [];
|
|
132
|
+
for (let index = 0; index <= parts.length; index += 1) {
|
|
133
|
+
const dirKey = parts.slice(0, index).join(sep);
|
|
134
|
+
const metaEntry = routeMetaFiles.get(dirKey);
|
|
135
|
+
if (!metaEntry) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const metaPolicy = await loadCachedRouteMeta(metaEntry.filePath);
|
|
139
|
+
if (metaPolicy) {
|
|
140
|
+
policies.push(metaPolicy);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (policies.length > 0) {
|
|
144
|
+
policy = mergeRoutePolicies(policies);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const record = { id, filePath, segments, policy };
|
|
148
|
+
if (enableNestedLayouts) {
|
|
149
|
+
const resolution = collectRouteLayouts(filePath, options.routesDir, extensionList);
|
|
150
|
+
record.layoutFiles = resolution.layoutFiles;
|
|
151
|
+
}
|
|
152
|
+
routes.push(record);
|
|
153
|
+
}
|
|
154
|
+
// Deterministic ordering ensures stable route ids and reproducible builds.
|
|
155
|
+
routes.sort((a, b) => a.id.localeCompare(b.id));
|
|
156
|
+
return { routes };
|
|
157
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-meta.d.ts","sourceRoot":"","sources":["../src/route-meta.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA6B/C,eAAO,MAAM,kBAAkB,GAAI,UAAU,WAAW,EAAE,KAAG,WAM5D,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAyBhF,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { HyperError } from "./errors.js";
|
|
3
|
+
const isPlainObject = (value) => {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const proto = Object.getPrototypeOf(value);
|
|
8
|
+
return proto === Object.prototype || proto === null;
|
|
9
|
+
};
|
|
10
|
+
const mergeObjects = (base, override) => {
|
|
11
|
+
const next = { ...base };
|
|
12
|
+
for (const [key, value] of Object.entries(override)) {
|
|
13
|
+
if (value === undefined) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const previous = next[key];
|
|
17
|
+
if (isPlainObject(previous) && isPlainObject(value)) {
|
|
18
|
+
next[key] = mergeObjects(previous, value);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
next[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return next;
|
|
25
|
+
};
|
|
26
|
+
export const mergeRoutePolicies = (policies) => {
|
|
27
|
+
// Important: merge root→leaf so deeper routes override parents.
|
|
28
|
+
return policies.reduce((acc, policy) => mergeObjects(acc, policy), {});
|
|
29
|
+
};
|
|
30
|
+
export const loadRouteMeta = async (filePath) => {
|
|
31
|
+
try {
|
|
32
|
+
const module = await import(pathToFileURL(filePath).href);
|
|
33
|
+
const policy = module.route ?? module.default;
|
|
34
|
+
if (policy === undefined || policy === null) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!isPlainObject(policy)) {
|
|
38
|
+
throw new HyperError("ROUTE_META_INVALID", "Route meta must export a plain object as `route`.", { filePath });
|
|
39
|
+
}
|
|
40
|
+
return policy;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof HyperError) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
throw new HyperError("ROUTE_META_LOAD_FAILED", "Failed to load route meta module.", { filePath, cause: error instanceof Error ? error.message : String(error) });
|
|
47
|
+
}
|
|
48
|
+
};
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type RouteSegment = {
|
|
2
|
+
type: "static";
|
|
3
|
+
value: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: "dynamic";
|
|
6
|
+
name: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: "catchAll";
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
export type RoutePolicy = Record<string, unknown>;
|
|
12
|
+
export interface RouteRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
filePath: string;
|
|
15
|
+
segments: RouteSegment[];
|
|
16
|
+
policy?: RoutePolicy;
|
|
17
|
+
layoutFiles?: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface RouteGraph {
|
|
20
|
+
routes: RouteRecord[];
|
|
21
|
+
}
|
|
22
|
+
export interface MatchResult {
|
|
23
|
+
route: RouteRecord;
|
|
24
|
+
params: Record<string, string | string[]>;
|
|
25
|
+
}
|
|
26
|
+
export declare const matchRoute: (pathname: string, routeGraph: RouteGraph) => MatchResult | null;
|
|
27
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CAC3C;AAwFD,eAAO,MAAM,UAAU,GAAI,UAAU,MAAM,EAAE,YAAY,UAAU,KAAG,WAAW,GAAG,IAanF,CAAC"}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const normalizePathname = (pathname) => {
|
|
2
|
+
const clean = pathname.split("?")[0]?.split("#")[0] ?? "";
|
|
3
|
+
if (!clean) {
|
|
4
|
+
return "/";
|
|
5
|
+
}
|
|
6
|
+
const withSlash = clean.startsWith("/") ? clean : `/${clean}`;
|
|
7
|
+
if (withSlash.length > 1 && withSlash.endsWith("/")) {
|
|
8
|
+
return withSlash.slice(0, -1);
|
|
9
|
+
}
|
|
10
|
+
return withSlash;
|
|
11
|
+
};
|
|
12
|
+
const splitPath = (pathname) => normalizePathname(pathname)
|
|
13
|
+
.split("/")
|
|
14
|
+
.filter((segment) => segment.length > 0);
|
|
15
|
+
const routePriority = (route) => {
|
|
16
|
+
let hasDynamic = false;
|
|
17
|
+
let hasCatchAll = false;
|
|
18
|
+
for (const segment of route.segments) {
|
|
19
|
+
if (segment.type === "catchAll") {
|
|
20
|
+
hasCatchAll = true;
|
|
21
|
+
}
|
|
22
|
+
if (segment.type === "dynamic") {
|
|
23
|
+
hasDynamic = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const tier = hasCatchAll ? 2 : hasDynamic ? 1 : 0;
|
|
27
|
+
return { tier, length: route.segments.length, id: route.id };
|
|
28
|
+
};
|
|
29
|
+
const compareRoutes = (left, right) => {
|
|
30
|
+
const a = routePriority(left);
|
|
31
|
+
const b = routePriority(right);
|
|
32
|
+
if (a.tier !== b.tier) {
|
|
33
|
+
return a.tier - b.tier;
|
|
34
|
+
}
|
|
35
|
+
if (a.length !== b.length) {
|
|
36
|
+
return b.length - a.length;
|
|
37
|
+
}
|
|
38
|
+
return a.id.localeCompare(b.id);
|
|
39
|
+
};
|
|
40
|
+
const matchSegments = (segments, pathnameSegments) => {
|
|
41
|
+
const params = {};
|
|
42
|
+
let index = 0;
|
|
43
|
+
for (const segment of segments) {
|
|
44
|
+
if (segment.type === "catchAll") {
|
|
45
|
+
// Important: catch-all requires at least one segment (non-optional catch-all).
|
|
46
|
+
const rest = pathnameSegments.slice(index);
|
|
47
|
+
if (rest.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
params[segment.name] = rest;
|
|
51
|
+
return { params };
|
|
52
|
+
}
|
|
53
|
+
const value = pathnameSegments[index];
|
|
54
|
+
if (!value) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (segment.type === "static") {
|
|
58
|
+
if (segment.value !== value) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (segment.type === "dynamic") {
|
|
63
|
+
params[segment.name] = value;
|
|
64
|
+
}
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
if (index !== pathnameSegments.length) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return { params };
|
|
71
|
+
};
|
|
72
|
+
export const matchRoute = (pathname, routeGraph) => {
|
|
73
|
+
// Critical: normalize and apply stable priority order to avoid mismatched routing.
|
|
74
|
+
const segments = splitPath(pathname);
|
|
75
|
+
const orderedRoutes = [...routeGraph.routes].sort(compareRoutes);
|
|
76
|
+
for (const route of orderedRoutes) {
|
|
77
|
+
const result = matchSegments(route.segments, segments);
|
|
78
|
+
if (result) {
|
|
79
|
+
return { route, params: result.params };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { HeadDescriptor, RouteParams } from "./page-api.js";
|
|
2
|
+
import type { RouteGraph } from "./router.js";
|
|
3
|
+
export type EntrySpec = string;
|
|
4
|
+
export interface UIAdapterEntryContext {
|
|
5
|
+
routeId?: string;
|
|
6
|
+
routeGraph?: RouteGraph;
|
|
7
|
+
basePath?: string;
|
|
8
|
+
rootDir?: string;
|
|
9
|
+
entryDir?: string;
|
|
10
|
+
routeRoot?: string;
|
|
11
|
+
chunks?: Record<string, unknown>;
|
|
12
|
+
ssrAdapter?: string;
|
|
13
|
+
uiOptions?: Record<string, unknown>;
|
|
14
|
+
adapterOptions?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface UIAdapterRenderContext {
|
|
17
|
+
routeId: string;
|
|
18
|
+
params?: RouteParams;
|
|
19
|
+
props: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface UIAdapterRenderResult {
|
|
22
|
+
html: string;
|
|
23
|
+
head?: HeadDescriptor;
|
|
24
|
+
status?: number;
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export interface UIAdapterStreamTarget {
|
|
28
|
+
write: (chunk: string | Uint8Array) => void;
|
|
29
|
+
end: () => void;
|
|
30
|
+
}
|
|
31
|
+
export interface UIAdapterStream {
|
|
32
|
+
pipe: (target: UIAdapterStreamTarget, options?: {
|
|
33
|
+
end?: boolean;
|
|
34
|
+
}) => void;
|
|
35
|
+
on?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
36
|
+
once?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
37
|
+
off?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
38
|
+
removeListener?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
39
|
+
}
|
|
40
|
+
export interface UIAdapterRenderStreamResult {
|
|
41
|
+
stream: UIAdapterStream;
|
|
42
|
+
head?: HeadDescriptor;
|
|
43
|
+
status?: number;
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
abort?: (reason?: unknown) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface UIAdapter {
|
|
48
|
+
name: string;
|
|
49
|
+
createClientEntry?: (ctx: UIAdapterEntryContext) => EntrySpec;
|
|
50
|
+
createServerEntry?: (ctx: UIAdapterEntryContext) => EntrySpec;
|
|
51
|
+
renderToHtml?: (ctx: UIAdapterRenderContext) => Promise<UIAdapterRenderResult> | UIAdapterRenderResult;
|
|
52
|
+
renderToStream?: (ctx: UIAdapterRenderContext) => Promise<UIAdapterRenderStreamResult> | UIAdapterRenderStreamResult;
|
|
53
|
+
serializeProps?: (props: Record<string, unknown>) => string;
|
|
54
|
+
getHead?: (ctx: UIAdapterRenderContext) => HeadDescriptor;
|
|
55
|
+
hmrIntegration?: HmrIntegration;
|
|
56
|
+
}
|
|
57
|
+
export type UIAdapterFactory = (options: Record<string, unknown>) => UIAdapter;
|
|
58
|
+
export type UIAdapterRegistry = Record<string, UIAdapterFactory>;
|
|
59
|
+
export type HmrRefreshBoundary = (...args: unknown[]) => unknown;
|
|
60
|
+
export type HmrOverlayHandler = (...args: unknown[]) => unknown;
|
|
61
|
+
export interface HmrIntegration {
|
|
62
|
+
refreshBoundary?: HmrRefreshBoundary;
|
|
63
|
+
overlay?: HmrOverlayHandler;
|
|
64
|
+
}
|
|
65
|
+
export declare const resolveUIAdapter: (adapter: string | UIAdapterFactory, options?: Record<string, unknown>, registry?: UIAdapterRegistry) => UIAdapter;
|
|
66
|
+
export declare const resolveHmrIntegration: (adapter?: Pick<UIAdapter, "hmrIntegration">) => HmrIntegration;
|
|
67
|
+
//# sourceMappingURL=ui-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-adapter.d.ts","sourceRoot":"","sources":["../src/ui-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { HyperError } from "./errors.js";
|
|
2
|
+
const assertValidAdapter = (adapter, source) => {
|
|
3
|
+
if (!adapter || typeof adapter !== "object") {
|
|
4
|
+
throw new HyperError("CONFIG_INVALID", `UI adapter factory must return an object (${source}).`);
|
|
5
|
+
}
|
|
6
|
+
if (!adapter.name) {
|
|
7
|
+
throw new HyperError("CONFIG_INVALID", `UI adapter must declare a name (${source}).`);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
export const resolveUIAdapter = (adapter, options = {}, registry = {}) => {
|
|
11
|
+
if (typeof adapter === "function") {
|
|
12
|
+
const resolved = adapter(options);
|
|
13
|
+
assertValidAdapter(resolved, "factory");
|
|
14
|
+
return resolved;
|
|
15
|
+
}
|
|
16
|
+
const factory = registry[adapter];
|
|
17
|
+
if (!factory) {
|
|
18
|
+
throw new HyperError("CONFIG_INVALID", `Unknown UI adapter: ${adapter}`, {
|
|
19
|
+
adapter,
|
|
20
|
+
available: Object.keys(registry),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const resolved = factory(options);
|
|
24
|
+
assertValidAdapter(resolved, adapter);
|
|
25
|
+
return resolved;
|
|
26
|
+
};
|
|
27
|
+
export const resolveHmrIntegration = (adapter) => adapter?.hmrIntegration ?? {};
|