@velarscript/node 0.12.0 → 0.13.0
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/README.md +40 -11
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +99 -23
- package/dist/compiler.js.map +1 -1
- package/dist/http-runtime.d.ts.map +1 -1
- package/dist/http-runtime.js +19 -7
- package/dist/http-runtime.js.map +1 -1
- package/dist/node-host-runtime.d.ts.map +1 -1
- package/dist/node-host-runtime.js +14 -7
- package/dist/node-host-runtime.js.map +1 -1
- package/dist/node-host-worker-runtime.d.ts.map +1 -1
- package/dist/node-host-worker-runtime.js +59 -31
- package/dist/node-host-worker-runtime.js.map +1 -1
- package/dist/project-config.d.ts +1 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/route-shape.d.ts +20 -0
- package/dist/route-shape.d.ts.map +1 -0
- package/dist/route-shape.js +36 -0
- package/dist/route-shape.js.map +1 -0
- package/dist/serve-runtime.d.ts.map +1 -1
- package/dist/serve-runtime.js +312 -52
- package/dist/serve-runtime.js.map +1 -1
- package/dist/server-analyzer.d.ts +52 -0
- package/dist/server-analyzer.d.ts.map +1 -1
- package/dist/server-analyzer.js +377 -12
- package/dist/server-analyzer.js.map +1 -1
- package/dist/server-ast.d.ts +13 -1
- package/dist/server-ast.d.ts.map +1 -1
- package/dist/server-ast.js.map +1 -1
- package/dist/server-emitter.d.ts +1 -0
- package/dist/server-emitter.d.ts.map +1 -1
- package/dist/server-emitter.js +13 -2
- package/dist/server-emitter.js.map +1 -1
- package/dist/server-parser.d.ts +2 -0
- package/dist/server-parser.d.ts.map +1 -1
- package/dist/server-parser.js +45 -20
- package/dist/server-parser.js.map +1 -1
- package/dist/websocket-runtime.d.ts.map +1 -1
- package/dist/websocket-runtime.js +73 -17
- package/dist/websocket-runtime.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve-runtime.js","sourceRoot":"","sources":["../src/serve-runtime.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"serve-runtime.js","sourceRoot":"","sources":["../src/serve-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAE,MAAM,kBAAkB,CAAC;AAEpE,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CAw/BL,gCAAgC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA63E5E,CAAC,SAAS,EAAE,CAAC"}
|
|
@@ -18,6 +18,16 @@ export declare function parseRouteResultHint(value: string | undefined): {
|
|
|
18
18
|
export declare class VelarNodeAnalyzer extends Analyzer {
|
|
19
19
|
private readonly contextualRouteParameters;
|
|
20
20
|
private readonly routeInputs;
|
|
21
|
+
/** Servers declared by the module under analysis, the only spread targets this analyzer can resolve. */
|
|
22
|
+
private readonly moduleServers;
|
|
23
|
+
/** Module-level `const alias = name` and `let alias = name` bindings, so a spread of an alias resolves to the server it names. */
|
|
24
|
+
private readonly moduleServerAliases;
|
|
25
|
+
/** Local names imported from velar/serve that name a path-preserving combinator, so a spread of a call resolves through it. */
|
|
26
|
+
private readonly moduleServeCombinators;
|
|
27
|
+
/** One answer per `let` alias name to "was this binding ever reassigned?", because the predicate walks the whole program. */
|
|
28
|
+
private readonly stableAliases;
|
|
29
|
+
/** The program under analysis, held for the alias-stability walk. */
|
|
30
|
+
private moduleProgram;
|
|
21
31
|
private readonly nodeModulePath;
|
|
22
32
|
constructor(context?: AnalysisContext, extensions?: readonly CompilerAnalysisExtension[]);
|
|
23
33
|
analyze(program: Program): readonly import("@velarscript/compiler").Diagnostic[];
|
|
@@ -27,6 +37,48 @@ export declare class VelarNodeAnalyzer extends Analyzer {
|
|
|
27
37
|
readonly kind: string;
|
|
28
38
|
}, parameter: Parameter): ValueType | null;
|
|
29
39
|
private analyzeServer;
|
|
40
|
+
/**
|
|
41
|
+
* Enters one route into this server's shape map and compares it against every route already
|
|
42
|
+
* entered, whether that route was written here or composed in by a spread. Composition is why the
|
|
43
|
+
* entries carry an origin: a conflicting route the author cannot see in his own file has to name
|
|
44
|
+
* the server it came from.
|
|
45
|
+
*/
|
|
46
|
+
private recordRoute;
|
|
47
|
+
/**
|
|
48
|
+
* The routes and fallback a spread composes into the server that writes it, or null when the
|
|
49
|
+
* spread is not statically resolvable. This analyzer sees one module, so a spread contributes
|
|
50
|
+
* only when its value reaches a server declared in this module: a plain identifier, an alias of
|
|
51
|
+
* one, or a velar/serve combinator call around one — `use`, `bodyLimit`, `docs` and `lifecycle`
|
|
52
|
+
* carry paths through unchanged, and `prefix` translates them by its literal path. An imported
|
|
53
|
+
* server, a computed prefix path, or any other expression is let through unchecked, because a
|
|
54
|
+
* false conflict here would block a correct program; D90 R19's runtime referee judges the final
|
|
55
|
+
* table at assembly instead. Composition is followed transitively; the visited set bounds a
|
|
56
|
+
* cycle and starts holding the composing server, so a cycle never folds a server's own routes
|
|
57
|
+
* back into itself and reports each as conflicting with itself.
|
|
58
|
+
*/
|
|
59
|
+
private composedItems;
|
|
60
|
+
/**
|
|
61
|
+
* The server declaration a spread value names, or null when it is anything else. A
|
|
62
|
+
* `const other = base` alias chain of this module's own servers resolves too, because the alias
|
|
63
|
+
* holds exactly that ServeApp — and so does a `let` alias the whole module never reassigns,
|
|
64
|
+
* because an unwritten `let` holds its initializer exactly as a `const` does. A reassigned or
|
|
65
|
+
* ambiguous `let`, a member path, a conditional, an import, or a parameter contributes nothing.
|
|
66
|
+
* A call resolves through the path-preserving velar/serve combinators when the callee still
|
|
67
|
+
* reaches its velar/serve import: `prefix` with a literal path translates what its app argument
|
|
68
|
+
* declares, and `use`/`bodyLimit`/`docs`/`lifecycle` pass it through untouched. A computed
|
|
69
|
+
* prefix path contributes nothing — the assembly-time referee owns it. An alias's initializer
|
|
70
|
+
* re-enters this resolver whole, so `const scoped = prefix("/api", routes)` resolves exactly as
|
|
71
|
+
* the spelled-out spread does; the followed set bounds an alias cycle.
|
|
72
|
+
*/
|
|
73
|
+
private resolveComposedServer;
|
|
74
|
+
/** Whether a `let` alias has never been reassigned, asked once per name and program. */
|
|
75
|
+
private aliasBindingIsStable;
|
|
76
|
+
/**
|
|
77
|
+
* Whether a name still reaches the declaration this module recorded for it. An import, a
|
|
78
|
+
* shadowing binding, or a parameter of the same name reaches a different binding.
|
|
79
|
+
*/
|
|
80
|
+
private resolvesTo;
|
|
81
|
+
private analyzeNotFound;
|
|
30
82
|
private analyzeRoute;
|
|
31
83
|
}
|
|
32
84
|
export declare function inferNodeIntrinsic(context: CompilerIntrinsicAnalysisContext): ValueType | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-analyzer.d.ts","sourceRoot":"","sources":["../src/server-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EASR,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,
|
|
1
|
+
{"version":3,"file":"server-analyzer.d.ts","sourceRoot":"","sources":["../src/server-analyzer.ts"],"names":[],"mappings":"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"}
|
package/dist/server-analyzer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Analyzer,
|
|
1
|
+
import { Analyzer, bindingNeverReassigned, boolType, describeType, nullType, numberType, optionalOf, stringType, unknownType, spanIdentity, } from "@velarscript/compiler/extension";
|
|
2
|
+
import { routeShape } from "./route-shape.js";
|
|
2
3
|
import { isNodeServerStatement } from "./server-ast.js";
|
|
3
4
|
import { isNodeProviderType, isNodeRouteInputType, isServeRequestType, nodeProviderResult, nodeProviderType, nodeRouteInputType, nodeRouteInputValue, serveAppType, serveRequestType, } from "./server-types.js";
|
|
4
5
|
const routeHintPrefix = "node.route-param:";
|
|
@@ -8,6 +9,7 @@ const sseEventType = { kind: "object", fields: new Map([
|
|
|
8
9
|
["data", stringType], ["event", optionalOf(stringType)], ["id", optionalOf(stringType)], ["retry", optionalOf(numberType)],
|
|
9
10
|
]), optionalFields: new Set(["event", "id", "retry"]) };
|
|
10
11
|
const sseSendType = { kind: "function", parameterNames: ["event"], parameters: [{ kind: "union", members: [stringType, sseEventType] }], requiredParameters: 1, result: { kind: "promise", value: nullType } };
|
|
12
|
+
const serveCombinators = new Set(["prefix", "use", "bodyLimit", "docs", "lifecycle"]);
|
|
11
13
|
export function routeParameterHint(source, kind, schema, descriptor = false) {
|
|
12
14
|
return `${routeHintPrefix}${JSON.stringify({ source, kind, schema, descriptor })}`;
|
|
13
15
|
}
|
|
@@ -45,12 +47,47 @@ export function parseRouteResultHint(value) {
|
|
|
45
47
|
export class VelarNodeAnalyzer extends Analyzer {
|
|
46
48
|
contextualRouteParameters = new Map();
|
|
47
49
|
routeInputs = new Map();
|
|
50
|
+
/** Servers declared by the module under analysis, the only spread targets this analyzer can resolve. */
|
|
51
|
+
moduleServers = new Map();
|
|
52
|
+
/** Module-level `const alias = name` and `let alias = name` bindings, so a spread of an alias resolves to the server it names. */
|
|
53
|
+
moduleServerAliases = new Map();
|
|
54
|
+
/** Local names imported from velar/serve that name a path-preserving combinator, so a spread of a call resolves through it. */
|
|
55
|
+
moduleServeCombinators = new Map();
|
|
56
|
+
/** One answer per `let` alias name to "was this binding ever reassigned?", because the predicate walks the whole program. */
|
|
57
|
+
stableAliases = new Map();
|
|
58
|
+
/** The program under analysis, held for the alias-stability walk. */
|
|
59
|
+
moduleProgram = null;
|
|
48
60
|
nodeModulePath;
|
|
49
61
|
constructor(context = {}, extensions = []) {
|
|
50
62
|
super(context, extensions);
|
|
51
63
|
this.nodeModulePath = context.path ?? null;
|
|
52
64
|
}
|
|
53
65
|
analyze(program) {
|
|
66
|
+
this.moduleServers.clear();
|
|
67
|
+
this.moduleServerAliases.clear();
|
|
68
|
+
this.moduleServeCombinators.clear();
|
|
69
|
+
this.stableAliases.clear();
|
|
70
|
+
this.moduleProgram = program;
|
|
71
|
+
for (const statement of program.body) {
|
|
72
|
+
if (isNodeServerStatement(statement)) {
|
|
73
|
+
if (!this.moduleServers.has(statement.name))
|
|
74
|
+
this.moduleServers.set(statement.name, statement);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (statement.kind === "ImportDeclaration" && statement.source === "velar/serve") {
|
|
78
|
+
for (const specifier of statement.specifiers) {
|
|
79
|
+
if (specifier.namespace || !serveCombinators.has(specifier.imported))
|
|
80
|
+
continue;
|
|
81
|
+
if (!this.moduleServeCombinators.has(specifier.local)) {
|
|
82
|
+
this.moduleServeCombinators.set(specifier.local, { imported: specifier.imported, span: specifier.span });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const alias = moduleServerAlias(statement);
|
|
88
|
+
if (alias && !this.moduleServerAliases.has(alias.name))
|
|
89
|
+
this.moduleServerAliases.set(alias.name, alias);
|
|
90
|
+
}
|
|
54
91
|
if (!(this.nodeModulePath ?? "").endsWith(".test.vel")) {
|
|
55
92
|
for (const statement of program.body) {
|
|
56
93
|
if ((statement.kind === "ImportDeclaration" || statement.kind === "ReExportDeclaration") && statement.source === "velar/server-test") {
|
|
@@ -93,22 +130,221 @@ export class VelarNodeAnalyzer extends Analyzer {
|
|
|
93
130
|
if (!this.isPredeclared(statement))
|
|
94
131
|
this.declareBinding(statement.name, false, serveAppType, statement.span);
|
|
95
132
|
const routes = new Map();
|
|
133
|
+
let notFound = null;
|
|
96
134
|
for (const item of statement.items) {
|
|
97
135
|
if (item.kind === "NodeServerSpread") {
|
|
98
136
|
this.requireAssignable(this.inferExpression(item.value, serveAppType), serveAppType, item.value.span);
|
|
137
|
+
const composed = this.composedItems(statement, item);
|
|
138
|
+
if (!composed)
|
|
139
|
+
continue;
|
|
140
|
+
if (composed.notFound) {
|
|
141
|
+
if (notFound)
|
|
142
|
+
this.typeError(describeFallbackCollision(notFound, composed.notFound), item.span);
|
|
143
|
+
else
|
|
144
|
+
notFound = composed.notFound;
|
|
145
|
+
}
|
|
146
|
+
for (const entry of composed.routes)
|
|
147
|
+
this.recordRoute(entry, routes, item.span);
|
|
99
148
|
continue;
|
|
100
149
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
if (item.kind === "NodeNotFoundDeclaration") {
|
|
151
|
+
const written = { fallback: item, spread: null, origin: [] };
|
|
152
|
+
if (notFound)
|
|
153
|
+
this.typeError(describeFallbackCollision(notFound, written), item.span);
|
|
154
|
+
else
|
|
155
|
+
notFound = written;
|
|
156
|
+
this.analyzeNotFound(item);
|
|
157
|
+
continue;
|
|
106
158
|
}
|
|
107
|
-
|
|
108
|
-
routes.set(key, item);
|
|
159
|
+
this.recordRoute({ route: item, path: item.path, spread: null, origin: [] }, routes, item.pathSpan);
|
|
109
160
|
this.analyzeRoute(item);
|
|
110
161
|
}
|
|
111
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Enters one route into this server's shape map and compares it against every route already
|
|
165
|
+
* entered, whether that route was written here or composed in by a spread. Composition is why the
|
|
166
|
+
* entries carry an origin: a conflicting route the author cannot see in his own file has to name
|
|
167
|
+
* the server it came from.
|
|
168
|
+
*/
|
|
169
|
+
recordRoute(entry, routes, span) {
|
|
170
|
+
const key = `${entry.route.method} ${routeShape(entry.path)}`;
|
|
171
|
+
const previous = routes.get(key);
|
|
172
|
+
if (previous) {
|
|
173
|
+
this.typeError(describeRouteCollision(previous, entry), span);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Two routes of one method can share a concrete path. Where one declares a literal at every
|
|
177
|
+
// position the other does and more, the router's literal-beats-capture score picks it every
|
|
178
|
+
// time, which is the intended precedence behind '/users/me' beside '/users/{id:string}'.
|
|
179
|
+
// Where neither is more specific the winner is declaration order alone and the loser can
|
|
180
|
+
// never run, so the overlap is an error. A pair whose shared path is unrealizable — a
|
|
181
|
+
// '{n:number}' capture against the literal 'b' — is not an overlap.
|
|
182
|
+
const segments = routeSegments(entry.path);
|
|
183
|
+
for (const other of routes.values()) {
|
|
184
|
+
if (other.route.method !== entry.route.method)
|
|
185
|
+
continue;
|
|
186
|
+
// Two routes one spread composed in were already compared against each other while that
|
|
187
|
+
// server was analyzed; reporting them again here would duplicate its diagnostic.
|
|
188
|
+
if (entry.spread !== null && other.spread === entry.spread)
|
|
189
|
+
continue;
|
|
190
|
+
const declared = routeSegments(other.path);
|
|
191
|
+
if (routeSpecificityDecides(declared, segments))
|
|
192
|
+
continue;
|
|
193
|
+
const shared = routeSharedPath(declared, segments);
|
|
194
|
+
if (shared === null)
|
|
195
|
+
continue;
|
|
196
|
+
this.typeError(`Route ${describeComposedRoute(other)} overlaps ${describeComposedRoute(entry)}; both match '${shared}' and neither is more specific — narrow or remove one`, span);
|
|
197
|
+
}
|
|
198
|
+
routes.set(key, entry);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The routes and fallback a spread composes into the server that writes it, or null when the
|
|
202
|
+
* spread is not statically resolvable. This analyzer sees one module, so a spread contributes
|
|
203
|
+
* only when its value reaches a server declared in this module: a plain identifier, an alias of
|
|
204
|
+
* one, or a velar/serve combinator call around one — `use`, `bodyLimit`, `docs` and `lifecycle`
|
|
205
|
+
* carry paths through unchanged, and `prefix` translates them by its literal path. An imported
|
|
206
|
+
* server, a computed prefix path, or any other expression is let through unchecked, because a
|
|
207
|
+
* false conflict here would block a correct program; D90 R19's runtime referee judges the final
|
|
208
|
+
* table at assembly instead. Composition is followed transitively; the visited set bounds a
|
|
209
|
+
* cycle and starts holding the composing server, so a cycle never folds a server's own routes
|
|
210
|
+
* back into itself and reports each as conflicting with itself.
|
|
211
|
+
*/
|
|
212
|
+
composedItems(statement, spread) {
|
|
213
|
+
const target = this.resolveComposedServer(spread.value);
|
|
214
|
+
if (!target)
|
|
215
|
+
return null;
|
|
216
|
+
const routes = [];
|
|
217
|
+
const shapes = new Set();
|
|
218
|
+
const visited = new Set([statement]);
|
|
219
|
+
let notFound = null;
|
|
220
|
+
const collect = (server, prefix, origin) => {
|
|
221
|
+
if (visited.has(server))
|
|
222
|
+
return;
|
|
223
|
+
visited.add(server);
|
|
224
|
+
for (const item of server.items) {
|
|
225
|
+
if (item.kind === "NodeServerSpread") {
|
|
226
|
+
const nested = this.resolveComposedServer(item.value);
|
|
227
|
+
if (nested)
|
|
228
|
+
collect(nested.server, prefix + nested.prefix, [...origin, nested.server.name]);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (item.kind === "NodeNotFoundDeclaration") {
|
|
232
|
+
// The runtime refuses `prefix` around an app with @notFound outright, so a fallback seen
|
|
233
|
+
// through a prefix never reaches any composed table and composing it here would report
|
|
234
|
+
// a duplicate the program cannot have.
|
|
235
|
+
if (prefix === "")
|
|
236
|
+
notFound ??= { fallback: item, spread, origin };
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
// A server that conflicts with itself already reported it; one entry per shape is what
|
|
240
|
+
// reaches the composing server, exactly as one entry per shape reaches its own map.
|
|
241
|
+
const path = prefixedRoutePath(prefix, item.path);
|
|
242
|
+
const shape = `${item.method} ${routeShape(path)}`;
|
|
243
|
+
if (shapes.has(shape))
|
|
244
|
+
continue;
|
|
245
|
+
shapes.add(shape);
|
|
246
|
+
routes.push({ route: item, path, spread, origin });
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
collect(target.server, target.prefix, [target.server.name]);
|
|
250
|
+
return { routes, notFound };
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The server declaration a spread value names, or null when it is anything else. A
|
|
254
|
+
* `const other = base` alias chain of this module's own servers resolves too, because the alias
|
|
255
|
+
* holds exactly that ServeApp — and so does a `let` alias the whole module never reassigns,
|
|
256
|
+
* because an unwritten `let` holds its initializer exactly as a `const` does. A reassigned or
|
|
257
|
+
* ambiguous `let`, a member path, a conditional, an import, or a parameter contributes nothing.
|
|
258
|
+
* A call resolves through the path-preserving velar/serve combinators when the callee still
|
|
259
|
+
* reaches its velar/serve import: `prefix` with a literal path translates what its app argument
|
|
260
|
+
* declares, and `use`/`bodyLimit`/`docs`/`lifecycle` pass it through untouched. A computed
|
|
261
|
+
* prefix path contributes nothing — the assembly-time referee owns it. An alias's initializer
|
|
262
|
+
* re-enters this resolver whole, so `const scoped = prefix("/api", routes)` resolves exactly as
|
|
263
|
+
* the spelled-out spread does; the followed set bounds an alias cycle.
|
|
264
|
+
*/
|
|
265
|
+
resolveComposedServer(value, followed = new Set()) {
|
|
266
|
+
if (value.kind === "CallExpression" && !value.optional) {
|
|
267
|
+
if (value.callee.kind !== "IdentifierExpression")
|
|
268
|
+
return null;
|
|
269
|
+
const combinator = this.moduleServeCombinators.get(value.callee.name);
|
|
270
|
+
if (!combinator || !this.resolvesTo(value.callee.name, combinator.span))
|
|
271
|
+
return null;
|
|
272
|
+
const appIndex = combinator.imported === "prefix" ? 1 : 0;
|
|
273
|
+
const app = value.arguments[appIndex];
|
|
274
|
+
if (!app || value.argumentNames?.slice(0, appIndex + 1).some((argumentName) => argumentName !== null))
|
|
275
|
+
return null;
|
|
276
|
+
const inner = this.resolveComposedServer(app, followed);
|
|
277
|
+
if (!inner)
|
|
278
|
+
return null;
|
|
279
|
+
if (combinator.imported !== "prefix")
|
|
280
|
+
return inner;
|
|
281
|
+
const path = value.arguments[0];
|
|
282
|
+
if (!path || path.kind !== "LiteralExpression" || typeof path.value !== "string")
|
|
283
|
+
return null;
|
|
284
|
+
// The same literal shapes the runtime accepts; anything else is the runtime referee's to
|
|
285
|
+
// refuse, and claiming routes for it here would report conflicts against a table that
|
|
286
|
+
// never assembles.
|
|
287
|
+
if (path.value === "/")
|
|
288
|
+
return inner;
|
|
289
|
+
if (!path.value.startsWith("/") || path.value.endsWith("/") || /[{}*?#\\]|\/\//u.test(path.value))
|
|
290
|
+
return null;
|
|
291
|
+
return { server: inner.server, prefix: path.value + inner.prefix };
|
|
292
|
+
}
|
|
293
|
+
if (value.kind !== "IdentifierExpression")
|
|
294
|
+
return null;
|
|
295
|
+
const name = value.name;
|
|
296
|
+
const declaration = this.moduleServers.get(name);
|
|
297
|
+
if (declaration)
|
|
298
|
+
return this.resolvesTo(name, declaration.span) ? { server: declaration, prefix: "" } : null;
|
|
299
|
+
const alias = this.moduleServerAliases.get(name);
|
|
300
|
+
if (!alias || followed.has(name) || !this.resolvesTo(name, alias.span))
|
|
301
|
+
return null;
|
|
302
|
+
if (alias.binding === "let" && !this.aliasBindingIsStable(alias))
|
|
303
|
+
return null;
|
|
304
|
+
followed.add(name);
|
|
305
|
+
return this.resolveComposedServer(alias.initializer, followed);
|
|
306
|
+
}
|
|
307
|
+
/** Whether a `let` alias has never been reassigned, asked once per name and program. */
|
|
308
|
+
aliasBindingIsStable(alias) {
|
|
309
|
+
const cached = this.stableAliases.get(alias.name);
|
|
310
|
+
if (cached !== undefined)
|
|
311
|
+
return cached;
|
|
312
|
+
const stable = this.moduleProgram !== null && bindingNeverReassigned(this.moduleProgram, alias.name, alias.span);
|
|
313
|
+
this.stableAliases.set(alias.name, stable);
|
|
314
|
+
return stable;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Whether a name still reaches the declaration this module recorded for it. An import, a
|
|
318
|
+
* shadowing binding, or a parameter of the same name reaches a different binding.
|
|
319
|
+
*/
|
|
320
|
+
resolvesTo(name, span) {
|
|
321
|
+
const binding = this.lookup(name);
|
|
322
|
+
return binding !== null && spanIdentity(binding.span) === spanIdentity(span);
|
|
323
|
+
}
|
|
324
|
+
analyzeNotFound(fallback) {
|
|
325
|
+
this.analyzeFunctionDeclaration(fallback, null, true, false, true, "Not-found fallback");
|
|
326
|
+
if (fallback.parameters.length > 1) {
|
|
327
|
+
this.typeError("@notFound accepts at most one Request parameter", fallback.signatureSpan);
|
|
328
|
+
}
|
|
329
|
+
const parameter = fallback.parameters[0];
|
|
330
|
+
if (parameter) {
|
|
331
|
+
if (parameter.rest)
|
|
332
|
+
this.typeError("@notFound Request cannot be a rest parameter", parameter.span);
|
|
333
|
+
if (parameter.defaultValue)
|
|
334
|
+
this.typeError("@notFound Request is supplied by the server and cannot have a default value", parameter.span);
|
|
335
|
+
if (!parameter.type)
|
|
336
|
+
this.typeError("@notFound Request requires the explicit Request type", parameter.span);
|
|
337
|
+
else {
|
|
338
|
+
const resolved = this.expandAliases(this.resolveValidatedAnnotation(parameter.type));
|
|
339
|
+
if (!isServeRequestType(resolved))
|
|
340
|
+
this.typeError(`@notFound parameter must be Request; received ${describeType(resolved)}`, parameter.span);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const result = this.expandAliases(this.inferredFunctionResult(fallback));
|
|
344
|
+
if (!isRouteResult(result, (identity) => this.fieldsOf(identity), new Set())) {
|
|
345
|
+
this.typeError(`@notFound must return Data or a response from velar/serve; received ${describeType(result)}`, fallback.returnType?.span ?? fallback.span);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
112
348
|
analyzeRoute(route) {
|
|
113
349
|
const pathNames = validateRoutePath(route.path, route.pathSpan, (message) => this.typeError(message, route.pathSpan));
|
|
114
350
|
this.analyzeFunctionDeclaration(route, null, true, false, true, "Route");
|
|
@@ -216,7 +452,7 @@ export function inferNodeIntrinsic(context) {
|
|
|
216
452
|
switch (intrinsic.name) {
|
|
217
453
|
case "serve.response.json": {
|
|
218
454
|
arity(1, 3);
|
|
219
|
-
const value = inferAt(0,
|
|
455
|
+
const value = inferAt(0, unknownType);
|
|
220
456
|
if (argumentAt(1))
|
|
221
457
|
inferAt(1, numberType);
|
|
222
458
|
if (argumentAt(2))
|
|
@@ -225,7 +461,7 @@ export function inferNodeIntrinsic(context) {
|
|
|
225
461
|
}
|
|
226
462
|
case "serve.response.created": {
|
|
227
463
|
arity(1, 2);
|
|
228
|
-
const value = inferAt(0,
|
|
464
|
+
const value = inferAt(0, unknownType);
|
|
229
465
|
if (argumentAt(1))
|
|
230
466
|
inferAt(1, optionalOf(responseHeadersType));
|
|
231
467
|
return nodeResponseValue("json", value, 201, "application/json");
|
|
@@ -619,8 +855,137 @@ function openApiObjectSchema(fields, fieldsOf, seen, enumValuesOf) {
|
|
|
619
855
|
additionalProperties: false,
|
|
620
856
|
};
|
|
621
857
|
}
|
|
622
|
-
|
|
623
|
-
|
|
858
|
+
/**
|
|
859
|
+
* A module-level `const name = expression` or `let name = expression` binding, the indirect
|
|
860
|
+
* spellings of a spread target that can still be exactly the value the initializer resolves to: a
|
|
861
|
+
* bare name, or a velar/serve combinator call the resolver sees through. A `let` alias resolves
|
|
862
|
+
* only after the stability predicate confirms the module never reassigns it — that check belongs
|
|
863
|
+
* to the resolver, which is the point that knows the whole program. A pattern binding is excluded
|
|
864
|
+
* because it never holds the whole value.
|
|
865
|
+
*/
|
|
866
|
+
function moduleServerAlias(statement) {
|
|
867
|
+
if (statement.kind !== "VariableDeclaration")
|
|
868
|
+
return null;
|
|
869
|
+
const declaration = statement;
|
|
870
|
+
if (declaration.pattern.kind !== "NameBindingPattern")
|
|
871
|
+
return null;
|
|
872
|
+
if (declaration.initializer.kind !== "IdentifierExpression" && declaration.initializer.kind !== "CallExpression")
|
|
873
|
+
return null;
|
|
874
|
+
return { name: declaration.pattern.name, initializer: declaration.initializer, binding: declaration.binding, span: declaration.pattern.span };
|
|
875
|
+
}
|
|
876
|
+
function describeComposedOrigin(entry) {
|
|
877
|
+
return entry.origin.map((name) => `'${name}'`).join(" → ");
|
|
878
|
+
}
|
|
879
|
+
function describeComposedRoute(entry) {
|
|
880
|
+
// The effective path: a route seen through prefix(...) collides at its translated address, and
|
|
881
|
+
// that address is the one the author has to narrow.
|
|
882
|
+
const route = `'${entry.route.method} ${entry.path}'`;
|
|
883
|
+
return entry.origin.length === 0 ? route : `${route} (composed in from ${describeComposedOrigin(entry)})`;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Why two routes claim one method and shape. Three causes reach this point and each names a
|
|
887
|
+
* different repair, so the message has to tell them apart: one server composed in along two paths
|
|
888
|
+
* declares its route once and cannot be narrowed at all, two routes spelling the same path have no
|
|
889
|
+
* parameter names to blame, and only the third is the shape collision parameter names hide.
|
|
890
|
+
*/
|
|
891
|
+
function describeRouteCollision(previous, entry) {
|
|
892
|
+
// One declaration reaching this server twice is only reachable through spreads: a server's own
|
|
893
|
+
// items are never collected back into it, so both sides carry an origin.
|
|
894
|
+
const declaring = entry.origin[entry.origin.length - 1];
|
|
895
|
+
if (previous.route === entry.route && declaring !== undefined) {
|
|
896
|
+
const paths = describeComposedOrigin(previous) === describeComposedOrigin(entry)
|
|
897
|
+
? `both times from ${describeComposedOrigin(entry)}`
|
|
898
|
+
: `from ${describeComposedOrigin(previous)} and from ${describeComposedOrigin(entry)}`;
|
|
899
|
+
return `Route '${entry.route.method} ${entry.path}' is composed in twice, ${paths}; '${declaring}' declares it once — remove one spread`;
|
|
900
|
+
}
|
|
901
|
+
if (previous.path === entry.path) {
|
|
902
|
+
return `Route ${describeComposedRoute(entry)} duplicates ${describeComposedRoute(previous)}; one method and path answer from a single route`;
|
|
903
|
+
}
|
|
904
|
+
return `Route ${describeComposedRoute(entry)} conflicts with ${describeComposedRoute(previous)}; parameter names do not make two route shapes distinct`;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Both sides of a duplicate @notFound. A fallback a spread composes in is invisible in the author's
|
|
908
|
+
* own file, so naming one contributor and not the other leaves him looking for a declaration that
|
|
909
|
+
* is not there; two spreads each composing one name neither by default.
|
|
910
|
+
*/
|
|
911
|
+
function describeFallbackCollision(previous, entry) {
|
|
912
|
+
const rule = "A server can declare only one @notFound fallback";
|
|
913
|
+
if (previous.origin.length === 0 && entry.origin.length === 0)
|
|
914
|
+
return rule;
|
|
915
|
+
const source = (fallback, second) => {
|
|
916
|
+
const article = second ? "another" : "one";
|
|
917
|
+
return fallback.origin.length === 0
|
|
918
|
+
? `this server declares ${article}`
|
|
919
|
+
: `${describeComposedOrigin(fallback)} composes ${article} in`;
|
|
920
|
+
};
|
|
921
|
+
return `${rule}; ${source(previous, false)} and ${source(entry, true)}`;
|
|
922
|
+
}
|
|
923
|
+
/** A composed route's address as the runtime's `prefix` will spell it: the literal prefix, then the path. */
|
|
924
|
+
function prefixedRoutePath(prefix, path) {
|
|
925
|
+
if (prefix === "")
|
|
926
|
+
return path;
|
|
927
|
+
return prefix + (path === "/" ? "" : path);
|
|
928
|
+
}
|
|
929
|
+
const routeDecimalPattern = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u;
|
|
930
|
+
function routeSegments(path) {
|
|
931
|
+
return path.split("/").map((segment) => {
|
|
932
|
+
const match = /^\{([A-Za-z_][A-Za-z0-9_]*):([A-Za-z_][A-Za-z0-9_]*)\}$/u.exec(segment);
|
|
933
|
+
return match ? { capture: match[1], captureType: match[2] } : { literal: segment };
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function routeCaptureAdmits(captureType, literal) {
|
|
937
|
+
if (captureType === "number")
|
|
938
|
+
return routeDecimalPattern.test(literal) && Number.isFinite(Number(literal));
|
|
939
|
+
if (captureType === "bool")
|
|
940
|
+
return literal === "true" || literal === "false";
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
function routeSharedSegment(left, right) {
|
|
944
|
+
if ("literal" in left) {
|
|
945
|
+
if ("literal" in right)
|
|
946
|
+
return left.literal === right.literal ? left.literal : null;
|
|
947
|
+
return routeCaptureAdmits(right.captureType, left.literal) ? left.literal : null;
|
|
948
|
+
}
|
|
949
|
+
if ("literal" in right)
|
|
950
|
+
return routeCaptureAdmits(left.captureType, right.literal) ? right.literal : null;
|
|
951
|
+
const types = new Set([left.captureType, right.captureType]);
|
|
952
|
+
if (types.has("number") && types.has("bool"))
|
|
953
|
+
return null;
|
|
954
|
+
if (types.has("number"))
|
|
955
|
+
return "1";
|
|
956
|
+
if (types.has("bool"))
|
|
957
|
+
return "true";
|
|
958
|
+
return left.capture;
|
|
959
|
+
}
|
|
960
|
+
function routeSharedPath(left, right) {
|
|
961
|
+
if (left.length !== right.length)
|
|
962
|
+
return null;
|
|
963
|
+
const shared = [];
|
|
964
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
965
|
+
const segment = routeSharedSegment(left[index], right[index]);
|
|
966
|
+
if (segment === null)
|
|
967
|
+
return null;
|
|
968
|
+
shared.push(segment);
|
|
969
|
+
}
|
|
970
|
+
return shared.join("/");
|
|
971
|
+
}
|
|
972
|
+
function routeLiteralPositions(segments) {
|
|
973
|
+
const positions = new Set();
|
|
974
|
+
for (let index = 0; index < segments.length; index += 1)
|
|
975
|
+
if ("literal" in segments[index])
|
|
976
|
+
positions.add(index);
|
|
977
|
+
return positions;
|
|
978
|
+
}
|
|
979
|
+
function routeSpecificityDecides(left, right) {
|
|
980
|
+
const leftLiterals = routeLiteralPositions(left);
|
|
981
|
+
const rightLiterals = routeLiteralPositions(right);
|
|
982
|
+
if (leftLiterals.size === rightLiterals.size)
|
|
983
|
+
return false;
|
|
984
|
+
const [subset, superset] = leftLiterals.size < rightLiterals.size ? [leftLiterals, rightLiterals] : [rightLiterals, leftLiterals];
|
|
985
|
+
for (const position of subset)
|
|
986
|
+
if (!superset.has(position))
|
|
987
|
+
return false;
|
|
988
|
+
return true;
|
|
624
989
|
}
|
|
625
990
|
function validateRoutePath(path, _sourceSpan, report) {
|
|
626
991
|
const names = new Set();
|