@rangojs/router 0.0.0-experimental.138 → 0.0.0-experimental.139
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 +273 -924
- package/dist/bin/rango.js +28 -14
- package/dist/vite/index.js +472 -77
- package/package.json +33 -19
- 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/host-router/SKILL.md +45 -3
- package/skills/layout/SKILL.md +26 -9
- package/skills/middleware/SKILL.md +6 -2
- package/skills/observability/SKILL.md +23 -1
- package/skills/parallel/SKILL.md +11 -3
- package/skills/rango/SKILL.md +71 -23
- package/skills/route/SKILL.md +19 -3
- package/skills/router-setup/SKILL.md +1 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/vercel/SKILL.md +128 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +77 -27
- package/src/cache/index.ts +12 -0
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +948 -0
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/state.ts +1 -1
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +84 -2
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/virtual-entries.ts +72 -0
- package/src/vite/rango.ts +128 -21
- package/src/vite/utils/shared-utils.ts +52 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vercel
|
|
3
|
+
description: Deploy a Rango app to Vercel Functions (Build Output API v3)
|
|
4
|
+
argument-hint:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Vercel deployment
|
|
8
|
+
|
|
9
|
+
The `vercel` preset builds like the `node` preset (Vercel runs Node Functions, not Workers): rango owns the RSC entry, folds `process.env.NODE_ENV` for the SSR/RSC build, and after `vite build` assembles a `.vercel/output` directory (Build Output API v3) from `dist/` — a single streaming Node Function plus the static client assets.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @vercel/functions
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// vite.config.ts
|
|
19
|
+
import { defineConfig } from "vite";
|
|
20
|
+
import react from "@vitejs/plugin-react";
|
|
21
|
+
import { rango } from "@rangojs/router/vite";
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
plugins: [react(), rango({ preset: "vercel" })],
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`@vercel/functions` is required: it backs the generated function launcher (`waitUntil`) and `VercelCacheStore`. The build fails with a clear error if it is missing.
|
|
29
|
+
|
|
30
|
+
`vite build` produces `.vercel/output`; deploy with the Vercel CLI (`vercel deploy --prebuilt`) or via Git integration.
|
|
31
|
+
|
|
32
|
+
## Function configuration
|
|
33
|
+
|
|
34
|
+
Per-function knobs go under `vercel` and are written into `.vc-config.json`:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
rango({
|
|
38
|
+
preset: "vercel",
|
|
39
|
+
vercel: {
|
|
40
|
+
runtime: "nodejs22.x", // default
|
|
41
|
+
maxDuration: 30, // seconds, default
|
|
42
|
+
memory: 1024, // MB (platform default when omitted)
|
|
43
|
+
regions: ["fra1"], // pin regions (platform default when omitted)
|
|
44
|
+
functionName: "index", // the <name>.func dir + config.json route
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Runtime Cache
|
|
50
|
+
|
|
51
|
+
`VercelCacheStore` wraps the Vercel Runtime Cache. Locally (no `process.env.VERCEL`) fall back to an in-memory store so dev/preview work without the platform:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import {
|
|
55
|
+
MemorySegmentCacheStore,
|
|
56
|
+
VercelCacheStore,
|
|
57
|
+
} from "@rangojs/router/cache";
|
|
58
|
+
import { getCache, waitUntil } from "@vercel/functions";
|
|
59
|
+
|
|
60
|
+
const defaults = { ttl: 60, swr: 300 };
|
|
61
|
+
const memoryStore = new MemorySegmentCacheStore({ defaults });
|
|
62
|
+
|
|
63
|
+
function resolveCache() {
|
|
64
|
+
if (process.env.VERCEL) {
|
|
65
|
+
return {
|
|
66
|
+
store: new VercelCacheStore({
|
|
67
|
+
cache: getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID }),
|
|
68
|
+
waitUntil,
|
|
69
|
+
defaults,
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { store: memoryStore };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const router = createRouter({ cache: resolveCache }).routes(/* ... */);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The cache factory receives `(env, ctx)`; on Vercel `env` is `process.env` and `ctx` is `{ waitUntil }`.
|
|
80
|
+
|
|
81
|
+
## Host routers (multi-app)
|
|
82
|
+
|
|
83
|
+
A multi-app host router deploys as a **single function** running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model). Two requirements:
|
|
84
|
+
|
|
85
|
+
1. The host module exports the `HostRouter` **instance** (default export, or a named `hostRouter`/`router` export) — not a Cloudflare-style `{ fetch }` object, because rango owns the entry and calls `match()` for you.
|
|
86
|
+
2. Point at the host entry (a host app has several `createRouter()` sub-apps, so auto-discovery can't pick one). rango auto-detects a lone `createHostRouter()` file; set `hostRouter` to be explicit:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
rango({ preset: "vercel", hostRouter: "./src/worker.rsc.tsx" });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// src/worker.rsc.tsx
|
|
94
|
+
import { createHostRouter } from "@rangojs/router/host";
|
|
95
|
+
|
|
96
|
+
export const hostRouter = createHostRouter();
|
|
97
|
+
hostRouter.host(["admin.*"]).lazy(() => import("./apps/admin/handler.js"));
|
|
98
|
+
hostRouter.host(["."]).lazy(() => import("./apps/site/handler.js"));
|
|
99
|
+
|
|
100
|
+
export default hostRouter; // the instance
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`{ env, ctx }` is threaded unchanged from the function to each matched sub-app's handler and its `cache(env, ctx)` factory. See the `host-router` skill for sub-app structure and routing patterns.
|
|
104
|
+
|
|
105
|
+
## Tracing (custom spans)
|
|
106
|
+
|
|
107
|
+
Vercel exposes tracing through OpenTelemetry. `createVercelTracing()` (from `@rangojs/router/vercel`) emits the router's `rango.*` phase spans onto the global OTel tracer that `@vercel/otel`'s `registerOTel()` installs:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// instrumentation.ts — install the provider, then export the tracing config so
|
|
111
|
+
// importing this module is what runs registerOTel(). A Rango/Vite app does NOT
|
|
112
|
+
// auto-load `instrumentation.ts` like Next.js does, so a standalone
|
|
113
|
+
// registerOTel() that nothing imports is a silent no-op.
|
|
114
|
+
import { registerOTel } from "@vercel/otel";
|
|
115
|
+
import { createVercelTracing } from "@rangojs/router/vercel";
|
|
116
|
+
registerOTel({ serviceName: "my-app" });
|
|
117
|
+
export const tracing = createVercelTracing();
|
|
118
|
+
|
|
119
|
+
// router.tsx — importing `tracing` runs instrumentation.ts (and registerOTel)
|
|
120
|
+
import { tracing } from "./instrumentation.js";
|
|
121
|
+
export const router = createRouter({ tracing }).routes(/* ... */);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`createVercelTracing(opts?)` takes `{ enabled, spans, tracerName, tracer }` — same phase set as `createCloudflareTracing` (`rango.request/middleware/action/loader/render/ssr`). Caveats: Node-runtime only (Vercel custom spans are unsupported on Edge); `registerOTel()` must run before the first request; `@vercel/otel` is what unlocks Vercel Session Tracing + Trace Drains. The deploy bundles `@vercel/otel` and its `@opentelemetry/*` peers into the function (no `node_modules` at runtime), so they must be installed. See `examples/vercel-basic` for a worked hybrid setup and the `observability` skill for the cross-platform tracing model.
|
|
125
|
+
|
|
126
|
+
## Local validation without deploying
|
|
127
|
+
|
|
128
|
+
`vite preview` serves the static client assets only. To preview the RSC **function**, serve the assembled `.vercel/output` behind filesystem-then-function routing — `examples/vercel-basic/scripts/preview.mjs` does this (and `pnpm preview:vercel` runs it). For a faithful deploy test (isolated filesystem, ESM, self-contained bundle), `examples/vercel-basic/scripts/smoke.mjs` serves it from a temp dir outside the repo. Both share `scripts/serve-vercel-output.mjs`.
|
|
@@ -54,8 +54,33 @@ function isRoutableSourceFile(name: string): boolean {
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function
|
|
57
|
+
function isExcludedScanDir(name: string): boolean {
|
|
58
|
+
return (
|
|
59
|
+
name === "node_modules" ||
|
|
60
|
+
name === "dist" ||
|
|
61
|
+
name === "coverage" ||
|
|
62
|
+
name === "__tests__" ||
|
|
63
|
+
name === "__mocks__" ||
|
|
64
|
+
name.startsWith(".")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Recursively collect source files whose code contains `pattern` (a comment- or
|
|
70
|
+
* string-only mention is ignored via firstCodeMatchIndex). Shared by createRouter
|
|
71
|
+
* and createHostRouter discovery, which differ only in the call pattern and
|
|
72
|
+
* `stopAtMatchDir`: createRouter treats a directory containing a match as a router
|
|
73
|
+
* root and stops descending it; createHostRouter descends the whole tree (the host
|
|
74
|
+
* entry sits above the sub-app router roots).
|
|
75
|
+
*
|
|
76
|
+
* `pattern` is the non-global tester (no lastIndex state); `patternG` is its global
|
|
77
|
+
* twin for the code-region scan.
|
|
78
|
+
*/
|
|
79
|
+
function findCallSiteFilesRecursive(
|
|
58
80
|
dir: string,
|
|
81
|
+
pattern: RegExp,
|
|
82
|
+
patternG: RegExp,
|
|
83
|
+
stopAtMatchDir: boolean,
|
|
59
84
|
filter: ScanFilter | undefined,
|
|
60
85
|
results: string[],
|
|
61
86
|
): void {
|
|
@@ -70,21 +95,12 @@ function findRouterFilesRecursive(
|
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
const childDirs: string[] = [];
|
|
73
|
-
const
|
|
98
|
+
const matchesInDir: string[] = [];
|
|
74
99
|
|
|
75
100
|
for (const entry of entries) {
|
|
76
101
|
const fullPath = join(dir, entry.name);
|
|
77
102
|
if (entry.isDirectory()) {
|
|
78
|
-
if (
|
|
79
|
-
entry.name === "node_modules" ||
|
|
80
|
-
entry.name === "dist" ||
|
|
81
|
-
entry.name === "coverage" ||
|
|
82
|
-
entry.name === "__tests__" ||
|
|
83
|
-
entry.name === "__mocks__" ||
|
|
84
|
-
entry.name.startsWith(".")
|
|
85
|
-
)
|
|
86
|
-
continue;
|
|
87
|
-
childDirs.push(fullPath);
|
|
103
|
+
if (!isExcludedScanDir(entry.name)) childDirs.push(fullPath);
|
|
88
104
|
continue;
|
|
89
105
|
}
|
|
90
106
|
|
|
@@ -100,26 +116,29 @@ function findRouterFilesRecursive(
|
|
|
100
116
|
// so a mention inside a comment or string is not mistaken for a real
|
|
101
117
|
// router file (which previously triggered a spurious "Multiple routers
|
|
102
118
|
// found" error).
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0
|
|
106
|
-
) {
|
|
107
|
-
routerFilesInDir.push(fullPath);
|
|
119
|
+
if (pattern.test(source) && firstCodeMatchIndex(source, patternG) >= 0) {
|
|
120
|
+
matchesInDir.push(fullPath);
|
|
108
121
|
}
|
|
109
122
|
} catch {
|
|
110
123
|
continue;
|
|
111
124
|
}
|
|
112
125
|
}
|
|
113
126
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
results.push(...matchesInDir);
|
|
128
|
+
|
|
129
|
+
// createRouter (stopAtMatchDir): a directory that contains a match is a router
|
|
130
|
+
// root, so deeper directories are skipped. createHostRouter: always descend.
|
|
131
|
+
if (!stopAtMatchDir || matchesInDir.length === 0) {
|
|
132
|
+
for (const childDir of childDirs) {
|
|
133
|
+
findCallSiteFilesRecursive(
|
|
134
|
+
childDir,
|
|
135
|
+
pattern,
|
|
136
|
+
patternG,
|
|
137
|
+
stopAtMatchDir,
|
|
138
|
+
filter,
|
|
139
|
+
results,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
123
142
|
}
|
|
124
143
|
}
|
|
125
144
|
|
|
@@ -552,7 +571,38 @@ export function detectUnresolvableIncludesForUrlsFile(
|
|
|
552
571
|
*/
|
|
553
572
|
export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
|
|
554
573
|
const result: string[] = [];
|
|
555
|
-
|
|
574
|
+
findCallSiteFilesRecursive(
|
|
575
|
+
root,
|
|
576
|
+
ROUTER_CALL_PATTERN,
|
|
577
|
+
ROUTER_CALL_PATTERN_G,
|
|
578
|
+
true,
|
|
579
|
+
filter,
|
|
580
|
+
result,
|
|
581
|
+
);
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const HOST_ROUTER_CALL_PATTERN = /\bcreateHostRouter\s*[<(]/;
|
|
586
|
+
const HOST_ROUTER_CALL_PATTERN_G = /\bcreateHostRouter\s*[<(]/g;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Scan for files containing createHostRouter() and return their paths. Unlike
|
|
590
|
+
* findRouterFiles, this does NOT stop at the first router-root directory -- a host
|
|
591
|
+
* entry typically sits above the sub-app router roots, so the whole tree is scanned.
|
|
592
|
+
*/
|
|
593
|
+
export function findHostRouterFiles(
|
|
594
|
+
root: string,
|
|
595
|
+
filter?: ScanFilter,
|
|
596
|
+
): string[] {
|
|
597
|
+
const result: string[] = [];
|
|
598
|
+
findCallSiteFilesRecursive(
|
|
599
|
+
root,
|
|
600
|
+
HOST_ROUTER_CALL_PATTERN,
|
|
601
|
+
HOST_ROUTER_CALL_PATTERN_G,
|
|
602
|
+
false,
|
|
603
|
+
filter,
|
|
604
|
+
result,
|
|
605
|
+
);
|
|
556
606
|
return result;
|
|
557
607
|
}
|
|
558
608
|
|
package/src/cache/index.ts
CHANGED
|
@@ -24,6 +24,18 @@ export {
|
|
|
24
24
|
KV_READ_TIMEOUT_MS,
|
|
25
25
|
} from "./cf/index.js";
|
|
26
26
|
|
|
27
|
+
export {
|
|
28
|
+
VercelCacheStore,
|
|
29
|
+
type VercelCacheStoreOptions,
|
|
30
|
+
type VercelRuntimeCache,
|
|
31
|
+
type VercelCacheDebug,
|
|
32
|
+
type VercelCacheReadDebugEvent,
|
|
33
|
+
type VercelCacheReadOutcome,
|
|
34
|
+
VERCEL_MAX_ITEM_BYTES,
|
|
35
|
+
VERCEL_MAX_TAGS_PER_ITEM,
|
|
36
|
+
VERCEL_MAX_TAG_BYTES,
|
|
37
|
+
} from "./vercel/index.js";
|
|
38
|
+
|
|
27
39
|
export { CacheScope, createCacheScope } from "./cache-scope.js";
|
|
28
40
|
|
|
29
41
|
export {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
VercelCacheStore,
|
|
3
|
+
type VercelCacheStoreOptions,
|
|
4
|
+
type VercelRuntimeCache,
|
|
5
|
+
type VercelCacheDebug,
|
|
6
|
+
type VercelCacheReadDebugEvent,
|
|
7
|
+
type VercelCacheReadOutcome,
|
|
8
|
+
VERCEL_MAX_ITEM_BYTES,
|
|
9
|
+
VERCEL_MAX_TAGS_PER_ITEM,
|
|
10
|
+
VERCEL_MAX_TAG_BYTES,
|
|
11
|
+
} from "./vercel-cache-store.js";
|