@vistagenic/vista 0.2.16 → 0.3.2
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/bin/vista.js +184 -177
- package/dist/ai/agent.d.ts +37 -0
- package/dist/ai/agent.js +385 -0
- package/dist/ai/embeddings.d.ts +11 -0
- package/dist/ai/embeddings.js +71 -0
- package/dist/ai/index.d.ts +10 -0
- package/dist/ai/index.js +26 -0
- package/dist/ai/memory.d.ts +16 -0
- package/dist/ai/memory.js +39 -0
- package/dist/ai/observability.d.ts +21 -0
- package/dist/ai/observability.js +69 -0
- package/dist/ai/providers/anthropic.d.ts +2 -0
- package/dist/ai/providers/anthropic.js +190 -0
- package/dist/ai/providers/base.d.ts +7 -0
- package/dist/ai/providers/base.js +85 -0
- package/dist/ai/providers/gemini.d.ts +2 -0
- package/dist/ai/providers/gemini.js +194 -0
- package/dist/ai/providers/index.d.ts +5 -0
- package/dist/ai/providers/index.js +21 -0
- package/dist/ai/providers/mock.d.ts +8 -0
- package/dist/ai/providers/mock.js +77 -0
- package/dist/ai/providers/openai.d.ts +2 -0
- package/dist/ai/providers/openai.js +211 -0
- package/dist/ai/rag.d.ts +35 -0
- package/dist/ai/rag.js +110 -0
- package/dist/ai/react/index.d.ts +1 -0
- package/dist/ai/react/index.js +17 -0
- package/dist/ai/react/react-server.d.ts +1 -0
- package/dist/ai/react/react-server.js +17 -0
- package/dist/ai/react/use-agent.d.ts +23 -0
- package/dist/ai/react/use-agent.js +138 -0
- package/dist/ai/stream.d.ts +24 -0
- package/dist/ai/stream.js +109 -0
- package/dist/ai/tool.d.ts +11 -0
- package/dist/ai/tool.js +47 -0
- package/dist/ai/types.d.ts +117 -0
- package/dist/ai/types.js +5 -0
- package/dist/auth/core.d.ts +140 -0
- package/dist/auth/core.js +165 -0
- package/dist/auth/index.d.ts +20 -8
- package/dist/auth/index.js +369 -16
- package/dist/auth/react-server.d.ts +1 -0
- package/dist/auth/react-server.js +10 -0
- package/dist/auth/react.d.ts +18 -0
- package/dist/auth/react.js +71 -0
- package/dist/bin/build-rsc.js +602 -604
- package/dist/bin/build.js +389 -388
- package/dist/bin/deploy-output.d.ts +2 -7
- package/dist/bin/deploy-output.js +3 -76
- package/dist/bin/deploy.d.ts +7 -0
- package/dist/bin/deploy.js +102 -0
- package/dist/bin/dev-error-overlay-snippet.js +778 -552
- package/dist/bin/devtools-indicator-snippet.js +387 -387
- package/dist/bin/generate.js +510 -248
- package/dist/build/manifest.d.ts +169 -139
- package/dist/build/manifest.js +423 -367
- package/dist/build/rsc/client-manifest.d.ts +62 -52
- package/dist/build/rsc/client-manifest.js +295 -193
- package/dist/build/rsc/client-reference-plugin.d.ts +37 -37
- package/dist/build/rsc/client-reference-plugin.js +160 -160
- package/dist/build/rsc/native-scanner.d.ts +135 -123
- package/dist/build/rsc/native-scanner.js +203 -170
- package/dist/build/rsc/react-client-reference-manifest.d.ts +28 -22
- package/dist/build/rsc/react-client-reference-manifest.js +240 -219
- package/dist/build/rsc/rsc-renderer.d.ts +99 -99
- package/dist/build/rsc/rsc-renderer.js +263 -263
- package/dist/build/rsc/server-component-loader.d.ts +19 -19
- package/dist/build/rsc/server-component-loader.js +91 -91
- package/dist/build/rsc/server-manifest.d.ts +17 -0
- package/dist/build/rsc/server-manifest.js +20 -0
- package/dist/build/standalone.js +337 -334
- package/dist/build/webpack/plugins/vista-flight-plugin.js +5 -5
- package/dist/client/dynamic.react-server.d.ts +2 -0
- package/dist/client/dynamic.react-server.js +23 -0
- package/dist/client/link.react-server.d.ts +2 -0
- package/dist/client/link.react-server.js +11 -0
- package/dist/client/navigation.react-server.d.ts +1 -0
- package/dist/client/navigation.react-server.js +17 -0
- package/dist/client/router.react-server.d.ts +1 -0
- package/dist/client/router.react-server.js +17 -0
- package/dist/client/rsc-router.react-server.d.ts +1 -0
- package/dist/client/rsc-router.react-server.js +17 -0
- package/dist/client/script.react-server.d.ts +2 -0
- package/dist/client/script.react-server.js +23 -0
- package/dist/components/client-island.d.ts +34 -34
- package/dist/components/client-island.js +75 -75
- package/dist/config.d.ts +17 -0
- package/dist/config.js +60 -1
- package/dist/deploy/adapters/cloudflare.d.ts +2 -0
- package/dist/deploy/adapters/cloudflare.js +124 -0
- package/dist/deploy/adapters/docker.d.ts +2 -0
- package/dist/deploy/adapters/docker.js +112 -0
- package/dist/deploy/adapters/index.d.ts +15 -0
- package/dist/deploy/adapters/index.js +24 -0
- package/dist/deploy/adapters/netlify.d.ts +2 -0
- package/dist/deploy/adapters/netlify.js +115 -0
- package/dist/deploy/adapters/render.d.ts +2 -0
- package/dist/deploy/adapters/render.js +95 -0
- package/dist/deploy/adapters/vercel.d.ts +7 -0
- package/dist/deploy/adapters/vercel.js +164 -0
- package/dist/deploy/cli-runner.d.ts +8 -0
- package/dist/deploy/cli-runner.js +58 -0
- package/dist/deploy/detect.d.ts +6 -0
- package/dist/deploy/detect.js +77 -0
- package/dist/deploy/index.d.ts +24 -0
- package/dist/deploy/index.js +112 -0
- package/dist/deploy/preflight.d.ts +8 -0
- package/dist/deploy/preflight.js +80 -0
- package/dist/deploy/types.d.ts +48 -0
- package/dist/deploy/types.js +2 -0
- package/dist/deploy/utils.d.ts +18 -0
- package/dist/deploy/utils.js +70 -0
- package/dist/dev-error.d.ts +14 -0
- package/dist/dev-error.js +1763 -705
- package/dist/index.d.ts +23 -21
- package/dist/index.js +61 -57
- package/dist/server/cookie-parse.d.ts +4 -0
- package/dist/server/cookie-parse.js +14 -0
- package/dist/server/engine.js +7 -26
- package/dist/server/index.d.ts +109 -102
- package/dist/server/index.js +315 -286
- package/dist/server/middleware-runner.d.ts +125 -57
- package/dist/server/middleware-runner.js +615 -218
- package/dist/server/middleware-security.d.ts +36 -0
- package/dist/server/middleware-security.js +183 -0
- package/dist/server/module-compile-hook.js +695 -662
- package/dist/server/route-handler-registry.d.ts +65 -0
- package/dist/server/route-handler-registry.js +195 -0
- package/dist/server/route-patterns.d.ts +67 -0
- package/dist/server/route-patterns.js +186 -0
- package/dist/server/rsc-engine.js +75 -92
- package/dist/server/rsc-module-system.d.ts +33 -33
- package/dist/server/rsc-module-system.js +87 -87
- package/dist/server/rsc-upstream.js +892 -888
- package/dist/server/static-generator.js +10 -10
- package/dist/server/typed-api-runtime.d.ts +18 -0
- package/dist/server/typed-api-runtime.js +635 -507
- package/dist/server/vista-import-map.js +134 -123
- package/dist/stack/index.d.ts +34 -32
- package/dist/stack/index.js +49 -45
- package/dist/stack/server/caller.d.ts +13 -0
- package/dist/stack/server/caller.js +42 -0
- package/dist/stack/server/executor.d.ts +40 -36
- package/dist/stack/server/executor.js +222 -174
- package/dist/stack/server/index.d.ts +11 -10
- package/dist/stack/server/index.js +24 -23
- package/dist/stack/server/procedure.d.ts +20 -18
- package/dist/stack/server/procedure.js +66 -58
- package/dist/stack/server/types.d.ts +113 -100
- package/dist/theme/react-server.d.ts +10 -0
- package/dist/theme/react-server.js +16 -0
- package/package.json +28 -3
- package/LICENSE +0 -21
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery and resolution of file-based API route handlers (`app/**\/route.{ts,tsx,js,jsx}`).
|
|
3
|
+
*
|
|
4
|
+
* One scan implementation serves both sides of the framework:
|
|
5
|
+
* - the build scanner (build/rsc/server-manifest.ts) records what exists, so route
|
|
6
|
+
* handlers land in the emitted manifests alongside pages
|
|
7
|
+
* - the request path (server/typed-api-runtime.ts) resolves a URL to a handler file
|
|
8
|
+
* plus its dynamic params
|
|
9
|
+
*
|
|
10
|
+
* Keeping them on the same function is what stops the build manifest and the runtime
|
|
11
|
+
* from disagreeing about which files are routes.
|
|
12
|
+
*/
|
|
13
|
+
import { type ParsedRoute, type RouteParams, type RouteSegmentType } from './route-patterns';
|
|
14
|
+
/** Supported HTTP methods for a route handler, in canonical order. */
|
|
15
|
+
export declare const ROUTE_HANDLER_METHODS: readonly ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
16
|
+
export type RouteHandlerMethod = (typeof ROUTE_HANDLER_METHODS)[number];
|
|
17
|
+
export interface DiscoveredRouteHandler {
|
|
18
|
+
/** URL pattern, e.g. `/api/users/:id`. */
|
|
19
|
+
pattern: string;
|
|
20
|
+
/** Absolute path of the route file. */
|
|
21
|
+
filePath: string;
|
|
22
|
+
/** Filesystem segments from `app/` to the route file's directory. */
|
|
23
|
+
sourceSegments: string[];
|
|
24
|
+
/** Route shape, using the same vocabulary as page routes. */
|
|
25
|
+
type: RouteSegmentType;
|
|
26
|
+
/** HTTP methods the file appears to export. */
|
|
27
|
+
methods: RouteHandlerMethod[];
|
|
28
|
+
/** Runtime requested via `export const runtime`, when present. */
|
|
29
|
+
runtime?: string;
|
|
30
|
+
/** Parsed segments, retained so the resolver does not re-parse per request. */
|
|
31
|
+
parsed: ParsedRoute;
|
|
32
|
+
}
|
|
33
|
+
export interface ResolvedRouteHandler {
|
|
34
|
+
filePath: string;
|
|
35
|
+
pattern: string;
|
|
36
|
+
params: RouteParams;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Scan an app directory for route handler files.
|
|
40
|
+
*
|
|
41
|
+
* Results are ordered most-specific first, so the first match during resolution is
|
|
42
|
+
* the correct one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function discoverRouteHandlers(appDir: string): DiscoveredRouteHandler[];
|
|
45
|
+
/**
|
|
46
|
+
* Cached variant of {@link discoverRouteHandlers}.
|
|
47
|
+
*
|
|
48
|
+
* Production builds scan once. Dev re-scans at most every {@link DEV_SCAN_TTL_MS},
|
|
49
|
+
* which keeps newly added route files visible without turning every request into a
|
|
50
|
+
* full directory walk.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getRouteHandlers(appDir: string, options?: {
|
|
53
|
+
isDev?: boolean;
|
|
54
|
+
}): DiscoveredRouteHandler[];
|
|
55
|
+
/** Drop cached discovery results. Exported for tests and for watch-mode invalidation. */
|
|
56
|
+
export declare function clearRouteHandlerCache(appDir?: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a request path to a route handler file and its dynamic params.
|
|
59
|
+
*
|
|
60
|
+
* Returns null when no route file matches, leaving the caller free to fall through to
|
|
61
|
+
* pages, the typed API, or a 404.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveRouteHandler(appDir: string, requestPath: string, options?: {
|
|
64
|
+
isDev?: boolean;
|
|
65
|
+
}): ResolvedRouteHandler | null;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery and resolution of file-based API route handlers (`app/**\/route.{ts,tsx,js,jsx}`).
|
|
4
|
+
*
|
|
5
|
+
* One scan implementation serves both sides of the framework:
|
|
6
|
+
* - the build scanner (build/rsc/server-manifest.ts) records what exists, so route
|
|
7
|
+
* handlers land in the emitted manifests alongside pages
|
|
8
|
+
* - the request path (server/typed-api-runtime.ts) resolves a URL to a handler file
|
|
9
|
+
* plus its dynamic params
|
|
10
|
+
*
|
|
11
|
+
* Keeping them on the same function is what stops the build manifest and the runtime
|
|
12
|
+
* from disagreeing about which files are routes.
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.ROUTE_HANDLER_METHODS = void 0;
|
|
19
|
+
exports.discoverRouteHandlers = discoverRouteHandlers;
|
|
20
|
+
exports.getRouteHandlers = getRouteHandlers;
|
|
21
|
+
exports.clearRouteHandlerCache = clearRouteHandlerCache;
|
|
22
|
+
exports.resolveRouteHandler = resolveRouteHandler;
|
|
23
|
+
const fs_1 = __importDefault(require("fs"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
const route_patterns_1 = require("./route-patterns");
|
|
26
|
+
/** Supported HTTP methods for a route handler, in canonical order. */
|
|
27
|
+
exports.ROUTE_HANDLER_METHODS = [
|
|
28
|
+
'GET',
|
|
29
|
+
'HEAD',
|
|
30
|
+
'POST',
|
|
31
|
+
'PUT',
|
|
32
|
+
'PATCH',
|
|
33
|
+
'DELETE',
|
|
34
|
+
'OPTIONS',
|
|
35
|
+
];
|
|
36
|
+
const ROUTE_FILE_BASENAMES = new Set(['route']);
|
|
37
|
+
const ROUTE_FILE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
38
|
+
const SKIPPED_DIRECTORIES = new Set(['node_modules']);
|
|
39
|
+
/** How long a discovery result is reused in dev before the app dir is re-scanned. */
|
|
40
|
+
const DEV_SCAN_TTL_MS = 250;
|
|
41
|
+
function isRouteFile(fileName) {
|
|
42
|
+
const extension = path_1.default.extname(fileName);
|
|
43
|
+
if (!ROUTE_FILE_EXTENSIONS.has(extension)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return ROUTE_FILE_BASENAMES.has(path_1.default.basename(fileName, extension));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read the exported HTTP methods and requested runtime without executing the module.
|
|
50
|
+
*
|
|
51
|
+
* Regex-based on purpose: this mirrors how build/rsc/server-manifest.ts already reads
|
|
52
|
+
* exports, runs during a filesystem walk, and only feeds the manifest. The runtime
|
|
53
|
+
* dispatch path reads the real module exports, so a miss here degrades the manifest,
|
|
54
|
+
* never the request.
|
|
55
|
+
*/
|
|
56
|
+
function readRouteFileMetadata(filePath) {
|
|
57
|
+
let source = '';
|
|
58
|
+
try {
|
|
59
|
+
source = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { methods: [] };
|
|
63
|
+
}
|
|
64
|
+
const methods = [];
|
|
65
|
+
for (const method of exports.ROUTE_HANDLER_METHODS) {
|
|
66
|
+
const declaration = new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var)\\s+${method}\\b`);
|
|
67
|
+
const braced = new RegExp(`export\\s*\\{[^}]*\\b${method}\\b[^}]*\\}`);
|
|
68
|
+
if (declaration.test(source) || braced.test(source)) {
|
|
69
|
+
methods.push(method);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const runtimeMatch = /export\s+const\s+runtime\s*=\s*['"]([\w-]+)['"]/.exec(source);
|
|
73
|
+
return {
|
|
74
|
+
methods,
|
|
75
|
+
runtime: runtimeMatch ? runtimeMatch[1] : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function walkForRouteFiles(dir, appDir, results) {
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
if (entry.name.startsWith('.') || SKIPPED_DIRECTORIES.has(entry.name)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// Parallel slots and interception routes are page-tree concepts; a route
|
|
93
|
+
// handler underneath one is not reachable by URL.
|
|
94
|
+
if ((0, route_patterns_1.isParallelRouteSegment)(entry.name) || (0, route_patterns_1.isInterceptionRouteSegment)(entry.name)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
walkForRouteFiles(fullPath, appDir, results);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!entry.isFile() || !isRouteFile(entry.name)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const relativeDir = path_1.default.relative(appDir, dir);
|
|
104
|
+
const sourceSegments = relativeDir
|
|
105
|
+
.split(path_1.default.sep)
|
|
106
|
+
.filter((segment) => segment && segment !== '.');
|
|
107
|
+
const parsed = (0, route_patterns_1.parseRouteSegments)(sourceSegments);
|
|
108
|
+
if (!parsed) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const metadata = readRouteFileMetadata(fullPath);
|
|
112
|
+
results.push({
|
|
113
|
+
pattern: parsed.pattern,
|
|
114
|
+
filePath: fullPath,
|
|
115
|
+
sourceSegments,
|
|
116
|
+
type: parsed.type,
|
|
117
|
+
methods: metadata.methods,
|
|
118
|
+
runtime: metadata.runtime,
|
|
119
|
+
parsed,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Scan an app directory for route handler files.
|
|
125
|
+
*
|
|
126
|
+
* Results are ordered most-specific first, so the first match during resolution is
|
|
127
|
+
* the correct one.
|
|
128
|
+
*/
|
|
129
|
+
function discoverRouteHandlers(appDir) {
|
|
130
|
+
if (!appDir || !fs_1.default.existsSync(appDir)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const results = [];
|
|
134
|
+
walkForRouteFiles(appDir, appDir, results);
|
|
135
|
+
// A directory can hold at most one route file; if several extensions exist, keep a
|
|
136
|
+
// deterministic winner rather than letting readdir order decide.
|
|
137
|
+
const byPattern = new Map();
|
|
138
|
+
for (const entry of results) {
|
|
139
|
+
const existing = byPattern.get(entry.pattern);
|
|
140
|
+
if (!existing || entry.filePath.localeCompare(existing.filePath) < 0) {
|
|
141
|
+
byPattern.set(entry.pattern, entry);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return Array.from(byPattern.values()).sort((a, b) => (0, route_patterns_1.compareRouteSpecificity)(a.parsed, b.parsed));
|
|
145
|
+
}
|
|
146
|
+
const discoveryCache = new Map();
|
|
147
|
+
/**
|
|
148
|
+
* Cached variant of {@link discoverRouteHandlers}.
|
|
149
|
+
*
|
|
150
|
+
* Production builds scan once. Dev re-scans at most every {@link DEV_SCAN_TTL_MS},
|
|
151
|
+
* which keeps newly added route files visible without turning every request into a
|
|
152
|
+
* full directory walk.
|
|
153
|
+
*/
|
|
154
|
+
function getRouteHandlers(appDir, options = {}) {
|
|
155
|
+
const cached = discoveryCache.get(appDir);
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
if (cached && (!options.isDev || now - cached.scannedAt < DEV_SCAN_TTL_MS)) {
|
|
158
|
+
return cached.handlers;
|
|
159
|
+
}
|
|
160
|
+
const handlers = discoverRouteHandlers(appDir);
|
|
161
|
+
discoveryCache.set(appDir, { handlers, scannedAt: now });
|
|
162
|
+
return handlers;
|
|
163
|
+
}
|
|
164
|
+
/** Drop cached discovery results. Exported for tests and for watch-mode invalidation. */
|
|
165
|
+
function clearRouteHandlerCache(appDir) {
|
|
166
|
+
if (appDir) {
|
|
167
|
+
discoveryCache.delete(appDir);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
discoveryCache.clear();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Resolve a request path to a route handler file and its dynamic params.
|
|
174
|
+
*
|
|
175
|
+
* Returns null when no route file matches, leaving the caller free to fall through to
|
|
176
|
+
* pages, the typed API, or a 404.
|
|
177
|
+
*/
|
|
178
|
+
function resolveRouteHandler(appDir, requestPath, options = {}) {
|
|
179
|
+
const handlers = getRouteHandlers(appDir, options);
|
|
180
|
+
if (handlers.length === 0) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const requestSegments = (0, route_patterns_1.splitRequestPath)(requestPath);
|
|
184
|
+
for (const handler of handlers) {
|
|
185
|
+
const params = (0, route_patterns_1.matchRouteSegments)(handler.parsed, requestSegments);
|
|
186
|
+
if (params) {
|
|
187
|
+
return {
|
|
188
|
+
filePath: handler.filePath,
|
|
189
|
+
pattern: handler.pattern,
|
|
190
|
+
params,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route pattern parsing and matching for file-based API route handlers.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the build-time scanner (packages/vista/src/build/rsc/server-manifest.ts)
|
|
5
|
+
* and the request-time resolver (packages/vista/src/server/typed-api-runtime.ts) so
|
|
6
|
+
* both sides agree on what `app/api/users/[id]/route.ts` means.
|
|
7
|
+
*
|
|
8
|
+
* Patterns use the same `:name` / `:name*` shape the page router already emits, so
|
|
9
|
+
* `toRegexFromPattern()` in build/manifest.ts consumes them unchanged.
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally filesystem-free and framework-free: it is pure string
|
|
12
|
+
* work, which keeps it cheap to call per request and straightforward to test.
|
|
13
|
+
*/
|
|
14
|
+
/** A single parsed path segment of a route. */
|
|
15
|
+
export type RouteSegment = {
|
|
16
|
+
kind: 'static';
|
|
17
|
+
value: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'dynamic';
|
|
20
|
+
paramName: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'catch-all';
|
|
23
|
+
paramName: string;
|
|
24
|
+
optional: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type RouteSegmentType = 'static' | 'dynamic' | 'catch-all';
|
|
27
|
+
export interface ParsedRoute {
|
|
28
|
+
/** URL pattern, e.g. `/api/users/:id` or `/api/files/:path*`. */
|
|
29
|
+
pattern: string;
|
|
30
|
+
/** Parsed segments in order, route groups already removed. */
|
|
31
|
+
segments: RouteSegment[];
|
|
32
|
+
/** Coarse route shape, matching the vocabulary the page router uses. */
|
|
33
|
+
type: RouteSegmentType;
|
|
34
|
+
}
|
|
35
|
+
export type RouteParams = Record<string, string | string[]>;
|
|
36
|
+
/** `(marketing)` - grouping only, contributes nothing to the URL. */
|
|
37
|
+
export declare function isRouteGroupSegment(segment: string): boolean;
|
|
38
|
+
/** `@modal` - parallel route slot. Not addressable as an API route. */
|
|
39
|
+
export declare function isParallelRouteSegment(segment: string): boolean;
|
|
40
|
+
/** `(.)photo`, `(..)feed`, `(...)root` - interception routes. Not addressable either. */
|
|
41
|
+
export declare function isInterceptionRouteSegment(segment: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Turn filesystem segments (relative to `app/`) into a parsed route.
|
|
44
|
+
*
|
|
45
|
+
* Returns null when the path is not addressable as a URL - a parallel slot or an
|
|
46
|
+
* interception route. Route groups are dropped from the pattern but do not
|
|
47
|
+
* disqualify the route.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseRouteSegments(sourceSegments: string[]): ParsedRoute | null;
|
|
50
|
+
/** Split a request pathname into segments, ignoring query string and trailing slash. */
|
|
51
|
+
export declare function splitRequestPath(requestPath: string): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Match a parsed route against request path segments.
|
|
54
|
+
*
|
|
55
|
+
* Returns the extracted params, or null when the route does not match. A catch-all
|
|
56
|
+
* param is returned as a string array, mirroring the App Router's shape.
|
|
57
|
+
*/
|
|
58
|
+
export declare function matchRouteSegments(route: ParsedRoute, requestSegments: string[]): RouteParams | null;
|
|
59
|
+
/**
|
|
60
|
+
* Ordering for route resolution: the most specific route wins.
|
|
61
|
+
*
|
|
62
|
+
* Static beats dynamic beats catch-all, compared segment by segment, so
|
|
63
|
+
* `/api/users/me` is preferred over `/api/users/[id]`, which is preferred over
|
|
64
|
+
* `/api/users/[...rest]`. Ties fall back to the pattern string for stable,
|
|
65
|
+
* platform-independent ordering.
|
|
66
|
+
*/
|
|
67
|
+
export declare function compareRouteSpecificity(a: ParsedRoute, b: ParsedRoute): number;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Route pattern parsing and matching for file-based API route handlers.
|
|
4
|
+
*
|
|
5
|
+
* Shared by the build-time scanner (packages/vista/src/build/rsc/server-manifest.ts)
|
|
6
|
+
* and the request-time resolver (packages/vista/src/server/typed-api-runtime.ts) so
|
|
7
|
+
* both sides agree on what `app/api/users/[id]/route.ts` means.
|
|
8
|
+
*
|
|
9
|
+
* Patterns use the same `:name` / `:name*` shape the page router already emits, so
|
|
10
|
+
* `toRegexFromPattern()` in build/manifest.ts consumes them unchanged.
|
|
11
|
+
*
|
|
12
|
+
* This module is intentionally filesystem-free and framework-free: it is pure string
|
|
13
|
+
* work, which keeps it cheap to call per request and straightforward to test.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.isRouteGroupSegment = isRouteGroupSegment;
|
|
17
|
+
exports.isParallelRouteSegment = isParallelRouteSegment;
|
|
18
|
+
exports.isInterceptionRouteSegment = isInterceptionRouteSegment;
|
|
19
|
+
exports.parseRouteSegments = parseRouteSegments;
|
|
20
|
+
exports.splitRequestPath = splitRequestPath;
|
|
21
|
+
exports.matchRouteSegments = matchRouteSegments;
|
|
22
|
+
exports.compareRouteSpecificity = compareRouteSpecificity;
|
|
23
|
+
/** `(marketing)` - grouping only, contributes nothing to the URL. */
|
|
24
|
+
function isRouteGroupSegment(segment) {
|
|
25
|
+
return segment.length > 2 && segment.startsWith('(') && segment.endsWith(')');
|
|
26
|
+
}
|
|
27
|
+
/** `@modal` - parallel route slot. Not addressable as an API route. */
|
|
28
|
+
function isParallelRouteSegment(segment) {
|
|
29
|
+
return segment.startsWith('@');
|
|
30
|
+
}
|
|
31
|
+
/** `(.)photo`, `(..)feed`, `(...)root` - interception routes. Not addressable either. */
|
|
32
|
+
function isInterceptionRouteSegment(segment) {
|
|
33
|
+
return /^\(\.{1,3}\)/.test(segment) || segment.startsWith('(..)(..)');
|
|
34
|
+
}
|
|
35
|
+
function parseSegment(segment) {
|
|
36
|
+
// [[...slug]] - optional catch-all, also matches the parent path.
|
|
37
|
+
const optionalCatchAll = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(segment);
|
|
38
|
+
if (optionalCatchAll) {
|
|
39
|
+
return { kind: 'catch-all', paramName: optionalCatchAll[1], optional: true };
|
|
40
|
+
}
|
|
41
|
+
// [...slug] - catch-all, requires at least one segment.
|
|
42
|
+
const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(segment);
|
|
43
|
+
if (catchAll) {
|
|
44
|
+
return { kind: 'catch-all', paramName: catchAll[1], optional: false };
|
|
45
|
+
}
|
|
46
|
+
// [id] - single dynamic segment.
|
|
47
|
+
const dynamic = /^\[([^\].]+)\]$/.exec(segment);
|
|
48
|
+
if (dynamic) {
|
|
49
|
+
return { kind: 'dynamic', paramName: dynamic[1] };
|
|
50
|
+
}
|
|
51
|
+
return { kind: 'static', value: segment };
|
|
52
|
+
}
|
|
53
|
+
function segmentToPatternPart(segment) {
|
|
54
|
+
switch (segment.kind) {
|
|
55
|
+
case 'static':
|
|
56
|
+
return segment.value;
|
|
57
|
+
case 'dynamic':
|
|
58
|
+
return `:${segment.paramName}`;
|
|
59
|
+
case 'catch-all':
|
|
60
|
+
return `:${segment.paramName}*${segment.optional ? '?' : ''}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Turn filesystem segments (relative to `app/`) into a parsed route.
|
|
65
|
+
*
|
|
66
|
+
* Returns null when the path is not addressable as a URL - a parallel slot or an
|
|
67
|
+
* interception route. Route groups are dropped from the pattern but do not
|
|
68
|
+
* disqualify the route.
|
|
69
|
+
*/
|
|
70
|
+
function parseRouteSegments(sourceSegments) {
|
|
71
|
+
const segments = [];
|
|
72
|
+
for (const rawSegment of sourceSegments) {
|
|
73
|
+
if (!rawSegment)
|
|
74
|
+
continue;
|
|
75
|
+
if (isParallelRouteSegment(rawSegment) || isInterceptionRouteSegment(rawSegment)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (isRouteGroupSegment(rawSegment)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
segments.push(parseSegment(rawSegment));
|
|
82
|
+
}
|
|
83
|
+
const hasCatchAll = segments.some((segment) => segment.kind === 'catch-all');
|
|
84
|
+
const hasDynamic = segments.some((segment) => segment.kind === 'dynamic');
|
|
85
|
+
const type = hasCatchAll ? 'catch-all' : hasDynamic ? 'dynamic' : 'static';
|
|
86
|
+
const pattern = segments.length === 0 ? '/' : `/${segments.map(segmentToPatternPart).join('/')}`;
|
|
87
|
+
return { pattern, segments, type };
|
|
88
|
+
}
|
|
89
|
+
/** Split a request pathname into segments, ignoring query string and trailing slash. */
|
|
90
|
+
function splitRequestPath(requestPath) {
|
|
91
|
+
return String(requestPath || '/')
|
|
92
|
+
.split('?')[0]
|
|
93
|
+
.split('#')[0]
|
|
94
|
+
.replace(/\\/g, '/')
|
|
95
|
+
.split('/')
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((segment) => {
|
|
98
|
+
try {
|
|
99
|
+
return decodeURIComponent(segment);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// A malformed escape sequence is matched literally rather than throwing.
|
|
103
|
+
return segment;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Match a parsed route against request path segments.
|
|
109
|
+
*
|
|
110
|
+
* Returns the extracted params, or null when the route does not match. A catch-all
|
|
111
|
+
* param is returned as a string array, mirroring the App Router's shape.
|
|
112
|
+
*/
|
|
113
|
+
function matchRouteSegments(route, requestSegments) {
|
|
114
|
+
const params = {};
|
|
115
|
+
const { segments } = route;
|
|
116
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
117
|
+
const segment = segments[index];
|
|
118
|
+
if (segment.kind === 'catch-all') {
|
|
119
|
+
// A catch-all must be the final segment, and swallows everything left.
|
|
120
|
+
const rest = requestSegments.slice(index);
|
|
121
|
+
if (rest.length === 0) {
|
|
122
|
+
if (!segment.optional) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
params[segment.paramName] = [];
|
|
126
|
+
return params;
|
|
127
|
+
}
|
|
128
|
+
params[segment.paramName] = rest;
|
|
129
|
+
return params;
|
|
130
|
+
}
|
|
131
|
+
const requestSegment = requestSegments[index];
|
|
132
|
+
if (requestSegment === undefined) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (segment.kind === 'static') {
|
|
136
|
+
if (segment.value !== requestSegment) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Dynamic segments must not match an empty value.
|
|
142
|
+
if (requestSegment === '') {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
params[segment.paramName] = requestSegment;
|
|
146
|
+
}
|
|
147
|
+
// Every request segment has to be consumed, otherwise `/a` would match `/a/b`.
|
|
148
|
+
if (requestSegments.length !== segments.length) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return params;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Ordering for route resolution: the most specific route wins.
|
|
155
|
+
*
|
|
156
|
+
* Static beats dynamic beats catch-all, compared segment by segment, so
|
|
157
|
+
* `/api/users/me` is preferred over `/api/users/[id]`, which is preferred over
|
|
158
|
+
* `/api/users/[...rest]`. Ties fall back to the pattern string for stable,
|
|
159
|
+
* platform-independent ordering.
|
|
160
|
+
*/
|
|
161
|
+
function compareRouteSpecificity(a, b) {
|
|
162
|
+
const rank = (segment) => {
|
|
163
|
+
switch (segment.kind) {
|
|
164
|
+
case 'static':
|
|
165
|
+
return 0;
|
|
166
|
+
case 'dynamic':
|
|
167
|
+
return 1;
|
|
168
|
+
case 'catch-all':
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const length = Math.max(a.segments.length, b.segments.length);
|
|
173
|
+
for (let index = 0; index < length; index += 1) {
|
|
174
|
+
const segmentA = a.segments[index];
|
|
175
|
+
const segmentB = b.segments[index];
|
|
176
|
+
if (!segmentA)
|
|
177
|
+
return -1;
|
|
178
|
+
if (!segmentB)
|
|
179
|
+
return 1;
|
|
180
|
+
const difference = rank(segmentA) - rank(segmentB);
|
|
181
|
+
if (difference !== 0) {
|
|
182
|
+
return difference;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return a.pattern.localeCompare(b.pattern);
|
|
186
|
+
}
|