@somewhere-tech/cli 0.28.5 → 0.29.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.
Files changed (59) hide show
  1. package/README.md +58 -13
  2. package/dist/commands/deploy.js +33 -8
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/dev.js +298 -43
  5. package/dist/commands/dev.js.map +1 -1
  6. package/dist/commands/env.js +1 -1
  7. package/dist/commands/env.js.map +1 -1
  8. package/dist/commands/init.js +1 -1
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/project.js +2 -1
  11. package/dist/commands/project.js.map +1 -1
  12. package/dist/commands/promote.js +11 -12
  13. package/dist/commands/promote.js.map +1 -1
  14. package/dist/commands/rollback.js +1 -1
  15. package/dist/commands/rollback.js.map +1 -1
  16. package/dist/commands/status.js +2 -2
  17. package/dist/commands/status.js.map +1 -1
  18. package/dist/lib/envfile-write.js +1 -1
  19. package/dist/lib/envfile-write.js.map +1 -1
  20. package/dist/lib/output.js +2 -2
  21. package/dist/lib/output.js.map +1 -1
  22. package/dist/local/compiler-core.js +24 -0
  23. package/dist/local/compiler-core.js.map +1 -0
  24. package/dist/local/compiler.js +444 -0
  25. package/dist/local/compiler.js.map +1 -0
  26. package/dist/local/dev-server.js +487 -0
  27. package/dist/local/dev-server.js.map +1 -0
  28. package/dist/local/loader.js +70 -9
  29. package/dist/local/loader.js.map +1 -1
  30. package/dist/local/loopback.js +54 -0
  31. package/dist/local/loopback.js.map +1 -0
  32. package/dist/local/runtime.js +1 -1
  33. package/dist/local/runtime.js.map +1 -1
  34. package/dist/local/server.js +12 -22
  35. package/dist/local/server.js.map +1 -1
  36. package/node_modules/esbuild-wasm/LICENSE.md +21 -0
  37. package/node_modules/esbuild-wasm/README.md +3 -0
  38. package/node_modules/esbuild-wasm/bin/esbuild +91 -0
  39. package/node_modules/esbuild-wasm/esbuild.wasm +0 -0
  40. package/node_modules/esbuild-wasm/esm/browser.d.ts +705 -0
  41. package/node_modules/esbuild-wasm/esm/browser.js +2393 -0
  42. package/node_modules/esbuild-wasm/esm/browser.min.js +20 -0
  43. package/node_modules/esbuild-wasm/lib/browser.d.ts +705 -0
  44. package/node_modules/esbuild-wasm/lib/browser.js +2438 -0
  45. package/node_modules/esbuild-wasm/lib/browser.min.js +22 -0
  46. package/node_modules/esbuild-wasm/lib/main.d.ts +705 -0
  47. package/node_modules/esbuild-wasm/lib/main.js +2051 -0
  48. package/node_modules/esbuild-wasm/package.json +19 -0
  49. package/node_modules/esbuild-wasm/wasm_exec.js +561 -0
  50. package/node_modules/esbuild-wasm/wasm_exec_node.js +39 -0
  51. package/npm-shrinkwrap.json +17 -2
  52. package/package.json +7 -5
  53. package/runtime/VENDOR.json +7 -0
  54. package/runtime/compiler/VENDOR.json +27 -0
  55. package/runtime/compiler/compile-core.cjs +1775 -0
  56. package/runtime/compiler/graph-contract.cjs +124 -0
  57. package/runtime/compiler/typed-functions.cjs +545 -0
  58. package/runtime/platform-context.mjs +5422 -1802
  59. package/runtime/sw-init.mjs +51 -5
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ // Side-effect-free compiler graph contract. The compiler server and every
4
+ // checker import this exact module; there is no copied graph implementation.
5
+ const path = require('path');
6
+
7
+ const GRAPH_MAX_EDGES = 25_000;
8
+ const GRAPH_MAX_BYTES = 2 * 1024 * 1024;
9
+
10
+ function importGraphFromMetafile(metafile, root) {
11
+ const graph = {};
12
+ if (!metafile) return graph;
13
+ for (const [inputPath, info] of Object.entries(metafile.inputs || {})) {
14
+ const rel = path.relative(root, path.resolve(root, inputPath));
15
+ if (rel.startsWith('..') || rel.includes('node_modules')) continue;
16
+ graph[rel] = (info.imports || [])
17
+ .map((item) => path.relative(root, path.resolve(root, item.path)))
18
+ .filter((item) => !item.startsWith('..') && !item.includes('node_modules'));
19
+ }
20
+ return graph;
21
+ }
22
+
23
+ function stripMetafilePath(inputPath, root) {
24
+ let value = String(inputPath || '').replace(/\\/g, '/');
25
+ if (/^(?:https?:|node:|cloudflare:|workerd:)/.test(value)) return null;
26
+ const namespaced = /^([A-Za-z0-9_-]+):(.*)$/.exec(value);
27
+ if (namespaced) value = namespaced[2];
28
+ if (!value || /^(?:https?:|node:|cloudflare:|workerd:)/.test(value)) return null;
29
+ if (root) {
30
+ const absolute = path.isAbsolute(value) ? value : path.resolve(root, value);
31
+ value = path.relative(root, absolute).replace(/\\/g, '/');
32
+ } else if (path.isAbsolute(value)) {
33
+ return null;
34
+ }
35
+ value = value.replace(/^\.\//, '').replace(/^\/+/, '');
36
+ if (!value || value === '..' || value.startsWith('../') || value.startsWith('<') || value.includes('node_modules/')) return null;
37
+ return value;
38
+ }
39
+
40
+ function graphWithCounts(edges, originalEdges, originalBytes) {
41
+ const graph = {
42
+ edges,
43
+ truncated: true,
44
+ original_edges: originalEdges,
45
+ retained_edges: edges.length,
46
+ dropped_edges: Math.max(0, originalEdges - edges.length),
47
+ max_bytes: GRAPH_MAX_BYTES,
48
+ };
49
+ if (typeof originalBytes === 'number') graph.original_bytes = originalBytes;
50
+ return graph;
51
+ }
52
+
53
+ function graphJsonBytes(value) {
54
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
55
+ }
56
+
57
+ function truncateGraphEdgesForBytes(edges, entryFiles, originalEdges, originalBytes) {
58
+ const candidates = [];
59
+ const candidateKeys = new Set();
60
+ const degree = new Map();
61
+ const byNode = new Map();
62
+ for (const edge of edges) {
63
+ degree.set(edge.from, (degree.get(edge.from) || 0) + 1);
64
+ degree.set(edge.to, (degree.get(edge.to) || 0) + 1);
65
+ byNode.set(edge.from, [...(byNode.get(edge.from) || []), edge]);
66
+ byNode.set(edge.to, [...(byNode.get(edge.to) || []), edge]);
67
+ }
68
+ const add = (edge) => {
69
+ const key = `${edge.from}\0${edge.to}`;
70
+ if (candidateKeys.has(key)) return;
71
+ candidateKeys.add(key);
72
+ candidates.push(edge);
73
+ };
74
+ for (const edge of edges) if (entryFiles.has(edge.from) || entryFiles.has(edge.to)) add(edge);
75
+ for (const [node] of [...degree.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))) {
76
+ for (const edge of byNode.get(node) || []) add(edge);
77
+ }
78
+ let low = 0;
79
+ let high = candidates.length;
80
+ while (low < high) {
81
+ const mid = Math.ceil((low + high) / 2);
82
+ if (graphJsonBytes(graphWithCounts(candidates.slice(0, mid), originalEdges, originalBytes)) <= GRAPH_MAX_BYTES) low = mid;
83
+ else high = mid - 1;
84
+ }
85
+ return graphWithCounts(candidates.slice(0, low), originalEdges, originalBytes);
86
+ }
87
+
88
+ function graphFromMetafile(metafile, root) {
89
+ if (!metafile) return { edges: [] };
90
+ const edges = [];
91
+ const seen = new Set();
92
+ let originalEdges = 0;
93
+ for (const [inputPath, input] of Object.entries(metafile.inputs || {})) {
94
+ const from = stripMetafilePath(inputPath, root);
95
+ if (!from) continue;
96
+ for (const item of input.imports || []) {
97
+ const to = stripMetafilePath(item.path, root);
98
+ if (!to || to === from) continue;
99
+ const key = `${from}\0${to}`;
100
+ if (seen.has(key)) continue;
101
+ seen.add(key);
102
+ originalEdges++;
103
+ if (edges.length < GRAPH_MAX_EDGES) edges.push({ from, to });
104
+ }
105
+ }
106
+ edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
107
+ const entryFiles = new Set();
108
+ for (const output of Object.values(metafile.outputs || {})) {
109
+ const entry = output && output.entryPoint ? stripMetafilePath(output.entryPoint, root) : null;
110
+ if (entry) entryFiles.add(entry);
111
+ }
112
+ const stored = { edges };
113
+ const bytes = graphJsonBytes(stored);
114
+ if (bytes > GRAPH_MAX_BYTES) return truncateGraphEdgesForBytes(edges, entryFiles, originalEdges, bytes);
115
+ if (originalEdges > edges.length) return graphWithCounts(edges, originalEdges);
116
+ return stored;
117
+ }
118
+
119
+ module.exports = {
120
+ GRAPH_MAX_EDGES,
121
+ GRAPH_MAX_BYTES,
122
+ importGraphFromMetafile,
123
+ graphFromMetafile,
124
+ };
@@ -0,0 +1,545 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const SENTINEL = 'somewhere:v1';
8
+ const MODULE_NAME = 'somewhere:api';
9
+ const MANIFEST_PATH = '_internal/typed-functions.json';
10
+ const AMBIENT_FILE = '__somewhere_typed_functions.d.ts';
11
+ const CLIENT_FILE = '__somewhere_api.d.ts';
12
+
13
+ // One route-independent helper. Procedure paths exist only in the declaration
14
+ // and release manifest; adding a procedure adds zero browser JavaScript.
15
+ const RUNTIME_SOURCE = `class FunctionError extends Error{constructor(status,payload){super(payload&&typeof payload.message==="string"?payload.message:"Server function failed");this.name="FunctionError";this.status=status;this.code=payload&&typeof payload.code==="string"?payload.code:null}}const invoke=async(parts,input)=>{const response=await fetch("/api/"+parts.join("/"),{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"},body:input===undefined?undefined:JSON.stringify(input)});const payload=await response.json().catch(()=>({}));if(!response.ok)throw new FunctionError(response.status,payload);return payload};const branch=parts=>new Proxy(()=>{},{get:(_,part)=>part==="then"?undefined:branch([...parts,String(part)]),apply:(_,__,args)=>invoke(parts,args[0])});const api=branch([]);export{api,FunctionError};`;
16
+
17
+ const GLOBAL_TYPES = `
18
+ type __SomewhereJsonPrimitive = string | number | boolean | null;
19
+ type __SomewhereJson = __SomewhereJsonPrimitive | { [key: string]: __SomewhereJson } | __SomewhereJson[];
20
+ interface __SomewhereTypedRequest<Input> extends Request { json(): Promise<Input> }
21
+ type ServerFunction<Contract extends { input: unknown; output: unknown }> =
22
+ (req: __SomewhereTypedRequest<Contract["input"]>, sw: never) =>
23
+ Contract["output"] | Promise<Contract["output"]>;
24
+ `;
25
+
26
+ function nowMs() {
27
+ return Number(process.hrtime.bigint()) / 1e6;
28
+ }
29
+
30
+ function normalizePath(value) {
31
+ return value.split(path.sep).join('/').replace(/^\.\//, '');
32
+ }
33
+
34
+ function lineAndColumn(sourceFile, node) {
35
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
36
+ return { line: position.line + 1, column: position.character + 1 };
37
+ }
38
+
39
+ function displayFileName(sourceFile) {
40
+ return sourceFile.__somewhereRelativeName || normalizePath(sourceFile.fileName);
41
+ }
42
+
43
+ function warning(sourceFile, node, mismatch, fix) {
44
+ const where = lineAndColumn(sourceFile, node);
45
+ return `Typed function warning — ${displayFileName(sourceFile)}:${where.line}:${where.column}\n${mismatch}\nFix: ${fix}\nThis deploy continued; typed-function checks are warning-only.`;
46
+ }
47
+
48
+ function hasExportModifier(ts, node) {
49
+ return !!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
50
+ }
51
+
52
+ function exactSentinelNode(ts, sourceFile) {
53
+ for (const statement of sourceFile.statements) {
54
+ if (!ts.isVariableStatement(statement) || !hasExportModifier(ts, statement)) continue;
55
+ for (const declaration of statement.declarationList.declarations) {
56
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'typed') continue;
57
+ if (declaration.initializer && ts.isAsExpression(declaration.initializer)
58
+ && ts.isStringLiteral(declaration.initializer.expression)
59
+ && declaration.initializer.expression.text === SENTINEL) return declaration;
60
+ if (declaration.initializer && ts.isStringLiteral(declaration.initializer)
61
+ && declaration.initializer.text === SENTINEL) return declaration;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function supportedRoute(functionPath) {
68
+ if (!functionPath.startsWith('api/') || !/\.(?:ts|tsx)$/i.test(functionPath)) return false;
69
+ const withoutExtension = functionPath.replace(/\.(?:ts|tsx)$/i, '').slice(4);
70
+ const segments = withoutExtension.split('/');
71
+ return segments.length > 0 && segments.every((segment) => /^[A-Za-z_$][\w$]*$/.test(segment));
72
+ }
73
+
74
+ function routeDetails(functionPath) {
75
+ const segments = functionPath.replace(/\.(?:ts|tsx)$/i, '').slice(4).split('/');
76
+ return {
77
+ client: segments.join('.'),
78
+ route: `/api/${segments.join('/')}`,
79
+ segments,
80
+ };
81
+ }
82
+
83
+ function createCompilerOptions(ts, root, tsconfigText) {
84
+ let user = {};
85
+ if (typeof tsconfigText === 'string') {
86
+ const parsedText = ts.parseConfigFileTextToJson(path.join(root, 'tsconfig.json'), tsconfigText);
87
+ if (!parsedText.error) {
88
+ user = ts.convertCompilerOptionsFromJson(parsedText.config?.compilerOptions || {}, root).options;
89
+ }
90
+ }
91
+ return {
92
+ ...user,
93
+ noEmit: true,
94
+ strict: true,
95
+ skipLibCheck: true,
96
+ target: user.target ?? ts.ScriptTarget.ES2022,
97
+ module: user.module ?? ts.ModuleKind.ESNext,
98
+ moduleResolution: user.moduleResolution ?? ts.ModuleResolutionKind.Bundler,
99
+ jsx: user.jsx ?? ts.JsxEmit.ReactJSX,
100
+ allowJs: true,
101
+ checkJs: false,
102
+ };
103
+ }
104
+
105
+ function createProgram(ts, root, files, tsconfigText, generatedClient) {
106
+ const ambientPath = path.join(root, AMBIENT_FILE);
107
+ const clientPath = path.join(root, CLIENT_FILE);
108
+ const generated = new Map([[ambientPath, GLOBAL_TYPES]]);
109
+ const relativeNames = new Map(Object.keys(files).map((file) => [path.join(root, file), normalizePath(file)]));
110
+ if (generatedClient) generated.set(clientPath, generatedClient);
111
+ const options = createCompilerOptions(ts, root, tsconfigText);
112
+ const host = ts.createCompilerHost(options, true);
113
+ const readFile = host.readFile.bind(host);
114
+ const fileExists = host.fileExists.bind(host);
115
+ host.readFile = (fileName) => generated.get(fileName) ?? readFile(fileName);
116
+ host.fileExists = (fileName) => generated.has(fileName) || fileExists(fileName);
117
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
118
+ const text = generated.get(fileName);
119
+ if (text !== undefined) return ts.createSourceFile(fileName, text, languageVersion, true, ts.ScriptKind.TS);
120
+ const disk = readFile(fileName);
121
+ if (disk === undefined) return undefined;
122
+ const sourceFile = ts.createSourceFile(fileName, disk, languageVersion, true);
123
+ sourceFile.__somewhereRelativeName = relativeNames.get(fileName);
124
+ return sourceFile;
125
+ };
126
+ const roots = Object.keys(files)
127
+ .filter((file) => /\.(?:tsx?|jsx?|mjs|cjs)$/i.test(file))
128
+ .map((file) => path.join(root, file));
129
+ roots.push(ambientPath);
130
+ if (generatedClient) roots.push(clientPath);
131
+ return ts.createProgram({ rootNames: roots, options, host });
132
+ }
133
+
134
+ function typeIssue(ts, checker, type, seen = new Set()) {
135
+ if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return 'any or unknown';
136
+ if (type.flags & ts.TypeFlags.Never) return 'never';
137
+ if (type.flags & ts.TypeFlags.Undefined) return null;
138
+ if (type.isUnion?.()) {
139
+ for (const member of type.types) {
140
+ const issue = typeIssue(ts, checker, member, seen);
141
+ if (issue) return issue;
142
+ }
143
+ return null;
144
+ }
145
+ if (type.isIntersection?.()) return 'an intersection type';
146
+ if (type.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.Null | ts.TypeFlags.Void)) return null;
147
+ if (!(type.flags & ts.TypeFlags.Object)) return `non-JSON type ${checker.typeToString(type)}`;
148
+ if (seen.has(type)) return 'a recursive type';
149
+ seen.add(type);
150
+ const symbolName = type.getSymbol?.()?.getName?.();
151
+ if (symbolName === 'Date' || symbolName === 'Map' || symbolName === 'Set' || symbolName === 'Response' || symbolName === 'Blob') {
152
+ return `non-JSON type ${symbolName}`;
153
+ }
154
+ const arrayElement = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
155
+ if (arrayElement) {
156
+ const issue = typeIssue(ts, checker, arrayElement, seen);
157
+ seen.delete(type);
158
+ return issue;
159
+ }
160
+ for (const property of checker.getPropertiesOfType(type)) {
161
+ const declaration = property.valueDeclaration || property.declarations?.[0];
162
+ if (!declaration) continue;
163
+ const issue = typeIssue(ts, checker, checker.getTypeOfSymbolAtLocation(property, declaration), seen);
164
+ if (issue) {
165
+ seen.delete(type);
166
+ return `${property.getName()} contains ${issue}`;
167
+ }
168
+ }
169
+ seen.delete(type);
170
+ return null;
171
+ }
172
+
173
+ function serializeType(ts, checker, type, seen = new Set()) {
174
+ if (type.flags & ts.TypeFlags.Void) return 'void';
175
+ if (type.flags & ts.TypeFlags.StringLiteral) return JSON.stringify(type.value);
176
+ if (type.flags & ts.TypeFlags.NumberLiteral) return String(type.value);
177
+ if (type.flags & ts.TypeFlags.BooleanLiteral) return type.intrinsicName;
178
+ if (type.flags & ts.TypeFlags.StringLike) return 'string';
179
+ if (type.flags & ts.TypeFlags.NumberLike) return 'number';
180
+ if (type.flags & ts.TypeFlags.BooleanLike) return 'boolean';
181
+ if (type.flags & ts.TypeFlags.Null) return 'null';
182
+ if (type.flags & ts.TypeFlags.Undefined) return 'undefined';
183
+ if (type.isUnion?.()) return type.types.map((member) => serializeType(ts, checker, member, seen)).join(' | ');
184
+ if (seen.has(type)) return 'never';
185
+ seen.add(type);
186
+ const arrayElement = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
187
+ if (arrayElement) {
188
+ const rendered = `Array<${serializeType(ts, checker, arrayElement, seen)}>`;
189
+ seen.delete(type);
190
+ return rendered;
191
+ }
192
+ const fields = checker.getPropertiesOfType(type).map((property) => {
193
+ const declaration = property.valueDeclaration || property.declarations?.[0];
194
+ const propertyType = declaration ? checker.getTypeOfSymbolAtLocation(property, declaration) : checker.getDeclaredTypeOfSymbol(property);
195
+ const optional = !!(property.flags & ts.SymbolFlags.Optional);
196
+ const name = /^[A-Za-z_$][\w$]*$/.test(property.getName()) ? property.getName() : JSON.stringify(property.getName());
197
+ return `${name}${optional ? '?' : ''}: ${serializeType(ts, checker, propertyType, seen)}`;
198
+ });
199
+ seen.delete(type);
200
+ return `{ ${fields.join('; ')} }`;
201
+ }
202
+
203
+ function exportedSymbol(checker, sourceFile, name) {
204
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
205
+ return moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.getName() === name);
206
+ }
207
+
208
+ function defaultExportExpression(ts, sourceFile) {
209
+ for (const statement of sourceFile.statements) {
210
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) return statement.expression;
211
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement))
212
+ && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) return statement;
213
+ }
214
+ return null;
215
+ }
216
+
217
+ function unwrapExpression(ts, expression) {
218
+ let current = expression;
219
+ while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current)) current = current.expression;
220
+ return current;
221
+ }
222
+
223
+ function isEndpointCall(ts, expression) {
224
+ const node = unwrapExpression(ts, expression);
225
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
226
+ return node.expression.name.text === 'endpoint' && node.arguments.length > 0 && ts.isObjectLiteralExpression(node.arguments[0])
227
+ ? node.arguments[0]
228
+ : null;
229
+ }
230
+
231
+ function endpointBodyType(ts, node) {
232
+ if (!node || !ts.isObjectLiteralExpression(node)) return { text: 'void' };
233
+ const fields = [];
234
+ for (const property of node.properties) {
235
+ if (!ts.isPropertyAssignment(property)) return { error: 'body schema uses a computed or shorthand field' };
236
+ const name = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : null;
237
+ if (!name) return { error: 'body schema contains a field that the typed client cannot read' };
238
+ if (ts.isObjectLiteralExpression(property.initializer)) {
239
+ const nested = endpointBodyType(ts, property.initializer);
240
+ if (nested.error) return { error: `${name}.${nested.error}` };
241
+ fields.push(`${/^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name)}: ${nested.text}`);
242
+ continue;
243
+ }
244
+ if (!ts.isStringLiteral(property.initializer)) return { error: `body field ${name} is not a supported schema rule` };
245
+ let rule = property.initializer.text;
246
+ const optional = rule.endsWith('?');
247
+ if (optional) rule = rule.slice(0, -1);
248
+ const mapped = rule === 'string' || rule === 'email' ? 'string'
249
+ : rule === 'number' ? 'number'
250
+ : rule === 'boolean' ? 'boolean'
251
+ : rule === 'array' ? 'unknown[]'
252
+ : rule === 'object' ? 'Record<string, unknown>'
253
+ : null;
254
+ if (!mapped) return { error: `body field ${name} uses unsupported rule ${JSON.stringify(property.initializer.text)}` };
255
+ fields.push(`${/^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name)}${optional ? '?' : ''}: ${mapped}`);
256
+ }
257
+ return { text: `{ ${fields.join('; ')} }` };
258
+ }
259
+
260
+ function endpointHandler(ts, objectLiteral) {
261
+ for (const property of objectLiteral.properties) {
262
+ const name = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : null;
263
+ if (name === 'handler') {
264
+ if (ts.isPropertyAssignment(property)) return property.initializer;
265
+ if (ts.isMethodDeclaration(property)) return property;
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+
271
+ function propertyValue(ts, objectLiteral, wanted) {
272
+ for (const property of objectLiteral.properties) {
273
+ const name = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ? property.name.text : null;
274
+ if (name === wanted && ts.isPropertyAssignment(property)) return property.initializer;
275
+ }
276
+ return null;
277
+ }
278
+
279
+ function awaitedReturnType(ts, checker, node) {
280
+ const signature = checker.getSignatureFromDeclaration(node) || checker.getSignaturesOfType(checker.getTypeAtLocation(node), ts.SignatureKind.Call)[0];
281
+ if (!signature) return null;
282
+ return checker.getAwaitedType(checker.getReturnTypeOfSignature(signature));
283
+ }
284
+
285
+ function bareFunctionNode(ts, expression) {
286
+ const node = unwrapExpression(ts, expression);
287
+ return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) ? node : null;
288
+ }
289
+
290
+ function hasServerFunctionSatisfies(ts, expression) {
291
+ let current = expression;
292
+ while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current)) current = current.expression;
293
+ if (!ts.isSatisfiesExpression(current)) return false;
294
+ const type = current.type;
295
+ return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)
296
+ && type.typeName.text === 'ServerFunction'
297
+ && type.typeArguments?.length === 1
298
+ && ts.isTypeReferenceNode(type.typeArguments[0])
299
+ && ts.isIdentifier(type.typeArguments[0].typeName)
300
+ && type.typeArguments[0].typeName.text === 'Contract';
301
+ }
302
+
303
+ function buildDeclaration(procedures) {
304
+ const tree = {};
305
+ for (const procedure of procedures) {
306
+ let cursor = tree;
307
+ for (const segment of procedure.segments.slice(0, -1)) cursor = cursor[segment] ||= {};
308
+ cursor[procedure.segments.at(-1)] = procedure;
309
+ }
310
+ const render = (node, depth) => Object.entries(node).map(([name, value]) => {
311
+ if (value && value.route) {
312
+ const arg = value.input === 'void' ? '' : `input: ${value.input}`;
313
+ return `${name}: (${arg}) => Promise<${value.output}>`;
314
+ }
315
+ return `${name}: { ${render(value, depth + 1)} }`;
316
+ }).join('; ');
317
+ return `declare module ${JSON.stringify(MODULE_NAME)} {\n export class FunctionError extends Error { status: number; code: string | null }\n export const api: { ${render(tree, 1)} };\n}\n`;
318
+ }
319
+
320
+ function importApiNames(ts, sourceFile) {
321
+ const names = new Set();
322
+ for (const statement of sourceFile.statements) {
323
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== MODULE_NAME) continue;
324
+ for (const element of statement.importClause?.namedBindings?.elements || []) {
325
+ if ((element.propertyName?.text || element.name.text) === 'api') names.add(element.name.text);
326
+ }
327
+ }
328
+ return names;
329
+ }
330
+
331
+ function containsRootedApiCall(ts, node, names) {
332
+ let found = false;
333
+ const rootName = (expression) => {
334
+ let current = expression;
335
+ while (ts.isCallExpression(current) || ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)
336
+ || ts.isParenthesizedExpression(current) || ts.isAwaitExpression(current)) {
337
+ if (ts.isCallExpression(current)) current = current.expression;
338
+ else if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) current = current.expression;
339
+ else current = current.expression;
340
+ }
341
+ return ts.isIdentifier(current) ? current.text : null;
342
+ };
343
+ const visit = (child) => {
344
+ if ((ts.isCallExpression(child) || ts.isPropertyAccessExpression(child)) && names.has(rootName(child))) found = true;
345
+ if (!found) ts.forEachChild(child, visit);
346
+ };
347
+ visit(node);
348
+ return found;
349
+ }
350
+
351
+ function clientWarnings(ts, program, procedures) {
352
+ const checker = program.getTypeChecker();
353
+ const warnings = [];
354
+ const routeByClient = new Map(procedures.map((procedure) => [procedure.client, procedure.file]));
355
+ for (const sourceFile of program.getSourceFiles()) {
356
+ if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/')) continue;
357
+ const names = importApiNames(ts, sourceFile);
358
+ if (!names.size) continue;
359
+ const relevantStatements = sourceFile.statements.filter((statement) => containsRootedApiCall(ts, statement, names));
360
+ for (const diagnostic of program.getSemanticDiagnostics(sourceFile)) {
361
+ if (diagnostic.start === undefined || !relevantStatements.some((statement) => diagnostic.start >= statement.getStart() && diagnostic.start < statement.getEnd())) continue;
362
+ const node = relevantStatements.find((statement) => diagnostic.start >= statement.getStart() && diagnostic.start < statement.getEnd()) || sourceFile;
363
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ');
364
+ const called = procedures.find((procedure) => sourceFile.text.includes(`api.${procedure.client}`));
365
+ const contract = called ? routeByClient.get(called.client) : null;
366
+ warnings.push(warning(sourceFile, node, message, contract
367
+ ? `make this call match the input/output contract in ${contract}, or change that contract and its callers together.`
368
+ : `use a procedure exported by ${MODULE_NAME}, or add the matching typed function under api/.`));
369
+ }
370
+ }
371
+ return warnings;
372
+ }
373
+
374
+ function analyzeTypedFunctions(input) {
375
+ const { root, files, functionEntries, tsconfigText } = input;
376
+ const scanStarted = nowMs();
377
+ const candidates = functionEntries.filter((file) => typeof files[file] === 'string' && files[file].includes(SENTINEL));
378
+ const scanMs = nowMs() - scanStarted;
379
+ if (!candidates.length) {
380
+ return {
381
+ manifest: null,
382
+ summary: { procedures: 0, contract_digest: null, warnings: 0 },
383
+ warnings: [],
384
+ declaration: null,
385
+ runtime: RUNTIME_SOURCE,
386
+ timing_ms: { scan: scanMs, typecheck: 0, total: scanMs },
387
+ };
388
+ }
389
+ const ts = input.ts || input.loadTypescript();
390
+
391
+ const started = nowMs();
392
+ const program = createProgram(ts, root, files, tsconfigText, null);
393
+ const checker = program.getTypeChecker();
394
+ const warnings = [];
395
+ const procedures = [];
396
+ for (const file of candidates) {
397
+ const sourceFile = program.getSourceFile(path.join(root, file));
398
+ if (!sourceFile) continue;
399
+ const sentinel = exactSentinelNode(ts, sourceFile);
400
+ if (!sentinel) continue;
401
+ if (!supportedRoute(file)) {
402
+ warnings.push(warning(sourceFile, sentinel,
403
+ `This opted-in route cannot be represented by the v1 typed client (${file}).`,
404
+ `move it to a static api/name.ts path with JavaScript-identifier segments, or remove the typed sentinel and keep calling it with fetch.`));
405
+ continue;
406
+ }
407
+ const expression = defaultExportExpression(ts, sourceFile);
408
+ if (!expression) {
409
+ warnings.push(warning(sourceFile, sentinel, 'This opted-in file has no default server function export.',
410
+ `add a default function export, or remove the typed sentinel and keep calling the route with fetch.`));
411
+ continue;
412
+ }
413
+ const endpoint = isEndpointCall(ts, expression);
414
+ let inputType = null;
415
+ let outputType = null;
416
+ let outputNode = expression;
417
+ if (endpoint) {
418
+ const body = endpointBodyType(ts, propertyValue(ts, endpoint, 'body'));
419
+ if (body.error) {
420
+ warnings.push(warning(sourceFile, propertyValue(ts, endpoint, 'body') || endpoint, `The endpoint ${body.error}.`,
421
+ `use the documented string/email/number/boolean body rules, or remove the typed sentinel and use fetch.`));
422
+ continue;
423
+ }
424
+ inputType = body.text;
425
+ const handler = endpointHandler(ts, endpoint);
426
+ outputNode = handler || endpoint;
427
+ outputType = handler ? awaitedReturnType(ts, checker, handler) : null;
428
+ if (!handler || !outputType) {
429
+ warnings.push(warning(sourceFile, outputNode, 'The endpoint handler output type could not be read.',
430
+ `add an explicit Promise<YourJsonShape> return type to handler, or remove the typed sentinel.`));
431
+ continue;
432
+ }
433
+ } else {
434
+ if (!hasServerFunctionSatisfies(ts, expression)) {
435
+ warnings.push(warning(sourceFile, expression,
436
+ 'This bare opted-in handler is missing satisfies ServerFunction<Contract>.',
437
+ `export type Contract with input/output fields and add satisfies ServerFunction<Contract> to the default handler.`));
438
+ continue;
439
+ }
440
+ const contractSymbol = exportedSymbol(checker, sourceFile, 'Contract');
441
+ const contractDeclaration = contractSymbol?.declarations?.[0];
442
+ const contractType = contractSymbol && contractDeclaration ? checker.getDeclaredTypeOfSymbol(contractSymbol) : null;
443
+ const inputProperty = contractType && checker.getPropertyOfType(contractType, 'input');
444
+ const outputProperty = contractType && checker.getPropertyOfType(contractType, 'output');
445
+ if (!contractDeclaration || !inputProperty || !outputProperty) {
446
+ warnings.push(warning(sourceFile, expression, 'Contract must export both input and output types.',
447
+ `export type Contract = { input: YourInput; output: YourJsonOutput }.`));
448
+ continue;
449
+ }
450
+ const input = checker.getTypeOfSymbolAtLocation(inputProperty, contractDeclaration);
451
+ const output = checker.getTypeOfSymbolAtLocation(outputProperty, contractDeclaration);
452
+ const handler = bareFunctionNode(ts, expression);
453
+ const actualOutput = handler ? awaitedReturnType(ts, checker, handler) : null;
454
+ if (actualOutput && !checker.isTypeAssignableTo(actualOutput, output)) {
455
+ warnings.push(warning(sourceFile, handler,
456
+ `This function promises ${checker.typeToString(output)} but returns ${checker.typeToString(actualOutput)}.`,
457
+ `return the promised shape, or update Contract["output"] and its browser callers.`));
458
+ }
459
+ inputType = input;
460
+ outputType = output;
461
+ }
462
+
463
+ if (typeof inputType !== 'string') {
464
+ const issue = typeIssue(ts, checker, inputType);
465
+ if (issue) {
466
+ warnings.push(warning(sourceFile, expression, `The public input contains ${issue}; publishing it as typed would be unsafe.`,
467
+ `replace that boundary with an explicit JSON-serializable input type.`));
468
+ continue;
469
+ }
470
+ inputType = serializeType(ts, checker, inputType);
471
+ }
472
+ const outputIssue = typeIssue(ts, checker, outputType);
473
+ if (outputIssue) {
474
+ warnings.push(warning(sourceFile, outputNode, `The public output contains ${outputIssue}; publishing it as typed would be unsafe.`,
475
+ endpoint
476
+ ? `add an explicit JSON-serializable Promise<Output> annotation to handler.`
477
+ : `replace Contract["output"] with an explicit JSON-serializable type.`));
478
+ continue;
479
+ }
480
+ const outputText = serializeType(ts, checker, outputType);
481
+ const details = routeDetails(file);
482
+ procedures.push({
483
+ file,
484
+ client: details.client,
485
+ route: details.route,
486
+ segments: details.segments,
487
+ input: inputType,
488
+ output: outputText,
489
+ });
490
+ }
491
+
492
+ procedures.sort((a, b) => a.client.localeCompare(b.client));
493
+ for (let index = 1; index < procedures.length; index++) {
494
+ if (procedures[index - 1].client === procedures[index].client) {
495
+ const file = program.getSourceFile(path.join(root, procedures[index].file));
496
+ warnings.push(warning(file, file, `Two typed functions generate the same client name api.${procedures[index].client}.`,
497
+ `rename one function file so every typed procedure has a unique static path.`));
498
+ }
499
+ }
500
+ const declaration = buildDeclaration(procedures);
501
+ const clientProgram = createProgram(ts, root, files, tsconfigText, declaration);
502
+ warnings.push(...clientWarnings(ts, clientProgram, procedures));
503
+ const digestInput = procedures.map(({ client, route, input: procedureInput, output }) => ({ client, route, input: procedureInput, output }));
504
+ const manifestProcedures = procedures.map(({ file, client, route, input: procedureInput, output }) => ({ file, client, route, input: procedureInput, output }));
505
+ const contractDigest = crypto.createHash('sha256').update(JSON.stringify(digestInput)).digest('hex');
506
+ const typecheckMs = nowMs() - started;
507
+ const manifest = procedures.length || warnings.length ? {
508
+ version: 1,
509
+ contract_digest: contractDigest,
510
+ procedures: manifestProcedures,
511
+ declaration,
512
+ } : null;
513
+ return {
514
+ manifest,
515
+ summary: { procedures: procedures.length, contract_digest: contractDigest, warnings: warnings.length },
516
+ warnings,
517
+ declaration,
518
+ runtime: RUNTIME_SOURCE,
519
+ timing_ms: { scan: scanMs, typecheck: typecheckMs, total: scanMs + typecheckMs },
520
+ };
521
+ }
522
+
523
+ function virtualApiPlugin(esbuild) {
524
+ return {
525
+ name: 'somewhere-typed-api',
526
+ setup(build) {
527
+ build.onResolve({ filter: /^somewhere:api$/ }, () => ({ path: MODULE_NAME, namespace: 'somewhere-typed-api' }));
528
+ build.onLoad({ filter: /.*/, namespace: 'somewhere-typed-api' }, () => ({ contents: RUNTIME_SOURCE, loader: 'js' }));
529
+ },
530
+ };
531
+ }
532
+
533
+ module.exports = {
534
+ AMBIENT_FILE,
535
+ CLIENT_FILE,
536
+ MANIFEST_PATH,
537
+ MODULE_NAME,
538
+ RUNTIME_SOURCE,
539
+ SENTINEL,
540
+ analyzeTypedFunctions,
541
+ buildDeclaration,
542
+ exactSentinelNode,
543
+ supportedRoute,
544
+ virtualApiPlugin,
545
+ };