@vistagenic/vista 0.2.2 → 0.2.4
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 +30 -20
- package/dist/bin/build-rsc.js +81 -5
- package/dist/bin/build.js +25 -5
- package/dist/bin/generate.d.ts +7 -0
- package/dist/bin/generate.js +248 -0
- package/dist/build/manifest.js +23 -5
- package/dist/client/link.d.ts +1 -1
- package/dist/client/link.js +30 -11
- package/dist/config.d.ts +19 -0
- package/dist/config.js +62 -4
- package/dist/server/engine.js +23 -57
- package/dist/server/rsc-engine.js +179 -119
- package/dist/server/rsc-upstream.js +24 -19
- package/dist/server/static-generator.js +98 -0
- package/dist/server/structure-validator.js +1 -1
- package/dist/server/typed-api-runtime.d.ts +16 -0
- package/dist/server/typed-api-runtime.js +336 -0
- package/dist/stack/client/create-client.d.ts +2 -0
- package/dist/stack/client/create-client.js +195 -0
- package/dist/stack/client/error.d.ts +18 -0
- package/dist/stack/client/error.js +22 -0
- package/dist/stack/client/index.d.ts +9 -0
- package/dist/stack/client/index.js +13 -0
- package/dist/stack/client/types.d.ts +39 -0
- package/dist/stack/client/types.js +2 -0
- package/dist/stack/index.d.ts +32 -0
- package/dist/stack/index.js +45 -0
- package/dist/stack/server/executor.d.ts +36 -0
- package/dist/stack/server/executor.js +174 -0
- package/dist/stack/server/index.d.ts +10 -0
- package/dist/stack/server/index.js +23 -0
- package/dist/stack/server/merge-routers.d.ts +2 -0
- package/dist/stack/server/merge-routers.js +80 -0
- package/dist/stack/server/procedure.d.ts +18 -0
- package/dist/stack/server/procedure.js +58 -0
- package/dist/stack/server/router.d.ts +9 -0
- package/dist/stack/server/router.js +80 -0
- package/dist/stack/server/serialization.d.ts +9 -0
- package/dist/stack/server/serialization.js +119 -0
- package/dist/stack/server/types.d.ts +100 -0
- package/dist/stack/server/types.js +2 -0
- package/package.json +11 -2
|
@@ -77,6 +77,25 @@ function resolveFromWorkspace(specifier, cwd) {
|
|
|
77
77
|
function normalizeModulePath(filePath) {
|
|
78
78
|
return filePath.replace(/\\/g, '/').toLowerCase();
|
|
79
79
|
}
|
|
80
|
+
function shouldInvalidateDevModule(modulePath, cwd) {
|
|
81
|
+
const normalized = normalizeModulePath(modulePath);
|
|
82
|
+
const rootPrefix = normalizeModulePath(`${cwd}${path_1.default.sep}`);
|
|
83
|
+
if (!normalized.startsWith(rootPrefix))
|
|
84
|
+
return false;
|
|
85
|
+
if (normalized.includes('/node_modules/'))
|
|
86
|
+
return false;
|
|
87
|
+
if (normalized.includes(`/${constants_1.BUILD_DIR.toLowerCase()}/`))
|
|
88
|
+
return false;
|
|
89
|
+
return /\.(?:[cm]?[jt]sx?|json)$/i.test(normalized);
|
|
90
|
+
}
|
|
91
|
+
function clearProjectRequireCache(cwd) {
|
|
92
|
+
for (const key of Object.keys(require.cache)) {
|
|
93
|
+
if (!shouldInvalidateDevModule(key, cwd))
|
|
94
|
+
continue;
|
|
95
|
+
delete require.cache[key];
|
|
96
|
+
clientDirectiveCache.delete(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
80
99
|
function setupTypeScriptRuntime(cwd) {
|
|
81
100
|
try {
|
|
82
101
|
const swcPath = require.resolve('@swc-node/register', { paths: [cwd] });
|
|
@@ -174,18 +193,10 @@ function installSingleReactResolution() {
|
|
|
174
193
|
function installClientLoadHook(cwd, createClientModuleProxy) {
|
|
175
194
|
if (installedClientLoadHook)
|
|
176
195
|
return;
|
|
177
|
-
const appDir = path_1.default.join(cwd, 'app');
|
|
178
|
-
const componentsDir = path_1.default.join(cwd, 'components');
|
|
179
|
-
const normalizedAppDir = normalizeModulePath(appDir);
|
|
180
|
-
const normalizedComponentsDir = normalizeModulePath(componentsDir);
|
|
181
196
|
originalCompile = CjsModule.prototype._compile;
|
|
182
197
|
CjsModule.prototype._compile = function (content, filename) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
const isInComponentsTree = normalized.startsWith(normalizedComponentsDir);
|
|
186
|
-
if ((isInAppTree || isInComponentsTree) &&
|
|
187
|
-
/\.[jt]sx?$/.test(filename) &&
|
|
188
|
-
isClientBoundaryFile(filename, content)) {
|
|
198
|
+
const isJavaScriptModule = /\.[jt]sx?$/.test(filename);
|
|
199
|
+
if (isJavaScriptModule && isClientBoundaryFile(filename, content)) {
|
|
189
200
|
const moduleId = (0, url_1.pathToFileURL)(filename).href;
|
|
190
201
|
this.exports = createClientModuleProxy(moduleId);
|
|
191
202
|
return;
|
|
@@ -254,16 +265,10 @@ function extractParams(pathname, route) {
|
|
|
254
265
|
}
|
|
255
266
|
return params;
|
|
256
267
|
}
|
|
257
|
-
async function createRouteElement(route, context, isDev) {
|
|
268
|
+
async function createRouteElement(route, context, isDev, cwd) {
|
|
258
269
|
const { params, searchParams, req } = context;
|
|
259
270
|
if (isDev) {
|
|
260
|
-
|
|
261
|
-
if (key.includes(`${path_1.default.sep}app${path_1.default.sep}`) ||
|
|
262
|
-
key.includes(`${path_1.default.sep}components${path_1.default.sep}`)) {
|
|
263
|
-
delete require.cache[key];
|
|
264
|
-
clientDirectiveCache.delete(key);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
271
|
+
clearProjectRequireCache(cwd);
|
|
267
272
|
}
|
|
268
273
|
const PageModule = require(route.pagePath);
|
|
269
274
|
const PageComponent = PageModule.default;
|
|
@@ -429,7 +434,7 @@ function startUpstream() {
|
|
|
429
434
|
}
|
|
430
435
|
const params = extractParams(pathname, route);
|
|
431
436
|
const searchParams = Object.fromEntries(new URLSearchParams(req.query).entries());
|
|
432
|
-
const element = await createRouteElement(route, { params, searchParams, req }, isDev);
|
|
437
|
+
const element = await createRouteElement(route, { params, searchParams, req }, isDev, cwd);
|
|
433
438
|
res.setHeader('Content-Type', 'text/x-component');
|
|
434
439
|
res.setHeader('Vary', 'Accept');
|
|
435
440
|
const stream = flightServer.renderToPipeableStream(element, flightManifest, {
|
|
@@ -17,6 +17,102 @@ exports.revalidatePath = revalidatePath;
|
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
18
|
const fs_1 = __importDefault(require("fs"));
|
|
19
19
|
const static_cache_1 = require("./static-cache");
|
|
20
|
+
const CjsModule = require('module');
|
|
21
|
+
let staticRuntimeReady = false;
|
|
22
|
+
let reactResolutionInstalled = false;
|
|
23
|
+
let originalResolveFilename = null;
|
|
24
|
+
function installSingleReactResolution(cwd) {
|
|
25
|
+
if (reactResolutionInstalled)
|
|
26
|
+
return;
|
|
27
|
+
let reactPath;
|
|
28
|
+
let reactDomPath;
|
|
29
|
+
try {
|
|
30
|
+
reactPath = require.resolve('react', { paths: [cwd] });
|
|
31
|
+
reactDomPath = require.resolve('react-dom', { paths: [cwd] });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
try {
|
|
35
|
+
reactPath = require.resolve('react');
|
|
36
|
+
reactDomPath = require.resolve('react-dom');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
originalResolveFilename = CjsModule._resolveFilename;
|
|
43
|
+
CjsModule._resolveFilename = function (request, parent, isMain, options) {
|
|
44
|
+
if (request === 'react')
|
|
45
|
+
return reactPath;
|
|
46
|
+
if (request === 'react-dom')
|
|
47
|
+
return reactDomPath;
|
|
48
|
+
if (request.startsWith('react/')) {
|
|
49
|
+
const subPath = request.slice('react/'.length);
|
|
50
|
+
try {
|
|
51
|
+
return require.resolve(`react/${subPath}`, { paths: [path_1.default.dirname(reactPath)] });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// fall through
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (request.startsWith('react-dom/')) {
|
|
58
|
+
const subPath = request.slice('react-dom/'.length);
|
|
59
|
+
try {
|
|
60
|
+
return require.resolve(`react-dom/${subPath}`, { paths: [path_1.default.dirname(reactDomPath)] });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// fall through
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return originalResolveFilename.call(this, request, parent, isMain, options);
|
|
67
|
+
};
|
|
68
|
+
reactResolutionInstalled = true;
|
|
69
|
+
}
|
|
70
|
+
function setupTypeScriptRuntime(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
const swcPath = require.resolve('@swc-node/register', { paths: [cwd] });
|
|
73
|
+
require(swcPath);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// fallback
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const tsNodePath = require.resolve('ts-node', { paths: [cwd] });
|
|
81
|
+
require(tsNodePath).register({
|
|
82
|
+
transpileOnly: true,
|
|
83
|
+
compilerOptions: {
|
|
84
|
+
module: 'commonjs',
|
|
85
|
+
jsx: 'react-jsx',
|
|
86
|
+
moduleResolution: 'node16',
|
|
87
|
+
esModuleInterop: true,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// fallback
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
require.resolve('tsx', { paths: [cwd] });
|
|
97
|
+
require('tsx/cjs');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// no transpiler available
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function setupStaticGenerationRuntime(cwd) {
|
|
104
|
+
if (staticRuntimeReady)
|
|
105
|
+
return;
|
|
106
|
+
// Ignore CSS imports while requiring app modules for prerender.
|
|
107
|
+
require.extensions['.css'] = (m, filename) => {
|
|
108
|
+
if (filename.endsWith('.module.css')) {
|
|
109
|
+
m.exports = {};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
installSingleReactResolution(cwd);
|
|
113
|
+
setupTypeScriptRuntime(cwd);
|
|
114
|
+
staticRuntimeReady = true;
|
|
115
|
+
}
|
|
20
116
|
// ---------------------------------------------------------------------------
|
|
21
117
|
// Static param expansion
|
|
22
118
|
// ---------------------------------------------------------------------------
|
|
@@ -25,6 +121,7 @@ const static_cache_1 = require("./static-cache");
|
|
|
25
121
|
* and return the list of param sets.
|
|
26
122
|
*/
|
|
27
123
|
async function resolveStaticParams(route, cwd) {
|
|
124
|
+
setupStaticGenerationRuntime(cwd);
|
|
28
125
|
if (!route.hasGenerateStaticParams) {
|
|
29
126
|
return [];
|
|
30
127
|
}
|
|
@@ -80,6 +177,7 @@ function expandPattern(pattern, params) {
|
|
|
80
177
|
* handled by the upstream process.
|
|
81
178
|
*/
|
|
82
179
|
async function prerenderPage(urlPath, route, params, cwd) {
|
|
180
|
+
setupStaticGenerationRuntime(cwd);
|
|
83
181
|
try {
|
|
84
182
|
const React = require('react');
|
|
85
183
|
const { renderToString } = require('react-dom/server');
|
|
@@ -20,7 +20,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
20
20
|
// ============================================================================
|
|
21
21
|
const FILE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'];
|
|
22
22
|
const RESERVED_INTERNAL_SEGMENTS = new Set(['[not-found]']);
|
|
23
|
-
const VALID_SEGMENT_PATTERN = /^[a-zA-Z0-9_\-]+$|^\[\[\.\.\.[\w\-]+\]\]$|^\[[\w\-]+\]$|^\[\.\.\.[\
|
|
23
|
+
const VALID_SEGMENT_PATTERN = /^[a-zA-Z0-9_\-]+$|^\[\[\.\.\.[\w\-]+\]\]$|^\[[\w\-]+\]$|^\[\.\.\.[\w\-]+\]$|^\([\w\-]+\)$/;
|
|
24
24
|
const CONVENTION_FILES = new Set([
|
|
25
25
|
'page',
|
|
26
26
|
'layout',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type express from 'express';
|
|
2
|
+
import type { ResolvedTypedApiConfig } from '../config';
|
|
3
|
+
export declare function resolveLegacyApiRoutePath(cwd: string, requestPath: string): string | null;
|
|
4
|
+
export declare function runLegacyApiRoute(options: {
|
|
5
|
+
req: express.Request;
|
|
6
|
+
res: express.Response;
|
|
7
|
+
apiPath: string;
|
|
8
|
+
isDev: boolean;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
export declare function runTypedApiRoute(options: {
|
|
11
|
+
req: express.Request;
|
|
12
|
+
res: express.Response;
|
|
13
|
+
cwd: string;
|
|
14
|
+
isDev: boolean;
|
|
15
|
+
config: ResolvedTypedApiConfig;
|
|
16
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveLegacyApiRoutePath = resolveLegacyApiRoutePath;
|
|
7
|
+
exports.runLegacyApiRoute = runLegacyApiRoute;
|
|
8
|
+
exports.runTypedApiRoute = runTypedApiRoute;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const server_1 = require("../stack/server");
|
|
12
|
+
const TYPED_API_ENTRYPOINTS = [
|
|
13
|
+
path_1.default.join('app', 'api', 'typed.ts'),
|
|
14
|
+
path_1.default.join('app', 'api', 'typed.tsx'),
|
|
15
|
+
path_1.default.join('app', 'api', 'typed.js'),
|
|
16
|
+
path_1.default.join('app', 'api', 'typed.jsx'),
|
|
17
|
+
path_1.default.join('app', 'typed-api.ts'),
|
|
18
|
+
path_1.default.join('app', 'typed-api.tsx'),
|
|
19
|
+
path_1.default.join('app', 'typed-api.js'),
|
|
20
|
+
path_1.default.join('app', 'typed-api.jsx'),
|
|
21
|
+
];
|
|
22
|
+
class BodyLimitError extends Error {
|
|
23
|
+
status = 413;
|
|
24
|
+
constructor(limitBytes) {
|
|
25
|
+
super(`Typed API body exceeds configured limit (${limitBytes} bytes)`);
|
|
26
|
+
this.name = 'BodyLimitError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class BodyParseError extends Error {
|
|
30
|
+
status = 400;
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'BodyParseError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function isStackRouterLike(value) {
|
|
37
|
+
if (!value || typeof value !== 'object') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const candidate = value;
|
|
41
|
+
return (!!candidate.procedures &&
|
|
42
|
+
!!candidate.routes &&
|
|
43
|
+
!!candidate.metadata &&
|
|
44
|
+
typeof candidate.resolve === 'function');
|
|
45
|
+
}
|
|
46
|
+
function resolveTypedRouterFromModule(mod) {
|
|
47
|
+
const candidates = [
|
|
48
|
+
mod?.default,
|
|
49
|
+
mod?.router,
|
|
50
|
+
mod?.typedRouter,
|
|
51
|
+
mod?.api,
|
|
52
|
+
typeof mod?.createRouter === 'function' ? mod.createRouter() : null,
|
|
53
|
+
typeof mod?.createTypedRouter === 'function' ? mod.createTypedRouter() : null,
|
|
54
|
+
];
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (isStackRouterLike(candidate)) {
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function normalizeApiPath(pathname) {
|
|
63
|
+
if (!pathname.startsWith('/api')) {
|
|
64
|
+
return pathname || '/';
|
|
65
|
+
}
|
|
66
|
+
const stripped = pathname.slice('/api'.length);
|
|
67
|
+
return stripped ? stripped : '/';
|
|
68
|
+
}
|
|
69
|
+
function buildPathCandidates(pathname) {
|
|
70
|
+
const normalized = pathname || '/';
|
|
71
|
+
const apiNormalized = normalizeApiPath(normalized);
|
|
72
|
+
const dedup = new Set([normalized, apiNormalized]);
|
|
73
|
+
return Array.from(dedup);
|
|
74
|
+
}
|
|
75
|
+
function hasMethodMatch(router, pathname, method) {
|
|
76
|
+
const normalized = method.toLowerCase();
|
|
77
|
+
return router.resolve(pathname, normalized) !== null;
|
|
78
|
+
}
|
|
79
|
+
function hasRouteForAnyMethod(router, pathname) {
|
|
80
|
+
return hasMethodMatch(router, pathname, 'get') || hasMethodMatch(router, pathname, 'post');
|
|
81
|
+
}
|
|
82
|
+
async function parseRequestBody(req, bodySizeLimitBytes) {
|
|
83
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let size = 0;
|
|
88
|
+
for await (const chunk of req) {
|
|
89
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
90
|
+
size += buffer.length;
|
|
91
|
+
if (size > bodySizeLimitBytes) {
|
|
92
|
+
throw new BodyLimitError(bodySizeLimitBytes);
|
|
93
|
+
}
|
|
94
|
+
chunks.push(buffer);
|
|
95
|
+
}
|
|
96
|
+
if (chunks.length === 0) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const raw = Buffer.concat(chunks);
|
|
100
|
+
const contentType = String(req.headers['content-type'] || '')
|
|
101
|
+
.split(';')[0]
|
|
102
|
+
.trim()
|
|
103
|
+
.toLowerCase();
|
|
104
|
+
if (!contentType || contentType === 'application/json' || contentType.endsWith('+json')) {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(raw.toString('utf-8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new BodyParseError('Invalid JSON body for typed API request.');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (contentType === 'application/x-www-form-urlencoded') {
|
|
113
|
+
return Object.fromEntries(new URLSearchParams(raw.toString('utf-8')).entries());
|
|
114
|
+
}
|
|
115
|
+
if (contentType.startsWith('text/')) {
|
|
116
|
+
return raw.toString('utf-8');
|
|
117
|
+
}
|
|
118
|
+
return raw;
|
|
119
|
+
}
|
|
120
|
+
async function sendFetchResponse(res, response) {
|
|
121
|
+
response.headers.forEach((value, key) => {
|
|
122
|
+
res.setHeader(key, value);
|
|
123
|
+
});
|
|
124
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
125
|
+
const body = Buffer.from(arrayBuffer);
|
|
126
|
+
res.status(response.status).send(body);
|
|
127
|
+
}
|
|
128
|
+
function getTypedApiEntrypoint(cwd) {
|
|
129
|
+
for (const relativePath of TYPED_API_ENTRYPOINTS) {
|
|
130
|
+
const absolutePath = path_1.default.resolve(cwd, relativePath);
|
|
131
|
+
if (fs_1.default.existsSync(absolutePath)) {
|
|
132
|
+
return absolutePath;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
async function executeTypedRoute(router, options) {
|
|
138
|
+
const pathCandidates = buildPathCandidates(options.req.path);
|
|
139
|
+
const method = options.method.toLowerCase();
|
|
140
|
+
let selectedPath = null;
|
|
141
|
+
let routeExistsForDifferentMethod = false;
|
|
142
|
+
for (const candidate of pathCandidates) {
|
|
143
|
+
if (hasMethodMatch(router, candidate, method)) {
|
|
144
|
+
selectedPath = candidate;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (hasRouteForAnyMethod(router, candidate)) {
|
|
148
|
+
routeExistsForDifferentMethod = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!selectedPath) {
|
|
152
|
+
if (routeExistsForDifferentMethod) {
|
|
153
|
+
return {
|
|
154
|
+
kind: 'method-not-allowed',
|
|
155
|
+
status: 405,
|
|
156
|
+
error: `Method ${method.toUpperCase()} not allowed`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return { kind: 'not-found' };
|
|
160
|
+
}
|
|
161
|
+
const result = await (0, server_1.executeRoute)(router, {
|
|
162
|
+
path: selectedPath,
|
|
163
|
+
method,
|
|
164
|
+
req: {
|
|
165
|
+
method,
|
|
166
|
+
path: selectedPath,
|
|
167
|
+
query: options.query,
|
|
168
|
+
body: options.body,
|
|
169
|
+
headers: options.req.headers,
|
|
170
|
+
originalUrl: options.req.originalUrl,
|
|
171
|
+
url: options.req.url,
|
|
172
|
+
},
|
|
173
|
+
ctx: options.context,
|
|
174
|
+
env: options.env,
|
|
175
|
+
serialization: options.serialization,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
kind: 'handled',
|
|
179
|
+
status: 200,
|
|
180
|
+
payload: result.serializedData,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function resolveLegacyApiRoutePath(cwd, requestPath) {
|
|
184
|
+
if (!requestPath.startsWith('/api/')) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const apiRoute = requestPath.substring('/api/'.length);
|
|
188
|
+
const routeCandidates = [
|
|
189
|
+
path_1.default.resolve(cwd, 'app', 'api', apiRoute, 'route.ts'),
|
|
190
|
+
path_1.default.resolve(cwd, 'app', 'api', apiRoute, 'route.tsx'),
|
|
191
|
+
path_1.default.resolve(cwd, 'app', 'api', apiRoute, 'route.js'),
|
|
192
|
+
path_1.default.resolve(cwd, 'app', 'api', apiRoute, 'route.jsx'),
|
|
193
|
+
path_1.default.resolve(cwd, 'app', 'api', `${apiRoute}.ts`),
|
|
194
|
+
path_1.default.resolve(cwd, 'app', 'api', `${apiRoute}.tsx`),
|
|
195
|
+
path_1.default.resolve(cwd, 'app', 'api', `${apiRoute}.js`),
|
|
196
|
+
path_1.default.resolve(cwd, 'app', 'api', `${apiRoute}.jsx`),
|
|
197
|
+
];
|
|
198
|
+
for (const routePath of routeCandidates) {
|
|
199
|
+
if (fs_1.default.existsSync(routePath)) {
|
|
200
|
+
return routePath;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
async function runLegacyApiRoute(options) {
|
|
206
|
+
const { req, res, apiPath, isDev } = options;
|
|
207
|
+
if (isDev) {
|
|
208
|
+
delete require.cache[require.resolve(apiPath)];
|
|
209
|
+
}
|
|
210
|
+
const apiModule = require(apiPath);
|
|
211
|
+
const method = req.method?.toUpperCase() || 'GET';
|
|
212
|
+
const methodHandler = apiModule[method];
|
|
213
|
+
if (typeof methodHandler === 'function') {
|
|
214
|
+
const request = {
|
|
215
|
+
url: req.protocol + '://' + req.get('host') + req.originalUrl,
|
|
216
|
+
method: req.method,
|
|
217
|
+
headers: new Map(Object.entries(req.headers)),
|
|
218
|
+
json: async () => req.body,
|
|
219
|
+
text: async () => JSON.stringify(req.body),
|
|
220
|
+
nextUrl: {
|
|
221
|
+
pathname: req.path,
|
|
222
|
+
searchParams: new URLSearchParams(req.query),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const result = await methodHandler(request, { params: {} });
|
|
226
|
+
if (result && typeof result.json === 'function') {
|
|
227
|
+
const json = await result.json();
|
|
228
|
+
res.status(result.status || 200).json(json);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (result !== undefined) {
|
|
232
|
+
res.status(200).json(result);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
res.status(204).end();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (typeof apiModule.default === 'function') {
|
|
239
|
+
apiModule.default(req, res);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
res.status(405).json({ error: `Method ${method} not allowed` });
|
|
243
|
+
}
|
|
244
|
+
async function runTypedApiRoute(options) {
|
|
245
|
+
const { req, res, cwd, isDev, config } = options;
|
|
246
|
+
if (!config.enabled) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
const entrypoint = getTypedApiEntrypoint(cwd);
|
|
250
|
+
if (!entrypoint) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
if (isDev) {
|
|
255
|
+
delete require.cache[require.resolve(entrypoint)];
|
|
256
|
+
}
|
|
257
|
+
const typedModule = require(entrypoint);
|
|
258
|
+
const router = resolveTypedRouterFromModule(typedModule);
|
|
259
|
+
if (!router) {
|
|
260
|
+
res.status(500).json({
|
|
261
|
+
error: `Typed API entrypoint "${path_1.default.relative(cwd, entrypoint)}" does not export a valid stack router.`,
|
|
262
|
+
});
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
266
|
+
const body = await parseRequestBody(req, config.bodySizeLimitBytes);
|
|
267
|
+
const query = (req.query ?? {});
|
|
268
|
+
const contextFactory = typeof typedModule.createContext === 'function' ? typedModule.createContext : null;
|
|
269
|
+
const envFactory = typeof typedModule.createEnv === 'function' ? typedModule.createEnv : null;
|
|
270
|
+
const context = contextFactory ? await contextFactory({ req, res }) : {};
|
|
271
|
+
const env = envFactory ? await envFactory({ req, res }) : {};
|
|
272
|
+
const routeResult = await executeTypedRoute(router, {
|
|
273
|
+
req,
|
|
274
|
+
method,
|
|
275
|
+
query,
|
|
276
|
+
body,
|
|
277
|
+
serialization: config.serialization,
|
|
278
|
+
context: context ?? {},
|
|
279
|
+
env,
|
|
280
|
+
});
|
|
281
|
+
if (routeResult.kind === 'not-found') {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
if (routeResult.kind === 'method-not-allowed') {
|
|
285
|
+
res.status(routeResult.status).json({ error: routeResult.error });
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
res.status(routeResult.status).json(routeResult.payload);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
const typedError = error;
|
|
293
|
+
if (typedError instanceof BodyLimitError || typedError instanceof BodyParseError) {
|
|
294
|
+
res.status(typedError.status).json({ error: typedError.message });
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (typedError instanceof server_1.StackValidationError ||
|
|
298
|
+
typedError instanceof server_1.StackMethodNotAllowedError) {
|
|
299
|
+
const status = typeof typedError.status === 'number' ? typedError.status : 400;
|
|
300
|
+
res.status(status).json({ error: typedError.message });
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (typedError instanceof server_1.StackRouteNotFoundError) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
// Router-level error handler gets first chance.
|
|
307
|
+
try {
|
|
308
|
+
const entrypoint = getTypedApiEntrypoint(cwd);
|
|
309
|
+
if (entrypoint) {
|
|
310
|
+
if (isDev) {
|
|
311
|
+
delete require.cache[require.resolve(entrypoint)];
|
|
312
|
+
}
|
|
313
|
+
const typedModule = require(entrypoint);
|
|
314
|
+
const router = resolveTypedRouterFromModule(typedModule);
|
|
315
|
+
const errorHandler = router?.metadata?.errorHandler;
|
|
316
|
+
if (typeof errorHandler === 'function') {
|
|
317
|
+
const response = errorHandler(error, {
|
|
318
|
+
method: req.method,
|
|
319
|
+
path: req.path,
|
|
320
|
+
query: (req.query ?? {}),
|
|
321
|
+
headers: req.headers,
|
|
322
|
+
});
|
|
323
|
+
if (response instanceof Response) {
|
|
324
|
+
await sendFetchResponse(res, response);
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Ignore fallback handler errors and use generic 500 response below.
|
|
332
|
+
}
|
|
333
|
+
res.status(500).json({ error: 'Internal Server Error in Typed API' });
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
}
|