@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2151 -846
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +647 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +76 -28
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- 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-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +92 -182
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +53 -15
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +143 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -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 +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/// <reference path="./flight-runtime.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* renderToFlightString — REAL React Server Component (Flight) rendering for
|
|
4
|
+
* unit tests of @rangojs/router consumer apps.
|
|
5
|
+
*
|
|
6
|
+
* This module renders a server component tree to its Flight wire string using
|
|
7
|
+
* the same react-server-dom serializer the router uses at runtime. It runs in
|
|
8
|
+
* plain node (no Vite, no browser), but ONLY under the `react-server` export
|
|
9
|
+
* condition. The serializer is the VENDORED build shipped with
|
|
10
|
+
* @vitejs/plugin-rsc — the public `@vitejs/plugin-rsc/rsc` entry top-level
|
|
11
|
+
* imports Vite virtual modules and is not usable outside a Vite build.
|
|
12
|
+
*
|
|
13
|
+
* Run the example/tests for this module via the dedicated rsc vitest project
|
|
14
|
+
* (vitest.rsc.config.ts), which forces `--conditions=react-server` on the
|
|
15
|
+
* worker. The main vitest project must NOT use that condition (it would flip
|
|
16
|
+
* React to the no-hooks server build and break the ~50 tests that mock
|
|
17
|
+
* @vitejs/plugin-rsc/rsc).
|
|
18
|
+
*
|
|
19
|
+
* Scope / limitations (v1):
|
|
20
|
+
* - Server-only / leaf trees. A tree containing a CLIENT component emits an
|
|
21
|
+
* `I[...]` import row whose module id will not resolve against the empty `{}`
|
|
22
|
+
* client manifest used here — fine for snapshotting the SHAPE of the payload,
|
|
23
|
+
* but the client reference cannot be executed/hydrated. The interactive DOM
|
|
24
|
+
* render (`renderServer`) is deferred (see module TODO at bottom of report).
|
|
25
|
+
* - The vendored subpath is a private plugin-rsc path; a minor bump could move
|
|
26
|
+
* it. `assertFlightRuntimeAvailable()` provides a smoke check.
|
|
27
|
+
* - For stable snapshots, run under NODE_ENV=production: the production
|
|
28
|
+
* serializer drops the dev-only debug-info rows (the `N<timestamp>` reference
|
|
29
|
+
* row, the per-component `stack`/`env` rows, and `D{...}` timing rows),
|
|
30
|
+
* leaving just the rendered tree row(s).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { ReactNode } from "react";
|
|
34
|
+
// Vendored react-server-dom serializer. Resolves via plugin-rsc's
|
|
35
|
+
// `"./*": "./dist/*.js"` export to
|
|
36
|
+
// dist/vendor/react-server-dom/server.edge.js. Only loadable under the
|
|
37
|
+
// `react-server` export condition.
|
|
38
|
+
import * as RSDServer from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
|
|
39
|
+
import {
|
|
40
|
+
createRequestContext,
|
|
41
|
+
runWithRequestContext,
|
|
42
|
+
setRequestContextParams,
|
|
43
|
+
} from "../server/request-context.js";
|
|
44
|
+
import type { RscPayload } from "../rsc/types.js";
|
|
45
|
+
import type { ResolvedSegment } from "../types.js";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Options for {@link renderToFlightString}.
|
|
49
|
+
*/
|
|
50
|
+
export interface RenderToFlightStringOptions {
|
|
51
|
+
/** Request URL. Defaults to `http://localhost/`. */
|
|
52
|
+
url?: string;
|
|
53
|
+
/** Request headers (e.g. Cookie) visible to the server tree. */
|
|
54
|
+
headers?: HeadersInit;
|
|
55
|
+
/** Env / bindings exposed as `ctx.env`. Defaults to `{}`. */
|
|
56
|
+
env?: unknown;
|
|
57
|
+
/** Route params exposed via `ctx.params` and loader contexts. */
|
|
58
|
+
params?: Record<string, string>;
|
|
59
|
+
/** Matched route name (drives `ctx.routeName` and scoped reverse). */
|
|
60
|
+
routeName?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_URL = "http://localhost/";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Wrap a single element in the minimal ResolvedSegment + RscPayload shape that
|
|
67
|
+
* mirrors Rango's wire format, so the serialized output matches what a real
|
|
68
|
+
* route segment would emit.
|
|
69
|
+
*/
|
|
70
|
+
function wrapAsPayload(element: ReactNode, pathname: string): RscPayload {
|
|
71
|
+
const segment: ResolvedSegment = {
|
|
72
|
+
id: "test",
|
|
73
|
+
namespace: "",
|
|
74
|
+
type: "route",
|
|
75
|
+
index: 0,
|
|
76
|
+
component: element,
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
metadata: {
|
|
80
|
+
pathname,
|
|
81
|
+
segments: [segment],
|
|
82
|
+
version: "test",
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render a server component (or any ReactNode) to its Flight wire string.
|
|
89
|
+
*
|
|
90
|
+
* The element is wrapped in a minimal Rango segment + payload, then serialized
|
|
91
|
+
* with the vendored react-server-dom server. A request context is active for
|
|
92
|
+
* the duration of the render (drained INSIDE runWithRequestContext) so async
|
|
93
|
+
* server components can call getRequestContext(), read params, cookies, etc.
|
|
94
|
+
*
|
|
95
|
+
* Must run under the `react-server` export condition (see module header).
|
|
96
|
+
*/
|
|
97
|
+
export async function renderToFlightString(
|
|
98
|
+
element: ReactNode,
|
|
99
|
+
opts: RenderToFlightStringOptions = {},
|
|
100
|
+
): Promise<string> {
|
|
101
|
+
const url = new URL(opts.url ?? DEFAULT_URL);
|
|
102
|
+
const request = new Request(url, { headers: opts.headers });
|
|
103
|
+
const ctx = createRequestContext({
|
|
104
|
+
env: opts.env ?? {},
|
|
105
|
+
request,
|
|
106
|
+
url,
|
|
107
|
+
variables: {},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const payload = wrapAsPayload(element, url.pathname);
|
|
111
|
+
|
|
112
|
+
return runWithRequestContext(ctx, async () => {
|
|
113
|
+
setRequestContextParams(opts.params ?? {}, opts.routeName);
|
|
114
|
+
// Capture (do NOT rethrow) the first render error. The serializer calls
|
|
115
|
+
// onError from its own scheduled work; throwing there escapes as an
|
|
116
|
+
// unhandled rejection AND leaves the stream un-closed, so the drain below
|
|
117
|
+
// would hang until the test times out. Production's onError returns void
|
|
118
|
+
// (rsc-rendering.ts) so the stream completes with an error row. We mirror
|
|
119
|
+
// that — let the stream finish — then surface the error as a clean
|
|
120
|
+
// rejection after draining, so `await expect(...).rejects.toThrow()` works.
|
|
121
|
+
let renderError: unknown;
|
|
122
|
+
let didError = false;
|
|
123
|
+
const stream = RSDServer.renderToReadableStream(
|
|
124
|
+
payload,
|
|
125
|
+
{},
|
|
126
|
+
{
|
|
127
|
+
onError(error: unknown) {
|
|
128
|
+
if (!didError) {
|
|
129
|
+
didError = true;
|
|
130
|
+
renderError = error;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
// Drain inside the context so async components see ctx during streaming.
|
|
136
|
+
const text = await new Response(stream).text();
|
|
137
|
+
if (didError) throw renderError;
|
|
138
|
+
return text;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Volatile leading reference row: `:N<timestamp>` (dev debug-info anchor).
|
|
143
|
+
const REFERENCE_ROW_RE = /^:N[\d.]+\n/;
|
|
144
|
+
// Absolute file:// paths embedded in dev STACK rows. The serializer emits stack
|
|
145
|
+
// frames as `["Component","file:///abs/path.tsx",<line>,<col>,...]`, so the
|
|
146
|
+
// path is a quoted JSON string immediately followed by `",<line>,<col>`. The
|
|
147
|
+
// lookahead scopes the scrub to exactly that frame shape, leaving a legitimate
|
|
148
|
+
// `file://` href in RENDERED content (e.g. `{"href":"file:///x"}`) untouched.
|
|
149
|
+
const FILE_URL_RE = /file:\/\/[^"\\]+(?=",\d+,\d+)/g;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Scrub volatile bits from a Flight string so snapshots are stable across runs
|
|
153
|
+
* and machines:
|
|
154
|
+
* - the leading `:N<timestamp>` reference row (dev only),
|
|
155
|
+
* - absolute `file://...` paths inside dev stack rows.
|
|
156
|
+
*
|
|
157
|
+
* Under NODE_ENV=production these rows are already absent; normalize is a
|
|
158
|
+
* no-op safety net there. In dev mode it removes the machine/clock-specific
|
|
159
|
+
* noise while leaving the rendered tree intact.
|
|
160
|
+
*/
|
|
161
|
+
export function normalizeFlight(flight: string): string {
|
|
162
|
+
return flight
|
|
163
|
+
.replace(REFERENCE_ROW_RE, "")
|
|
164
|
+
.replace(FILE_URL_RE, "file://<path>");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Smoke check that the vendored serializer subpath still resolves and exposes
|
|
169
|
+
* `renderToReadableStream`. The vendored path is private to plugin-rsc; a minor
|
|
170
|
+
* bump could relocate it. Call this in a test to fail loudly with a clear
|
|
171
|
+
* message instead of an opaque import error.
|
|
172
|
+
*/
|
|
173
|
+
export function assertFlightRuntimeAvailable(): void {
|
|
174
|
+
if (typeof RSDServer.renderToReadableStream !== "function") {
|
|
175
|
+
throw new Error(
|
|
176
|
+
"Vendored react-server-dom serializer not available: " +
|
|
177
|
+
"@vitejs/plugin-rsc/vendor/react-server-dom/server.edge did not export " +
|
|
178
|
+
"renderToReadableStream. The private vendored subpath may have moved in " +
|
|
179
|
+
"a plugin-rsc upgrade.",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assertGeneratedRoutesMatch — pin the generated named-routes map against the
|
|
3
|
+
* router's runtime route map.
|
|
4
|
+
*
|
|
5
|
+
* The Vite plugin writes a `*.named-routes.gen.ts` file mapping route names to
|
|
6
|
+
* URL patterns; consumers import that map and pass it here. The check compares
|
|
7
|
+
* it to the router's runtime `routeMap`, catching drift when a route is added,
|
|
8
|
+
* removed, renamed, or its pattern changes without regenerating the file.
|
|
9
|
+
*
|
|
10
|
+
* Directionality (relative to the generated map):
|
|
11
|
+
* - missing: present in the generated map but NOT at runtime (stale entry).
|
|
12
|
+
* - extra: present at runtime but NOT in the generated map (ungenerated route).
|
|
13
|
+
* Auto-generated internal names (the "$path_" / "$prefix_" prefixes)
|
|
14
|
+
* are excluded — they live in the runtime map but are never written
|
|
15
|
+
* to the generated file, so they are not drift.
|
|
16
|
+
* - mismatch: present in both under the same name, but the patterns differ.
|
|
17
|
+
*
|
|
18
|
+
* When `generatedMap` is omitted, the global route map (getGlobalRouteMap()) is
|
|
19
|
+
* used as the generated side — useful when a single router has registered into
|
|
20
|
+
* the global map.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
24
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Router shape this check depends on: a runtime route map, plus the optional
|
|
28
|
+
* `findMatch` used to force-expand lazy `include()`d routes (see below).
|
|
29
|
+
*/
|
|
30
|
+
interface RouterWithRouteMap {
|
|
31
|
+
routeMap: Record<string, unknown>;
|
|
32
|
+
findMatch?: (pathname: string) => unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Derive a best-effort concrete path from a route pattern so `findMatch` can be
|
|
37
|
+
* invoked to expand a lazy include. `:param`, `:param(constraint)`, optional
|
|
38
|
+
* `:param?`, and `*` are all replaced with a literal segment. A constrained
|
|
39
|
+
* param may not match its constraint (so that one route's match fails), but
|
|
40
|
+
* since matching ANY route in an include expands ALL of the include's routes,
|
|
41
|
+
* a sibling route in the same include will still trigger expansion.
|
|
42
|
+
*/
|
|
43
|
+
function concretePath(pattern: string): string {
|
|
44
|
+
return (
|
|
45
|
+
pattern
|
|
46
|
+
.replace(/:[A-Za-z0-9_]+\([^)]*\)\??/g, "x") // :p(constraint) / optional
|
|
47
|
+
.replace(/:[A-Za-z0-9_]+\??/g, "x") // :p or :p?
|
|
48
|
+
.replace(/\*/g, "x") // wildcard
|
|
49
|
+
.replace(/\/{2,}/g, "/") || "/"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Force-expand the router's lazy `include()`d routes into `router.routeMap`.
|
|
55
|
+
*
|
|
56
|
+
* All Rango includes are lazy — their child routes only populate `routeMap` when
|
|
57
|
+
* the router first matches a path inside them (in production the build-time
|
|
58
|
+
* manifest virtual carries the full map; in a bare test that virtual is absent).
|
|
59
|
+
* To make the whole-app drift check work in a unit test, we trigger expansion by
|
|
60
|
+
* calling `findMatch` on a concrete path derived from each known pattern. This is
|
|
61
|
+
* idempotent and side-effect-free beyond populating the route map. Routers that
|
|
62
|
+
* don't expose `findMatch` (e.g. a plain `{ routeMap }` object) are left as-is.
|
|
63
|
+
*/
|
|
64
|
+
function expandLazyIncludes(
|
|
65
|
+
router: RouterWithRouteMap,
|
|
66
|
+
patterns: Iterable<string>,
|
|
67
|
+
): void {
|
|
68
|
+
const findMatch = router.findMatch;
|
|
69
|
+
if (typeof findMatch !== "function") return;
|
|
70
|
+
for (const pattern of patterns) {
|
|
71
|
+
try {
|
|
72
|
+
findMatch.call(router, concretePath(pattern));
|
|
73
|
+
} catch {
|
|
74
|
+
// A pattern that fails to match (constrained param, etc.) is fine — a
|
|
75
|
+
// sibling route in the same include still triggers expansion.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A single name/pattern mismatch: [routeName, generatedPattern, runtimePattern].
|
|
82
|
+
*/
|
|
83
|
+
export type GeneratedRouteMismatch = [
|
|
84
|
+
name: string,
|
|
85
|
+
generated: string,
|
|
86
|
+
runtime: string,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Structured diff between the generated route map and the runtime route map.
|
|
91
|
+
*/
|
|
92
|
+
export interface GeneratedRoutesDiff {
|
|
93
|
+
/** Names in the generated map but absent at runtime. */
|
|
94
|
+
missing: string[];
|
|
95
|
+
/** Names at runtime but absent from the generated map. */
|
|
96
|
+
extra: string[];
|
|
97
|
+
/** Names in both with differing patterns. */
|
|
98
|
+
mismatch: GeneratedRouteMismatch[];
|
|
99
|
+
/** True when missing, extra, and mismatch are all empty. */
|
|
100
|
+
ok: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a route map value to its pattern string. Route maps may carry
|
|
105
|
+
* either a bare pattern string or a `{ path, ... }` object (for response/search
|
|
106
|
+
* routes); compare on the `path`.
|
|
107
|
+
*/
|
|
108
|
+
function patternOf(value: unknown): string {
|
|
109
|
+
if (typeof value === "string") return value;
|
|
110
|
+
if (
|
|
111
|
+
value &&
|
|
112
|
+
typeof value === "object" &&
|
|
113
|
+
"path" in value &&
|
|
114
|
+
typeof (value as { path: unknown }).path === "string"
|
|
115
|
+
) {
|
|
116
|
+
return (value as { path: string }).path;
|
|
117
|
+
}
|
|
118
|
+
return String(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compute the diff between a router's runtime route map and a generated map.
|
|
123
|
+
*/
|
|
124
|
+
export function diffGeneratedRoutes(
|
|
125
|
+
router: RouterWithRouteMap,
|
|
126
|
+
generatedMap?: Record<string, unknown>,
|
|
127
|
+
): GeneratedRoutesDiff {
|
|
128
|
+
const generated = generatedMap ?? getGlobalRouteMap();
|
|
129
|
+
|
|
130
|
+
// Lazy `include()`d routes are absent from `routeMap` until first matched, so
|
|
131
|
+
// expand them first (using the generated patterns to drive the matches) —
|
|
132
|
+
// otherwise every included route is a false `missing`. No-op for plain
|
|
133
|
+
// `{ routeMap }` objects that don't expose `findMatch`.
|
|
134
|
+
expandLazyIncludes(
|
|
135
|
+
router,
|
|
136
|
+
Object.values(generated).map((v) => patternOf(v)),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const runtime = router.routeMap as Record<string, unknown>;
|
|
140
|
+
|
|
141
|
+
const missing: string[] = [];
|
|
142
|
+
const extra: string[] = [];
|
|
143
|
+
const mismatch: GeneratedRouteMismatch[] = [];
|
|
144
|
+
|
|
145
|
+
for (const name of Object.keys(generated)) {
|
|
146
|
+
if (!(name in runtime)) {
|
|
147
|
+
missing.push(name);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const gen = patternOf(generated[name]);
|
|
151
|
+
const run = patternOf(runtime[name]);
|
|
152
|
+
if (gen !== run) {
|
|
153
|
+
mismatch.push([name, gen, run]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const name of Object.keys(runtime)) {
|
|
158
|
+
// Auto-generated internal names ($path_*/$prefix_*) live in the runtime
|
|
159
|
+
// mergedRouteMap but are deliberately excluded from the generated
|
|
160
|
+
// *.named-routes.gen.ts file (route-types-writer / runtime-discovery skip
|
|
161
|
+
// them). Reporting them as `extra` would throw on a perfectly in-sync app
|
|
162
|
+
// that simply uses an unnamed path()/include() route, so skip them here to
|
|
163
|
+
// match exactly the surface the generator emits.
|
|
164
|
+
if (isAutoGeneratedRouteName(name)) continue;
|
|
165
|
+
if (!(name in generated)) {
|
|
166
|
+
extra.push(name);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
missing,
|
|
172
|
+
extra,
|
|
173
|
+
mismatch,
|
|
174
|
+
ok: missing.length === 0 && extra.length === 0 && mismatch.length === 0,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Assert the router's runtime route map matches the generated map. Throws a
|
|
180
|
+
* descriptive Error listing every missing, extra, and mismatched route when
|
|
181
|
+
* they diverge.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* import generated from "./router.named-routes.gen";
|
|
186
|
+
* import { router } from "./router";
|
|
187
|
+
*
|
|
188
|
+
* assertGeneratedRoutesMatch(router, generated);
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export function assertGeneratedRoutesMatch(
|
|
192
|
+
router: RouterWithRouteMap,
|
|
193
|
+
generatedMap?: Record<string, unknown>,
|
|
194
|
+
): void {
|
|
195
|
+
const diff = diffGeneratedRoutes(router, generatedMap);
|
|
196
|
+
if (diff.ok) return;
|
|
197
|
+
|
|
198
|
+
const lines: string[] = [
|
|
199
|
+
"Generated routes do not match the router's runtime route map.",
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
if (diff.missing.length > 0) {
|
|
203
|
+
lines.push(
|
|
204
|
+
` Missing (generated but not at runtime): ${diff.missing.join(", ")}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (diff.extra.length > 0) {
|
|
208
|
+
lines.push(
|
|
209
|
+
` Extra (at runtime but not generated): ${diff.extra.join(", ")}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (diff.mismatch.length > 0) {
|
|
213
|
+
lines.push(" Pattern mismatches (name: generated -> runtime):");
|
|
214
|
+
for (const [name, gen, run] of diff.mismatch) {
|
|
215
|
+
lines.push(` ${name}: ${gen} -> ${run}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
lines.push(
|
|
219
|
+
"Re-run the build / `rango` route generation to regenerate the *.named-routes.gen.ts file.",
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
throw new Error(lines.join("\n"));
|
|
223
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rangojs/router/testing
|
|
3
|
+
*
|
|
4
|
+
* Consumer-facing testing primitives for apps built on @rangojs/router.
|
|
5
|
+
*
|
|
6
|
+
* This is the entry for UNIT and INTEGRATION tests, meant to run under a
|
|
7
|
+
* Vite-driven Vitest project (the rango Vite plugin must be active so the
|
|
8
|
+
* `@rangojs/router:version` and related virtual modules the router internals
|
|
9
|
+
* import can resolve; alternatively alias `@rangojs/router:version`). Importing
|
|
10
|
+
* this module references neither React, @testing-library/react, @playwright/test,
|
|
11
|
+
* nor the RSC runtime — a unit suite testing only loaders/middleware/dispatch
|
|
12
|
+
* pulls in none of them.
|
|
13
|
+
*
|
|
14
|
+
* The other surfaces are SEPARATE entries because each pulls a dependency or
|
|
15
|
+
* runtime this barrel deliberately keeps out:
|
|
16
|
+
* - `@rangojs/router/testing/dom` — `renderRoute` (the RTL component stub). Kept
|
|
17
|
+
* separate so this barrel never references React, the browser runtime, or
|
|
18
|
+
* `@testing-library/react` types — a unit suite testing only loaders/middleware
|
|
19
|
+
* needs none of them.
|
|
20
|
+
* - `@rangojs/router/testing/e2e` — the Playwright harness (createRangoE2E,
|
|
21
|
+
* useFixture, parityDescribe, expectParity, matchers). Kept separate so it is
|
|
22
|
+
* loadable in a plain (non-Vite) Playwright runner, which cannot resolve the
|
|
23
|
+
* router-manifest virtual modules this barrel pulls in.
|
|
24
|
+
* - `@rangojs/router/testing/flight` — real Flight rendering. Its serializer
|
|
25
|
+
* (vendored react-server-dom) loads only under the `react-server` node
|
|
26
|
+
* condition and would throw if pulled into this barrel.
|
|
27
|
+
*
|
|
28
|
+
* Layers:
|
|
29
|
+
* - Unit: runMiddleware, runLoader
|
|
30
|
+
* - Integration: dispatch (request -> Response)
|
|
31
|
+
* - Cross-cut: assertCacheStatus, assertGeneratedRoutesMatch
|
|
32
|
+
* - Component: see @rangojs/router/testing/dom (renderRoute)
|
|
33
|
+
* - E2E: see @rangojs/router/testing/e2e
|
|
34
|
+
* - RSC: see @rangojs/router/testing/flight
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Unit
|
|
38
|
+
export { runMiddleware } from "./run-middleware.js";
|
|
39
|
+
export type {
|
|
40
|
+
RunMiddlewareOptions,
|
|
41
|
+
RunMiddlewareResult,
|
|
42
|
+
} from "./run-middleware.js";
|
|
43
|
+
export { runLoader } from "./run-loader.js";
|
|
44
|
+
export type {
|
|
45
|
+
RunLoaderOptions,
|
|
46
|
+
UseResolver,
|
|
47
|
+
TestLoaderContext,
|
|
48
|
+
} from "./run-loader.js";
|
|
49
|
+
|
|
50
|
+
// Integration
|
|
51
|
+
export { dispatch } from "./dispatch.js";
|
|
52
|
+
export type { DispatchOptions } from "./dispatch.js";
|
|
53
|
+
|
|
54
|
+
// renderRoute lives at `@rangojs/router/testing/dom` — it pulls React, the
|
|
55
|
+
// browser runtime, and @testing-library/react types, which this barrel keeps
|
|
56
|
+
// out so node-only unit suites depend on none of them.
|
|
57
|
+
|
|
58
|
+
// Cross-cutting: cache/prerender status
|
|
59
|
+
export {
|
|
60
|
+
assertCacheStatus,
|
|
61
|
+
parseCacheHeader,
|
|
62
|
+
createCacheSink,
|
|
63
|
+
filterCacheDecisions,
|
|
64
|
+
} from "./cache-status.js";
|
|
65
|
+
export type {
|
|
66
|
+
ExpectedCacheStatus,
|
|
67
|
+
CacheStatusTarget,
|
|
68
|
+
CacheSink,
|
|
69
|
+
} from "./cache-status.js";
|
|
70
|
+
|
|
71
|
+
// Cross-cutting: handle collect/accumulator
|
|
72
|
+
export { collectHandle } from "./collect-handle.js";
|
|
73
|
+
|
|
74
|
+
// Cross-cutting: generated-route drift
|
|
75
|
+
export {
|
|
76
|
+
diffGeneratedRoutes,
|
|
77
|
+
assertGeneratedRoutesMatch,
|
|
78
|
+
} from "./generated-routes.js";
|
|
79
|
+
export type {
|
|
80
|
+
GeneratedRoutesDiff,
|
|
81
|
+
GeneratedRouteMismatch,
|
|
82
|
+
} from "./generated-routes.js";
|
|
83
|
+
|
|
84
|
+
// Advanced: build a real RequestContext for bespoke loader/middleware setups
|
|
85
|
+
export {
|
|
86
|
+
createTestRequestContext,
|
|
87
|
+
runInRequestContext,
|
|
88
|
+
toRequest,
|
|
89
|
+
seedVariables,
|
|
90
|
+
} from "./internal/context.js";
|
|
91
|
+
export type {
|
|
92
|
+
CreateTestContextOptions,
|
|
93
|
+
TestRequestContext,
|
|
94
|
+
VarsInit,
|
|
95
|
+
} from "./internal/context.js";
|
|
96
|
+
|
|
97
|
+
// The low-level context runner that enters a RequestContext (the same one the
|
|
98
|
+
// RSC handler uses for server actions). Re-exported so a ctx built with
|
|
99
|
+
// createTestRequestContext can be entered directly; runInRequestContext is the
|
|
100
|
+
// one-call convenience over createTestRequestContext + runWithRequestContext.
|
|
101
|
+
export { runWithRequestContext } from "../server/request-context.js";
|
|
102
|
+
|
|
103
|
+
// The E2E harness is NOT re-exported here: it must be imported from
|
|
104
|
+
// `@rangojs/router/testing/e2e` so it stays loadable in a plain Playwright
|
|
105
|
+
// runner (this barrel pulls in router-manifest code that needs Vite virtuals).
|