@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/vite/index.ts
CHANGED
|
@@ -1,57 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
computeProductionHash,
|
|
12
|
-
transformClientRefs,
|
|
13
|
-
hashClientRefs,
|
|
14
|
-
} from "./plugins/client-ref-hashing.js";
|
|
15
|
-
export { createVersionInjectorPlugin } from "./plugins/version-injector.js";
|
|
16
|
-
export { createCjsToEsmPlugin } from "./plugins/cjs-to-esm.js";
|
|
1
|
+
/**
|
|
2
|
+
* Public API for @rangojs/router/vite
|
|
3
|
+
*
|
|
4
|
+
* Exports: rango() plugin factory, poke() dev utility plugin,
|
|
5
|
+
* and related option types. All other utilities are internal implementation
|
|
6
|
+
* details consumed via direct imports within the package.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { rango } from "./rango.js";
|
|
10
|
+
export { poke } from "./plugins/refresh-cmd.js";
|
|
17
11
|
|
|
18
|
-
// Types
|
|
19
12
|
export type {
|
|
20
|
-
RscEntries,
|
|
21
|
-
RscPluginOptions,
|
|
22
13
|
RangoNodeOptions,
|
|
23
14
|
RangoCloudflareOptions,
|
|
24
15
|
RangoOptions,
|
|
25
16
|
} from "./plugin-types.js";
|
|
26
|
-
|
|
27
|
-
// Utils
|
|
28
|
-
export {
|
|
29
|
-
sharedEsbuildOptions,
|
|
30
|
-
createVirtualEntriesPlugin,
|
|
31
|
-
onwarn,
|
|
32
|
-
getManualChunks,
|
|
33
|
-
} from "./utils/shared-utils.js";
|
|
34
|
-
export {
|
|
35
|
-
flattenLeafEntries,
|
|
36
|
-
buildRouteToStaticPrefix,
|
|
37
|
-
jsonParseExpression,
|
|
38
|
-
} from "./utils/manifest-utils.js";
|
|
39
|
-
export {
|
|
40
|
-
findMatchingParenInBundle,
|
|
41
|
-
extractHandlerExportsFromChunk,
|
|
42
|
-
evictHandlerCode,
|
|
43
|
-
} from "./utils/bundle-analysis.js";
|
|
44
|
-
export {
|
|
45
|
-
encodePathParam,
|
|
46
|
-
runWithConcurrency,
|
|
47
|
-
groupByConcurrency,
|
|
48
|
-
notifyOnError,
|
|
49
|
-
} from "./utils/prerender-utils.js";
|
|
50
|
-
export { printBanner, rangoVersion } from "./utils/banner.js";
|
|
51
|
-
|
|
52
|
-
// Core
|
|
53
|
-
export {
|
|
54
|
-
createRouterDiscoveryPlugin,
|
|
55
|
-
VIRTUAL_ROUTES_MANIFEST_ID,
|
|
56
|
-
} from "./router-discovery.js";
|
|
57
|
-
export { rango } from "./rango.js";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
const CLIENT_IN_SERVER_PROXY_PREFIX =
|
|
4
|
+
"virtual:vite-rsc/client-in-server-package-proxy/";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract the bare package name from an absolute node_modules path.
|
|
8
|
+
* Handles scoped packages (@org/name) and nested node_modules.
|
|
9
|
+
* Returns null if the path doesn't contain a valid package reference.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This is a lossy transformation. It maps a specific submodule path
|
|
12
|
+
* (e.g., pkg/internal/context.js) to the package root (pkg). The load()
|
|
13
|
+
* hook then re-exports via the bare specifier, which resolves to the
|
|
14
|
+
* package entry point. This works for packages that barrel-export their
|
|
15
|
+
* "use client" symbols from the root, which covers the common case
|
|
16
|
+
* (component libraries like @mantine/core, @chakra-ui/react, etc.).
|
|
17
|
+
* Packages whose client symbols are only available from deep subpaths
|
|
18
|
+
* (not re-exported from the root) would lose those symbols after the
|
|
19
|
+
* rewrite. A more precise approach would resolve through the package's
|
|
20
|
+
* exports map to find the correct entry point, but that adds significant
|
|
21
|
+
* complexity for a rare edge case.
|
|
22
|
+
* See: https://github.com/cloudflare/vinext/pull/413
|
|
23
|
+
*/
|
|
24
|
+
export function extractPackageName(absolutePath: string): string | null {
|
|
25
|
+
// Find the last /node_modules/ segment (handles nested node_modules)
|
|
26
|
+
const marker = "/node_modules/";
|
|
27
|
+
const idx = absolutePath.lastIndexOf(marker);
|
|
28
|
+
if (idx === -1) return null;
|
|
29
|
+
|
|
30
|
+
const afterModules = absolutePath.slice(idx + marker.length);
|
|
31
|
+
|
|
32
|
+
if (afterModules.startsWith("@")) {
|
|
33
|
+
// Scoped package: @org/name
|
|
34
|
+
const parts = afterModules.split("/");
|
|
35
|
+
if (parts.length < 2 || !parts[1]) return null;
|
|
36
|
+
return `${parts[0]}/${parts[1]}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Unscoped package: name
|
|
40
|
+
const name = afterModules.split("/")[0];
|
|
41
|
+
return name || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Vite plugin that deduplicates client references from third-party packages
|
|
46
|
+
* in dev mode.
|
|
47
|
+
*
|
|
48
|
+
* When @vitejs/plugin-rsc encounters a "use client" submodule inside a
|
|
49
|
+
* package imported from a server component, it creates a
|
|
50
|
+
* client-in-server-package-proxy virtual module that re-exports from the
|
|
51
|
+
* absolute file path. In the client environment, this absolute path bypasses
|
|
52
|
+
* Vite's pre-bundling, while direct client imports of the same package go
|
|
53
|
+
* through .vite/deps/. Two separate module instances are created, breaking
|
|
54
|
+
* React contexts (createContext runs twice, provider/consumer mismatch).
|
|
55
|
+
*
|
|
56
|
+
* This plugin intercepts absolute node_modules imports from proxy modules
|
|
57
|
+
* in the client environment and rewrites them to bare specifier imports
|
|
58
|
+
* that go through pre-bundling, ensuring a single module instance.
|
|
59
|
+
*
|
|
60
|
+
* Dev-only: production builds use the SSR manifest which handles module
|
|
61
|
+
* identity correctly.
|
|
62
|
+
*/
|
|
63
|
+
export function clientRefDedup(): Plugin {
|
|
64
|
+
let clientExclude: string[] = [];
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
name: "@rangojs/router:client-ref-dedup",
|
|
68
|
+
enforce: "pre",
|
|
69
|
+
apply: "serve",
|
|
70
|
+
|
|
71
|
+
configResolved(config: ResolvedConfig) {
|
|
72
|
+
// Respect user's optimizeDeps.exclude — if a package is explicitly
|
|
73
|
+
// excluded from pre-bundling, we shouldn't redirect it there.
|
|
74
|
+
const clientEnv = config.environments?.["client"];
|
|
75
|
+
clientExclude =
|
|
76
|
+
clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
resolveId(source, importer, options) {
|
|
80
|
+
// Only intercept in the client environment
|
|
81
|
+
if (this.environment?.name !== "client") return;
|
|
82
|
+
|
|
83
|
+
// Only handle imports from client-in-server-package-proxy virtual modules
|
|
84
|
+
if (!importer?.includes(CLIENT_IN_SERVER_PROXY_PREFIX)) return;
|
|
85
|
+
|
|
86
|
+
// Only handle absolute node_modules paths
|
|
87
|
+
if (!source.includes("/node_modules/")) return;
|
|
88
|
+
|
|
89
|
+
// Must have an importer
|
|
90
|
+
if (!importer) return;
|
|
91
|
+
|
|
92
|
+
const packageName = extractPackageName(source);
|
|
93
|
+
if (!packageName) return;
|
|
94
|
+
|
|
95
|
+
// Don't redirect packages that are excluded from optimization
|
|
96
|
+
if (clientExclude.includes(packageName)) return;
|
|
97
|
+
|
|
98
|
+
// Return a virtual module that re-exports via bare specifier
|
|
99
|
+
return `\0rango:dedup/${packageName}`;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
load(id) {
|
|
103
|
+
if (!id.startsWith("\0rango:dedup/")) return;
|
|
104
|
+
|
|
105
|
+
const packageName = id.slice("\0rango:dedup/".length);
|
|
106
|
+
|
|
107
|
+
// Re-export via bare specifier so Vite routes through pre-bundling
|
|
108
|
+
return [
|
|
109
|
+
`export * from ${JSON.stringify(packageName)};`,
|
|
110
|
+
`import * as __all__ from ${JSON.stringify(packageName)};`,
|
|
111
|
+
`export default __all__.default;`,
|
|
112
|
+
].join("\n");
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
-
import { relative
|
|
2
|
+
import { relative } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
|
|
5
5
|
// Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
|
|
@@ -33,11 +33,11 @@ export function computeProductionHash(
|
|
|
33
33
|
const absPath = decodeURIComponent(
|
|
34
34
|
refKey.slice(CLIENT_IN_SERVER_PKG_PROXY_PREFIX.length),
|
|
35
35
|
);
|
|
36
|
-
toHash =
|
|
36
|
+
toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
|
|
37
37
|
} else if (refKey.startsWith(FS_PREFIX)) {
|
|
38
38
|
// /@fs/abs/path.tsx -> hash(relative(root, "/abs/path.tsx"))
|
|
39
39
|
const absPath = refKey.slice(FS_PREFIX.length - 1); // keep leading /
|
|
40
|
-
toHash =
|
|
40
|
+
toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
|
|
41
41
|
} else if (refKey.startsWith("/")) {
|
|
42
42
|
// /src/Button.tsx -> hash("src/Button.tsx")
|
|
43
43
|
toHash = refKey.slice(1);
|
|
@@ -170,6 +170,8 @@ ${lazyImports.join(",\n")}
|
|
|
170
170
|
|
|
171
171
|
const fs = await import("node:fs/promises");
|
|
172
172
|
|
|
173
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage"]);
|
|
174
|
+
|
|
173
175
|
async function scanDir(dir: string): Promise<string[]> {
|
|
174
176
|
const results: string[] = [];
|
|
175
177
|
try {
|
|
@@ -177,7 +179,7 @@ ${lazyImports.join(",\n")}
|
|
|
177
179
|
for (const entry of entries) {
|
|
178
180
|
const fullPath = path.join(dir, entry.name);
|
|
179
181
|
if (entry.isDirectory()) {
|
|
180
|
-
if (entry.name
|
|
182
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
181
183
|
results.push(...(await scanDir(fullPath)));
|
|
182
184
|
}
|
|
183
185
|
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
@@ -191,8 +193,7 @@ ${lazyImports.join(",\n")}
|
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
try {
|
|
194
|
-
const
|
|
195
|
-
const files = await scanDir(srcDir);
|
|
196
|
+
const files = await scanDir(projectRoot);
|
|
196
197
|
|
|
197
198
|
for (const filePath of files) {
|
|
198
199
|
const content = await fs.readFile(filePath, "utf-8");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin that triggers a full browser reload when Ctrl+R is pressed
|
|
5
|
+
* in the terminal running the dev server.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { poke } from "@rangojs/router/vite";
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* plugins: [rango(), poke()],
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function poke(): Plugin {
|
|
17
|
+
return {
|
|
18
|
+
name: "vite-plugin-poke",
|
|
19
|
+
apply: "serve",
|
|
20
|
+
|
|
21
|
+
configureServer(server) {
|
|
22
|
+
const stdin = process.stdin;
|
|
23
|
+
|
|
24
|
+
// Raw mode delivers individual keystrokes as immediate single-byte
|
|
25
|
+
// events instead of waiting for Enter (cooked/line-buffered mode).
|
|
26
|
+
// Without it, Ctrl+R (0x12) is never delivered as a discrete byte.
|
|
27
|
+
// When stdin is a pipe (CI, spawned process) setRawMode is unavailable
|
|
28
|
+
// but data already arrives unbuffered, so the isTTY guard suffices.
|
|
29
|
+
const previousRawMode = stdin.isTTY ? stdin.isRaw : null;
|
|
30
|
+
if (stdin.isTTY) {
|
|
31
|
+
stdin.setRawMode(true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const onData = (data: Buffer) => {
|
|
35
|
+
if (data.length !== 1) return;
|
|
36
|
+
|
|
37
|
+
// Ctrl+C (0x03) — defensive fallback. This plugin enables raw mode
|
|
38
|
+
// before Vite's internal stdin handler is registered (user plugins
|
|
39
|
+
// run first), so there is a brief window where Ctrl+C would be
|
|
40
|
+
// swallowed. Re-emit SIGINT so the process exits as expected.
|
|
41
|
+
if (data[0] === 0x03) {
|
|
42
|
+
process.emit("SIGINT", "SIGINT");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Ctrl+R = 0x12 in raw mode
|
|
47
|
+
if (data[0] === 0x12) {
|
|
48
|
+
server.hot.send({ type: "full-reload", path: "*" });
|
|
49
|
+
server.config.logger.info(" browser reload (ctrl+r)", {
|
|
50
|
+
timestamp: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
stdin.on("data", onData);
|
|
56
|
+
|
|
57
|
+
server.httpServer?.on("close", () => {
|
|
58
|
+
stdin.off("data", onData);
|
|
59
|
+
if (stdin.isTTY && previousRawMode !== null) {
|
|
60
|
+
stdin.setRawMode(previousRawMode);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -97,7 +97,7 @@ export function useCacheTransform(): Plugin {
|
|
|
97
97
|
|
|
98
98
|
// Check for function-level "use cache" / "use cache: profileName"
|
|
99
99
|
// (only if there's no file-level directive but code still contains the string)
|
|
100
|
-
|
|
100
|
+
const functionResult = transformFunctionLevelUseCache(
|
|
101
101
|
code,
|
|
102
102
|
ast,
|
|
103
103
|
filePath,
|
|
@@ -105,6 +105,13 @@ export function useCacheTransform(): Plugin {
|
|
|
105
105
|
isBuild,
|
|
106
106
|
transformHoistInlineDirective,
|
|
107
107
|
);
|
|
108
|
+
|
|
109
|
+
// Always check for near-miss directives, even when valid directives
|
|
110
|
+
// exist. A file may contain both valid and invalid "use cache" directives
|
|
111
|
+
// in different functions — the invalid ones should still warn.
|
|
112
|
+
warnOnNearMissDirectives(ast, id, this.warn.bind(this));
|
|
113
|
+
|
|
114
|
+
if (functionResult) return functionResult;
|
|
108
115
|
},
|
|
109
116
|
};
|
|
110
117
|
}
|
|
@@ -118,19 +125,38 @@ function transformFileLevelUseCache(
|
|
|
118
125
|
isLayoutOrTemplate: boolean,
|
|
119
126
|
transformWrapExport: (typeof import("@vitejs/plugin-rsc/transforms"))["transformWrapExport"],
|
|
120
127
|
) {
|
|
128
|
+
// Collect non-function exports to report after wrapping
|
|
129
|
+
const nonFunctionExports: string[] = [];
|
|
130
|
+
|
|
121
131
|
const { exportNames, output } = transformWrapExport(code, ast, {
|
|
122
132
|
runtime: (value: string, name: string) => {
|
|
123
133
|
const funcId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
|
|
124
134
|
return `__rango_registerCachedFunction(${value}, ${JSON.stringify(funcId)}, "default")`;
|
|
125
135
|
},
|
|
126
136
|
rejectNonAsyncFunction: false,
|
|
127
|
-
filter: (name: string) => {
|
|
137
|
+
filter: (name: string, meta: { isFunction?: boolean }) => {
|
|
128
138
|
// Skip default export of layout/template files (they receive children)
|
|
129
139
|
if (name === "default" && isLayoutOrTemplate) return false;
|
|
140
|
+
// Non-function exports cannot be wrapped with registerCachedFunction
|
|
141
|
+
if (meta.isFunction === false) {
|
|
142
|
+
nonFunctionExports.push(name);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
130
145
|
return true;
|
|
131
146
|
},
|
|
132
147
|
});
|
|
133
148
|
|
|
149
|
+
if (nonFunctionExports.length > 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`[rango:use-cache] File-level "use cache" in ${sourceId} cannot wrap ` +
|
|
152
|
+
`non-function export${nonFunctionExports.length > 1 ? "s" : ""}: ` +
|
|
153
|
+
`${nonFunctionExports.map((n) => `"${n}"`).join(", ")}. ` +
|
|
154
|
+
`Only function exports can be cached. Either remove "use cache" from ` +
|
|
155
|
+
`the file level and add it inside individual functions, or move the ` +
|
|
156
|
+
`non-function exports to a separate module.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
if (exportNames.length === 0) {
|
|
135
161
|
// Even if no exports were wrapped, strip the directive
|
|
136
162
|
const s = new MagicString(code);
|
|
@@ -180,7 +206,7 @@ function transformFunctionLevelUseCache(
|
|
|
180
206
|
) {
|
|
181
207
|
try {
|
|
182
208
|
const { output, names } = transformHoistInlineDirective(code, ast, {
|
|
183
|
-
directive: /^use cache(:\s
|
|
209
|
+
directive: /^use cache(:\s*[\w-]+)?$/,
|
|
184
210
|
runtime: (
|
|
185
211
|
value: string,
|
|
186
212
|
name: string,
|
|
@@ -233,3 +259,65 @@ function findFileLevelDirective(
|
|
|
233
259
|
}
|
|
234
260
|
return null;
|
|
235
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The valid directive regex (must stay in sync with transformFunctionLevelUseCache).
|
|
265
|
+
*/
|
|
266
|
+
const VALID_DIRECTIVE_RE = /^use cache(:\s*[\w-]+)?$/;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Regex for near-miss: starts with "use cache:" but has invalid tokens.
|
|
270
|
+
*/
|
|
271
|
+
const NEAR_MISS_RE = /^use cache:\s*.+$/;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Walk the AST looking for string literals that look like malformed
|
|
275
|
+
* "use cache" directives and emit a Vite warning for each.
|
|
276
|
+
*
|
|
277
|
+
* This catches cases like `"use cache: bad.name"` or `"use cache: "`
|
|
278
|
+
* that the transform regex silently ignores.
|
|
279
|
+
*/
|
|
280
|
+
function warnOnNearMissDirectives(
|
|
281
|
+
ast: any,
|
|
282
|
+
fileId: string,
|
|
283
|
+
warn: (message: string) => void,
|
|
284
|
+
): void {
|
|
285
|
+
const visit = (node: any) => {
|
|
286
|
+
if (!node || typeof node !== "object") return;
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
node.type === "ExpressionStatement" &&
|
|
290
|
+
node.expression?.type === "Literal" &&
|
|
291
|
+
typeof node.expression.value === "string"
|
|
292
|
+
) {
|
|
293
|
+
const value = node.expression.value;
|
|
294
|
+
if (
|
|
295
|
+
value.startsWith("use cache") &&
|
|
296
|
+
NEAR_MISS_RE.test(value) &&
|
|
297
|
+
!VALID_DIRECTIVE_RE.test(value)
|
|
298
|
+
) {
|
|
299
|
+
const profilePart = value.slice("use cache:".length).trim();
|
|
300
|
+
warn(
|
|
301
|
+
`[rango:use-cache] "${value}" in ${fileId} has an invalid profile name "${profilePart}". ` +
|
|
302
|
+
`Profile names must match [a-zA-Z0-9_-]+. This directive will be ignored.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Walk into function bodies where directives appear
|
|
308
|
+
for (const key of Object.keys(node)) {
|
|
309
|
+
const child = node[key];
|
|
310
|
+
if (Array.isArray(child)) {
|
|
311
|
+
for (const item of child) {
|
|
312
|
+
visit(item);
|
|
313
|
+
}
|
|
314
|
+
} else if (child && typeof child === "object" && child.type) {
|
|
315
|
+
visit(child);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
for (const node of ast.body ?? []) {
|
|
321
|
+
visit(node);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -1,6 +1,116 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { parseAst, type Plugin } from "vite";
|
|
2
2
|
import { VIRTUAL_IDS, getVirtualVersionContent } from "./virtual-entries.js";
|
|
3
3
|
|
|
4
|
+
interface ClientModuleSignature {
|
|
5
|
+
key: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isCodeModule(id: string): boolean {
|
|
9
|
+
return /\.(tsx?|jsx?)($|\?)/.test(id);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeModuleId(id: string): string {
|
|
13
|
+
return id.split("?", 1)[0];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getClientModuleSignature(
|
|
17
|
+
source: string,
|
|
18
|
+
): ClientModuleSignature | undefined {
|
|
19
|
+
let program: any;
|
|
20
|
+
try {
|
|
21
|
+
program = parseAst(source, { jsx: true });
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let isUseClient = false;
|
|
27
|
+
for (const node of program.body ?? []) {
|
|
28
|
+
if (
|
|
29
|
+
node?.type === "ExpressionStatement" &&
|
|
30
|
+
node.expression?.type === "Literal" &&
|
|
31
|
+
typeof node.expression.value === "string"
|
|
32
|
+
) {
|
|
33
|
+
if (node.expression.value === "use client") {
|
|
34
|
+
isUseClient = true;
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isUseClient) return undefined;
|
|
42
|
+
|
|
43
|
+
const exports = new Set<string>();
|
|
44
|
+
let hasDefault = false;
|
|
45
|
+
let hasExportAll = false;
|
|
46
|
+
|
|
47
|
+
const collectBindingNames = (pattern: any) => {
|
|
48
|
+
if (!pattern) return;
|
|
49
|
+
if (pattern.type === "Identifier") {
|
|
50
|
+
exports.add(pattern.name);
|
|
51
|
+
} else if (pattern.type === "ObjectPattern") {
|
|
52
|
+
for (const prop of pattern.properties ?? []) {
|
|
53
|
+
if (prop?.type === "RestElement") {
|
|
54
|
+
collectBindingNames(prop.argument);
|
|
55
|
+
} else {
|
|
56
|
+
collectBindingNames(prop?.value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} else if (pattern.type === "ArrayPattern") {
|
|
60
|
+
for (const el of pattern.elements ?? []) {
|
|
61
|
+
if (el?.type === "RestElement") {
|
|
62
|
+
collectBindingNames(el.argument);
|
|
63
|
+
} else {
|
|
64
|
+
collectBindingNames(el);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const collectDeclarationNames = (declaration: any) => {
|
|
71
|
+
if (!declaration) return;
|
|
72
|
+
if (declaration.type === "VariableDeclaration") {
|
|
73
|
+
for (const decl of declaration.declarations ?? []) {
|
|
74
|
+
collectBindingNames(decl?.id);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
collectBindingNames(declaration.id);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (const node of program.body ?? []) {
|
|
82
|
+
if (node?.type === "ExportDefaultDeclaration") {
|
|
83
|
+
hasDefault = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (node?.type === "ExportAllDeclaration") {
|
|
87
|
+
hasExportAll = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (node?.type !== "ExportNamedDeclaration") continue;
|
|
91
|
+
|
|
92
|
+
collectDeclarationNames(node.declaration);
|
|
93
|
+
|
|
94
|
+
for (const specifier of node.specifiers ?? []) {
|
|
95
|
+
const exportedName =
|
|
96
|
+
specifier?.exported?.name ?? specifier?.exported?.value;
|
|
97
|
+
if (exportedName === "default") {
|
|
98
|
+
hasDefault = true;
|
|
99
|
+
} else if (typeof exportedName === "string") {
|
|
100
|
+
exports.add(exportedName);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
key: JSON.stringify({
|
|
107
|
+
default: hasDefault,
|
|
108
|
+
exportAll: hasExportAll,
|
|
109
|
+
exports: [...exports].sort(),
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
4
114
|
/**
|
|
5
115
|
* Plugin providing rsc-router:version virtual module.
|
|
6
116
|
* Exports VERSION that changes when RSC modules change (dev) or at build time (production).
|
|
@@ -23,6 +133,20 @@ export function createVersionPlugin(): Plugin {
|
|
|
23
133
|
let currentVersion = buildVersion;
|
|
24
134
|
let isDev = false;
|
|
25
135
|
let server: any = null;
|
|
136
|
+
const clientModuleSignatures = new Map<string, ClientModuleSignature>();
|
|
137
|
+
|
|
138
|
+
const bumpVersion = (reason: string) => {
|
|
139
|
+
currentVersion = Date.now().toString(16);
|
|
140
|
+
console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
|
|
141
|
+
|
|
142
|
+
const rscEnv = server?.environments?.rsc;
|
|
143
|
+
const versionMod = rscEnv?.moduleGraph?.getModuleById(
|
|
144
|
+
"\0" + VIRTUAL_IDS.version,
|
|
145
|
+
);
|
|
146
|
+
if (versionMod) {
|
|
147
|
+
rscEnv.moduleGraph.invalidateModule(versionMod);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
26
150
|
|
|
27
151
|
return {
|
|
28
152
|
name: "@rangojs/router:version",
|
|
@@ -34,6 +158,13 @@ export function createVersionPlugin(): Plugin {
|
|
|
34
158
|
|
|
35
159
|
configureServer(devServer) {
|
|
36
160
|
server = devServer;
|
|
161
|
+
|
|
162
|
+
devServer.watcher.on("unlink", (filePath) => {
|
|
163
|
+
if (!isDev) return;
|
|
164
|
+
if (!clientModuleSignatures.has(filePath)) return;
|
|
165
|
+
clientModuleSignatures.delete(filePath);
|
|
166
|
+
bumpVersion("Client module removed");
|
|
167
|
+
});
|
|
37
168
|
},
|
|
38
169
|
|
|
39
170
|
resolveId(id) {
|
|
@@ -50,8 +181,27 @@ export function createVersionPlugin(): Plugin {
|
|
|
50
181
|
return null;
|
|
51
182
|
},
|
|
52
183
|
|
|
184
|
+
transform(code, id) {
|
|
185
|
+
if (!isDev || !isCodeModule(id)) return null;
|
|
186
|
+
const normalizedId = normalizeModuleId(id);
|
|
187
|
+
if (
|
|
188
|
+
!code.includes("use client") &&
|
|
189
|
+
!clientModuleSignatures.has(normalizedId)
|
|
190
|
+
) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const signature = getClientModuleSignature(code);
|
|
195
|
+
if (signature) {
|
|
196
|
+
clientModuleSignatures.set(normalizedId, signature);
|
|
197
|
+
} else {
|
|
198
|
+
clientModuleSignatures.delete(normalizedId);
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
},
|
|
202
|
+
|
|
53
203
|
// Track RSC module changes and update version
|
|
54
|
-
hotUpdate(ctx) {
|
|
204
|
+
async hotUpdate(ctx) {
|
|
55
205
|
if (!isDev) return;
|
|
56
206
|
|
|
57
207
|
// Check if this is an RSC environment update (not client/ssr)
|
|
@@ -59,26 +209,46 @@ export function createVersionPlugin(): Plugin {
|
|
|
59
209
|
// In Vite 6, environment is accessed via `this.environment`
|
|
60
210
|
const isRscModule = this.environment?.name === "rsc";
|
|
61
211
|
|
|
62
|
-
if (isRscModule
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
212
|
+
if (!isRscModule) return;
|
|
213
|
+
|
|
214
|
+
if (isCodeModule(ctx.file)) {
|
|
215
|
+
const filePath = normalizeModuleId(ctx.file);
|
|
216
|
+
const previousSignature = clientModuleSignatures.get(filePath);
|
|
217
|
+
try {
|
|
218
|
+
const source = await ctx.read();
|
|
219
|
+
const nextSignature = getClientModuleSignature(source);
|
|
220
|
+
if (nextSignature) {
|
|
221
|
+
// "use client" file — compare export signatures.
|
|
222
|
+
// client-component-hmr may have cleared ctx.modules, so we
|
|
223
|
+
// cannot rely on ctx.modules.length for these files.
|
|
224
|
+
clientModuleSignatures.set(filePath, nextSignature);
|
|
225
|
+
if (
|
|
226
|
+
previousSignature &&
|
|
227
|
+
previousSignature.key === nextSignature.key
|
|
228
|
+
) {
|
|
229
|
+
return;
|
|
78
230
|
}
|
|
231
|
+
} else {
|
|
232
|
+
clientModuleSignatures.delete(filePath);
|
|
233
|
+
if (!previousSignature) {
|
|
234
|
+
// Not and never was "use client" — use module graph check.
|
|
235
|
+
// ctx.modules is reliable for pure server files (only
|
|
236
|
+
// client-component-hmr clears it for "use client" modules).
|
|
237
|
+
if (ctx.modules.length === 0) return;
|
|
238
|
+
}
|
|
239
|
+
// Was "use client" but directive removed — boundary changed,
|
|
240
|
+
// bump below.
|
|
79
241
|
}
|
|
242
|
+
} catch {
|
|
243
|
+
// Fail open: if we can't read or parse the update, invalidate.
|
|
80
244
|
}
|
|
245
|
+
} else {
|
|
246
|
+
// Non-code file (json, css, etc.) — only bump if it's actually
|
|
247
|
+
// referenced by the RSC module graph.
|
|
248
|
+
if (ctx.modules.length === 0) return;
|
|
81
249
|
}
|
|
250
|
+
|
|
251
|
+
bumpVersion("RSC module changed");
|
|
82
252
|
},
|
|
83
253
|
};
|
|
84
254
|
}
|