theokit 0.2.0 → 0.2.1
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/dist/actions-virtual-module-E6PU47GV.js +175 -0
- package/dist/actions-virtual-module-E6PU47GV.js.map +1 -0
- package/dist/actions-virtual-module-E7IELVHL.js +174 -0
- package/dist/actions-virtual-module-E7IELVHL.js.map +1 -0
- package/dist/app-typed-client-4L4L4BWD.js +289 -0
- package/dist/app-typed-client-4L4L4BWD.js.map +1 -0
- package/dist/app-typed-client-6PAT6VOL.js +289 -0
- package/dist/app-typed-client-6PAT6VOL.js.map +1 -0
- package/dist/{build-IQQYCA3B.js → build-EWZDEDT3.js} +37 -11
- package/dist/build-EWZDEDT3.js.map +1 -0
- package/dist/build-request-body-preview-EOP2Y4F3.js +52 -0
- package/dist/build-request-body-preview-EOP2Y4F3.js.map +1 -0
- package/dist/build-request-body-preview-II2235E6.js +54 -0
- package/dist/build-request-body-preview-II2235E6.js.map +1 -0
- package/dist/{chunk-GQSTRXXB.js → chunk-2BQYEBCK.js} +77 -68
- package/dist/chunk-2BQYEBCK.js.map +1 -0
- package/dist/{chunk-FRBYJANT.js → chunk-4W447H7M.js} +195 -41
- package/dist/chunk-4W447H7M.js.map +1 -0
- package/dist/chunk-7DHGV77J.js +140 -0
- package/dist/chunk-7DHGV77J.js.map +1 -0
- package/dist/{chunk-WQCPKA2L.js → chunk-C27AZN46.js} +570 -165
- package/dist/chunk-C27AZN46.js.map +1 -0
- package/dist/chunk-ERAZNHHR.js +73 -0
- package/dist/chunk-ERAZNHHR.js.map +1 -0
- package/dist/chunk-F3TG5FJV.js +97 -0
- package/dist/chunk-F3TG5FJV.js.map +1 -0
- package/dist/{chunk-OK2PJOLK.js → chunk-KJVEJILQ.js} +7 -3
- package/dist/chunk-KJVEJILQ.js.map +1 -0
- package/dist/{chunk-JBC7PFV4.js → chunk-M23FP2AS.js} +2 -2
- package/dist/chunk-M23FP2AS.js.map +1 -0
- package/dist/{chunk-S3JBIFKS.js → chunk-MG7H3LAW.js} +2 -2
- package/dist/chunk-MOT42NAH.js +165 -0
- package/dist/chunk-MOT42NAH.js.map +1 -0
- package/dist/{chunk-OEB3OIYP.js → chunk-OJHO2X7C.js} +497 -168
- package/dist/chunk-OJHO2X7C.js.map +1 -0
- package/dist/chunk-QLGES2HC.js +189 -0
- package/dist/chunk-QLGES2HC.js.map +1 -0
- package/dist/{chunk-JZ5WEG3S.js → chunk-SEVYARIU.js} +9 -5
- package/dist/chunk-SEVYARIU.js.map +1 -0
- package/dist/{chunk-ZK5JCE5J.js → chunk-TFECHXR5.js} +201 -43
- package/dist/chunk-TFECHXR5.js.map +1 -0
- package/dist/chunk-WSJKACWB.js +17 -0
- package/dist/chunk-WSJKACWB.js.map +1 -0
- package/dist/{chunk-3Q36SEDK.js → chunk-ZBWROIIO.js} +43 -2
- package/dist/{chunk-3Q36SEDK.js.map → chunk-ZBWROIIO.js.map} +1 -1
- package/dist/chunk-ZOXNJV2O.js +358 -0
- package/dist/chunk-ZOXNJV2O.js.map +1 -0
- package/dist/cli/index.js +22 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.d.ts +31 -1
- package/dist/client/index.js +100 -0
- package/dist/client/index.js.map +1 -1
- package/dist/{dev-VN6WS3CM.js → dev-74KPI5C5.js} +15 -7
- package/dist/dev-74KPI5C5.js.map +1 -0
- package/dist/dev-emit-3CHM73TV.js +390 -0
- package/dist/dev-emit-3CHM73TV.js.map +1 -0
- package/dist/dev-emit-I3CKOHML.js +44 -0
- package/dist/dev-emit-I3CKOHML.js.map +1 -0
- package/dist/devtools/entry.js +545 -181
- package/dist/devtools/entry.js.map +1 -1
- package/dist/{dispatcher-IK3FVMX5.js → dispatcher-AQJGI3HU.js} +2 -2
- package/dist/dispatcher-E6QV32BO.js +83 -0
- package/dist/dispatcher-E6QV32BO.js.map +1 -0
- package/dist/{dist-Q5VBFWVL.js → dist-R2Q7GVDW.js} +10357 -12068
- package/dist/dist-R2Q7GVDW.js.map +1 -0
- package/dist/{generate-SIO4JSKJ.js → generate-DT4IIAHF.js} +88 -22
- package/dist/generate-DT4IIAHF.js.map +1 -0
- package/dist/index.d.ts +108 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/{info-B3UOXFUF.js → info-VM7EK4JC.js} +3 -3
- package/dist/openapi-CBASKC7W.js +84 -0
- package/dist/openapi-CBASKC7W.js.map +1 -0
- package/dist/{registry-BB3UYAOG.js → registry-QGSNLJKJ.js} +2 -2
- package/dist/{routes-CAGFZKUK.js → routes-7O4NUDYS.js} +9 -7
- package/dist/{routes-CAGFZKUK.js.map → routes-7O4NUDYS.js.map} +1 -1
- package/dist/{schema-Cxq2qqwh.d.ts → schema-CjVly9c_.d.ts} +43 -1
- package/dist/{schema-MVB2FVK6.js → schema-JAKF23GR.js} +2 -2
- package/dist/server/cost/index.js +1 -1
- package/dist/server/index.d.ts +219 -47
- package/dist/server/index.js +35 -116
- package/dist/server/index.js.map +1 -1
- package/dist/{start-ZGWW5MOL.js → start-5JTFWF4T.js} +20 -14
- package/dist/start-5JTFWF4T.js.map +1 -0
- package/dist/{static-BH6JQNM2.js → static-6PZZBY7G.js} +4 -4
- package/dist/{upgrade-readiness-OHL36FQL.js → upgrade-readiness-Q46KTG2J.js} +6 -1
- package/dist/upgrade-readiness-Q46KTG2J.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +25 -1
- package/dist/vite-plugin/index.js +5 -2
- package/dist/{vite-plugin-WA3G45YY.js → vite-plugin-3EQP2JYM.js} +7 -6
- package/package.json +4 -2
- package/dist/build-IQQYCA3B.js.map +0 -1
- package/dist/chunk-FRBYJANT.js.map +0 -1
- package/dist/chunk-GQSTRXXB.js.map +0 -1
- package/dist/chunk-JBC7PFV4.js.map +0 -1
- package/dist/chunk-JZ5WEG3S.js.map +0 -1
- package/dist/chunk-OEB3OIYP.js.map +0 -1
- package/dist/chunk-OK2PJOLK.js.map +0 -1
- package/dist/chunk-WQCPKA2L.js.map +0 -1
- package/dist/chunk-ZK5JCE5J.js.map +0 -1
- package/dist/dev-VN6WS3CM.js.map +0 -1
- package/dist/dist-Q5VBFWVL.js.map +0 -1
- package/dist/generate-SIO4JSKJ.js.map +0 -1
- package/dist/start-ZGWW5MOL.js.map +0 -1
- package/dist/upgrade-readiness-OHL36FQL.js.map +0 -1
- /package/dist/{chunk-S3JBIFKS.js.map → chunk-MG7H3LAW.js.map} +0 -0
- /package/dist/{dispatcher-IK3FVMX5.js.map → dispatcher-AQJGI3HU.js.map} +0 -0
- /package/dist/{info-B3UOXFUF.js.map → info-VM7EK4JC.js.map} +0 -0
- /package/dist/{registry-BB3UYAOG.js.map → registry-QGSNLJKJ.js.map} +0 -0
- /package/dist/{schema-MVB2FVK6.js.map → schema-JAKF23GR.js.map} +0 -0
- /package/dist/{static-BH6JQNM2.js.map → static-6PZZBY7G.js.map} +0 -0
- /package/dist/{vite-plugin-WA3G45YY.js.map → vite-plugin-3EQP2JYM.js.map} +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "tsx/esm";
|
|
3
|
+
import {
|
|
4
|
+
walkSourceFiles
|
|
5
|
+
} from "./chunk-MOT42NAH.js";
|
|
6
|
+
|
|
7
|
+
// src/core/contracts/http-methods.ts
|
|
8
|
+
var HTTP_METHODS = [
|
|
9
|
+
"GET",
|
|
10
|
+
"POST",
|
|
11
|
+
"PUT",
|
|
12
|
+
"PATCH",
|
|
13
|
+
"DELETE",
|
|
14
|
+
"HEAD",
|
|
15
|
+
"OPTIONS"
|
|
16
|
+
];
|
|
17
|
+
var HTTP_METHOD_LOWERCASE = HTTP_METHODS.map((m) => m.toLowerCase());
|
|
18
|
+
|
|
19
|
+
// src/server/scan/scan.ts
|
|
20
|
+
import { existsSync, statSync } from "fs";
|
|
21
|
+
import { extname, join, relative } from "path";
|
|
22
|
+
|
|
23
|
+
// src/server/scan/detect-http-methods.ts
|
|
24
|
+
import { readFileSync } from "fs";
|
|
25
|
+
import { createRequire } from "module";
|
|
26
|
+
var require_ = createRequire(import.meta.url);
|
|
27
|
+
var ts = require_("typescript");
|
|
28
|
+
var HTTP_METHOD_NAMES = new Set(HTTP_METHODS);
|
|
29
|
+
function hasExportModifier(modifiers) {
|
|
30
|
+
if (!modifiers) return false;
|
|
31
|
+
for (const m of modifiers) {
|
|
32
|
+
if (m.kind === ts.SyntaxKind.ExportKeyword) return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function collectFromStatement(stmt, found) {
|
|
37
|
+
if (ts.isVariableStatement(stmt) && hasExportModifier(ts.getModifiers(stmt))) {
|
|
38
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
39
|
+
if (ts.isIdentifier(decl.name) && HTTP_METHOD_NAMES.has(decl.name.text)) {
|
|
40
|
+
found.add(decl.name.text);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && hasExportModifier(ts.getModifiers(stmt))) {
|
|
46
|
+
if (stmt.name && HTTP_METHOD_NAMES.has(stmt.name.text)) {
|
|
47
|
+
found.add(stmt.name.text);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (ts.isExportDeclaration(stmt) && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
52
|
+
for (const spec of stmt.exportClause.elements) {
|
|
53
|
+
if (HTTP_METHOD_NAMES.has(spec.name.text)) {
|
|
54
|
+
found.add(spec.name.text);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function detectExportedHttpMethods(filePath, content) {
|
|
60
|
+
const src = content ?? readFileSync(filePath, "utf-8");
|
|
61
|
+
const sourceFile = ts.createSourceFile(
|
|
62
|
+
filePath,
|
|
63
|
+
src,
|
|
64
|
+
ts.ScriptTarget.Latest,
|
|
65
|
+
/* setParentNodes */
|
|
66
|
+
false,
|
|
67
|
+
ts.ScriptKind.TS
|
|
68
|
+
);
|
|
69
|
+
const found = /* @__PURE__ */ new Set();
|
|
70
|
+
for (const stmt of sourceFile.statements) {
|
|
71
|
+
collectFromStatement(stmt, found);
|
|
72
|
+
}
|
|
73
|
+
return [...found].sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/server/scan/match.ts
|
|
77
|
+
function compilePattern(routePath) {
|
|
78
|
+
const paramNames = [];
|
|
79
|
+
const regexStr = routePath.replace(/:(?:\.\.\.)?([^/]+)/g, (match, name) => {
|
|
80
|
+
paramNames.push(name);
|
|
81
|
+
return match.startsWith(":...") ? "(.+)" : "([^/]+)";
|
|
82
|
+
});
|
|
83
|
+
return { pattern: new RegExp(`^${regexStr}$`), paramNames };
|
|
84
|
+
}
|
|
85
|
+
function matchRoute(url, routes) {
|
|
86
|
+
let path = url.split("?")[0];
|
|
87
|
+
if (path.length > 1 && path.endsWith("/")) {
|
|
88
|
+
path = path.slice(0, -1);
|
|
89
|
+
}
|
|
90
|
+
for (const route of routes) {
|
|
91
|
+
const match = route.pattern.exec(path);
|
|
92
|
+
if (match) {
|
|
93
|
+
const params = {};
|
|
94
|
+
route.paramNames.forEach((name, i) => {
|
|
95
|
+
params[name] = match[i + 1];
|
|
96
|
+
});
|
|
97
|
+
return { route, params };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/server/scan/scan.ts
|
|
104
|
+
var ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
105
|
+
function fileToRoutePath(filePath, routesDir) {
|
|
106
|
+
let rel = relative(routesDir, filePath);
|
|
107
|
+
const ext = extname(rel);
|
|
108
|
+
rel = rel.slice(0, -ext.length);
|
|
109
|
+
rel = rel.replace(/\\/g, "/");
|
|
110
|
+
if (rel.endsWith("/index")) {
|
|
111
|
+
rel = rel.slice(0, -6);
|
|
112
|
+
} else if (rel === "index") {
|
|
113
|
+
rel = "";
|
|
114
|
+
}
|
|
115
|
+
rel = rel.replace(/\[\.\.\.([^\]]+)\]/g, ":...$1");
|
|
116
|
+
rel = rel.replace(/\[([^\]]+)\]/g, ":$1");
|
|
117
|
+
return `/api/${rel}`;
|
|
118
|
+
}
|
|
119
|
+
function scanServerRoutes(serverDir) {
|
|
120
|
+
const routesDir = join(serverDir, "routes");
|
|
121
|
+
if (!existsSync(routesDir) || !statSync(routesDir).isDirectory()) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const results = [];
|
|
125
|
+
walkSourceFiles(routesDir, { extensions: ROUTE_EXTENSIONS }, (absPath) => {
|
|
126
|
+
const routePath = fileToRoutePath(absPath, routesDir);
|
|
127
|
+
const { pattern, paramNames } = compilePattern(routePath);
|
|
128
|
+
const methods = detectExportedHttpMethods(absPath);
|
|
129
|
+
results.push({
|
|
130
|
+
filePath: absPath,
|
|
131
|
+
routePath,
|
|
132
|
+
paramNames,
|
|
133
|
+
pattern,
|
|
134
|
+
methods
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
const conflicting = results.find((r) => r.routePath === "/api/__theo_batch__");
|
|
138
|
+
if (conflicting) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Server route ${conflicting.filePath} resolves to '/api/__theo_batch__' which is reserved for the batch endpoint. Rename the route or disable batching in theo.config.ts.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const isCatchAll = (route) => route.routePath.includes(":...");
|
|
144
|
+
results.sort((a, b) => {
|
|
145
|
+
const aStatic = a.paramNames.length === 0;
|
|
146
|
+
const bStatic = b.paramNames.length === 0;
|
|
147
|
+
const aCatchAll = isCatchAll(a);
|
|
148
|
+
const bCatchAll = isCatchAll(b);
|
|
149
|
+
if (aStatic && !bStatic) return -1;
|
|
150
|
+
if (!aStatic && bStatic) return 1;
|
|
151
|
+
if (aCatchAll && !bCatchAll) return 1;
|
|
152
|
+
if (!aCatchAll && bCatchAll) return -1;
|
|
153
|
+
return a.routePath.localeCompare(b.routePath);
|
|
154
|
+
});
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/server/scan/ws-scan.ts
|
|
159
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
160
|
+
import { extname as extname2, join as join2, relative as relative2 } from "path";
|
|
161
|
+
var WS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
162
|
+
function scanWebSocketRoutes(serverDir) {
|
|
163
|
+
const wsDir = join2(serverDir, "ws");
|
|
164
|
+
if (!existsSync2(wsDir) || !statSync2(wsDir).isDirectory()) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const results = [];
|
|
168
|
+
walkSourceFiles(wsDir, { extensions: WS_EXTENSIONS }, (absPath) => {
|
|
169
|
+
let rel = relative2(wsDir, absPath);
|
|
170
|
+
rel = rel.replace(/\\/g, "/");
|
|
171
|
+
rel = rel.slice(0, -extname2(rel).length);
|
|
172
|
+
if (rel.endsWith("/index")) rel = rel.slice(0, -6);
|
|
173
|
+
else if (rel === "index") rel = "";
|
|
174
|
+
results.push({
|
|
175
|
+
filePath: absPath,
|
|
176
|
+
wsPath: `/ws/${rel}`
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
return results;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export {
|
|
183
|
+
compilePattern,
|
|
184
|
+
matchRoute,
|
|
185
|
+
HTTP_METHODS,
|
|
186
|
+
scanServerRoutes,
|
|
187
|
+
scanWebSocketRoutes
|
|
188
|
+
};
|
|
189
|
+
//# sourceMappingURL=chunk-QLGES2HC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/contracts/http-methods.ts","../src/server/scan/scan.ts","../src/server/scan/detect-http-methods.ts","../src/server/scan/match.ts","../src/server/scan/ws-scan.ts"],"sourcesContent":["/**\n * Canonical HTTP method set used by the typed client codegen + Proxy runtime.\n * Kept here (under `server/scan/`) because the SCAN is the first consumer —\n * `vite-plugin/app-typed-client.ts` (Phase 2) and `client/app-client.ts` (Phase 3)\n * import from here to ensure a single source of truth.\n *\n * Lowercase variants are derived via `.toLowerCase()` at call sites; we do not\n * export them separately to avoid drift between the two lists.\n */\n\nexport const HTTP_METHODS = [\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'HEAD',\n 'OPTIONS',\n] as const\n\nexport type HttpMethod = (typeof HTTP_METHODS)[number]\n\nexport const HTTP_METHOD_LOWERCASE = HTTP_METHODS.map((m) => m.toLowerCase()) as readonly string[]\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks `serverDir/routes/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, statSync } from 'node:fs'\nimport { extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nimport { detectExportedHttpMethods } from './detect-http-methods.js'\nimport { compilePattern, type ServerRouteNode } from './match.js'\n\nconst ROUTE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\n\nfunction fileToRoutePath(filePath: string, routesDir: string): string {\n let rel = relative(routesDir, filePath)\n // Strip extension\n const ext = extname(rel)\n rel = rel.slice(0, -ext.length)\n // Normalize separators\n rel = rel.replace(/\\\\/g, '/')\n // Strip index suffix\n if (rel.endsWith('/index')) {\n rel = rel.slice(0, -6)\n } else if (rel === 'index') {\n rel = ''\n }\n // Replace [...param] with :...param (catch-all, before regular params).\n // Replace [param] with :param. Inputs are file paths bounded by the\n // OS filename limit; the bracket capture is bounded by `]`.\n rel = rel.replace(/\\[\\.\\.\\.([^\\]]+)\\]/g, ':...$1')\n // eslint-disable-next-line sonarjs/slow-regex -- bounded by `]`; input is a single filename\n rel = rel.replace(/\\[([^\\]]+)\\]/g, ':$1')\n return `/api/${rel}`\n}\n\nexport function scanServerRoutes(serverDir: string): ServerRouteNode[] {\n const routesDir = join(serverDir, 'routes')\n if (!existsSync(routesDir) || !statSync(routesDir).isDirectory()) {\n return []\n }\n\n const results: ServerRouteNode[] = []\n walkSourceFiles(routesDir, { extensions: ROUTE_EXTENSIONS }, (absPath) => {\n const routePath = fileToRoutePath(absPath, routesDir)\n const { pattern, paramNames } = compilePattern(routePath)\n const methods = detectExportedHttpMethods(absPath)\n results.push({\n filePath: absPath,\n routePath,\n paramNames,\n pattern,\n methods,\n })\n })\n\n // T1.4 / EC-2 — refuse to scan if a user route collides with the reserved\n // batch endpoint path. User must rename or disable batching.\n const conflicting = results.find((r) => r.routePath === '/api/__theo_batch__')\n if (conflicting) {\n throw new Error(\n `Server route ${conflicting.filePath} resolves to '/api/__theo_batch__' which is reserved for the batch endpoint. Rename the route or disable batching in theo.config.ts.`,\n )\n }\n\n // Sort: static first, then dynamic, then catch-all last\n const isCatchAll = (route: ServerRouteNode) => route.routePath.includes(':...')\n results.sort((a, b) => {\n const aStatic = a.paramNames.length === 0\n const bStatic = b.paramNames.length === 0\n const aCatchAll = isCatchAll(a)\n const bCatchAll = isCatchAll(b)\n\n // Static routes first\n if (aStatic && !bStatic) return -1\n if (!aStatic && bStatic) return 1\n // Catch-all routes last\n if (aCatchAll && !bCatchAll) return 1\n if (!aCatchAll && bCatchAll) return -1\n return a.routePath.localeCompare(b.routePath)\n })\n\n return results\n}\n","/**\n * Detect which HTTP-method named exports a route file declares.\n *\n * Uses the TypeScript compiler API (not regex) per G1 edge-case review EC-4:\n * regex over file content emits false positives for `// export const GET = ...`\n * in comments and `` `export const GET = ...` `` in template literals. AST\n * walking avoids both classes of bug.\n *\n * `typescript` ships as CommonJS with internal dynamic `require('fs')`.\n * When loaded via ESM `import`, the dynamic requires fail at module-bootstrap.\n * We use `createRequire(import.meta.url)` to keep the package on its native\n * CJS path. The type-only namespace import gives us the AST helpers shape.\n *\n * Returns the set of HTTP methods (uppercase) the file exports. Empty array\n * means the file has no HTTP exports (the route file is util-only).\n */\n\nimport { readFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\n\nimport type * as TS from 'typescript'\n\nimport { HTTP_METHODS, type HttpMethod } from '../../core/contracts/http-methods.js'\n\nconst require_ = createRequire(import.meta.url)\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nconst ts = require_('typescript') as typeof TS\n\nconst HTTP_METHOD_NAMES = new Set<string>(HTTP_METHODS)\n\nfunction hasExportModifier(modifiers: readonly TS.Modifier[] | undefined): boolean {\n if (!modifiers) return false\n for (const m of modifiers) {\n if (m.kind === ts.SyntaxKind.ExportKeyword) return true\n }\n return false\n}\n\nfunction collectFromStatement(stmt: TS.Statement, found: Set<HttpMethod>): void {\n // `export const GET = ...` / `export function GET ...` / `export async function GET ...`\n if (ts.isVariableStatement(stmt) && hasExportModifier(ts.getModifiers(stmt))) {\n for (const decl of stmt.declarationList.declarations) {\n if (ts.isIdentifier(decl.name) && HTTP_METHOD_NAMES.has(decl.name.text)) {\n found.add(decl.name.text as HttpMethod)\n }\n }\n return\n }\n\n if (\n (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) &&\n hasExportModifier(ts.getModifiers(stmt))\n ) {\n if (stmt.name && HTTP_METHOD_NAMES.has(stmt.name.text)) {\n found.add(stmt.name.text as HttpMethod)\n }\n return\n }\n\n // `export { GET }` / `export { handler as GET } from './shared'` (EC-5)\n if (ts.isExportDeclaration(stmt) && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {\n for (const spec of stmt.exportClause.elements) {\n // spec.name is the exported (re-)name; spec.propertyName is the original (when renamed)\n if (HTTP_METHOD_NAMES.has(spec.name.text)) {\n found.add(spec.name.text as HttpMethod)\n }\n }\n }\n}\n\nexport function detectExportedHttpMethods(filePath: string, content?: string): HttpMethod[] {\n const src = content ?? readFileSync(filePath, 'utf-8')\n const sourceFile = ts.createSourceFile(\n filePath,\n src,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ false,\n ts.ScriptKind.TS,\n )\n const found = new Set<HttpMethod>()\n for (const stmt of sourceFile.statements) {\n collectFromStatement(stmt, found)\n }\n return [...found].sort()\n}\n","export interface ServerRouteNode {\n filePath: string\n routePath: string\n paramNames: string[]\n pattern: RegExp\n /** HTTP methods (uppercase) the route file exports. Optional for backward\n * compatibility with manifests generated before G1. Empty array means the\n * file has no HTTP exports (util-only); undefined means \"not detected\". */\n methods?: string[]\n}\n\nexport function compilePattern(routePath: string): {\n pattern: RegExp\n paramNames: string[]\n} {\n const paramNames: string[] = []\n // Single pass: handle both catch-all (:...name) and regular (:name) params\n const regexStr = routePath.replace(/:(?:\\.\\.\\.)?([^/]+)/g, (match: string, name: string) => {\n paramNames.push(name)\n // Catch-all matches across slashes, regular matches single segment\n return match.startsWith(':...') ? '(.+)' : '([^/]+)'\n })\n // `regexStr` is derived from a developer-authored route path (build-time\n // input, not HTTP-controlled). The `security/detect-non-literal-regexp`\n // rule cannot see this constraint — disable narrowly.\n // eslint-disable-next-line security/detect-non-literal-regexp -- route pattern from build-time scan, never HTTP input\n return { pattern: new RegExp(`^${regexStr}$`), paramNames }\n}\n\nexport function matchRoute(\n url: string,\n routes: ServerRouteNode[],\n): { route: ServerRouteNode; params: Record<string, string> } | null {\n // Strip query string and trailing slash\n let path = url.split('?')[0]\n if (path.length > 1 && path.endsWith('/')) {\n path = path.slice(0, -1)\n }\n\n for (const route of routes) {\n const match = route.pattern.exec(path)\n if (match) {\n const params: Record<string, string> = {}\n route.paramNames.forEach((name, i) => {\n params[name] = match[i + 1]\n })\n return { route, params }\n }\n }\n return null\n}\n","/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time scanner: walks `serverDir/ws/` derived from cwd.\n * No HTTP input ever reaches these fs calls.\n */\nimport { existsSync, statSync } from 'node:fs'\nimport { extname, join, relative } from 'node:path'\n\nimport { walkSourceFiles } from '../_internal/scan-walker.js'\n\nconst WS_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])\n\nexport interface WebSocketRouteNode {\n filePath: string\n wsPath: string\n}\n\nexport function scanWebSocketRoutes(serverDir: string): WebSocketRouteNode[] {\n const wsDir = join(serverDir, 'ws')\n if (!existsSync(wsDir) || !statSync(wsDir).isDirectory()) {\n return []\n }\n\n const results: WebSocketRouteNode[] = []\n walkSourceFiles(wsDir, { extensions: WS_EXTENSIONS }, (absPath) => {\n let rel = relative(wsDir, absPath)\n rel = rel.replace(/\\\\/g, '/')\n rel = rel.slice(0, -extname(rel).length)\n if (rel.endsWith('/index')) rel = rel.slice(0, -6)\n else if (rel === 'index') rel = ''\n results.push({\n filePath: absPath,\n wsPath: `/ws/${rel}`,\n })\n })\n return results\n}\n"],"mappings":"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","names":["existsSync","statSync","extname","join","relative","join","existsSync","statSync","relative","extname"]}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import "tsx/esm";
|
|
3
3
|
import {
|
|
4
4
|
compilePattern,
|
|
5
|
-
scanServerActions,
|
|
6
5
|
scanServerRoutes,
|
|
7
6
|
scanWebSocketRoutes
|
|
8
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QLGES2HC.js";
|
|
8
|
+
import {
|
|
9
|
+
scanServerActions
|
|
10
|
+
} from "./chunk-MOT42NAH.js";
|
|
9
11
|
|
|
10
12
|
// src/server/scan/manifest.ts
|
|
11
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
@@ -20,7 +22,8 @@ function generateManifest(serverDir) {
|
|
|
20
22
|
routes: routes.map((r) => ({
|
|
21
23
|
filePath: relative(serverDir, r.filePath),
|
|
22
24
|
routePath: r.routePath,
|
|
23
|
-
paramNames: r.paramNames
|
|
25
|
+
paramNames: r.paramNames,
|
|
26
|
+
...r.methods !== void 0 ? { methods: r.methods } : {}
|
|
24
27
|
})),
|
|
25
28
|
actions: actions.map((a) => ({
|
|
26
29
|
filePath: relative(serverDir, a.filePath),
|
|
@@ -49,7 +52,8 @@ function loadManifest(distDir, serverDir) {
|
|
|
49
52
|
filePath: resolve(serverDir, r.filePath),
|
|
50
53
|
routePath: r.routePath,
|
|
51
54
|
paramNames,
|
|
52
|
-
pattern
|
|
55
|
+
pattern,
|
|
56
|
+
...r.methods !== void 0 ? { methods: r.methods } : {}
|
|
53
57
|
};
|
|
54
58
|
});
|
|
55
59
|
const actions = raw.actions.map((a) => ({
|
|
@@ -68,4 +72,4 @@ export {
|
|
|
68
72
|
writeManifest,
|
|
69
73
|
loadManifest
|
|
70
74
|
};
|
|
71
|
-
//# sourceMappingURL=chunk-
|
|
75
|
+
//# sourceMappingURL=chunk-SEVYARIU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/scan/manifest.ts"],"sourcesContent":["/* eslint-disable security/detect-non-literal-fs-filename --\n * Build-time manifest emitter / loader. All paths derived from `distDir`\n * + `serverDir`, themselves resolved from `process.cwd()`. No HTTP input.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'\nimport { join, resolve, relative } from 'node:path'\n\nimport { scanServerActions } from './action-scan.js'\nimport type { ActionNode } from './action-scan.js'\nimport { compilePattern } from './match.js'\nimport type { ServerRouteNode } from './match.js'\nimport { scanServerRoutes } from './scan.js'\nimport { scanWebSocketRoutes } from './ws-scan.js'\nimport type { WebSocketRouteNode } from './ws-scan.js'\n\n// --- Manifest Types ---\n\nexport interface ManifestRoute {\n filePath: string\n routePath: string\n paramNames: string[]\n /** HTTP methods (uppercase) the route file exports. Optional — manifests\n * generated before G1 omit this; loaders treat absence as \"unknown\". */\n methods?: string[]\n}\n\nexport interface ManifestAction {\n filePath: string\n actionPath: string\n}\n\nexport interface ManifestWebSocket {\n filePath: string\n wsPath: string\n}\n\nexport interface TheoManifest {\n version: 1\n generatedAt: string\n routes: ManifestRoute[]\n actions: ManifestAction[]\n websockets: ManifestWebSocket[]\n}\n\nexport interface LoadedManifest {\n routes: ServerRouteNode[]\n actions: ActionNode[]\n websockets: WebSocketRouteNode[]\n}\n\n// --- Generate ---\n\nexport function generateManifest(serverDir: string): TheoManifest {\n const routes = scanServerRoutes(serverDir)\n const actions = scanServerActions(serverDir)\n const websockets = scanWebSocketRoutes(serverDir)\n\n return {\n version: 1,\n generatedAt: new Date().toISOString(),\n routes: routes.map((r) => ({\n filePath: relative(serverDir, r.filePath),\n routePath: r.routePath,\n paramNames: r.paramNames,\n ...(r.methods !== undefined ? { methods: r.methods } : {}),\n })),\n actions: actions.map((a) => ({\n filePath: relative(serverDir, a.filePath),\n actionPath: a.actionPath,\n })),\n websockets: websockets.map((w) => ({\n filePath: relative(serverDir, w.filePath),\n wsPath: w.wsPath,\n })),\n }\n}\n\n// --- Write ---\n\nexport function writeManifest(manifest: TheoManifest, outputDir: string): void {\n mkdirSync(outputDir, { recursive: true })\n const manifestPath = join(outputDir, 'manifest.json')\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))\n}\n\n// --- Load ---\n\nexport function loadManifest(distDir: string, serverDir: string): LoadedManifest {\n const manifestPath = join(distDir, 'manifest.json')\n\n if (!existsSync(manifestPath)) {\n throw new Error(`No manifest found at ${manifestPath}. Run \"theo build\" first.`)\n }\n\n const raw = JSON.parse(readFileSync(manifestPath, 'utf-8')) as TheoManifest\n\n const routes: ServerRouteNode[] = raw.routes.map((r) => {\n const { pattern, paramNames } = compilePattern(r.routePath)\n return {\n filePath: resolve(serverDir, r.filePath),\n routePath: r.routePath,\n paramNames,\n pattern,\n ...(r.methods !== undefined ? { methods: r.methods } : {}),\n }\n })\n\n const actions: ActionNode[] = raw.actions.map((a) => ({\n filePath: resolve(serverDir, a.filePath),\n actionPath: a.actionPath,\n }))\n\n const websockets: WebSocketRouteNode[] = raw.websockets.map((w) => ({\n filePath: resolve(serverDir, w.filePath),\n wsPath: w.wsPath,\n }))\n\n return { routes, actions, websockets }\n}\n"],"mappings":";;;;;;;;;;;;AAIA,SAAS,YAAY,cAAc,eAAe,iBAAiB;AACnE,SAAS,MAAM,SAAS,gBAAgB;AA+CjC,SAAS,iBAAiB,WAAiC;AAChE,QAAM,SAAS,iBAAiB,SAAS;AACzC,QAAM,UAAU,kBAAkB,SAAS;AAC3C,QAAM,aAAa,oBAAoB,SAAS;AAEhD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,QAAQ,OAAO,IAAI,CAAC,OAAO;AAAA,MACzB,UAAU,SAAS,WAAW,EAAE,QAAQ;AAAA,MACxC,WAAW,EAAE;AAAA,MACb,YAAY,EAAE;AAAA,MACd,GAAI,EAAE,YAAY,SAAY,EAAE,SAAS,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1D,EAAE;AAAA,IACF,SAAS,QAAQ,IAAI,CAAC,OAAO;AAAA,MAC3B,UAAU,SAAS,WAAW,EAAE,QAAQ;AAAA,MACxC,YAAY,EAAE;AAAA,IAChB,EAAE;AAAA,IACF,YAAY,WAAW,IAAI,CAAC,OAAO;AAAA,MACjC,UAAU,SAAS,WAAW,EAAE,QAAQ;AAAA,MACxC,QAAQ,EAAE;AAAA,IACZ,EAAE;AAAA,EACJ;AACF;AAIO,SAAS,cAAc,UAAwB,WAAyB;AAC7E,YAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AACxC,QAAM,eAAe,KAAK,WAAW,eAAe;AACpD,gBAAc,cAAc,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAC/D;AAIO,SAAS,aAAa,SAAiB,WAAmC;AAC/E,QAAM,eAAe,KAAK,SAAS,eAAe;AAElD,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,UAAM,IAAI,MAAM,wBAAwB,YAAY,2BAA2B;AAAA,EACjF;AAEA,QAAM,MAAM,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;AAE1D,QAAM,SAA4B,IAAI,OAAO,IAAI,CAAC,MAAM;AACtD,UAAM,EAAE,SAAS,WAAW,IAAI,eAAe,EAAE,SAAS;AAC1D,WAAO;AAAA,MACL,UAAU,QAAQ,WAAW,EAAE,QAAQ;AAAA,MACvC,WAAW,EAAE;AAAA,MACb;AAAA,MACA;AAAA,MACA,GAAI,EAAE,YAAY,SAAY,EAAE,SAAS,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1D;AAAA,EACF,CAAC;AAED,QAAM,UAAwB,IAAI,QAAQ,IAAI,CAAC,OAAO;AAAA,IACpD,UAAU,QAAQ,WAAW,EAAE,QAAQ;AAAA,IACvC,YAAY,EAAE;AAAA,EAChB,EAAE;AAEF,QAAM,aAAmC,IAAI,WAAW,IAAI,CAAC,OAAO;AAAA,IAClE,UAAU,QAAQ,WAAW,EAAE,QAAQ;AAAA,IACvC,QAAQ,EAAE;AAAA,EACZ,EAAE;AAEF,SAAO,EAAE,QAAQ,SAAS,WAAW;AACvC;","names":[]}
|
|
@@ -13,16 +13,10 @@ import {
|
|
|
13
13
|
resolveTransformer,
|
|
14
14
|
safeAudit,
|
|
15
15
|
sendError
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import {
|
|
18
|
-
matchRoute,
|
|
19
|
-
scanServerActions,
|
|
20
|
-
scanServerRoutes,
|
|
21
|
-
scanWebSocketRoutes
|
|
22
|
-
} from "./chunk-GQSTRXXB.js";
|
|
16
|
+
} from "./chunk-C27AZN46.js";
|
|
23
17
|
import {
|
|
24
18
|
loadConfig
|
|
25
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-MG7H3LAW.js";
|
|
26
20
|
import {
|
|
27
21
|
broadcastToDevtools
|
|
28
22
|
} from "./chunk-5ODOE6EF.js";
|
|
@@ -33,6 +27,14 @@ import {
|
|
|
33
27
|
import {
|
|
34
28
|
buildServicesProxyConfig
|
|
35
29
|
} from "./chunk-WZ7CPVQB.js";
|
|
30
|
+
import {
|
|
31
|
+
matchRoute,
|
|
32
|
+
scanServerRoutes,
|
|
33
|
+
scanWebSocketRoutes
|
|
34
|
+
} from "./chunk-QLGES2HC.js";
|
|
35
|
+
import {
|
|
36
|
+
scanServerActions
|
|
37
|
+
} from "./chunk-MOT42NAH.js";
|
|
36
38
|
|
|
37
39
|
// src/vite-plugin/index.ts
|
|
38
40
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -468,6 +470,54 @@ function generateRouteManifest(tree) {
|
|
|
468
470
|
return lines.join("\n");
|
|
469
471
|
}
|
|
470
472
|
|
|
473
|
+
// src/server/security/csrf-readiness-store.ts
|
|
474
|
+
var CsrfReadinessStore = class _CsrfReadinessStore {
|
|
475
|
+
static MAX_ENTRIES = 1e3;
|
|
476
|
+
entries = /* @__PURE__ */ new Map();
|
|
477
|
+
keyFor(event) {
|
|
478
|
+
return `${event.method}\0${event.path}\0${event.reason}`;
|
|
479
|
+
}
|
|
480
|
+
record(event) {
|
|
481
|
+
const key = this.keyFor(event);
|
|
482
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
483
|
+
const existing = this.entries.get(key);
|
|
484
|
+
if (existing) {
|
|
485
|
+
existing.count++;
|
|
486
|
+
existing.lastSeen = now;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (this.entries.size >= _CsrfReadinessStore.MAX_ENTRIES) {
|
|
490
|
+
const firstKey = this.entries.keys().next().value;
|
|
491
|
+
if (firstKey !== void 0) this.entries.delete(firstKey);
|
|
492
|
+
}
|
|
493
|
+
this.entries.set(key, { count: 1, firstSeen: now, lastSeen: now });
|
|
494
|
+
}
|
|
495
|
+
summary() {
|
|
496
|
+
let total = 0;
|
|
497
|
+
const routes = [];
|
|
498
|
+
for (const [key, entry] of this.entries) {
|
|
499
|
+
const [method, path, reason] = key.split("\0");
|
|
500
|
+
routes.push({
|
|
501
|
+
method,
|
|
502
|
+
path,
|
|
503
|
+
reason,
|
|
504
|
+
count: entry.count,
|
|
505
|
+
firstSeen: entry.firstSeen,
|
|
506
|
+
lastSeen: entry.lastSeen
|
|
507
|
+
});
|
|
508
|
+
total += entry.count;
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
512
|
+
totalEvents: total,
|
|
513
|
+
routes
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
reset() {
|
|
517
|
+
this.entries.clear();
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
471
521
|
// src/vite-plugin/action-middleware.ts
|
|
472
522
|
import { randomUUID } from "crypto";
|
|
473
523
|
var PREFIX = "/api/__actions/";
|
|
@@ -536,13 +586,17 @@ function createActionMiddleware(vite, serverDir, options) {
|
|
|
536
586
|
csrfMode,
|
|
537
587
|
disallowed
|
|
538
588
|
);
|
|
539
|
-
logRequest(
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
589
|
+
logRequest(
|
|
590
|
+
{
|
|
591
|
+
method: req.method ?? "POST",
|
|
592
|
+
url,
|
|
593
|
+
status: res.statusCode,
|
|
594
|
+
duration: Date.now() - start,
|
|
595
|
+
requestId
|
|
596
|
+
},
|
|
597
|
+
void 0,
|
|
598
|
+
req
|
|
599
|
+
);
|
|
546
600
|
})();
|
|
547
601
|
};
|
|
548
602
|
}
|
|
@@ -974,6 +1028,35 @@ async function handleBatchInline(req, res, batching, requestId) {
|
|
|
974
1028
|
sendError(res, "BATCH_ERROR", message, 400, void 0, requestId);
|
|
975
1029
|
}
|
|
976
1030
|
}
|
|
1031
|
+
async function runPluginsBeforeRouteMatch(pluginRunner, reqCtx) {
|
|
1032
|
+
const { req, res, requestId, start, url } = reqCtx;
|
|
1033
|
+
try {
|
|
1034
|
+
const hookResult = await pluginRunner.runOnRequest({
|
|
1035
|
+
request: req,
|
|
1036
|
+
response: res,
|
|
1037
|
+
ctx: {},
|
|
1038
|
+
requestId
|
|
1039
|
+
});
|
|
1040
|
+
if (hookResult.shortCircuited) {
|
|
1041
|
+
logRequest(
|
|
1042
|
+
{
|
|
1043
|
+
method: req.method ?? "GET",
|
|
1044
|
+
url,
|
|
1045
|
+
status: res.statusCode,
|
|
1046
|
+
duration: Date.now() - start,
|
|
1047
|
+
requestId
|
|
1048
|
+
},
|
|
1049
|
+
void 0,
|
|
1050
|
+
req
|
|
1051
|
+
);
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
sendError(res, "PLUGIN_ERROR", err.message, 500, void 0, requestId);
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
977
1060
|
function createApiMiddleware(vite, serverDir, rateLimitConfigOrOptions) {
|
|
978
1061
|
const opts = rateLimitConfigOrOptions && "windowMs" in rateLimitConfigOrOptions ? { rateLimitConfig: rateLimitConfigOrOptions } : rateLimitConfigOrOptions ?? {};
|
|
979
1062
|
const loadModule = createViteLoader(vite);
|
|
@@ -991,7 +1074,7 @@ function createApiMiddleware(vite, serverDir, rateLimitConfigOrOptions) {
|
|
|
991
1074
|
const csrfReadinessStore = opts.csrfReadinessStore;
|
|
992
1075
|
const servicesProxyPrefixes = opts.servicesProxyPrefixes ?? [];
|
|
993
1076
|
return (req, res, next) => {
|
|
994
|
-
|
|
1077
|
+
const handle = async () => {
|
|
995
1078
|
const url = req.url ?? "";
|
|
996
1079
|
if (await handleCspReportIfMatch(req, res, url, { auditLogger, onCspViolation })) {
|
|
997
1080
|
return;
|
|
@@ -1015,6 +1098,9 @@ function createApiMiddleware(vite, serverDir, rateLimitConfigOrOptions) {
|
|
|
1015
1098
|
if (applyRateLimitOr429(req, res, { rateLimiter, requestId, url, start })) {
|
|
1016
1099
|
return;
|
|
1017
1100
|
}
|
|
1101
|
+
if (pluginRunner && await runPluginsBeforeRouteMatch(pluginRunner, { req, res, requestId, start, url })) {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1018
1104
|
if (await handleBatchIfMatch(req, res, { url, batching, requestId, start })) {
|
|
1019
1105
|
return;
|
|
1020
1106
|
}
|
|
@@ -1026,13 +1112,17 @@ function createApiMiddleware(vite, serverDir, rateLimitConfigOrOptions) {
|
|
|
1026
1112
|
const suggestion = findSuggestion(urlPath, routePaths);
|
|
1027
1113
|
const msg = suggestion ? `API route not found: ${urlPath}. Did you mean: ${suggestion}?` : "API route not found";
|
|
1028
1114
|
sendError(res, "NOT_FOUND", msg, 404, void 0, requestId);
|
|
1029
|
-
logRequest(
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1115
|
+
logRequest(
|
|
1116
|
+
{
|
|
1117
|
+
method: req.method ?? "GET",
|
|
1118
|
+
url,
|
|
1119
|
+
status: 404,
|
|
1120
|
+
duration: Date.now() - start,
|
|
1121
|
+
requestId
|
|
1122
|
+
},
|
|
1123
|
+
void 0,
|
|
1124
|
+
req
|
|
1125
|
+
);
|
|
1036
1126
|
return;
|
|
1037
1127
|
}
|
|
1038
1128
|
const method = (req.method ?? "GET").toUpperCase();
|
|
@@ -1050,8 +1140,13 @@ function createApiMiddleware(vite, serverDir, rateLimitConfigOrOptions) {
|
|
|
1050
1140
|
csrfMode,
|
|
1051
1141
|
disallowed
|
|
1052
1142
|
});
|
|
1053
|
-
logRequest(
|
|
1054
|
-
|
|
1143
|
+
logRequest(
|
|
1144
|
+
{ method, url, status: res.statusCode, duration: Date.now() - start, requestId },
|
|
1145
|
+
void 0,
|
|
1146
|
+
req
|
|
1147
|
+
);
|
|
1148
|
+
};
|
|
1149
|
+
void handle();
|
|
1055
1150
|
};
|
|
1056
1151
|
}
|
|
1057
1152
|
|
|
@@ -1077,7 +1172,7 @@ function resolveExportSubpath(pkgRoot, subpath) {
|
|
|
1077
1172
|
}
|
|
1078
1173
|
let target;
|
|
1079
1174
|
if (typeof exp === "string") target = exp;
|
|
1080
|
-
else if (
|
|
1175
|
+
else if (typeof exp === "object") {
|
|
1081
1176
|
const e = exp;
|
|
1082
1177
|
target = e.import ?? e.default;
|
|
1083
1178
|
}
|
|
@@ -1147,7 +1242,9 @@ var DEFAULT_RESOLVED = {
|
|
|
1147
1242
|
disallowed: void 0,
|
|
1148
1243
|
cors: void 0,
|
|
1149
1244
|
auditLogger: void 0,
|
|
1150
|
-
devtoolsEnabled: true
|
|
1245
|
+
devtoolsEnabled: true,
|
|
1246
|
+
openapi: void 0,
|
|
1247
|
+
distDir: ".theo"
|
|
1151
1248
|
};
|
|
1152
1249
|
async function resolvePluginConfig(projectRoot) {
|
|
1153
1250
|
try {
|
|
@@ -1176,7 +1273,9 @@ async function resolvePluginConfig(projectRoot) {
|
|
|
1176
1273
|
disallowed: userConfig.security?.disallowed,
|
|
1177
1274
|
cors: userConfig.security?.cors,
|
|
1178
1275
|
auditLogger,
|
|
1179
|
-
devtoolsEnabled: userConfig.devtools !== false
|
|
1276
|
+
devtoolsEnabled: userConfig.devtools !== false,
|
|
1277
|
+
openapi: userConfig.openapi,
|
|
1278
|
+
distDir: userConfig.distDir
|
|
1180
1279
|
};
|
|
1181
1280
|
} catch {
|
|
1182
1281
|
return DEFAULT_RESOLVED;
|
|
@@ -1513,9 +1612,7 @@ async function integrateUseTheoUI(cwd, opts) {
|
|
|
1513
1612
|
}
|
|
1514
1613
|
const tailwindPlugins = normalizePluginReturn(tailwindPlugin);
|
|
1515
1614
|
if (tailwindPlugins === null) {
|
|
1516
|
-
console.warn(
|
|
1517
|
-
`[theokit] @tailwindcss/vite returned unexpected shape. Skipping auto-config.`
|
|
1518
|
-
);
|
|
1615
|
+
console.warn(`[theokit] @tailwindcss/vite returned unexpected shape. Skipping auto-config.`);
|
|
1519
1616
|
return [];
|
|
1520
1617
|
}
|
|
1521
1618
|
const uiPlugins = normalizePluginReturn(uiPlugin);
|
|
@@ -1789,7 +1886,42 @@ async function theoPluginAsync(rootOrOptions) {
|
|
|
1789
1886
|
})
|
|
1790
1887
|
);
|
|
1791
1888
|
}
|
|
1792
|
-
|
|
1889
|
+
const { appTypedClientPlugin } = await import("./app-typed-client-4L4L4BWD.js");
|
|
1890
|
+
const appClientPlugin = appTypedClientPlugin({
|
|
1891
|
+
cwd: projectRoot,
|
|
1892
|
+
serverDir: resolve4(projectRoot, "server"),
|
|
1893
|
+
distDir: resolve4(projectRoot, ".theo")
|
|
1894
|
+
});
|
|
1895
|
+
const { actionsVirtualModule } = await import("./actions-virtual-module-E6PU47GV.js");
|
|
1896
|
+
const actionsPlugin = actionsVirtualModule({
|
|
1897
|
+
serverDir: resolve4(projectRoot, "server"),
|
|
1898
|
+
distDir: resolve4(projectRoot, ".theo")
|
|
1899
|
+
});
|
|
1900
|
+
return [
|
|
1901
|
+
theoPlugin(rootOrOptions),
|
|
1902
|
+
...uiPlugins,
|
|
1903
|
+
...servicesPlugins,
|
|
1904
|
+
appClientPlugin,
|
|
1905
|
+
actionsPlugin
|
|
1906
|
+
];
|
|
1907
|
+
}
|
|
1908
|
+
function buildOptimizeDepsInclude(projectRoot, viteOptimizeDeps) {
|
|
1909
|
+
const include = [];
|
|
1910
|
+
if (existsSync5(resolve4(projectRoot, "node_modules", "@usetheo", "ui"))) {
|
|
1911
|
+
include.push("@usetheo/ui");
|
|
1912
|
+
}
|
|
1913
|
+
if (existsSync5(resolve4(projectRoot, "node_modules", "lucide-react"))) {
|
|
1914
|
+
include.push("lucide-react");
|
|
1915
|
+
}
|
|
1916
|
+
include.push("devalue");
|
|
1917
|
+
if (Array.isArray(viteOptimizeDeps)) {
|
|
1918
|
+
for (const pkg of viteOptimizeDeps) {
|
|
1919
|
+
if (typeof pkg === "string" && pkg.length > 0 && !include.includes(pkg)) {
|
|
1920
|
+
include.push(pkg);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return include;
|
|
1793
1925
|
}
|
|
1794
1926
|
function theoPlugin(rootOrOptions) {
|
|
1795
1927
|
const options = typeof rootOrOptions === "string" ? { root: rootOrOptions } : rootOrOptions ?? {};
|
|
@@ -1808,6 +1940,8 @@ function theoPlugin(rootOrOptions) {
|
|
|
1808
1940
|
let cors;
|
|
1809
1941
|
let auditLogger;
|
|
1810
1942
|
let devtoolsEnabled = true;
|
|
1943
|
+
let resolvedOpenApi;
|
|
1944
|
+
let resolvedDistDir = ".theo";
|
|
1811
1945
|
let isDevMode = false;
|
|
1812
1946
|
let configLoadedOnce = false;
|
|
1813
1947
|
return {
|
|
@@ -1826,6 +1960,8 @@ function theoPlugin(rootOrOptions) {
|
|
|
1826
1960
|
cors = resolved.cors;
|
|
1827
1961
|
auditLogger = resolved.auditLogger;
|
|
1828
1962
|
devtoolsEnabled = resolved.devtoolsEnabled;
|
|
1963
|
+
resolvedOpenApi = resolved.openapi;
|
|
1964
|
+
resolvedDistDir = resolved.distDir;
|
|
1829
1965
|
},
|
|
1830
1966
|
// Vite calls this hook (sync return is fine — no awaits left after
|
|
1831
1967
|
// the auto-chain moved to theoPluginAsync). The @usetheo/ui +
|
|
@@ -1833,13 +1969,7 @@ function theoPlugin(rootOrOptions) {
|
|
|
1833
1969
|
// Vite drops plugins returned from a child plugin's config() hook.
|
|
1834
1970
|
config() {
|
|
1835
1971
|
const ext = existsSync5(resolve4(theoSrcDir, "index.ts")) ? ".ts" : ".js";
|
|
1836
|
-
const optimizeDepsInclude =
|
|
1837
|
-
if (existsSync5(resolve4(projectRoot, "node_modules", "@usetheo", "ui"))) {
|
|
1838
|
-
optimizeDepsInclude.push("@usetheo/ui");
|
|
1839
|
-
}
|
|
1840
|
-
if (existsSync5(resolve4(projectRoot, "node_modules", "lucide-react"))) {
|
|
1841
|
-
optimizeDepsInclude.push("lucide-react");
|
|
1842
|
-
}
|
|
1972
|
+
const optimizeDepsInclude = buildOptimizeDepsInclude(projectRoot, options.viteOptimizeDeps);
|
|
1843
1973
|
const warmupClientFiles = [];
|
|
1844
1974
|
for (const candidate of ["app/layout.tsx", "app/page.tsx", "app/page.jsx"]) {
|
|
1845
1975
|
const full = resolve4(projectRoot, candidate);
|
|
@@ -1848,7 +1978,10 @@ function theoPlugin(rootOrOptions) {
|
|
|
1848
1978
|
const servicesProxy = options.services && Object.keys(options.services).length > 0 ? buildServicesProxyConfig(options.services) : void 0;
|
|
1849
1979
|
return {
|
|
1850
1980
|
envPrefix: "THEO_PUBLIC_",
|
|
1851
|
-
optimizeDeps:
|
|
1981
|
+
optimizeDeps: {
|
|
1982
|
+
...optimizeDepsInclude.length > 0 ? { include: optimizeDepsInclude } : {},
|
|
1983
|
+
force: true
|
|
1984
|
+
},
|
|
1852
1985
|
server: {
|
|
1853
1986
|
...warmupClientFiles.length > 0 ? { warmup: { clientFiles: warmupClientFiles } } : {},
|
|
1854
1987
|
...servicesProxy !== void 0 ? { proxy: servicesProxy } : {},
|
|
@@ -1963,7 +2096,7 @@ function theoPlugin(rootOrOptions) {
|
|
|
1963
2096
|
].join("\n");
|
|
1964
2097
|
}
|
|
1965
2098
|
},
|
|
1966
|
-
configureServer(server) {
|
|
2099
|
+
async configureServer(server) {
|
|
1967
2100
|
isDevMode = true;
|
|
1968
2101
|
globalThis.__theoViteHotServer = server;
|
|
1969
2102
|
server.ws.on("theo:devtools:request-manifest", () => {
|
|
@@ -1978,6 +2111,7 @@ function theoPlugin(rootOrOptions) {
|
|
|
1978
2111
|
createActionMiddleware(server, serverDir, { pluginRunner, csrfMode, disallowed })
|
|
1979
2112
|
);
|
|
1980
2113
|
const servicesProxyPrefixes = options.services && Object.keys(options.services).length > 0 ? Object.values(options.services).map((s) => s.proxy) : [];
|
|
2114
|
+
const csrfReadinessStore = options.csrfReadinessStore ?? new CsrfReadinessStore();
|
|
1981
2115
|
server.middlewares.use(
|
|
1982
2116
|
createApiMiddleware(server, serverDir, {
|
|
1983
2117
|
rateLimitConfig: options.rateLimit,
|
|
@@ -1989,7 +2123,8 @@ function theoPlugin(rootOrOptions) {
|
|
|
1989
2123
|
disallowed,
|
|
1990
2124
|
cors,
|
|
1991
2125
|
auditLogger,
|
|
1992
|
-
servicesProxyPrefixes
|
|
2126
|
+
servicesProxyPrefixes,
|
|
2127
|
+
csrfReadinessStore
|
|
1993
2128
|
})
|
|
1994
2129
|
);
|
|
1995
2130
|
const configPath = resolve4(projectRoot, "theo.config.ts");
|
|
@@ -2018,6 +2153,29 @@ function theoPlugin(rootOrOptions) {
|
|
|
2018
2153
|
}
|
|
2019
2154
|
server.watcher.on("add", handleRouteChange);
|
|
2020
2155
|
server.watcher.on("unlink", handleRouteChange);
|
|
2156
|
+
if (resolvedOpenApi !== void 0) {
|
|
2157
|
+
const openApiCfg = resolvedOpenApi;
|
|
2158
|
+
const { reEmitOpenApi } = await import("./dev-emit-I3CKOHML.js");
|
|
2159
|
+
const openApiServerDir = resolve4(projectRoot, "server");
|
|
2160
|
+
const openApiDistDir = resolve4(projectRoot, resolvedDistDir);
|
|
2161
|
+
const isRouteFileForOpenApi = (file) => file.startsWith(openApiServerDir) && /\.(ts|tsx|js|mjs)$/.test(file);
|
|
2162
|
+
void reEmitOpenApi(openApiServerDir, openApiDistDir, openApiCfg);
|
|
2163
|
+
server.watcher.on("change", (file) => {
|
|
2164
|
+
if (isRouteFileForOpenApi(file)) {
|
|
2165
|
+
void reEmitOpenApi(openApiServerDir, openApiDistDir, openApiCfg);
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
server.watcher.on("add", (file) => {
|
|
2169
|
+
if (isRouteFileForOpenApi(file)) {
|
|
2170
|
+
void reEmitOpenApi(openApiServerDir, openApiDistDir, openApiCfg);
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
server.watcher.on("unlink", (file) => {
|
|
2174
|
+
if (isRouteFileForOpenApi(file)) {
|
|
2175
|
+
void reEmitOpenApi(openApiServerDir, openApiDistDir, openApiCfg);
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2021
2179
|
setupWsUpgrade(server, projectRoot);
|
|
2022
2180
|
server.httpServer?.once("close", () => {
|
|
2023
2181
|
;
|
|
@@ -2035,4 +2193,4 @@ export {
|
|
|
2035
2193
|
theoPluginAsync,
|
|
2036
2194
|
theoPlugin
|
|
2037
2195
|
};
|
|
2038
|
-
//# sourceMappingURL=chunk-
|
|
2196
|
+
//# sourceMappingURL=chunk-TFECHXR5.js.map
|