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