@rangojs/router 0.0.0-experimental.98 → 0.0.0-experimental.98914650
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 +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +60 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +22 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +3 -6
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -11
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +20 -58
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -47
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
package/src/handle.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { missingInjectedIdError } from "./missing-id-error.js";
|
|
2
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Handle definition for accumulating data across route segments.
|
|
3
6
|
*
|
|
@@ -43,10 +46,10 @@ function defaultCollect<T>(segments: T[][]): T[] {
|
|
|
43
46
|
// Used by useHandle() to recover collect when handle is deserialized from RSC prop.
|
|
44
47
|
const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// Monotonic counter for runtime fallback ids (see createHandle). Only used
|
|
50
|
+
// when no build id was injected (a bare unit test).
|
|
51
|
+
let runtimeHandleIdCounter = 0;
|
|
52
|
+
|
|
50
53
|
export function getCollectFn(
|
|
51
54
|
id: string,
|
|
52
55
|
): ((segments: unknown[][]) => unknown) | undefined {
|
|
@@ -93,28 +96,36 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
93
96
|
collect?: (segments: TData[][]) => TAccumulated,
|
|
94
97
|
__injectedId?: string,
|
|
95
98
|
): Handle<TData, TAccumulated> {
|
|
96
|
-
|
|
99
|
+
let handleId = __injectedId ?? "";
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// No build-injected id. Under a test runner: fall back to a synthetic id so the
|
|
102
|
+
// collect registers below and the handle is exercisable in tests (useHandle,
|
|
103
|
+
// collectHandle, renderRoute's `handles` run the REAL collect). Otherwise (dev
|
|
104
|
+
// or a real build) it means an UNSUPPORTED handler shape the plugin skipped —
|
|
105
|
+
// fail loud. The rich, stack-parsing diagnostic stays behind the NODE_ENV check
|
|
106
|
+
// so a production build folds it away and tree-shakes missing-id-error.ts out,
|
|
107
|
+
// shipping the small throw instead. isUnderTestRunner() is runtime-safe.
|
|
108
|
+
if (!handleId) {
|
|
109
|
+
if (isUnderTestRunner()) {
|
|
110
|
+
handleId = `__rango_runtime_handle_${runtimeHandleIdCounter++}`;
|
|
111
|
+
} else if (process.env.NODE_ENV !== "production") {
|
|
112
|
+
throw missingInjectedIdError("Handle", "createHandle");
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error(
|
|
115
|
+
"[rango] Handle is missing $$id — the build plugin did not inject one. " +
|
|
116
|
+
"Export it as `export const X = createHandle(...)`.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
const collectFn =
|
|
107
122
|
collect ??
|
|
108
123
|
(defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
handleId,
|
|
115
|
-
collectFn as (segments: unknown[][]) => unknown,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
125
|
+
collectRegistry.set(
|
|
126
|
+
handleId,
|
|
127
|
+
collectFn as (segments: unknown[][]) => unknown,
|
|
128
|
+
);
|
|
118
129
|
|
|
119
130
|
return {
|
|
120
131
|
__brand: "handle" as const,
|
|
@@ -122,9 +133,6 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
122
133
|
};
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
/**
|
|
126
|
-
* Type guard to check if a value is a Handle.
|
|
127
|
-
*/
|
|
128
136
|
export function isHandle(value: unknown): value is Handle<unknown, unknown> {
|
|
129
137
|
return (
|
|
130
138
|
typeof value === "object" &&
|
|
@@ -151,7 +159,7 @@ export function collectHandleData<TData, TAccumulated>(
|
|
|
151
159
|
const collectFn = getCollectFn(handle.$$id);
|
|
152
160
|
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
153
161
|
console.warn(
|
|
154
|
-
`[
|
|
162
|
+
`[rango] Handle "${handle.$$id}" has no registered collect function. ` +
|
|
155
163
|
`Falling back to flat array. Ensure the handle module is imported so ` +
|
|
156
164
|
`createHandle() runs and registers the collect function.`,
|
|
157
165
|
);
|
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -97,24 +97,18 @@ function isPromise(value: unknown): value is Promise<unknown> {
|
|
|
97
97
|
return value !== null && typeof value === "object" && "then" in value;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/**
|
|
101
|
-
* Render a single meta descriptor as a React element.
|
|
102
|
-
*/
|
|
103
100
|
function renderMetaDescriptor(
|
|
104
101
|
descriptor: MetaDescriptorBase,
|
|
105
102
|
index: number,
|
|
106
103
|
): React.ReactNode {
|
|
107
|
-
// charset
|
|
108
104
|
if (hasCharSet(descriptor)) {
|
|
109
105
|
return <meta key="charSet" charSet={descriptor.charSet} />;
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
// title
|
|
113
108
|
if (hasTitle(descriptor)) {
|
|
114
109
|
return <title key="title">{descriptor.title}</title>;
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
// name + content (description, viewport, etc.)
|
|
118
112
|
if (hasNameContent(descriptor)) {
|
|
119
113
|
return (
|
|
120
114
|
<meta
|
|
@@ -125,7 +119,6 @@ function renderMetaDescriptor(
|
|
|
125
119
|
);
|
|
126
120
|
}
|
|
127
121
|
|
|
128
|
-
// property + content (Open Graph, etc.)
|
|
129
122
|
if (hasPropertyContent(descriptor)) {
|
|
130
123
|
return (
|
|
131
124
|
<meta
|
|
@@ -136,7 +129,6 @@ function renderMetaDescriptor(
|
|
|
136
129
|
);
|
|
137
130
|
}
|
|
138
131
|
|
|
139
|
-
// http-equiv + content
|
|
140
132
|
if (hasHttpEquivContent(descriptor)) {
|
|
141
133
|
return (
|
|
142
134
|
<meta
|
|
@@ -147,7 +139,6 @@ function renderMetaDescriptor(
|
|
|
147
139
|
);
|
|
148
140
|
}
|
|
149
141
|
|
|
150
|
-
// JSON-LD structured data
|
|
151
142
|
if (hasScriptLdJson(descriptor)) {
|
|
152
143
|
const json = JSON.stringify(descriptor["script:ld+json"]);
|
|
153
144
|
return (
|
|
@@ -159,7 +150,6 @@ function renderMetaDescriptor(
|
|
|
159
150
|
);
|
|
160
151
|
}
|
|
161
152
|
|
|
162
|
-
// Custom tagName (meta or link with arbitrary attributes)
|
|
163
153
|
if (hasTagName(descriptor)) {
|
|
164
154
|
const { tagName, ...rest } = descriptor;
|
|
165
155
|
if (tagName === "link") {
|
|
@@ -180,7 +170,6 @@ function renderMetaDescriptor(
|
|
|
180
170
|
}
|
|
181
171
|
}
|
|
182
172
|
|
|
183
|
-
// Fallback: treat as meta attributes
|
|
184
173
|
return (
|
|
185
174
|
<meta
|
|
186
175
|
key={`meta-fallback-${index}`}
|
|
@@ -189,9 +178,6 @@ function renderMetaDescriptor(
|
|
|
189
178
|
);
|
|
190
179
|
}
|
|
191
180
|
|
|
192
|
-
/**
|
|
193
|
-
* Wrapper component to resolve a Promise<MetaDescriptorBase> using use().
|
|
194
|
-
*/
|
|
195
181
|
function AsyncMetaTag({
|
|
196
182
|
promise,
|
|
197
183
|
index,
|
|
@@ -39,18 +39,29 @@ export interface BreadcrumbItem {
|
|
|
39
39
|
/**
|
|
40
40
|
* Collect function for Breadcrumbs handle.
|
|
41
41
|
* Flattens segments in parent-to-child order with deduplication by href
|
|
42
|
-
* (last item for each href wins).
|
|
42
|
+
* (last item for each href wins). Deferred slots (`ctx.use(Breadcrumbs).defer()`)
|
|
43
|
+
* arrive as pending Promise entries with no href yet; they are passed through by
|
|
44
|
+
* identity and excluded from the href dedup so concurrent deferred crumbs do not
|
|
45
|
+
* all collapse under a single `undefined` href.
|
|
43
46
|
*/
|
|
44
47
|
function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] {
|
|
45
48
|
const all = segments.flat();
|
|
46
|
-
const seen = new Map<string, number>();
|
|
47
49
|
|
|
50
|
+
const isResolvedItem = (item: unknown): item is BreadcrumbItem =>
|
|
51
|
+
item != null &&
|
|
52
|
+
typeof item === "object" &&
|
|
53
|
+
typeof (item as { then?: unknown }).then !== "function" &&
|
|
54
|
+
typeof (item as { href?: unknown }).href === "string";
|
|
55
|
+
|
|
56
|
+
const seen = new Map<string, number>();
|
|
48
57
|
for (let i = 0; i < all.length; i++) {
|
|
49
|
-
seen.set(all[i].href, i);
|
|
58
|
+
if (isResolvedItem(all[i])) seen.set(all[i].href, i);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
//
|
|
53
|
-
return all.filter(
|
|
61
|
+
// Deferred items bypass dedup (excluded via !isResolvedItem check).
|
|
62
|
+
return all.filter(
|
|
63
|
+
(item, index) => !isResolvedItem(item) || seen.get(item.href) === index,
|
|
64
|
+
);
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/**
|
package/src/handles/meta.ts
CHANGED
|
@@ -35,9 +35,6 @@ import type {
|
|
|
35
35
|
UnsetDescriptor,
|
|
36
36
|
} from "../router/types.js";
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* Type guard for unset descriptor
|
|
40
|
-
*/
|
|
41
38
|
function isUnsetDescriptor(
|
|
42
39
|
descriptor: MetaDescriptor,
|
|
43
40
|
): descriptor is UnsetDescriptor {
|
|
@@ -49,9 +46,6 @@ function isUnsetDescriptor(
|
|
|
49
46
|
);
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
/**
|
|
53
|
-
* Type guard for title descriptor (any form)
|
|
54
|
-
*/
|
|
55
49
|
function isTitleDescriptor(
|
|
56
50
|
descriptor: MetaDescriptor,
|
|
57
51
|
): descriptor is { title: TitleDescriptor } {
|
|
@@ -62,9 +56,6 @@ function isTitleDescriptor(
|
|
|
62
56
|
);
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
/**
|
|
66
|
-
* Type guard for title template descriptor
|
|
67
|
-
*/
|
|
68
59
|
function isTitleTemplate(
|
|
69
60
|
title: TitleDescriptor,
|
|
70
61
|
): title is { template: string; default: string } {
|
|
@@ -76,21 +67,13 @@ function isTitleTemplate(
|
|
|
76
67
|
);
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
/**
|
|
80
|
-
* Type guard for absolute title descriptor
|
|
81
|
-
*/
|
|
82
70
|
function isAbsoluteTitle(
|
|
83
71
|
title: TitleDescriptor,
|
|
84
72
|
): title is { absolute: string } {
|
|
85
73
|
return typeof title === "object" && title !== null && "absolute" in title;
|
|
86
74
|
}
|
|
87
75
|
|
|
88
|
-
/**
|
|
89
|
-
* Get a unique key for a meta descriptor for deduplication.
|
|
90
|
-
* Returns undefined for descriptors that shouldn't be deduplicated.
|
|
91
|
-
*/
|
|
92
76
|
function getMetaKey(descriptor: MetaDescriptor): string | undefined {
|
|
93
|
-
// Skip unset descriptors - they are processed separately
|
|
94
77
|
if (isUnsetDescriptor(descriptor)) {
|
|
95
78
|
return undefined;
|
|
96
79
|
}
|
|
@@ -110,13 +93,10 @@ function getMetaKey(descriptor: MetaDescriptor): string | undefined {
|
|
|
110
93
|
return `httpEquiv:${descriptor.httpEquiv}`;
|
|
111
94
|
}
|
|
112
95
|
if ("script:ld+json" in descriptor) {
|
|
113
|
-
// JSON-LD scripts can have multiple, don't dedupe by default
|
|
114
96
|
return undefined;
|
|
115
97
|
}
|
|
116
98
|
if ("tagName" in descriptor) {
|
|
117
|
-
// For link tags, dedupe by rel if present
|
|
118
99
|
if (descriptor.tagName === "link" && "rel" in descriptor) {
|
|
119
|
-
// Some link rels should be unique (canonical), others not (stylesheet)
|
|
120
100
|
const uniqueRels = ["canonical", "icon", "apple-touch-icon"];
|
|
121
101
|
if (uniqueRels.includes(descriptor.rel as string)) {
|
|
122
102
|
return `link:${descriptor.rel}`;
|
|
@@ -136,9 +116,6 @@ const defaultMetaDescriptors: MetaDescriptor[] = [
|
|
|
136
116
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
137
117
|
];
|
|
138
118
|
|
|
139
|
-
/**
|
|
140
|
-
* Helper to add or replace a descriptor in the result array
|
|
141
|
-
*/
|
|
142
119
|
function addOrReplace(
|
|
143
120
|
result: MetaDescriptor[],
|
|
144
121
|
keyToIndex: Map<string, number>,
|
|
@@ -155,9 +132,6 @@ function addOrReplace(
|
|
|
155
132
|
}
|
|
156
133
|
}
|
|
157
134
|
|
|
158
|
-
/**
|
|
159
|
-
* Helper to update indices after removing an element
|
|
160
|
-
*/
|
|
161
135
|
function updateIndicesAfterRemoval(
|
|
162
136
|
keyToIndex: Map<string, number>,
|
|
163
137
|
removedIndex: number,
|
|
@@ -169,17 +143,11 @@ function updateIndicesAfterRemoval(
|
|
|
169
143
|
}
|
|
170
144
|
}
|
|
171
145
|
|
|
172
|
-
/**
|
|
173
|
-
* Collect function for Meta handle.
|
|
174
|
-
* Includes default meta descriptors, then deduplicates by key with later routes overriding earlier ones.
|
|
175
|
-
* Supports title templates, absolute titles, and unset descriptors.
|
|
176
|
-
*/
|
|
177
146
|
function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
178
147
|
const result: MetaDescriptor[] = [];
|
|
179
148
|
const keyToIndex = new Map<string, number>();
|
|
180
149
|
let titleTemplate: string | undefined;
|
|
181
150
|
|
|
182
|
-
// Add defaults first so they can be overridden
|
|
183
151
|
for (const descriptor of defaultMetaDescriptors) {
|
|
184
152
|
const key = getMetaKey(descriptor);
|
|
185
153
|
if (key !== undefined) {
|
|
@@ -190,7 +158,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
190
158
|
|
|
191
159
|
for (const descriptors of segments) {
|
|
192
160
|
for (const descriptor of descriptors) {
|
|
193
|
-
// Handle unset descriptors
|
|
194
161
|
if (isUnsetDescriptor(descriptor)) {
|
|
195
162
|
const keyToRemove = descriptor.unset;
|
|
196
163
|
if (keyToIndex.has(keyToRemove)) {
|
|
@@ -202,14 +169,11 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
202
169
|
continue;
|
|
203
170
|
}
|
|
204
171
|
|
|
205
|
-
// Handle title descriptors with template/absolute support
|
|
206
172
|
if (isTitleDescriptor(descriptor)) {
|
|
207
173
|
const titleValue = descriptor.title;
|
|
208
174
|
|
|
209
175
|
if (isTitleTemplate(titleValue)) {
|
|
210
|
-
// Store template for subsequent title descriptors in child segments
|
|
211
176
|
titleTemplate = titleValue.template;
|
|
212
|
-
// Set the default title
|
|
213
177
|
addOrReplace(
|
|
214
178
|
result,
|
|
215
179
|
keyToIndex,
|
|
@@ -220,7 +184,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
220
184
|
}
|
|
221
185
|
|
|
222
186
|
if (isAbsoluteTitle(titleValue)) {
|
|
223
|
-
// Absolute title bypasses any template
|
|
224
187
|
addOrReplace(
|
|
225
188
|
result,
|
|
226
189
|
keyToIndex,
|
|
@@ -230,7 +193,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
230
193
|
continue;
|
|
231
194
|
}
|
|
232
195
|
|
|
233
|
-
// String title - apply template if one exists
|
|
234
196
|
const finalTitle = titleTemplate
|
|
235
197
|
? titleTemplate.replace("%s", titleValue as string)
|
|
236
198
|
: titleValue;
|
|
@@ -243,7 +205,6 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
243
205
|
continue;
|
|
244
206
|
}
|
|
245
207
|
|
|
246
|
-
// Handle all other descriptors
|
|
247
208
|
const key = getMetaKey(descriptor);
|
|
248
209
|
addOrReplace(result, keyToIndex, descriptor, key);
|
|
249
210
|
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cookie Override Handler
|
|
3
|
-
*
|
|
4
|
-
* Manages cookie-based host override for development environments.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import type { HostOverrideConfig } from "./types.js";
|
|
8
2
|
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
9
3
|
import { matchPattern, parseRequest } from "./pattern-matcher.js";
|
|
@@ -13,9 +7,6 @@ import {
|
|
|
13
7
|
HostValidationError,
|
|
14
8
|
} from "./errors.js";
|
|
15
9
|
|
|
16
|
-
/**
|
|
17
|
-
* Parse cookies from request
|
|
18
|
-
*/
|
|
19
10
|
export function parseCookies(request: Request): Record<string, string> {
|
|
20
11
|
const cookieHeader = request.headers.get("cookie");
|
|
21
12
|
if (!cookieHeader) {
|
|
@@ -40,24 +31,15 @@ export function parseCookies(request: Request): Record<string, string> {
|
|
|
40
31
|
return cookies;
|
|
41
32
|
}
|
|
42
33
|
|
|
43
|
-
/**
|
|
44
|
-
* Get cookie value from request
|
|
45
|
-
*/
|
|
46
34
|
export function getCookie(request: Request, name: string): string | undefined {
|
|
47
35
|
const cookies = parseCookies(request);
|
|
48
36
|
return cookies[name];
|
|
49
37
|
}
|
|
50
38
|
|
|
51
|
-
/**
|
|
52
|
-
* Create Set-Cookie header to delete a cookie
|
|
53
|
-
*/
|
|
54
39
|
export function createDeleteCookieHeader(name: string): string {
|
|
55
40
|
return `${name}=; Max-Age=0; Path=/; Secure; HttpOnly`;
|
|
56
41
|
}
|
|
57
42
|
|
|
58
|
-
/**
|
|
59
|
-
* Create error response with cookie deletion
|
|
60
|
-
*/
|
|
61
43
|
export function createCookieErrorResponse(
|
|
62
44
|
cookieName: string,
|
|
63
45
|
message: string,
|
|
@@ -77,9 +59,6 @@ export function createCookieErrorResponse(
|
|
|
77
59
|
);
|
|
78
60
|
}
|
|
79
61
|
|
|
80
|
-
/**
|
|
81
|
-
* Check if current host is allowed to use override
|
|
82
|
-
*/
|
|
83
62
|
export function isHostAllowed(
|
|
84
63
|
request: Request,
|
|
85
64
|
allowedHosts: string[],
|
|
@@ -95,12 +74,6 @@ export function isHostAllowed(
|
|
|
95
74
|
return false;
|
|
96
75
|
}
|
|
97
76
|
|
|
98
|
-
/**
|
|
99
|
-
* Handle cookie override logic
|
|
100
|
-
*
|
|
101
|
-
* Returns overridden hostname if valid, original hostname if no override.
|
|
102
|
-
* Throws errors for invalid overrides.
|
|
103
|
-
*/
|
|
104
77
|
export function handleCookieOverride(
|
|
105
78
|
request: Request,
|
|
106
79
|
config: HostOverrideConfig | undefined,
|
|
@@ -115,46 +88,37 @@ export function handleCookieOverride(
|
|
|
115
88
|
const cookieValue = getCookie(request, cookieName);
|
|
116
89
|
const { hostname: originalHostname } = parseRequest(request);
|
|
117
90
|
|
|
118
|
-
// No cookie - return original hostname
|
|
119
91
|
if (!cookieValue) {
|
|
120
92
|
return originalHostname;
|
|
121
93
|
}
|
|
122
94
|
|
|
123
|
-
// Check if current host is allowed
|
|
124
95
|
const allowed = isHostAllowed(request, allowedHosts);
|
|
125
96
|
|
|
126
|
-
// If not allowed, throw error
|
|
127
97
|
if (!allowed) {
|
|
128
98
|
throw new HostOverrideNotAllowedError(originalHostname, cookieName, {
|
|
129
99
|
cause: { cookieValue, currentHost: originalHostname },
|
|
130
100
|
});
|
|
131
101
|
}
|
|
132
102
|
|
|
133
|
-
// If allowed and has custom validation, run it
|
|
134
103
|
if (validate) {
|
|
135
104
|
try {
|
|
136
105
|
const validatedHostname = validate(request, cookieValue, input);
|
|
137
106
|
return validatedHostname;
|
|
138
107
|
} catch (error) {
|
|
139
|
-
// Wrap in HostValidationError
|
|
140
108
|
const message = error instanceof Error ? error.message : String(error);
|
|
141
109
|
throw new HostValidationError(message, error);
|
|
142
110
|
}
|
|
143
111
|
}
|
|
144
112
|
|
|
145
|
-
// Default validation - verify it's a valid hostname using URL constructor
|
|
146
113
|
try {
|
|
147
|
-
// Try to construct a URL with the hostname to validate it
|
|
148
114
|
const testUrl = new URL(`https://${cookieValue}`);
|
|
149
115
|
|
|
150
|
-
// Ensure the hostname matches what we provided (URL constructor normalizes it)
|
|
151
116
|
if (testUrl.hostname !== cookieValue) {
|
|
152
117
|
throw new InvalidHostnameError(cookieValue, {
|
|
153
118
|
cause: { original: cookieValue, normalized: testUrl.hostname },
|
|
154
119
|
});
|
|
155
120
|
}
|
|
156
121
|
} catch (error) {
|
|
157
|
-
// If URL constructor failed, throw InvalidHostnameError with cause
|
|
158
122
|
if (error instanceof InvalidHostnameError) {
|
|
159
123
|
throw error;
|
|
160
124
|
}
|
package/src/host/errors.ts
CHANGED
|
@@ -4,16 +4,10 @@
|
|
|
4
4
|
* All host router errors extend HostRouterError for easy instance checking.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Error options with cause
|
|
9
|
-
*/
|
|
10
7
|
interface ErrorOptions {
|
|
11
8
|
cause?: unknown;
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
/**
|
|
15
|
-
* Base error class for all host router errors
|
|
16
|
-
*/
|
|
17
11
|
export class HostRouterError extends Error {
|
|
18
12
|
cause?: unknown;
|
|
19
13
|
|
|
@@ -27,9 +21,6 @@ export class HostRouterError extends Error {
|
|
|
27
21
|
}
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
/**
|
|
31
|
-
* Error thrown when pattern validation fails
|
|
32
|
-
*/
|
|
33
24
|
export class InvalidPatternError extends HostRouterError {
|
|
34
25
|
constructor(pattern: string, reason: string, options?: ErrorOptions) {
|
|
35
26
|
super(`Invalid pattern "${pattern}": ${reason}`, options);
|
|
@@ -38,9 +29,6 @@ export class InvalidPatternError extends HostRouterError {
|
|
|
38
29
|
}
|
|
39
30
|
}
|
|
40
31
|
|
|
41
|
-
/**
|
|
42
|
-
* Error thrown when cookie override is not allowed
|
|
43
|
-
*/
|
|
44
32
|
export class HostOverrideNotAllowedError extends HostRouterError {
|
|
45
33
|
constructor(currentHost: string, cookieName: string, options?: ErrorOptions) {
|
|
46
34
|
super(
|
|
@@ -52,9 +40,6 @@ export class HostOverrideNotAllowedError extends HostRouterError {
|
|
|
52
40
|
}
|
|
53
41
|
}
|
|
54
42
|
|
|
55
|
-
/**
|
|
56
|
-
* Error thrown when cookie hostname is invalid
|
|
57
|
-
*/
|
|
58
43
|
export class InvalidHostnameError extends HostRouterError {
|
|
59
44
|
constructor(hostname: string, options?: ErrorOptions) {
|
|
60
45
|
super(`Invalid hostname format: "${hostname}"`, options);
|
|
@@ -63,9 +48,6 @@ export class InvalidHostnameError extends HostRouterError {
|
|
|
63
48
|
}
|
|
64
49
|
}
|
|
65
50
|
|
|
66
|
-
/**
|
|
67
|
-
* Error thrown when custom validation fails
|
|
68
|
-
*/
|
|
69
51
|
export class HostValidationError extends HostRouterError {
|
|
70
52
|
constructor(message: string, cause?: unknown) {
|
|
71
53
|
super(message, { cause });
|
|
@@ -74,9 +56,6 @@ export class HostValidationError extends HostRouterError {
|
|
|
74
56
|
}
|
|
75
57
|
}
|
|
76
58
|
|
|
77
|
-
/**
|
|
78
|
-
* Error thrown when no route matches
|
|
79
|
-
*/
|
|
80
59
|
export class NoRouteMatchError extends HostRouterError {
|
|
81
60
|
constructor(hostname: string, pathname: string, options?: ErrorOptions) {
|
|
82
61
|
super(`No route matched for ${hostname}${pathname}`, options);
|
|
@@ -85,9 +64,6 @@ export class NoRouteMatchError extends HostRouterError {
|
|
|
85
64
|
}
|
|
86
65
|
}
|
|
87
66
|
|
|
88
|
-
/**
|
|
89
|
-
* Error thrown when handler type is invalid
|
|
90
|
-
*/
|
|
91
67
|
export class InvalidHandlerError extends HostRouterError {
|
|
92
68
|
constructor(handler: unknown, options?: ErrorOptions) {
|
|
93
69
|
super(`Invalid handler type: ${typeof handler}`, options);
|
package/src/host/index.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* const router = createHostRouter();
|
|
13
13
|
*
|
|
14
|
-
* router.host(['.']).
|
|
15
|
-
* router.host(['admin.*']).
|
|
14
|
+
* router.host(['.']).lazy(() => import('./apps/main'));
|
|
15
|
+
* router.host(['admin.*']).lazy(() => import('./apps/admin'));
|
|
16
16
|
*
|
|
17
17
|
* export default {
|
|
18
18
|
* fetch(request) {
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* }
|
|
21
21
|
* };
|
|
22
22
|
* ```
|
|
23
|
+
*
|
|
24
|
+
* The host surface (`Handler`, `Middleware`, `match`, `HostOverrideConfig.validate`)
|
|
25
|
+
* types `input` as `RouterRequestInput<any>` by design: a host router fans out to
|
|
26
|
+
* heterogeneous sub-apps with differing env/vars shapes, so there is no single
|
|
27
|
+
* `TEnv`/`TVars` to thread through. `input.env`/`input.vars` are therefore `any`
|
|
28
|
+
* here; the typed env shape lives on each sub-app's `createRouter<TEnv>()`.
|
|
23
29
|
*/
|
|
24
30
|
|
|
25
31
|
// Core router
|