@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -63,7 +63,8 @@ export function tryTrieMatch(
|
|
|
63
63
|
// same value the regex matcher produces for the bare prefix. Without this
|
|
64
64
|
// the trie misses, the regex fallback runs, and its no-config branch emits
|
|
65
65
|
// a corrupt slice-off redirect. The static terminal still wins above.
|
|
66
|
-
|
|
66
|
+
// A one-or-more catch-all (`w1`, from `:name+`) rejects this empty case.
|
|
67
|
+
if (trie.w && !trie.w.w1) {
|
|
67
68
|
return validateAndBuild(
|
|
68
69
|
trie.w,
|
|
69
70
|
[],
|
|
@@ -192,8 +193,9 @@ function walkTrie(
|
|
|
192
193
|
// walkTrie otherwise only reaches node.w in the index<length branch below,
|
|
193
194
|
// so without this a request to the wildcard's own prefix misses the trie
|
|
194
195
|
// and the regex fallback emits a corrupt redirect. A static terminal
|
|
195
|
-
// (node.r) still wins.
|
|
196
|
-
|
|
196
|
+
// (node.r) still wins. A one-or-more catch-all (`w1`, from `:name+`) rejects
|
|
197
|
+
// this empty case — it requires at least one trailing segment.
|
|
198
|
+
if (node.w && !node.w.w1) {
|
|
197
199
|
const validatedParams = leafConstraintsPass(node.w, paramValues, "");
|
|
198
200
|
if (validatedParams) {
|
|
199
201
|
return {
|
|
@@ -250,14 +252,20 @@ function walkTrie(
|
|
|
250
252
|
|
|
251
253
|
if (node.w) {
|
|
252
254
|
const rest = joinRemainingSegments(segments, index);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
255
|
+
// A one-or-more catch-all (`w1`, from `:name+`) requires at least one
|
|
256
|
+
// non-empty trailing segment. `rest` can still be "" here on a malformed
|
|
257
|
+
// double-slash URL (e.g. `/docs//` splits to a trailing "" segment), so
|
|
258
|
+
// guard this in-path site the same way the root and base-case sites are.
|
|
259
|
+
if (!(node.w.w1 && rest === "")) {
|
|
260
|
+
const validatedParams = leafConstraintsPass(node.w, paramValues, rest);
|
|
261
|
+
if (validatedParams) {
|
|
262
|
+
return {
|
|
263
|
+
leaf: node.w,
|
|
264
|
+
paramValues: [...paramValues],
|
|
265
|
+
wildcardValue: rest,
|
|
266
|
+
validatedParams,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
261
269
|
}
|
|
262
270
|
}
|
|
263
271
|
|
package/src/router/url-params.ts
CHANGED
|
@@ -42,3 +42,16 @@ export function encodePathSegment(value: string): string {
|
|
|
42
42
|
(match) => PATH_SAFE_ESCAPES[match.toUpperCase()] ?? match,
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Encode a catch-all remainder: encode each `/`-separated segment but keep the
|
|
48
|
+
* separators, so `a/b c` -> `a/b%20c` (not `a%2Fb%20c`). Shared by the reverse
|
|
49
|
+
* helper and the build-time prerender substitution so both produce identical
|
|
50
|
+
* URLs. `encode` defaults to the path-safe `encodePathSegment`.
|
|
51
|
+
*/
|
|
52
|
+
export function encodePathRemainder(
|
|
53
|
+
value: string,
|
|
54
|
+
encode: (segment: string) => string = encodePathSegment,
|
|
55
|
+
): string {
|
|
56
|
+
return value.split("/").map(encode).join("/");
|
|
57
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -111,6 +111,10 @@ import {
|
|
|
111
111
|
} from "./router/prerender-match.js";
|
|
112
112
|
import { resolveStateCookieName } from "./router/state-cookie-name.js";
|
|
113
113
|
import { resolvePrefetchCacheTTL } from "./router/prefetch-cache-ttl.js";
|
|
114
|
+
import {
|
|
115
|
+
resolvePrefetchCacheSize,
|
|
116
|
+
resolvePrefetchConcurrency,
|
|
117
|
+
} from "./router/prefetch-limits.js";
|
|
114
118
|
|
|
115
119
|
// Re-export public types and values from extracted modules
|
|
116
120
|
export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
|
|
@@ -150,6 +154,8 @@ export function createRouter<TEnv = any>(
|
|
|
150
154
|
nonce,
|
|
151
155
|
version,
|
|
152
156
|
prefetchCacheTTL: prefetchCacheTTLOption,
|
|
157
|
+
prefetchCacheSize: prefetchCacheSizeOption,
|
|
158
|
+
prefetchConcurrency: prefetchConcurrencyOption,
|
|
153
159
|
stateCookiePrefix: stateCookiePrefixOption,
|
|
154
160
|
warmup: warmupOption,
|
|
155
161
|
allowDebugManifest: allowDebugManifestOption = false,
|
|
@@ -242,6 +248,14 @@ export function createRouter<TEnv = any>(
|
|
|
242
248
|
const prefetchCacheControl: string | false =
|
|
243
249
|
resolvedPrefetchCacheTTL.cacheControl;
|
|
244
250
|
|
|
251
|
+
// Resolve client-side prefetch limits (in-memory cache size and queue
|
|
252
|
+
// concurrency). Both are positive-integer counts; sub-1/non-finite inputs
|
|
253
|
+
// fall back to the defaults. Shipped to the client in payload metadata.
|
|
254
|
+
const prefetchCacheSize = resolvePrefetchCacheSize(prefetchCacheSizeOption);
|
|
255
|
+
const prefetchConcurrency = resolvePrefetchConcurrency(
|
|
256
|
+
prefetchConcurrencyOption,
|
|
257
|
+
);
|
|
258
|
+
|
|
245
259
|
// Resolve warmup enabled flag (default: true)
|
|
246
260
|
const warmupEnabled = warmupOption !== false;
|
|
247
261
|
|
|
@@ -406,14 +420,17 @@ export function createRouter<TEnv = any>(
|
|
|
406
420
|
|
|
407
421
|
// Wrapper to pass debugPerformance to external createMetricsStore.
|
|
408
422
|
// Also checks per-request flag set by ctx.debugPerformance() in middleware.
|
|
423
|
+
// With no active request context there is nowhere to hang the store, so return
|
|
424
|
+
// undefined: an orphan store would collect metrics no reader can reach (nothing
|
|
425
|
+
// holds it, and appendMetric(undefined, ...) is already a no-op).
|
|
409
426
|
const getMetricsStore = () => {
|
|
410
427
|
const reqCtx = _getRequestContext();
|
|
411
428
|
const enabled = debugPerformance || !!reqCtx?._debugPerformance;
|
|
412
|
-
if (!enabled) return undefined;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
429
|
+
if (!enabled || !reqCtx) return undefined;
|
|
430
|
+
// Anchor a mid-request store to the true request entry (reqCtx._handlerStart),
|
|
431
|
+
// not this call's performance.now(); undefined falls back to now() inside
|
|
432
|
+
// createMetricsStore (metrics.ts).
|
|
433
|
+
reqCtx._metricsStore ??= createMetricsStore(true, reqCtx._handlerStart);
|
|
417
434
|
return reqCtx._metricsStore;
|
|
418
435
|
};
|
|
419
436
|
|
|
@@ -424,11 +441,6 @@ export function createRouter<TEnv = any>(
|
|
|
424
441
|
const findNearestNotFoundBoundary = (entry: EntryData | null) =>
|
|
425
442
|
findNotFoundBoundary(entry, defaultNotFoundBoundary);
|
|
426
443
|
|
|
427
|
-
// Helper to get handleStore from request context
|
|
428
|
-
const getHandleStore = (): HandleStore | undefined => {
|
|
429
|
-
return _getRequestContext()?._handleStore;
|
|
430
|
-
};
|
|
431
|
-
|
|
432
444
|
// Track a pending handler promise (non-blocking).
|
|
433
445
|
// Attaches a side-effect .catch() to report streaming handler errors to onError
|
|
434
446
|
// without altering the rejection chain (React's streaming error boundary still handles it).
|
|
@@ -439,13 +451,14 @@ export function createRouter<TEnv = any>(
|
|
|
439
451
|
segmentType?: string;
|
|
440
452
|
},
|
|
441
453
|
): Promise<T> => {
|
|
442
|
-
|
|
454
|
+
// One ALS read serves both the store lookup and the onError closure.
|
|
455
|
+
const reqCtx = _getRequestContext();
|
|
456
|
+
const store = reqCtx?._handleStore;
|
|
443
457
|
const tracked = store ? store.track(promise) : promise;
|
|
444
458
|
|
|
445
459
|
// Report streaming handler errors to onError as a side-effect.
|
|
446
460
|
// The rejection still propagates to the RSC stream for client error boundaries.
|
|
447
461
|
// Captures request context eagerly (closure) so the catch handler has full context.
|
|
448
|
-
const reqCtx = _getRequestContext();
|
|
449
462
|
if (reqCtx && onError) {
|
|
450
463
|
tracked.catch((error) => {
|
|
451
464
|
callOnError(error, "handler", {
|
|
@@ -487,8 +500,12 @@ export function createRouter<TEnv = any>(
|
|
|
487
500
|
? getRequestId(errorContext.request)
|
|
488
501
|
: undefined
|
|
489
502
|
: undefined;
|
|
503
|
+
// Derived once here for both the loader.start and loader.end emits (the
|
|
504
|
+
// loader.error emit uses ctx.loaderName from wrapLoaderWithErrorHandling).
|
|
505
|
+
const loaderName = telemetrySink
|
|
506
|
+
? segmentId.split(".").pop() || "unknown"
|
|
507
|
+
: "";
|
|
490
508
|
if (telemetrySink) {
|
|
491
|
-
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
492
509
|
safeEmit(telemetry, {
|
|
493
510
|
type: "loader.start",
|
|
494
511
|
timestamp: loaderStart,
|
|
@@ -542,7 +559,6 @@ export function createRouter<TEnv = any>(
|
|
|
542
559
|
|
|
543
560
|
// Emit loader.end after the promise settles (fire-and-forget)
|
|
544
561
|
if (telemetrySink) {
|
|
545
|
-
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
546
562
|
result.then((r) => {
|
|
547
563
|
safeEmit(telemetry, {
|
|
548
564
|
type: "loader.end",
|
|
@@ -606,8 +622,15 @@ export function createRouter<TEnv = any>(
|
|
|
606
622
|
routerId,
|
|
607
623
|
};
|
|
608
624
|
|
|
609
|
-
|
|
610
|
-
|
|
625
|
+
// Must return the Promise from _evaluateLazyEntry: an async include provider
|
|
626
|
+
// (`() => import("./routes")`) resolves off the startup path, and createFindMatch
|
|
627
|
+
// awaits this to know when the import + expansion have completed. Dropping it
|
|
628
|
+
// (typing this `void`) makes the import fire-and-forget, so findMatch spins the
|
|
629
|
+
// lazy-eval retry loop to its cap and returns null on the first request to any
|
|
630
|
+
// async include whose prefix isn't already covered by a unique precomputed entry
|
|
631
|
+
// (nested includes, shared prefixes, regex fallback).
|
|
632
|
+
function evaluateLazyEntry(entry: RouteEntry<TEnv>): void | Promise<void> {
|
|
633
|
+
return _evaluateLazyEntry(entry, lazyEvalDeps);
|
|
611
634
|
}
|
|
612
635
|
|
|
613
636
|
// Create findMatch with single-entry cache, bound to router state
|
|
@@ -968,6 +991,8 @@ export function createRouter<TEnv = any>(
|
|
|
968
991
|
// Expose prefetch cache settings
|
|
969
992
|
prefetchCacheControl,
|
|
970
993
|
prefetchCacheTTL,
|
|
994
|
+
prefetchCacheSize,
|
|
995
|
+
prefetchConcurrency,
|
|
971
996
|
|
|
972
997
|
// Expose the resolved rango state cookie name for the server-side writer
|
|
973
998
|
// (invalidateClientCache) and for shipping to the client in metadata.
|
|
@@ -985,6 +1010,12 @@ export function createRouter<TEnv = any>(
|
|
|
985
1010
|
// Expose resolved span tracing for the handler (Cloudflare custom spans)
|
|
986
1011
|
tracing: resolvedTracing,
|
|
987
1012
|
|
|
1013
|
+
// Expose the raw telemetry sink so handler-level emitters (timeout, origin
|
|
1014
|
+
// rejection, late-handle handler.error) can emit outside the match ALS.
|
|
1015
|
+
// Raw (not the resolveSink no-op wrapper) so router.telemetry stays
|
|
1016
|
+
// undefined when unconfigured and call sites gate on truthiness.
|
|
1017
|
+
telemetry: telemetrySink,
|
|
1018
|
+
|
|
988
1019
|
// Expose debug manifest flag for handler
|
|
989
1020
|
allowDebugManifest: allowDebugManifestOption,
|
|
990
1021
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full (initial-load / document) RSC payload builder.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from rsc-rendering.ts so both the foreground render AND the PPR
|
|
5
|
+
* background shell capture (shell-capture.ts) build the SAME payload shape over
|
|
6
|
+
* their own handle store. `resume` requires the captured tree to match the served
|
|
7
|
+
* tree; building an identical payload over the same replayed segments is what
|
|
8
|
+
* makes that hold. Keep this byte-identical to the normal full-render path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MatchResult } from "../types.js";
|
|
12
|
+
import type { RscPayload } from "./types.js";
|
|
13
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
14
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
15
|
+
import type { HandleStore } from "../server/handle-store.js";
|
|
16
|
+
import { gateTransitions } from "./transition-gate.js";
|
|
17
|
+
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the metadata payload for a full (non-partial) document render.
|
|
21
|
+
*
|
|
22
|
+
* @param handleStore - the store whose resolved handle stream feeds the payload;
|
|
23
|
+
* the foreground passes reqCtx's store, the background capture passes its fresh
|
|
24
|
+
* derived-context store.
|
|
25
|
+
*/
|
|
26
|
+
export function buildFullPayload(
|
|
27
|
+
m: MatchResult,
|
|
28
|
+
// env is opaque here (the payload never reads it); `any` avoids the invariance
|
|
29
|
+
// friction of HandlerContext<TEnv> vs the ambient RequestContext env at the two
|
|
30
|
+
// call sites (foreground render + background capture).
|
|
31
|
+
ctx: HandlerContext<any>,
|
|
32
|
+
url: URL,
|
|
33
|
+
reqCtx: RequestContext<any>,
|
|
34
|
+
handleStore: HandleStore,
|
|
35
|
+
): RscPayload {
|
|
36
|
+
return {
|
|
37
|
+
metadata: {
|
|
38
|
+
pathname: url.pathname,
|
|
39
|
+
routerId: ctx.router.id,
|
|
40
|
+
basename: ctx.router.basename,
|
|
41
|
+
segments: gateTransitions(m.segments, reqCtx, ctx.router.onError),
|
|
42
|
+
matched: m.matched,
|
|
43
|
+
diff: m.diff,
|
|
44
|
+
resolvedIds: m.resolvedIds,
|
|
45
|
+
params: m.params,
|
|
46
|
+
isPartial: false,
|
|
47
|
+
rootLayout: ctx.router.rootLayout,
|
|
48
|
+
// Full render: resolve deferred handle values server-side so SSR markup and
|
|
49
|
+
// the first sync useHandle read see resolved values. Partial payloads
|
|
50
|
+
// (rsc-rendering.ts) keep streaming (handleStore.stream()).
|
|
51
|
+
handles: resolvedHandleStream(handleStore),
|
|
52
|
+
version: ctx.version,
|
|
53
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
54
|
+
prefetchCacheSize: ctx.router.prefetchCacheSize,
|
|
55
|
+
prefetchConcurrency: ctx.router.prefetchConcurrency,
|
|
56
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
57
|
+
themeConfig: ctx.router.themeConfig,
|
|
58
|
+
// Carry warmupEnabled on the initial full-render payload so the client
|
|
59
|
+
// respects warmup:false from first load. The 404 and PE payloads already
|
|
60
|
+
// include it; without it here warmup could never be disabled on the
|
|
61
|
+
// normal full-load path (partial payloads omit it by design).
|
|
62
|
+
warmupEnabled: ctx.router.warmupEnabled,
|
|
63
|
+
// Carry strictMode on the initial full-render payload so the browser
|
|
64
|
+
// entry knows whether to wrap hydration in React.StrictMode. Partial
|
|
65
|
+
// (navigation) payloads omit it by design; StrictMode is decided once.
|
|
66
|
+
strictMode: ctx.router.strictMode,
|
|
67
|
+
initialTheme: reqCtx.theme,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/rsc/handler.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
buildRouteMiddlewareEntries,
|
|
32
32
|
} from "./helpers.js";
|
|
33
33
|
import { guardOutgoingRedirect } from "./redirect-guard.js";
|
|
34
|
+
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
34
35
|
import {
|
|
35
36
|
isWebSocketUpgradeResponse,
|
|
36
37
|
appendVaryAccept,
|
|
@@ -84,7 +85,8 @@ import {
|
|
|
84
85
|
appendMetric,
|
|
85
86
|
buildMetricsTiming,
|
|
86
87
|
} from "../router/metrics.js";
|
|
87
|
-
import { observePhase,
|
|
88
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
89
|
+
import { safeEmit, resolveSink, getRequestId } from "../router/telemetry.js";
|
|
88
90
|
import {
|
|
89
91
|
startSSRSetup,
|
|
90
92
|
getSSRSetup,
|
|
@@ -245,16 +247,19 @@ export function createRSCHandler<
|
|
|
245
247
|
metadata: { timeout: true, phase, durationMs },
|
|
246
248
|
});
|
|
247
249
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
250
|
+
if (router.telemetry) {
|
|
251
|
+
safeEmit(resolveSink(router.telemetry), {
|
|
252
|
+
type: "request.timeout",
|
|
253
|
+
timestamp: performance.now(),
|
|
254
|
+
requestId: getRequestId(request),
|
|
255
|
+
phase,
|
|
256
|
+
pathname: url.pathname,
|
|
257
|
+
routeKey,
|
|
258
|
+
actionId,
|
|
259
|
+
durationMs,
|
|
260
|
+
customHandler: !!router.onTimeout,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
258
263
|
|
|
259
264
|
if (router.onTimeout) {
|
|
260
265
|
try {
|
|
@@ -461,6 +466,11 @@ export function createRSCHandler<
|
|
|
461
466
|
stateCookieName: router.resolvedStateCookieName,
|
|
462
467
|
version,
|
|
463
468
|
});
|
|
469
|
+
// Thread the true request entry timestamp onto the context so a metrics
|
|
470
|
+
// store created MID-request (ctx.debugPerformance() / getMetricsStore) anchors
|
|
471
|
+
// to the real start, not the opt-in moment — phases that began earlier then
|
|
472
|
+
// report non-negative offsets. Set unconditionally: debug may be enabled later.
|
|
473
|
+
requestContext._handlerStart = handlerStart;
|
|
464
474
|
if (earlyMetricsStore) {
|
|
465
475
|
requestContext._debugPerformance = true;
|
|
466
476
|
requestContext._metricsStore = earlyMetricsStore;
|
|
@@ -568,8 +578,10 @@ export function createRSCHandler<
|
|
|
568
578
|
if (metricsStore) {
|
|
569
579
|
// When the store was created at handler start (earlyMetricsStore),
|
|
570
580
|
// handler:total covers the full request. When ctx.debugPerformance()
|
|
571
|
-
// created the store mid-request
|
|
572
|
-
//
|
|
581
|
+
// created the store mid-request its requestStart is now the threaded
|
|
582
|
+
// _handlerStart (== handlerStart), so both branches yield the true
|
|
583
|
+
// request entry; reading the store's own anchor keeps this correct even
|
|
584
|
+
// if a store ever lands without the threading (falls back to its start).
|
|
573
585
|
const totalStart = earlyMetricsStore
|
|
574
586
|
? handlerStart
|
|
575
587
|
: metricsStore.requestStart;
|
|
@@ -589,7 +601,13 @@ export function createRSCHandler<
|
|
|
589
601
|
|
|
590
602
|
const fullTiming = timingParts.join(", ");
|
|
591
603
|
if (fullTiming && !isWebSocketUpgradeResponse(response)) {
|
|
592
|
-
|
|
604
|
+
try {
|
|
605
|
+
response.headers.set("Server-Timing", fullTiming);
|
|
606
|
+
} catch {
|
|
607
|
+
// Immutable headers (e.g. a passed-through platform Response) — drop
|
|
608
|
+
// the timing header, never the response. Instrumentation must not
|
|
609
|
+
// 500 a request.
|
|
610
|
+
}
|
|
593
611
|
}
|
|
594
612
|
|
|
595
613
|
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
@@ -736,15 +754,18 @@ export function createRSCHandler<
|
|
|
736
754
|
},
|
|
737
755
|
});
|
|
738
756
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
757
|
+
if (router.telemetry) {
|
|
758
|
+
safeEmit(resolveSink(router.telemetry), {
|
|
759
|
+
type: "request.origin-rejected",
|
|
760
|
+
timestamp: performance.now(),
|
|
761
|
+
requestId: getRequestId(request),
|
|
762
|
+
method: request.method,
|
|
763
|
+
pathname: url.pathname,
|
|
764
|
+
phase: originPhase,
|
|
765
|
+
origin: request.headers.get("origin"),
|
|
766
|
+
host: request.headers.get("host"),
|
|
767
|
+
});
|
|
768
|
+
}
|
|
748
769
|
|
|
749
770
|
return originResult;
|
|
750
771
|
}
|
|
@@ -786,15 +807,18 @@ export function createRSCHandler<
|
|
|
786
807
|
params: reqCtx.params as Record<string, string>,
|
|
787
808
|
handledByBoundary: true,
|
|
788
809
|
});
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
810
|
+
if (router.telemetry) {
|
|
811
|
+
safeEmit(resolveSink(router.telemetry), {
|
|
812
|
+
type: "handler.error",
|
|
813
|
+
timestamp: performance.now(),
|
|
814
|
+
requestId: getRequestId(request),
|
|
815
|
+
error,
|
|
816
|
+
handledByBoundary: true,
|
|
817
|
+
pathname: url.pathname,
|
|
818
|
+
routeKey: reqCtx._routeName,
|
|
819
|
+
params: reqCtx.params as Record<string, string>,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
798
822
|
};
|
|
799
823
|
|
|
800
824
|
// Set route params early so all execution paths can access ctx.params.
|
|
@@ -1131,9 +1155,12 @@ export function createRSCHandler<
|
|
|
1131
1155
|
params: {},
|
|
1132
1156
|
isPartial: false,
|
|
1133
1157
|
rootLayout: router.rootLayout,
|
|
1134
|
-
|
|
1158
|
+
// Full (404) render: resolve deferred handle values server-side.
|
|
1159
|
+
handles: resolvedHandleStream(handleStore),
|
|
1135
1160
|
version,
|
|
1136
1161
|
prefetchCacheTTL: router.prefetchCacheTTL,
|
|
1162
|
+
prefetchCacheSize: router.prefetchCacheSize,
|
|
1163
|
+
prefetchConcurrency: router.prefetchConcurrency,
|
|
1137
1164
|
stateCookieName: router.resolvedStateCookieName,
|
|
1138
1165
|
themeConfig: router.themeConfig,
|
|
1139
1166
|
warmupEnabled: router.warmupEnabled,
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -32,7 +32,7 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
32
32
|
): Promise<void> {
|
|
33
33
|
const { generateManifestFull } =
|
|
34
34
|
await import("../build/generate-manifest.js");
|
|
35
|
-
const generated = generateManifestFull(
|
|
35
|
+
const generated = await generateManifestFull(
|
|
36
36
|
router.urlpatterns,
|
|
37
37
|
undefined,
|
|
38
38
|
router.basename ? { urlPrefix: router.basename } : undefined,
|
package/src/rsc/nonce.ts
CHANGED
|
@@ -8,11 +8,20 @@ import { createVar } from "../context-var.js";
|
|
|
8
8
|
/**
|
|
9
9
|
* Typed ContextVar token for CSP nonce.
|
|
10
10
|
*
|
|
11
|
-
* Use this to
|
|
11
|
+
* Use this to READ the nonce in middleware or handlers:
|
|
12
12
|
* ```ts
|
|
13
13
|
* import { nonce } from "@rangojs/router";
|
|
14
14
|
* const value = ctx.get(nonce); // string | undefined
|
|
15
15
|
* ```
|
|
16
|
+
*
|
|
17
|
+
* Supply the nonce via the `createRouter({ nonce })` provider, not by writing
|
|
18
|
+
* this token yourself. The provider value is threaded into the router's SSR
|
|
19
|
+
* machinery (NonceContext/useNonce, Scripts/MetaTags attributes, the inlined
|
|
20
|
+
* Flight payload scripts) AND sets this token. A direct `ctx.set(nonce, value)`
|
|
21
|
+
* in middleware runs AFTER the SSR nonce is resolved: the value is readable via
|
|
22
|
+
* `ctx.get(nonce)` and gates PPR shell capture (a per-request nonce must never
|
|
23
|
+
* bake into a shared shell), but the router will not apply it to its own
|
|
24
|
+
* scripts — useNonce() stays undefined for that request.
|
|
16
25
|
*/
|
|
17
26
|
export const nonce: ContextVar<string> = createVar<string>();
|
|
18
27
|
|
|
@@ -14,6 +14,8 @@ import { getSSRSetup } from "./ssr-setup.js";
|
|
|
14
14
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
15
15
|
import { executeMiddleware } from "../router/middleware.js";
|
|
16
16
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
17
|
+
import { gateTransitions } from "./transition-gate.js";
|
|
18
|
+
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
17
19
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
18
20
|
import {
|
|
19
21
|
createResponseWithMergedHeaders,
|
|
@@ -152,6 +154,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
152
154
|
handleStore,
|
|
153
155
|
nonce,
|
|
154
156
|
useActionStateId,
|
|
157
|
+
true, // an action ran and threw
|
|
155
158
|
);
|
|
156
159
|
if (errorHtml) return errorHtml;
|
|
157
160
|
|
|
@@ -205,6 +208,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
205
208
|
handleStore,
|
|
206
209
|
nonce,
|
|
207
210
|
directActionId,
|
|
211
|
+
true, // an action ran and threw
|
|
208
212
|
);
|
|
209
213
|
if (errorHtml) return errorHtml;
|
|
210
214
|
|
|
@@ -269,6 +273,14 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
269
273
|
headers,
|
|
270
274
|
});
|
|
271
275
|
|
|
276
|
+
// JS/PE parity: this is an action's revalidation render, so mark it BEFORE
|
|
277
|
+
// matching — a stale `foregroundOnAction` cache entry must re-execute in the
|
|
278
|
+
// foreground during the re-render, exactly as the JS path's
|
|
279
|
+
// revalidateAfterAction does. The transition({ when }) gate fields below are
|
|
280
|
+
// set post-match (the gate reads them after rendering); foregroundOnAction
|
|
281
|
+
// reads _inActionRevalidation during the match, so it must be set here.
|
|
282
|
+
getRequestContext()._inActionRevalidation = true;
|
|
283
|
+
|
|
272
284
|
const match = await ctx.router.match(renderRequest, { env });
|
|
273
285
|
|
|
274
286
|
if (match.redirect) {
|
|
@@ -278,19 +290,34 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
278
290
|
});
|
|
279
291
|
}
|
|
280
292
|
|
|
293
|
+
// Expose the no-JS action to the transition({ when }) gate. currentUrl/Params
|
|
294
|
+
// are absent on this full-render path (no navigation snapshot); useActionState
|
|
295
|
+
// ids are block-scoped, so only a direct action id is available here.
|
|
296
|
+
// actionUrl is the page the action was submitted from (this request's url).
|
|
297
|
+
const peReqCtx = getRequestContext();
|
|
298
|
+
peReqCtx._gateActionId = directActionId ?? undefined;
|
|
299
|
+
peReqCtx._gateActionUrl = new URL(url);
|
|
300
|
+
peReqCtx._gateActionResult = actionResult;
|
|
301
|
+
peReqCtx._gateFormData = formData;
|
|
302
|
+
|
|
281
303
|
const payload: RscPayload = {
|
|
282
304
|
metadata: {
|
|
283
305
|
pathname: url.pathname,
|
|
284
306
|
routerId: ctx.router.id,
|
|
285
307
|
basename: ctx.router.basename,
|
|
286
|
-
segments:
|
|
308
|
+
segments: gateTransitions(
|
|
309
|
+
match.segments,
|
|
310
|
+
getRequestContext(),
|
|
311
|
+
ctx.router.onError,
|
|
312
|
+
),
|
|
287
313
|
matched: match.matched,
|
|
288
314
|
diff: match.diff,
|
|
289
315
|
resolvedIds: match.resolvedIds,
|
|
290
316
|
params: match.params,
|
|
291
317
|
isPartial: false,
|
|
292
318
|
rootLayout: ctx.router.rootLayout,
|
|
293
|
-
|
|
319
|
+
// PE full render: resolve deferred handle values server-side.
|
|
320
|
+
handles: resolvedHandleStream(handleStore),
|
|
294
321
|
version: ctx.version,
|
|
295
322
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
296
323
|
themeConfig: ctx.router.themeConfig,
|
|
@@ -368,7 +395,22 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
368
395
|
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
369
396
|
nonce: string | undefined,
|
|
370
397
|
actionId?: string | null,
|
|
398
|
+
// True when an action actually ran and threw (vs a malformed form body, where
|
|
399
|
+
// no action executed). Drives _inActionRevalidation for JS/PE parity — it must
|
|
400
|
+
// NOT be inferred from actionId, since a useActionState bound action can run
|
|
401
|
+
// and throw with no $$id (actionId === undefined) yet still be an action error.
|
|
402
|
+
actionRan = false,
|
|
371
403
|
): Promise<Response | null> {
|
|
404
|
+
// JS/PE parity for an action-triggered error re-render: a stale
|
|
405
|
+
// `foregroundOnAction` cache entry inside the error boundary must foreground
|
|
406
|
+
// too, exactly as the JS path (revalidateAfterAction sets this unconditionally
|
|
407
|
+
// before rendering the error boundary). Set BEFORE matchError (the cached fn
|
|
408
|
+
// runs during it). Gated on actionRan, NOT actionId — a malformed form body
|
|
409
|
+
// (actionRan=false) ran no action and must keep SWR.
|
|
410
|
+
if (actionRan) {
|
|
411
|
+
getRequestContext()._inActionRevalidation = true;
|
|
412
|
+
}
|
|
413
|
+
|
|
372
414
|
let errorResult;
|
|
373
415
|
try {
|
|
374
416
|
errorResult = await ctx.router.matchError(request, { env }, error, "route");
|
|
@@ -395,12 +437,26 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
395
437
|
|
|
396
438
|
setRequestContextParams(errorResult.params, errorResult.routeName);
|
|
397
439
|
|
|
440
|
+
// Only the failing action id + URL are in scope here (no formData/actionResult
|
|
441
|
+
// thread into this helper). Expose the URL only when the action id is known:
|
|
442
|
+
// this helper also handles malformed form bodies before action detection, and
|
|
443
|
+
// those should not look like action-triggered renders to transition({ when }).
|
|
444
|
+
if (actionId != null) {
|
|
445
|
+
const peErrCtx = getRequestContext();
|
|
446
|
+
peErrCtx._gateActionId = actionId;
|
|
447
|
+
peErrCtx._gateActionUrl = new URL(url);
|
|
448
|
+
}
|
|
449
|
+
|
|
398
450
|
const payload: RscPayload = {
|
|
399
451
|
metadata: {
|
|
400
452
|
pathname: url.pathname,
|
|
401
453
|
routerId: ctx.router.id,
|
|
402
454
|
basename: ctx.router.basename,
|
|
403
|
-
segments:
|
|
455
|
+
segments: gateTransitions(
|
|
456
|
+
errorResult.segments,
|
|
457
|
+
getRequestContext(),
|
|
458
|
+
ctx.router.onError,
|
|
459
|
+
),
|
|
404
460
|
matched: errorResult.matched,
|
|
405
461
|
diff: errorResult.diff,
|
|
406
462
|
resolvedIds: errorResult.resolvedIds,
|
|
@@ -408,7 +464,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
408
464
|
isPartial: false,
|
|
409
465
|
isError: true,
|
|
410
466
|
rootLayout: ctx.router.rootLayout,
|
|
411
|
-
|
|
467
|
+
// PE error-boundary full render: resolve deferred handle values server-side.
|
|
468
|
+
handles: resolvedHandleStream(handleStore),
|
|
412
469
|
version: ctx.version,
|
|
413
470
|
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
414
471
|
themeConfig: ctx.router.themeConfig,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
resolveSameOriginRedirect,
|
|
39
39
|
resolveExternalRedirect,
|
|
40
40
|
isExternalRedirect,
|
|
41
|
+
safeSameOriginLanding,
|
|
41
42
|
EXTERNAL_REDIRECT_MARKER,
|
|
42
43
|
} from "../redirect-origin.js";
|
|
43
44
|
import { carryOverRedirectHeaders } from "./helpers.js";
|
|
@@ -79,7 +80,7 @@ export function guardOutgoingRedirect(
|
|
|
79
80
|
|
|
80
81
|
// Cross-origin (or unsafe-scheme external): neutralize to a safe same-origin
|
|
81
82
|
// landing.
|
|
82
|
-
const safeTarget = basename
|
|
83
|
+
const safeTarget = safeSameOriginLanding(basename);
|
|
83
84
|
if (process.env.NODE_ENV !== "production") {
|
|
84
85
|
console.error(
|
|
85
86
|
`[rango] Blocked cross-origin redirect to "${location}"; sent to ` +
|