@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c
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 +4 -0
- package/README.md +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +455 -15
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +85 -276
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- 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 +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
|
@@ -2,7 +2,12 @@ import type { Plugin, ResolvedConfig } from "vite";
|
|
|
2
2
|
import { parseAst } from "vite";
|
|
3
3
|
import MagicString from "magic-string";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
normalizePath,
|
|
7
|
+
hashId,
|
|
8
|
+
makeStubId,
|
|
9
|
+
detectImports,
|
|
10
|
+
} from "./expose-id-utils.js";
|
|
6
11
|
import {
|
|
7
12
|
transformInlineHandlers,
|
|
8
13
|
type VirtualHandlerEntry,
|
|
@@ -23,6 +28,7 @@ import {
|
|
|
23
28
|
getImportedFnNames,
|
|
24
29
|
collectCreateExportBindings,
|
|
25
30
|
buildUnsupportedShapeWarning,
|
|
31
|
+
isExportOnlyFile,
|
|
26
32
|
} from "./expose-ids/export-analysis.js";
|
|
27
33
|
import {
|
|
28
34
|
hasCreateLoaderImport,
|
|
@@ -34,6 +40,7 @@ import {
|
|
|
34
40
|
transformLocationState,
|
|
35
41
|
generateWholeFileStubs,
|
|
36
42
|
generateExprStubs,
|
|
43
|
+
stubHandlerExprs,
|
|
37
44
|
transformHandlerIds,
|
|
38
45
|
} from "./expose-ids/handler-transform.js";
|
|
39
46
|
|
|
@@ -385,7 +392,9 @@ ${lazyImports.join(",\n")}
|
|
|
385
392
|
if (stubResult) return stubResult;
|
|
386
393
|
}
|
|
387
394
|
|
|
388
|
-
// --- PrerenderHandler: non-RSC stub replacement ---
|
|
395
|
+
// --- PrerenderHandler: non-RSC whole-file stub replacement ---
|
|
396
|
+
// When ALL exports are Prerender() calls, replace the entire file.
|
|
397
|
+
// Mixed-export files are handled in the unified pipeline below.
|
|
389
398
|
if (hasPrerenderHandlerCode && !isRscEnv) {
|
|
390
399
|
const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
|
|
391
400
|
const bindings = getBindings(code, fnNames);
|
|
@@ -397,16 +406,6 @@ ${lazyImports.join(",\n")}
|
|
|
397
406
|
isBuild,
|
|
398
407
|
);
|
|
399
408
|
if (wholeFile) return wholeFile;
|
|
400
|
-
|
|
401
|
-
const exprStubs = generateExprStubs(
|
|
402
|
-
PRERENDER_CONFIG,
|
|
403
|
-
bindings,
|
|
404
|
-
code,
|
|
405
|
-
filePath,
|
|
406
|
-
id,
|
|
407
|
-
isBuild,
|
|
408
|
-
);
|
|
409
|
-
if (exprStubs) return exprStubs;
|
|
410
409
|
}
|
|
411
410
|
|
|
412
411
|
// --- PrerenderHandler: RSC build module tracking ---
|
|
@@ -467,7 +466,8 @@ ${lazyImports.join(",\n")}
|
|
|
467
466
|
}
|
|
468
467
|
}
|
|
469
468
|
|
|
470
|
-
// --- StaticHandler: non-RSC stub replacement ---
|
|
469
|
+
// --- StaticHandler: non-RSC whole-file stub replacement ---
|
|
470
|
+
// When ALL exports are Static() calls, replace the entire file.
|
|
471
471
|
if (hasStaticHandlerCode && !isRscEnv) {
|
|
472
472
|
const fnNames = getFnNames(STATIC_CONFIG.fnName);
|
|
473
473
|
const bindings = getBindings(code, fnNames);
|
|
@@ -479,16 +479,212 @@ ${lazyImports.join(",\n")}
|
|
|
479
479
|
isBuild,
|
|
480
480
|
);
|
|
481
481
|
if (wholeFile) return wholeFile;
|
|
482
|
+
}
|
|
482
483
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
484
|
+
// --- Mixed-type whole-file stub replacement (non-RSC) ---
|
|
485
|
+
// When the individual whole-file checks above fail (each only checks
|
|
486
|
+
// one type), the file has mixed exports (e.g. createLoader + Prerender).
|
|
487
|
+
// Gather ALL stub-safe bindings and check if they cover every export.
|
|
488
|
+
// If yes, replace the entire file with stubs — this strips server-only
|
|
489
|
+
// imports (node:fs, DB clients, etc.) that would crash in the browser.
|
|
490
|
+
//
|
|
491
|
+
// Only applies when the file contains Prerender/Static (the handler
|
|
492
|
+
// types that bring server-only code). Files with only loaders, handles,
|
|
493
|
+
// or locationState are handled correctly by the unified pipeline below.
|
|
494
|
+
//
|
|
495
|
+
// Loader, Prerender, and Static exports become plain { __brand, $$id }
|
|
496
|
+
// stubs. createHandle and createLocationState need their create*()
|
|
497
|
+
// functions to execute (collect registration / __rsc_ls_key), so their
|
|
498
|
+
// call expressions are preserved with only a @rangojs/router import.
|
|
499
|
+
// This strips all server-only imports while keeping the correct
|
|
500
|
+
// client contract for every export type.
|
|
501
|
+
if (!isRscEnv && (hasPrerenderHandlerCode || hasStaticHandlerCode)) {
|
|
502
|
+
const prerenderFnNames = hasPrerenderHandlerCode
|
|
503
|
+
? getFnNames(PRERENDER_CONFIG.fnName)
|
|
504
|
+
: [];
|
|
505
|
+
const staticFnNames = hasStaticHandlerCode
|
|
506
|
+
? getFnNames(STATIC_CONFIG.fnName)
|
|
507
|
+
: [];
|
|
508
|
+
const loaderFnNames = hasLoaderCode ? getFnNames("createLoader") : [];
|
|
509
|
+
const handleFnNames = hasHandleCode ? getFnNames("createHandle") : [];
|
|
510
|
+
const lsFnNames = hasLocationStateCode
|
|
511
|
+
? getFnNames("createLocationState")
|
|
512
|
+
: [];
|
|
513
|
+
|
|
514
|
+
// Collect ALL recognized bindings to check export coverage
|
|
515
|
+
const allBindings: CreateExportBinding[] = [];
|
|
516
|
+
for (const fnNames of [
|
|
517
|
+
prerenderFnNames,
|
|
518
|
+
staticFnNames,
|
|
519
|
+
loaderFnNames,
|
|
520
|
+
handleFnNames,
|
|
521
|
+
lsFnNames,
|
|
522
|
+
]) {
|
|
523
|
+
if (fnNames.length > 0) {
|
|
524
|
+
allBindings.push(...getBindings(code, fnNames));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Check if preserved createHandle/createLocationState calls
|
|
529
|
+
// reference non-exported locals (e.g. helper functions, constants).
|
|
530
|
+
// If so, the whole-file stub would strip those locals, breaking
|
|
531
|
+
// the call. Fall through to the unified pipeline instead.
|
|
532
|
+
let canStubWholeFile =
|
|
533
|
+
allBindings.length > 0 && isExportOnlyFile(code, allBindings);
|
|
534
|
+
|
|
535
|
+
if (
|
|
536
|
+
canStubWholeFile &&
|
|
537
|
+
(handleFnNames.length > 0 || lsFnNames.length > 0)
|
|
538
|
+
) {
|
|
539
|
+
const exportedLocals = new Set(allBindings.map((b) => b.localName));
|
|
540
|
+
// Collect bindings that would be stripped by whole-file replacement:
|
|
541
|
+
// local declarations and imported bindings from non-@rangojs/router
|
|
542
|
+
// modules. This is a regex-based heuristic — it intentionally skips
|
|
543
|
+
// edge cases (class decls, destructured bindings, combined
|
|
544
|
+
// default+named imports) since those rarely appear in route files.
|
|
545
|
+
const strippedBindings: string[] = [];
|
|
546
|
+
|
|
547
|
+
// Skip React Fast Refresh temporaries (_c, _c2, ...) which are
|
|
548
|
+
// injected by @vitejs/plugin-react in the client environment and
|
|
549
|
+
// would falsely trigger the bailout.
|
|
550
|
+
const localDeclPattern =
|
|
551
|
+
/(?:^|;|\n)\s*(?:const|let|var|function)\s+(\w+)/g;
|
|
552
|
+
let declMatch: RegExpExecArray | null;
|
|
553
|
+
while ((declMatch = localDeclPattern.exec(code)) !== null) {
|
|
554
|
+
const name = declMatch[1];
|
|
555
|
+
if (!exportedLocals.has(name) && !/^_c\d*$/.test(name)) {
|
|
556
|
+
strippedBindings.push(name);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const importPattern =
|
|
561
|
+
/import\s*\{([^}]*)\}\s*from\s*["'](?!@rangojs\/router)[^"']*["']/g;
|
|
562
|
+
let importMatch: RegExpExecArray | null;
|
|
563
|
+
while ((importMatch = importPattern.exec(code)) !== null) {
|
|
564
|
+
for (const spec of importMatch[1].split(",")) {
|
|
565
|
+
const m = spec
|
|
566
|
+
.trim()
|
|
567
|
+
.match(/^[A-Za-z_$][\w$]*(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
|
|
568
|
+
if (m) strippedBindings.push(m[1] || m[0].trim().split(/\s/)[0]);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const defaultImportPattern =
|
|
572
|
+
/import\s+([A-Za-z_$][\w$]*)\s+from\s*["'](?!@rangojs\/router)[^"']*["']/g;
|
|
573
|
+
while ((importMatch = defaultImportPattern.exec(code)) !== null) {
|
|
574
|
+
strippedBindings.push(importMatch[1]);
|
|
575
|
+
}
|
|
576
|
+
const nsImportPattern =
|
|
577
|
+
/import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s*["'](?!@rangojs\/router)[^"']*["']/g;
|
|
578
|
+
while ((importMatch = nsImportPattern.exec(code)) !== null) {
|
|
579
|
+
strippedBindings.push(importMatch[1]);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (strippedBindings.length > 0) {
|
|
583
|
+
const preservedBindings = allBindings.filter((b) => {
|
|
584
|
+
const fc = code.slice(b.callExprStart, b.callOpenParenPos + 1);
|
|
585
|
+
return (
|
|
586
|
+
handleFnNames.some((n) => fc.includes(n)) ||
|
|
587
|
+
lsFnNames.some((n) => fc.includes(n))
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
const strippedRe = new RegExp(
|
|
591
|
+
`\\b(?:${strippedBindings.join("|")})\\b`,
|
|
592
|
+
);
|
|
593
|
+
canStubWholeFile = !preservedBindings.some((b) => {
|
|
594
|
+
const expr = code.slice(b.callExprStart, b.callCloseParenPos + 1);
|
|
595
|
+
return strippedRe.test(expr);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (canStubWholeFile) {
|
|
601
|
+
const lines: string[] = [];
|
|
602
|
+
const neededImports: string[] = [];
|
|
603
|
+
if (handleFnNames.length > 0) neededImports.push("createHandle");
|
|
604
|
+
if (lsFnNames.length > 0) neededImports.push("createLocationState");
|
|
605
|
+
if (neededImports.length > 0) {
|
|
606
|
+
lines.push(
|
|
607
|
+
`import { ${neededImports.join(", ")} } from "@rangojs/router";`,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
for (const binding of allBindings) {
|
|
612
|
+
const fnCall = code.slice(
|
|
613
|
+
binding.callExprStart,
|
|
614
|
+
binding.callOpenParenPos + 1,
|
|
615
|
+
);
|
|
616
|
+
const isHandle = handleFnNames.some((n) => fnCall.includes(n));
|
|
617
|
+
const isLocationState = lsFnNames.some((n) => fnCall.includes(n));
|
|
618
|
+
|
|
619
|
+
// Aliases share the primary name's ID (matches server transforms).
|
|
620
|
+
const primaryName = binding.exportNames[0];
|
|
621
|
+
const stubId = makeStubId(filePath, primaryName, isBuild);
|
|
622
|
+
|
|
623
|
+
if (isHandle || isLocationState) {
|
|
624
|
+
// Rewrite alias to canonical name since the stub file only
|
|
625
|
+
// imports canonical names from @rangojs/router.
|
|
626
|
+
// Strip React Fast Refresh `_c = ` wrappers from args
|
|
627
|
+
// (e.g. `_c = (segments) => ...` → `(segments) => ...`)
|
|
628
|
+
const rawArgs = code
|
|
629
|
+
.slice(binding.callOpenParenPos + 1, binding.callCloseParenPos)
|
|
630
|
+
.replace(/\b_c\d*\s*=\s*/g, "");
|
|
631
|
+
const canonicalName = isHandle
|
|
632
|
+
? "createHandle"
|
|
633
|
+
: "createLocationState";
|
|
634
|
+
const activeFnNames = isHandle ? handleFnNames : lsFnNames;
|
|
635
|
+
|
|
636
|
+
// Reconstruct the function name (handling aliases + generics)
|
|
637
|
+
let rawCallee = code.slice(
|
|
638
|
+
binding.callExprStart,
|
|
639
|
+
binding.callOpenParenPos,
|
|
640
|
+
);
|
|
641
|
+
for (const alias of activeFnNames) {
|
|
642
|
+
if (alias !== canonicalName && rawCallee.startsWith(alias)) {
|
|
643
|
+
rawCallee = canonicalName + rawCallee.slice(alias.length);
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (isHandle) {
|
|
649
|
+
// createHandle checks __injectedId DURING the call, so $$id
|
|
650
|
+
// must be a parameter, not a post-call property assignment.
|
|
651
|
+
const idParam =
|
|
652
|
+
binding.argCount === 0
|
|
653
|
+
? `undefined, "${stubId}"`
|
|
654
|
+
: `, "${stubId}"`;
|
|
655
|
+
lines.push(
|
|
656
|
+
`export const ${primaryName} = ${rawCallee}(${rawArgs}${idParam});`,
|
|
657
|
+
);
|
|
658
|
+
lines.push(`${primaryName}.$$id = "${stubId}";`);
|
|
659
|
+
} else {
|
|
660
|
+
lines.push(
|
|
661
|
+
`export const ${primaryName} = ${rawCallee}(${rawArgs});`,
|
|
662
|
+
);
|
|
663
|
+
lines.push(
|
|
664
|
+
`${primaryName}.__rsc_ls_key = "__rsc_ls_${stubId}";`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
for (const name of binding.exportNames.slice(1)) {
|
|
668
|
+
lines.push(`export const ${name} = ${primaryName};`);
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
let brand = "loader";
|
|
672
|
+
if (prerenderFnNames.some((n) => fnCall.includes(n))) {
|
|
673
|
+
brand = PRERENDER_CONFIG.brand;
|
|
674
|
+
} else if (staticFnNames.some((n) => fnCall.includes(n))) {
|
|
675
|
+
brand = STATIC_CONFIG.brand;
|
|
676
|
+
}
|
|
677
|
+
lines.push(
|
|
678
|
+
`export const ${primaryName} = { __brand: "${brand}", $$id: "${stubId}" };`,
|
|
679
|
+
);
|
|
680
|
+
for (const name of binding.exportNames.slice(1)) {
|
|
681
|
+
lines.push(`export const ${name} = ${primaryName};`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return { code: lines.join("\n") + "\n", map: null };
|
|
687
|
+
}
|
|
492
688
|
}
|
|
493
689
|
|
|
494
690
|
// --- StaticHandler: RSC build module tracking ---
|
|
@@ -535,27 +731,48 @@ ${lazyImports.join(",\n")}
|
|
|
535
731
|
isBuild,
|
|
536
732
|
) || changed;
|
|
537
733
|
}
|
|
538
|
-
if (hasPrerenderHandlerCode
|
|
734
|
+
if (hasPrerenderHandlerCode) {
|
|
539
735
|
const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
736
|
+
const bindings = getBindings(code, fnNames);
|
|
737
|
+
if (isRscEnv) {
|
|
738
|
+
changed =
|
|
739
|
+
transformHandlerIds(
|
|
740
|
+
PRERENDER_CONFIG,
|
|
741
|
+
bindings,
|
|
742
|
+
s,
|
|
743
|
+
filePath,
|
|
744
|
+
isBuild,
|
|
745
|
+
) || changed;
|
|
746
|
+
} else {
|
|
747
|
+
// Non-RSC mixed-export file: replace Prerender() calls with stubs
|
|
748
|
+
// on the shared MagicString so sourcemaps stay accurate.
|
|
749
|
+
changed =
|
|
750
|
+
stubHandlerExprs(
|
|
751
|
+
PRERENDER_CONFIG,
|
|
752
|
+
bindings,
|
|
753
|
+
s,
|
|
754
|
+
filePath,
|
|
755
|
+
isBuild,
|
|
756
|
+
) || changed;
|
|
757
|
+
}
|
|
548
758
|
}
|
|
549
|
-
if (hasStaticHandlerCode
|
|
759
|
+
if (hasStaticHandlerCode) {
|
|
550
760
|
const fnNames = getFnNames(STATIC_CONFIG.fnName);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
761
|
+
const bindings = getBindings(code, fnNames);
|
|
762
|
+
if (isRscEnv) {
|
|
763
|
+
changed =
|
|
764
|
+
transformHandlerIds(
|
|
765
|
+
STATIC_CONFIG,
|
|
766
|
+
bindings,
|
|
767
|
+
s,
|
|
768
|
+
filePath,
|
|
769
|
+
isBuild,
|
|
770
|
+
) || changed;
|
|
771
|
+
} else {
|
|
772
|
+
changed =
|
|
773
|
+
stubHandlerExprs(STATIC_CONFIG, bindings, s, filePath, isBuild) ||
|
|
774
|
+
changed;
|
|
775
|
+
}
|
|
559
776
|
}
|
|
560
777
|
|
|
561
778
|
if (!changed) return;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Performance Tracks — RSDW client patch
|
|
3
|
+
*
|
|
4
|
+
* Patches the RSDW client so _debugInfo recovery works for plain-object
|
|
5
|
+
* payloads (our RscPayload shape). Without this, the Server Components
|
|
6
|
+
* track in Chrome DevTools stays empty.
|
|
7
|
+
*
|
|
8
|
+
* React's flushComponentPerformance uses splice(0) to empty _debugInfo
|
|
9
|
+
* after resolution, then recovers it from the resolved value — but only
|
|
10
|
+
* for arrays, async iterables, React elements, and lazy types. Since our
|
|
11
|
+
* RscPayload is a plain object, _debugInfo is lost. This patch relaxes
|
|
12
|
+
* the check so _debugInfo is recovered from any object.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Plugin } from "vite";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
|
|
18
|
+
const RSDW_PATCH_RE =
|
|
19
|
+
/((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
|
|
20
|
+
|
|
21
|
+
function buildPatchReplacement(match: string, debugInfoVar: string): string {
|
|
22
|
+
return `${match}
|
|
23
|
+
if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.status) {
|
|
24
|
+
var _resolved = "function" === typeof resolveLazy ? resolveLazy(root.value) : root.value;
|
|
25
|
+
if ("object" === typeof _resolved && null !== _resolved && isArrayImpl(_resolved._debugInfo)) {
|
|
26
|
+
${debugInfoVar} = _resolved._debugInfo;
|
|
27
|
+
}
|
|
28
|
+
}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function patchRsdwClientDebugInfoRecovery(code: string): {
|
|
32
|
+
code: string;
|
|
33
|
+
debugInfoVar: string | null;
|
|
34
|
+
} {
|
|
35
|
+
const match = code.match(RSDW_PATCH_RE);
|
|
36
|
+
if (!match) {
|
|
37
|
+
return { code, debugInfoVar: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
code: code.replace(match[1]!, buildPatchReplacement(match[1]!, match[2]!)),
|
|
42
|
+
debugInfoVar: match[2]!,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function performanceTracksOptimizeDepsPlugin(): {
|
|
47
|
+
name: string;
|
|
48
|
+
setup(build: any): void;
|
|
49
|
+
} {
|
|
50
|
+
return {
|
|
51
|
+
name: "@rangojs/router:performance-tracks-optimize-deps",
|
|
52
|
+
setup(build: any): void {
|
|
53
|
+
build.onLoad(
|
|
54
|
+
{
|
|
55
|
+
filter:
|
|
56
|
+
/react-server-dom-webpack-client\.browser\.(development|production)\.js$/,
|
|
57
|
+
},
|
|
58
|
+
async (args: { path: string }) => {
|
|
59
|
+
const code = await readFile(args.path, "utf8");
|
|
60
|
+
const patched = patchRsdwClientDebugInfoRecovery(code);
|
|
61
|
+
return {
|
|
62
|
+
contents: patched.code,
|
|
63
|
+
loader: "js",
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function performanceTracksPlugin(): Plugin {
|
|
72
|
+
return {
|
|
73
|
+
name: "@rangojs/router:performance-tracks",
|
|
74
|
+
|
|
75
|
+
transform(code, id) {
|
|
76
|
+
if (!id.includes("react-server-dom") || !id.includes("client")) return;
|
|
77
|
+
const patched = patchRsdwClientDebugInfoRecovery(code);
|
|
78
|
+
if (!patched.debugInfoVar) return;
|
|
79
|
+
if (process.env.INTERNAL_RANGO_DEBUG)
|
|
80
|
+
console.log(
|
|
81
|
+
"[perf-tracks] patched RSDW client (var:",
|
|
82
|
+
patched.debugInfoVar,
|
|
83
|
+
")",
|
|
84
|
+
);
|
|
85
|
+
return patched.code;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin that triggers a full browser reload from terminal input.
|
|
5
|
+
*
|
|
6
|
+
* This plugin is intentionally passive:
|
|
7
|
+
* - it never enables raw mode on stdin
|
|
8
|
+
* - it never restores terminal state
|
|
9
|
+
* - it reacts to Ctrl+R when that raw byte reaches the process
|
|
10
|
+
* - it also supports safe line-based fallbacks like "e" + Enter
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { poke } from "@rangojs/router/vite";
|
|
15
|
+
*
|
|
16
|
+
* export default defineConfig({
|
|
17
|
+
* plugins: [rango(), poke()],
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function poke(): Plugin {
|
|
22
|
+
return {
|
|
23
|
+
name: "vite-plugin-poke",
|
|
24
|
+
apply: "serve",
|
|
25
|
+
|
|
26
|
+
configureServer(server) {
|
|
27
|
+
const stdin = process.stdin;
|
|
28
|
+
const debug = process.env.RANGO_POKE_DEBUG === "1";
|
|
29
|
+
|
|
30
|
+
const triggerReload = (source: string) => {
|
|
31
|
+
server.hot.send({ type: "full-reload", path: "*" });
|
|
32
|
+
server.config.logger.info(` browser reload (${source})`, {
|
|
33
|
+
timestamp: true,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const toBuffer = (chunk: string | Buffer): Buffer => {
|
|
38
|
+
return typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const formatChunk = (chunk: string | Buffer): string => {
|
|
42
|
+
const data = toBuffer(chunk);
|
|
43
|
+
const hex = Array.from(data)
|
|
44
|
+
.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`)
|
|
45
|
+
.join(" ");
|
|
46
|
+
const ascii = Array.from(data)
|
|
47
|
+
.map((byte) => {
|
|
48
|
+
if (byte >= 0x20 && byte <= 0x7e) return String.fromCharCode(byte);
|
|
49
|
+
if (byte === 0x0a) return "\\n";
|
|
50
|
+
if (byte === 0x0d) return "\\r";
|
|
51
|
+
if (byte === 0x09) return "\\t";
|
|
52
|
+
return ".";
|
|
53
|
+
})
|
|
54
|
+
.join("");
|
|
55
|
+
return `len=${data.length} hex=[${hex}] ascii="${ascii}"`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const readCtrlR = (chunk: string | Buffer): boolean => {
|
|
59
|
+
const data =
|
|
60
|
+
typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
61
|
+
return data.length === 1 && data[0] === 0x12;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const readSubmittedCommands = (chunk: string | Buffer): string[] => {
|
|
65
|
+
const text = toBuffer(chunk)
|
|
66
|
+
.toString("utf8")
|
|
67
|
+
.replace(/\r\n/g, "\n")
|
|
68
|
+
.replace(/\r/g, "\n");
|
|
69
|
+
|
|
70
|
+
if (!text.includes("\n")) return [];
|
|
71
|
+
|
|
72
|
+
const lines = text.split("\n");
|
|
73
|
+
lines.pop();
|
|
74
|
+
return lines;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (debug) {
|
|
78
|
+
server.config.logger.info(
|
|
79
|
+
` poke debug enabled (isTTY=${stdin.isTTY ? "yes" : "no"}, isRaw=${stdin.isTTY ? (stdin.isRaw ? "yes" : "no") : "n/a"})`,
|
|
80
|
+
{ timestamp: true },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (stdin.isTTY) {
|
|
85
|
+
server.config.logger.info(
|
|
86
|
+
" poke ready: press e + enter to reload browser (ctrl+r also works when available)",
|
|
87
|
+
{ timestamp: true },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const onData = (data: string | Buffer) => {
|
|
92
|
+
if (debug) {
|
|
93
|
+
server.config.logger.info(` poke stdin ${formatChunk(data)}`, {
|
|
94
|
+
timestamp: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Only react to the exact Ctrl+R byte when some host terminal or
|
|
99
|
+
// wrapper already delivers it to this process. We intentionally do
|
|
100
|
+
// not enable raw mode here because that can steal Vite shortcuts
|
|
101
|
+
// like "r" / "q" and interfere with terminal-level controls.
|
|
102
|
+
if (readCtrlR(data)) {
|
|
103
|
+
triggerReload("ctrl+r");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const command of readSubmittedCommands(data)) {
|
|
108
|
+
if (command === "e") {
|
|
109
|
+
triggerReload("e+enter");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (command === "\u001br") {
|
|
114
|
+
triggerReload("option+r+enter");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
stdin.on("data", onData);
|
|
121
|
+
|
|
122
|
+
server.httpServer?.on("close", () => {
|
|
123
|
+
stdin.off("data", onData);
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -135,8 +135,11 @@ export function createVersionPlugin(): Plugin {
|
|
|
135
135
|
let server: any = null;
|
|
136
136
|
const clientModuleSignatures = new Map<string, ClientModuleSignature>();
|
|
137
137
|
|
|
138
|
+
let versionCounter = 0;
|
|
138
139
|
const bumpVersion = (reason: string) => {
|
|
139
|
-
|
|
140
|
+
// Use timestamp + counter to guarantee uniqueness even when multiple
|
|
141
|
+
// bumps happen within the same millisecond (e.g. cascading HMR events).
|
|
142
|
+
currentVersion = Date.now().toString(16) + String(++versionCounter);
|
|
140
143
|
console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
|
|
141
144
|
|
|
142
145
|
const rscEnv = server?.environments?.rsc;
|
|
@@ -211,6 +214,15 @@ export function createVersionPlugin(): Plugin {
|
|
|
211
214
|
|
|
212
215
|
if (!isRscModule) return;
|
|
213
216
|
|
|
217
|
+
// Skip re-bumping when the version virtual module itself is invalidated
|
|
218
|
+
// (our own bumpVersion() invalidates it, which re-triggers hotUpdate).
|
|
219
|
+
if (
|
|
220
|
+
ctx.modules.length === 1 &&
|
|
221
|
+
ctx.modules[0].id === "\0" + VIRTUAL_IDS.version
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
214
226
|
if (isCodeModule(ctx.file)) {
|
|
215
227
|
const filePath = normalizeModuleId(ctx.file);
|
|
216
228
|
const previousSignature = clientModuleSignatures.get(filePath);
|