@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.
@@ -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`.
@@ -33,6 +33,7 @@ export {
33
33
  findNestedRouterConflict,
34
34
  formatNestedRouterConflictError,
35
35
  findRouterFiles,
36
+ findHostRouterFiles,
36
37
  writeCombinedRouteTypes,
37
38
  genFileTsPath,
38
39
  resolveSearchSchemas,
@@ -54,8 +54,33 @@ function isRoutableSourceFile(name: string): boolean {
54
54
  );
55
55
  }
56
56
 
57
- function findRouterFilesRecursive(
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 routerFilesInDir: string[] = [];
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
- ROUTER_CALL_PATTERN.test(source) &&
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
- // A directory that contains a router file is treated as a router root.
115
- // Once found, deeper directories are skipped to avoid redundant scans.
116
- if (routerFilesInDir.length > 0) {
117
- results.push(...routerFilesInDir);
118
- return;
119
- }
120
-
121
- for (const childDir of childDirs) {
122
- findRouterFilesRecursive(childDir, filter, results);
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
- findRouterFilesRecursive(root, filter, result);
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
 
@@ -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";