@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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Connect } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bake the resolved INTERNAL_RANGO_DEBUG value into the router's `internal-debug`
|
|
5
|
+
* module so the flag reaches the CLIENT debug logs by just setting the env var.
|
|
6
|
+
*
|
|
7
|
+
* internal-debug.ts normally reads the flag via `typeof __RANGO_DEBUG__`, a Vite
|
|
8
|
+
* define. That delivery is unreliable on the client: in dev Vite ships the define
|
|
9
|
+
* only as an injected global whose presence varies across consumer setups, so the
|
|
10
|
+
* module can fall through to `process.env` (undefined in the browser) and the FE
|
|
11
|
+
* debug flag silently stays false while the server logs work. A Vite `transform`
|
|
12
|
+
* runs on the module regardless of how (or whether) the define is delivered, in
|
|
13
|
+
* both dev and build and for every environment, so the discovery plugin uses this
|
|
14
|
+
* to replace the module with the resolved literal.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Scope to the router's own internal-debug module: the published package
|
|
19
|
+
* (`/@rangojs/router/`, incl. pnpm's nested layout) or the monorepo workspace
|
|
20
|
+
* (`/packages/rangojs-router/`). The package-anchored path avoids matching a
|
|
21
|
+
* consumer file that merely sits under a directory named `rangojs-router`.
|
|
22
|
+
* Accepts module ids and dev-server URLs (`/@fs/...internal-debug.ts?v=abc`).
|
|
23
|
+
*/
|
|
24
|
+
export function isRouterInternalDebugId(id: string): boolean {
|
|
25
|
+
if (!id.includes("internal-debug")) return false;
|
|
26
|
+
const norm = id.replace(/\\/g, "/");
|
|
27
|
+
return (
|
|
28
|
+
/\/internal-debug\.[cm]?[jt]sx?(\?|$)/.test(norm) &&
|
|
29
|
+
(norm.includes("/@rangojs/router/") ||
|
|
30
|
+
norm.includes("/packages/rangojs-router/"))
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transform: replace the module with the resolved literal.
|
|
36
|
+
* Returns null for any module that is not the router's internal-debug module.
|
|
37
|
+
*/
|
|
38
|
+
export function injectClientDebugFlag(
|
|
39
|
+
id: string,
|
|
40
|
+
): { code: string; map: null } | null {
|
|
41
|
+
// Cheap early-out: this hook runs on every module in every environment.
|
|
42
|
+
if (!isRouterInternalDebugId(id)) return null;
|
|
43
|
+
// Emit the whole module: internal-debug.ts has a single export, kept in sync.
|
|
44
|
+
return {
|
|
45
|
+
code: `export const INTERNAL_RANGO_DEBUG = ${!!process.env.INTERNAL_RANGO_DEBUG};\n`,
|
|
46
|
+
map: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Dev middleware companion: serve the internal-debug module `no-cache` so the
|
|
52
|
+
* browser revalidates it (etag) instead of trusting an immutable cache entry.
|
|
53
|
+
*
|
|
54
|
+
* Why: the transform bakes the flag into module CONTENT, but a published
|
|
55
|
+
* consumer resolves the module into node_modules, where dev serves it as
|
|
56
|
+
* `internal-debug.ts?v=<hash>` with `Cache-Control: max-age=31536000,immutable`.
|
|
57
|
+
* That `?v=` hash does not vary with env vars (verified on Vite 8: getConfigHash
|
|
58
|
+
* hashes NODE_ENV, resolve, plugin names, optimizeDeps -- not arbitrary env
|
|
59
|
+
* state), so toggling the flag changed the content under an unchanged immutable
|
|
60
|
+
* URL: a browser that ever loaded the app with the flag off kept the
|
|
61
|
+
* baked-`false` module across dev-server restarts, and INTERNAL_RANGO_DEBUG
|
|
62
|
+
* never reached the FE logs while the server logs worked. The monorepo was
|
|
63
|
+
* immune -- workspace source is outside node_modules and served no-cache --
|
|
64
|
+
* which is why this bit only npm consumers.
|
|
65
|
+
*
|
|
66
|
+
* internal-debug.ts is the ONLY flag-varying module in the graph (its importers
|
|
67
|
+
* are byte-identical across flag states), so forcing revalidation for this one
|
|
68
|
+
* tiny module is sufficient and costs one conditional request per session. The
|
|
69
|
+
* middleware is unconditional (not gated on the flag) so an already-poisoned
|
|
70
|
+
* cache heals in both directions. Alternatives that do not work: a plugin
|
|
71
|
+
* `resolveId` appending a flag query never fires in Vite 8 dev for
|
|
72
|
+
* fs-resolvable relative imports, and the pre-#621 `define` no longer rotates
|
|
73
|
+
* the optimizer hash (define contents are not part of getConfigHash).
|
|
74
|
+
*/
|
|
75
|
+
export function internalDebugNoCacheMiddleware(): Connect.NextHandleFunction {
|
|
76
|
+
return function rangoInternalDebugNoCache(req, res, next) {
|
|
77
|
+
if (req.url && isRouterInternalDebugId(req.url)) {
|
|
78
|
+
const setHeader = res.setHeader.bind(res);
|
|
79
|
+
res.setHeader = (name, value) => {
|
|
80
|
+
return setHeader(
|
|
81
|
+
name,
|
|
82
|
+
name.toLowerCase() === "cache-control" ? "no-cache" : value,
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
next();
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -46,7 +46,11 @@ import { existsSync } from "node:fs";
|
|
|
46
46
|
import { resolve, join } from "node:path";
|
|
47
47
|
import { createRequire } from "node:module";
|
|
48
48
|
import { pathToFileURL } from "node:url";
|
|
49
|
-
import
|
|
49
|
+
import { escapeRegExp } from "../../regex-escape.js";
|
|
50
|
+
import type {
|
|
51
|
+
RangoVercelOptions,
|
|
52
|
+
VercelPresetOptions,
|
|
53
|
+
} from "../plugin-types.js";
|
|
50
54
|
|
|
51
55
|
// Minimal structural types for the esbuild API we use, resolved dynamically from
|
|
52
56
|
// the app so @rangojs/router does not depend on esbuild's type package.
|
|
@@ -100,14 +104,106 @@ export function assertVercelNodeRuntime(runtime: string | undefined): void {
|
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The function name becomes a `.func` directory segment and the config.json
|
|
109
|
+
* route `dest` (`/${functionName}`). An empty or space/slash-containing value
|
|
110
|
+
* would land raw in both, producing a broken function path, so restrict it to a
|
|
111
|
+
* safe single path segment and fail loudly rather than emit unroutable output.
|
|
112
|
+
*/
|
|
113
|
+
export function assertValidVercelFunctionName(functionName: string): void {
|
|
114
|
+
if (!/^[A-Za-z0-9._-]+$/.test(functionName)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[rango] preset "vercel": invalid functionName ${JSON.stringify(
|
|
117
|
+
functionName,
|
|
118
|
+
)}. Use letters, digits, ".", "_" or "-" only (it becomes the .func directory and the routing dest).`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The `.vc-config.json` body: a Node serverless function with streaming. */
|
|
124
|
+
export function buildVercelVcConfig(
|
|
125
|
+
vercel: VercelPresetOptions,
|
|
126
|
+
): Record<string, unknown> {
|
|
127
|
+
const vcConfig: Record<string, unknown> = {
|
|
128
|
+
runtime: vercel.runtime ?? "nodejs22.x",
|
|
129
|
+
handler: "index.mjs",
|
|
130
|
+
launcherType: "Nodejs",
|
|
131
|
+
shouldAddHelpers: false,
|
|
132
|
+
supportsResponseStreaming: true,
|
|
133
|
+
maxDuration: vercel.maxDuration ?? 30,
|
|
134
|
+
};
|
|
135
|
+
if (vercel.memory != null) vcConfig.memory = vercel.memory;
|
|
136
|
+
if (vercel.regions != null) vcConfig.regions = vercel.regions;
|
|
137
|
+
return vcConfig;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The Build Output API v3 `config.json` body. Routes, in order: long-cache the
|
|
142
|
+
* content-hashed assets under `${assetsDir}/` (safe to serve `immutable`;
|
|
143
|
+
* without this Vercel serves them `max-age=0, must-revalidate` and browsers
|
|
144
|
+
* re-request every asset on each visit), then the filesystem handler, then
|
|
145
|
+
* everything to the function. The header route uses `continue: true` so it falls
|
|
146
|
+
* through to the filesystem handler that actually serves the file.
|
|
147
|
+
*
|
|
148
|
+
* Route `src` values are REGEXES on Vercel, so the prefix is regex-escaped
|
|
149
|
+
* (an unescaped `assetsDir: "static.v2"` would immutable-stamp any
|
|
150
|
+
* `/static?v2/...` path, including function-rendered pages). An empty
|
|
151
|
+
* assetsDir (assets at the outDir root) gets no header route at all: there is
|
|
152
|
+
* no prefix separating hashed from non-hashed files, so fall back to Vercel's
|
|
153
|
+
* safe default headers. Two accepted edges, matching what other framework
|
|
154
|
+
* adapters (Astro, SvelteKit) emit: files a user places in
|
|
155
|
+
* `public/${assetsDir}/` land under the same prefix and are also stamped
|
|
156
|
+
* immutable (assemble() warns about the collision), and a request for a
|
|
157
|
+
* MISSING asset falls through to the function whose 404 carries the header.
|
|
158
|
+
*/
|
|
159
|
+
export function buildVercelOutputConfig(
|
|
160
|
+
functionName: string,
|
|
161
|
+
assetsDir: string,
|
|
162
|
+
): { version: number; routes: unknown[] } {
|
|
163
|
+
const assetsPrefix = assetsDir.replace(/^\/+|\/+$/g, "");
|
|
164
|
+
const assetHeaderRoute = assetsPrefix
|
|
165
|
+
? [
|
|
166
|
+
{
|
|
167
|
+
src: `/${escapeRegExp(assetsPrefix)}/(.*)`,
|
|
168
|
+
headers: { "cache-control": "public, max-age=31536000, immutable" },
|
|
169
|
+
continue: true,
|
|
170
|
+
},
|
|
171
|
+
]
|
|
172
|
+
: [];
|
|
173
|
+
return {
|
|
174
|
+
version: 3,
|
|
175
|
+
routes: [
|
|
176
|
+
...assetHeaderRoute,
|
|
177
|
+
{ handle: "filesystem" },
|
|
178
|
+
{ src: "/(.*)", dest: `/${functionName}` },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
103
183
|
async function assemble(
|
|
104
184
|
root: string,
|
|
105
185
|
options: RangoVercelOptions,
|
|
186
|
+
assetsDir: string,
|
|
187
|
+
publicDir: string,
|
|
106
188
|
): Promise<void> {
|
|
107
189
|
const vercel = options.vercel ?? {};
|
|
108
190
|
// Validate config before touching the build output.
|
|
109
191
|
assertVercelNodeRuntime(vercel.runtime);
|
|
110
192
|
|
|
193
|
+
// Files in public/<assetsDir>/ are copied into the same output prefix as the
|
|
194
|
+
// content-hashed build assets, so the immutable cache-control route stamps
|
|
195
|
+
// them too -- replacing such a file after a deploy never reaches returning
|
|
196
|
+
// visitors. Warn instead of silently pinning.
|
|
197
|
+
const assetsPrefix = assetsDir.replace(/^\/+|\/+$/g, "");
|
|
198
|
+
if (publicDir && assetsPrefix && existsSync(join(publicDir, assetsPrefix))) {
|
|
199
|
+
console.warn(
|
|
200
|
+
`[rango] preset "vercel": ${join(publicDir, assetsPrefix)} exists. ` +
|
|
201
|
+
`Files under public/${assetsPrefix}/ share the /${assetsPrefix}/ URL prefix ` +
|
|
202
|
+
`with hashed build assets and will be served with a one-year immutable ` +
|
|
203
|
+
`cache-control header. Move un-hashed public files out of "${assetsPrefix}/".`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
111
207
|
const dist = join(root, "dist");
|
|
112
208
|
for (const dir of ["client", "rsc", "ssr"]) {
|
|
113
209
|
if (!existsSync(join(dist, dir))) {
|
|
@@ -117,6 +213,7 @@ async function assemble(
|
|
|
117
213
|
}
|
|
118
214
|
}
|
|
119
215
|
const functionName = vercel.functionName ?? "index";
|
|
216
|
+
assertValidVercelFunctionName(functionName);
|
|
120
217
|
const out = join(root, ".vercel", "output");
|
|
121
218
|
const funcDir = join(out, "functions", `${functionName}.func`);
|
|
122
219
|
|
|
@@ -235,35 +332,16 @@ async function assemble(
|
|
|
235
332
|
);
|
|
236
333
|
|
|
237
334
|
// 4. Function config: Node serverless with response streaming.
|
|
238
|
-
const vcConfig: Record<string, unknown> = {
|
|
239
|
-
runtime: vercel.runtime ?? "nodejs22.x",
|
|
240
|
-
handler: "index.mjs",
|
|
241
|
-
launcherType: "Nodejs",
|
|
242
|
-
shouldAddHelpers: false,
|
|
243
|
-
supportsResponseStreaming: true,
|
|
244
|
-
maxDuration: vercel.maxDuration ?? 30,
|
|
245
|
-
};
|
|
246
|
-
if (vercel.memory != null) vcConfig.memory = vercel.memory;
|
|
247
|
-
if (vercel.regions != null) vcConfig.regions = vercel.regions;
|
|
248
335
|
await writeFile(
|
|
249
336
|
join(funcDir, ".vc-config.json"),
|
|
250
|
-
JSON.stringify(
|
|
337
|
+
JSON.stringify(buildVercelVcConfig(vercel), null, 2) + "\n",
|
|
251
338
|
);
|
|
252
339
|
|
|
253
|
-
// 5. Routing
|
|
340
|
+
// 5. Routing config (immutable assets -> filesystem -> function).
|
|
254
341
|
await writeFile(
|
|
255
342
|
join(out, "config.json"),
|
|
256
|
-
JSON.stringify(
|
|
257
|
-
|
|
258
|
-
version: 3,
|
|
259
|
-
routes: [
|
|
260
|
-
{ handle: "filesystem" },
|
|
261
|
-
{ src: "/(.*)", dest: `/${functionName}` },
|
|
262
|
-
],
|
|
263
|
-
},
|
|
264
|
-
null,
|
|
265
|
-
2,
|
|
266
|
-
) + "\n",
|
|
343
|
+
JSON.stringify(buildVercelOutputConfig(functionName, assetsDir), null, 2) +
|
|
344
|
+
"\n",
|
|
267
345
|
);
|
|
268
346
|
|
|
269
347
|
console.log(
|
|
@@ -274,11 +352,22 @@ async function assemble(
|
|
|
274
352
|
export function createVercelOutputPlugin(options: RangoVercelOptions): Plugin {
|
|
275
353
|
let root = process.cwd();
|
|
276
354
|
let isBuild = false;
|
|
355
|
+
// The client build's assetsDir (Vite default "assets"); used to scope the
|
|
356
|
+
// immutable cache-control route to the content-hashed asset output.
|
|
357
|
+
let assetsDir = "assets";
|
|
358
|
+
// Resolved publicDir ("" when disabled); used to warn when public/<assetsDir>
|
|
359
|
+
// exists, since its un-hashed files share the immutable-header prefix.
|
|
360
|
+
let publicDir = "";
|
|
277
361
|
return {
|
|
278
362
|
name: "@rangojs/router:vercel-output",
|
|
279
363
|
configResolved(config) {
|
|
280
364
|
root = resolve(config.root);
|
|
281
365
|
isBuild = config.command === "build";
|
|
366
|
+
assetsDir =
|
|
367
|
+
config.environments?.client?.build?.assetsDir ??
|
|
368
|
+
config.build?.assetsDir ??
|
|
369
|
+
"assets";
|
|
370
|
+
publicDir = config.publicDir || "";
|
|
282
371
|
},
|
|
283
372
|
// buildApp runs once after the whole multi-environment build (rsc, client,
|
|
284
373
|
// ssr), so dist/ is complete here. closeBundle is unusable for this: it
|
|
@@ -288,7 +377,7 @@ export function createVercelOutputPlugin(options: RangoVercelOptions): Plugin {
|
|
|
288
377
|
order: "post",
|
|
289
378
|
async handler() {
|
|
290
379
|
if (!isBuild) return;
|
|
291
|
-
await assemble(root, options);
|
|
380
|
+
await assemble(root, options, assetsDir, publicDir);
|
|
292
381
|
},
|
|
293
382
|
},
|
|
294
383
|
};
|
|
@@ -2,12 +2,25 @@ import type { Plugin } from "vite";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import * as Vite from "vite";
|
|
4
4
|
import { resolveRscEntryFromConfig } from "../utils/shared-utils.js";
|
|
5
|
+
import { RSC_ENTRY_BOOTSTRAP_IMPORTS } from "./virtual-entries.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Plugin that auto-injects VERSION and
|
|
8
|
-
* If a custom entry.rsc file uses createRSCHandler but
|
|
9
|
-
* this transform adds the import and property
|
|
10
|
-
*
|
|
8
|
+
* Plugin that auto-injects VERSION, routes-manifest, and loader-manifest into
|
|
9
|
+
* custom entry.rsc files. If a custom entry.rsc file uses createRSCHandler but
|
|
10
|
+
* doesn't pass version, this transform adds the import and property
|
|
11
|
+
* automatically. It also ensures the routes-manifest and loader-manifest
|
|
12
|
+
* virtual modules are always imported.
|
|
13
|
+
*
|
|
14
|
+
* The loader-manifest import is what makes fetchable loaders resolvable on a
|
|
15
|
+
* custom worker entry. The virtual RSC entry (getVirtualEntryRSC) imports the
|
|
16
|
+
* loader manifest itself, but a hand-written entry (e.g. a Cloudflare
|
|
17
|
+
* worker.rsc.tsx) does not — so without this injection, setLoaderImports() is
|
|
18
|
+
* never bundled, lazyLoaderImports stays null at runtime, and a fetchable
|
|
19
|
+
* loader that is never imported by the server graph (not registered via
|
|
20
|
+
* loader(), reachable only through a client component) cannot be found by the
|
|
21
|
+
* _rsc_loader endpoint in production. Dev still works because it resolves
|
|
22
|
+
* loaders by parsing their id into a file path; only production relied on the
|
|
23
|
+
* manifest, hence the production-only failure this fixes.
|
|
11
24
|
* @internal
|
|
12
25
|
*/
|
|
13
26
|
export function createVersionInjectorPlugin(
|
|
@@ -36,9 +49,11 @@ export function createVersionInjectorPlugin(
|
|
|
36
49
|
return null;
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
]
|
|
52
|
+
// Same startup bootstrap imports the generated virtual RSC entry uses,
|
|
53
|
+
// from the single shared list so the two paths cannot drift.
|
|
54
|
+
const prepend: string[] = RSC_ENTRY_BOOTSTRAP_IMPORTS.map(
|
|
55
|
+
(id) => `import "${id}";`,
|
|
56
|
+
);
|
|
42
57
|
|
|
43
58
|
let newCode = code;
|
|
44
59
|
const needsVersion =
|
|
@@ -38,9 +38,14 @@ initializeApp().catch(console.error);
|
|
|
38
38
|
|
|
39
39
|
export const VIRTUAL_ENTRY_SSR: string = `
|
|
40
40
|
import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
|
|
41
|
-
import { renderToReadableStream } from "react-dom/server.edge";
|
|
41
|
+
import { renderToReadableStream, resume } from "react-dom/server.edge";
|
|
42
|
+
import { prerender } from "react-dom/static.edge";
|
|
42
43
|
import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
|
|
43
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
createSSRHandler,
|
|
46
|
+
createShellCaptureHandler,
|
|
47
|
+
createShellResumeHandler,
|
|
48
|
+
} from "@rangojs/router/ssr";
|
|
44
49
|
|
|
45
50
|
export const renderHTML = createSSRHandler({
|
|
46
51
|
createFromReadableStream,
|
|
@@ -49,9 +54,54 @@ export const renderHTML = createSSRHandler({
|
|
|
49
54
|
loadBootstrapScriptContent: () =>
|
|
50
55
|
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
51
56
|
});
|
|
57
|
+
|
|
58
|
+
export const captureShellHTML = createShellCaptureHandler({
|
|
59
|
+
createFromReadableStream,
|
|
60
|
+
renderToReadableStream,
|
|
61
|
+
injectRSCPayload,
|
|
62
|
+
prerender,
|
|
63
|
+
resume,
|
|
64
|
+
loadBootstrapScriptContent: () =>
|
|
65
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const resumeShellHTML = createShellResumeHandler({
|
|
69
|
+
createFromReadableStream,
|
|
70
|
+
renderToReadableStream,
|
|
71
|
+
injectRSCPayload,
|
|
72
|
+
prerender,
|
|
73
|
+
resume,
|
|
74
|
+
loadBootstrapScriptContent: () =>
|
|
75
|
+
import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
76
|
+
});
|
|
52
77
|
`.trim();
|
|
53
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Virtual modules an RSC entry must import at startup to register the data the
|
|
81
|
+
* request handler needs before the first request arrives:
|
|
82
|
+
*
|
|
83
|
+
* - routes-manifest: the pre-generated route map so href()/matching work on a
|
|
84
|
+
* cold start (full map in build, in-memory no-op in dev).
|
|
85
|
+
* - loader-manifest: setLoaderImports() for fetchable loaders. Critical for
|
|
86
|
+
* serverless/multi-process deployments and for fetchable loaders reachable
|
|
87
|
+
* only through a client component — without it those loaders are never
|
|
88
|
+
* registered for the _rsc_loader endpoint and fail in production.
|
|
89
|
+
*
|
|
90
|
+
* Single source of truth: both the generated virtual RSC entry below and the
|
|
91
|
+
* custom-entry injector (version-injector) consume this list, so a new
|
|
92
|
+
* bootstrap manifest cannot be added to one path and forgotten on the other.
|
|
93
|
+
* That exact drift (loader-manifest present here but missing from the injector)
|
|
94
|
+
* is what left fetchable loaders unresolved on custom worker entries.
|
|
95
|
+
*/
|
|
96
|
+
export const RSC_ENTRY_BOOTSTRAP_IMPORTS: readonly string[] = [
|
|
97
|
+
"virtual:rsc-router/routes-manifest",
|
|
98
|
+
"virtual:rsc-router/loader-manifest",
|
|
99
|
+
];
|
|
100
|
+
|
|
54
101
|
export function getVirtualEntryRSC(routerPath: string): string {
|
|
102
|
+
const bootstrapImports = RSC_ENTRY_BOOTSTRAP_IMPORTS.map(
|
|
103
|
+
(id) => `import "${id}";`,
|
|
104
|
+
).join("\n");
|
|
55
105
|
return `
|
|
56
106
|
import {
|
|
57
107
|
renderToReadableStream,
|
|
@@ -65,15 +115,9 @@ import { router } from "${routerPath}";
|
|
|
65
115
|
import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
|
|
66
116
|
import { VERSION } from "@rangojs/router:version";
|
|
67
117
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
import "virtual:rsc-router/loader-manifest";
|
|
72
|
-
|
|
73
|
-
// Import pre-generated route manifest so href() works immediately on cold start.
|
|
74
|
-
// In build mode, this contains the full route map generated at build time.
|
|
75
|
-
// In dev mode, this is a no-op (manifest is populated in-memory by the discovery plugin).
|
|
76
|
-
import "virtual:rsc-router/routes-manifest";
|
|
118
|
+
// Startup bootstrap imports (routes + loader manifests). See
|
|
119
|
+
// RSC_ENTRY_BOOTSTRAP_IMPORTS — the same list the custom-entry injector uses.
|
|
120
|
+
${bootstrapImports}
|
|
77
121
|
|
|
78
122
|
// Lazily create the handler on first request so that ESM live bindings
|
|
79
123
|
// have resolved by the time we read \`router\`. During HMR the module may
|
|
@@ -109,7 +153,7 @@ export default function handler(request, env) {
|
|
|
109
153
|
export function getVirtualEntryRSCHost(hostEntryPath: string): string {
|
|
110
154
|
return `
|
|
111
155
|
import * as __hostEntry from "${hostEntryPath}";
|
|
112
|
-
import {
|
|
156
|
+
import { isNoRouteMatchError } from "@rangojs/router/host";
|
|
113
157
|
|
|
114
158
|
// Register every sub-app's fetchable loaders + route manifests at startup, same
|
|
115
159
|
// as the single-router entry. Discovery's host fallback populates these for all
|
|
@@ -119,11 +163,12 @@ import "virtual:rsc-router/routes-manifest";
|
|
|
119
163
|
|
|
120
164
|
// The host entry module must export the HostRouter instance (createHostRouter()),
|
|
121
165
|
// as a default export or a named \`hostRouter\`/\`router\` export. A Cloudflare-style
|
|
122
|
-
// \`export default { fetch }\` object is not a HostRouter and is rejected
|
|
166
|
+
// \`export default { fetch }\` object is not a HostRouter and is rejected (on first
|
|
167
|
+
// request; see the lazy resolution below).
|
|
123
168
|
// We require BOTH .match() and .host(): a regular createRouter() also exposes
|
|
124
169
|
// .match(), so matching on .match() alone would accept an ordinary router and then
|
|
125
170
|
// return its MatchResult (not a Response) at runtime. .host() is unique to a
|
|
126
|
-
// HostRouter, so it disambiguates a mistaken \`hostRouter\` path
|
|
171
|
+
// HostRouter, so it disambiguates a mistaken \`hostRouter\` path.
|
|
127
172
|
// Exports are read dynamically (m[name]) so Rollup does not emit IMPORT_IS_UNDEFINED
|
|
128
173
|
// warnings for the named exports a default-only host module legitimately omits.
|
|
129
174
|
const __resolveHostRouter = (m) => {
|
|
@@ -138,13 +183,14 @@ const __resolveHostRouter = (m) => {
|
|
|
138
183
|
}
|
|
139
184
|
return undefined;
|
|
140
185
|
};
|
|
141
|
-
const hostRouter = __resolveHostRouter(__hostEntry);
|
|
142
186
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
187
|
+
// Resolve + validate the HostRouter lazily on first request, mirroring the
|
|
188
|
+
// single-router entry's \`_handler\`. During HMR the host module can re-evaluate
|
|
189
|
+
// before its createHostRouter() export has resolved; validating at module-
|
|
190
|
+
// evaluation time would then throw a spurious "must export a HostRouter" error
|
|
191
|
+
// overlay for a perfectly valid app. Resolving on first request lets the live
|
|
192
|
+
// binding settle first.
|
|
193
|
+
let _hostRouter;
|
|
148
194
|
|
|
149
195
|
// input = { env, ctx } from the launcher / node server. The host router threads
|
|
150
196
|
// it unchanged to each matched sub-app's handler and cache factory.
|
|
@@ -152,10 +198,22 @@ if (!hostRouter) {
|
|
|
152
198
|
// an unmatched host into a response: catch NoRouteMatchError and return 404
|
|
153
199
|
// (parity with the documented Cloudflare catch). Other errors propagate.
|
|
154
200
|
export default async function handler(request, input) {
|
|
201
|
+
if (!_hostRouter) {
|
|
202
|
+
_hostRouter = __resolveHostRouter(__hostEntry);
|
|
203
|
+
if (!_hostRouter) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"[rango] The host entry (${hostEntryPath}) must export a HostRouter instance (createHostRouter()) for the node/vercel preset: a default export, or a named 'hostRouter'/'router' export. An ordinary createRouter() is not a host router (it has no .host()), and a Cloudflare-style 'export default { fetch }' object is not supported on this preset."
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
155
209
|
try {
|
|
156
|
-
return await
|
|
210
|
+
return await _hostRouter.match(request, input);
|
|
157
211
|
} catch (err) {
|
|
158
|
-
|
|
212
|
+
// isNoRouteMatchError also matches by name: a workspace with a duplicated
|
|
213
|
+
// @rangojs/router copy can throw a NoRouteMatchError whose prototype differs
|
|
214
|
+
// from this module's import, so a bare instanceof would turn an
|
|
215
|
+
// unmatched-host 404 into a 500.
|
|
216
|
+
if (isNoRouteMatchError(err)) {
|
|
159
217
|
return new Response("Not Found", { status: 404 });
|
|
160
218
|
}
|
|
161
219
|
throw err;
|
package/src/vite/rango.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PluginOption } from "vite";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { exposeActionId } from "./plugins/expose-action-id.js";
|
|
5
5
|
import {
|
|
@@ -23,6 +23,7 @@ import { createVersionPlugin } from "./plugins/version-plugin.js";
|
|
|
23
23
|
import {
|
|
24
24
|
sharedRolldownOptions,
|
|
25
25
|
createVirtualEntriesPlugin,
|
|
26
|
+
normalizeHostRouterEntry,
|
|
26
27
|
onwarn,
|
|
27
28
|
getManualChunks,
|
|
28
29
|
} from "./utils/shared-utils.js";
|
|
@@ -266,7 +267,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
266
267
|
|
|
267
268
|
// 1. Explicit host entry wins: serve the createHostRouter() instance.
|
|
268
269
|
if (explicitHostRouter) {
|
|
269
|
-
routerRef.path =
|
|
270
|
+
routerRef.path = normalizeHostRouterEntry(
|
|
271
|
+
explicitHostRouter,
|
|
272
|
+
root,
|
|
273
|
+
existsSync,
|
|
274
|
+
);
|
|
270
275
|
routerRef.kind = "host";
|
|
271
276
|
return;
|
|
272
277
|
}
|
|
@@ -279,7 +284,16 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
279
284
|
// hostRouter.match()).
|
|
280
285
|
const hostCandidates = findHostRouterFiles(root);
|
|
281
286
|
if (hostCandidates.length === 1) {
|
|
282
|
-
|
|
287
|
+
const hostPath = toRootRelative(hostCandidates[0]);
|
|
288
|
+
// Auto-detection preempts single-router discovery, so name the winning
|
|
289
|
+
// file: a stale prototype still containing `createHostRouter(` would
|
|
290
|
+
// otherwise silently become the served entry in place of the app.
|
|
291
|
+
// eslint-disable-next-line no-console
|
|
292
|
+
console.info(
|
|
293
|
+
`[rango] Serving host router entry ${hostPath} (auto-detected). ` +
|
|
294
|
+
`Set the \`hostRouter\` option to override.`,
|
|
295
|
+
);
|
|
296
|
+
routerRef.path = hostPath;
|
|
283
297
|
routerRef.kind = "host";
|
|
284
298
|
return;
|
|
285
299
|
}
|
|
@@ -335,21 +349,17 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
335
349
|
// into the rsc + ssr builds instead of externalizing them (the node
|
|
336
350
|
// default, which only works because `vite preview` runs where
|
|
337
351
|
// node_modules exists). node: builtins stay external automatically.
|
|
352
|
+
//
|
|
353
|
+
// BUILD ONLY. In `vite dev` this must NOT apply: noExternal forces every
|
|
354
|
+
// server dependency through the RSC/SSR dev module runners, which cannot
|
|
355
|
+
// load many CJS packages (pg, mysql2, most SDKs, @vercel/functions) and
|
|
356
|
+
// crashes the dev server -- while the same app runs fine under
|
|
357
|
+
// preset: "node" and in the production build. Gating on `build` restores
|
|
358
|
+
// node-preset dev semantics (deps externalized to Node's require).
|
|
338
359
|
const vercelServerEnv =
|
|
339
|
-
preset === "vercel"
|
|
360
|
+
preset === "vercel" && configEnv.command === "build"
|
|
340
361
|
? { resolve: { noExternal: true as const } }
|
|
341
362
|
: undefined;
|
|
342
|
-
// Dev only: @vercel/functions is a CJS package (consumed at its root for
|
|
343
|
-
// getCache/waitUntil) whose nested internal requires vite's RSC + SSR dev
|
|
344
|
-
// module runners cannot load directly, so a vercel app using
|
|
345
|
-
// VercelCacheStore crashes on `vite dev`. Pre-bundle it for the server
|
|
346
|
-
// environments (the production build bundles it via noExternal above, so
|
|
347
|
-
// this is serve-only). The preset already requires @vercel/functions at
|
|
348
|
-
// build time, so the unconditional include matches the existing contract.
|
|
349
|
-
const vercelServerInclude =
|
|
350
|
-
preset === "vercel" && configEnv.command === "serve"
|
|
351
|
-
? ["@vercel/functions"]
|
|
352
|
-
: [];
|
|
353
363
|
return {
|
|
354
364
|
...(vercelDefine ? { define: vercelDefine } : {}),
|
|
355
365
|
optimizeDeps: {
|
|
@@ -407,7 +417,6 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
407
417
|
nested(
|
|
408
418
|
"@vitejs/plugin-rsc/vendor/react-server-dom/client.edge",
|
|
409
419
|
),
|
|
410
|
-
...vercelServerInclude,
|
|
411
420
|
],
|
|
412
421
|
exclude: excludeDeps,
|
|
413
422
|
rolldownOptions: sharedRolldownOptions,
|
|
@@ -424,7 +433,6 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
424
433
|
nested(
|
|
425
434
|
"@vitejs/plugin-rsc/vendor/react-server-dom/server.edge",
|
|
426
435
|
),
|
|
427
|
-
...vercelServerInclude,
|
|
428
436
|
],
|
|
429
437
|
// Vite 8 does not propagate the top-level optimizeDeps.exclude
|
|
430
438
|
// (set in config()) to non-client envs, so the rsc env must set
|
|
@@ -532,8 +540,10 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
532
540
|
);
|
|
533
541
|
|
|
534
542
|
// Vercel preset: assemble .vercel/output from dist/ after the build. Pushed
|
|
535
|
-
// last so its (
|
|
536
|
-
// rsc-env postprocess
|
|
543
|
+
// last so its buildApp (order "post") hook runs after the discovery plugin's
|
|
544
|
+
// rsc-env postprocess. buildApp fires once after the whole multi-environment
|
|
545
|
+
// build, so dist/ is complete (closeBundle is unusable here -- it fires per
|
|
546
|
+
// environment, twice for ssr; see the plugin's own note).
|
|
537
547
|
if (preset === "vercel") {
|
|
538
548
|
plugins.push(
|
|
539
549
|
createVercelOutputPlugin(resolvedOptions as RangoVercelOptions),
|