fastscript 0.1.1 → 2.0.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +31 -2
  2. package/LICENSE +33 -21
  3. package/README.md +568 -59
  4. package/node_modules/@fastscript/core-private/BOUNDARY.json +15 -0
  5. package/node_modules/@fastscript/core-private/README.md +5 -0
  6. package/node_modules/@fastscript/core-private/package.json +34 -0
  7. package/node_modules/@fastscript/core-private/src/asset-optimizer.mjs +67 -0
  8. package/node_modules/@fastscript/core-private/src/audit-log.mjs +50 -0
  9. package/node_modules/@fastscript/core-private/src/auth-flows.mjs +29 -0
  10. package/node_modules/@fastscript/core-private/src/auth.mjs +115 -0
  11. package/node_modules/@fastscript/core-private/src/bench.mjs +45 -0
  12. package/node_modules/@fastscript/core-private/src/build.mjs +670 -0
  13. package/node_modules/@fastscript/core-private/src/cache.mjs +248 -0
  14. package/node_modules/@fastscript/core-private/src/check.mjs +22 -0
  15. package/node_modules/@fastscript/core-private/src/cli.mjs +95 -0
  16. package/node_modules/@fastscript/core-private/src/compat.mjs +128 -0
  17. package/node_modules/@fastscript/core-private/src/create.mjs +278 -0
  18. package/node_modules/@fastscript/core-private/src/csp.mjs +26 -0
  19. package/node_modules/@fastscript/core-private/src/db-cli.mjs +185 -0
  20. package/node_modules/@fastscript/core-private/src/db-postgres-collection.mjs +110 -0
  21. package/node_modules/@fastscript/core-private/src/db-postgres.mjs +40 -0
  22. package/node_modules/@fastscript/core-private/src/db.mjs +103 -0
  23. package/node_modules/@fastscript/core-private/src/deploy.mjs +662 -0
  24. package/node_modules/@fastscript/core-private/src/dev.mjs +5 -0
  25. package/node_modules/@fastscript/core-private/src/docs-search.mjs +35 -0
  26. package/node_modules/@fastscript/core-private/src/env.mjs +118 -0
  27. package/node_modules/@fastscript/core-private/src/export.mjs +83 -0
  28. package/node_modules/@fastscript/core-private/src/fs-diagnostics.mjs +70 -0
  29. package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +141 -0
  30. package/node_modules/@fastscript/core-private/src/fs-formatter.mjs +66 -0
  31. package/node_modules/@fastscript/core-private/src/fs-linter.mjs +274 -0
  32. package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +91 -0
  33. package/node_modules/@fastscript/core-private/src/fs-parser.mjs +980 -0
  34. package/node_modules/@fastscript/core-private/src/generated/docs-search-index.mjs +3182 -0
  35. package/node_modules/@fastscript/core-private/src/i18n.mjs +25 -0
  36. package/node_modules/@fastscript/core-private/src/interop.mjs +16 -0
  37. package/node_modules/@fastscript/core-private/src/jobs.mjs +378 -0
  38. package/node_modules/@fastscript/core-private/src/logger.mjs +27 -0
  39. package/node_modules/@fastscript/core-private/src/metrics.mjs +45 -0
  40. package/node_modules/@fastscript/core-private/src/middleware.mjs +14 -0
  41. package/node_modules/@fastscript/core-private/src/migrate.mjs +81 -0
  42. package/node_modules/@fastscript/core-private/src/migration-wizard.mjs +16 -0
  43. package/node_modules/@fastscript/core-private/src/module-loader.mjs +46 -0
  44. package/node_modules/@fastscript/core-private/src/oauth-providers.mjs +103 -0
  45. package/node_modules/@fastscript/core-private/src/observability.mjs +21 -0
  46. package/node_modules/@fastscript/core-private/src/plugins.mjs +194 -0
  47. package/node_modules/@fastscript/core-private/src/retention.mjs +57 -0
  48. package/node_modules/@fastscript/core-private/src/routes.mjs +178 -0
  49. package/node_modules/@fastscript/core-private/src/scheduler.mjs +104 -0
  50. package/node_modules/@fastscript/core-private/src/security.mjs +233 -0
  51. package/node_modules/@fastscript/core-private/src/server-runtime.mjs +849 -0
  52. package/node_modules/@fastscript/core-private/src/serverless-handler.mjs +20 -0
  53. package/node_modules/@fastscript/core-private/src/session-policy.mjs +38 -0
  54. package/node_modules/@fastscript/core-private/src/start.mjs +10 -0
  55. package/node_modules/@fastscript/core-private/src/storage.mjs +155 -0
  56. package/node_modules/@fastscript/core-private/src/style-primitives.mjs +538 -0
  57. package/node_modules/@fastscript/core-private/src/style-system.mjs +461 -0
  58. package/node_modules/@fastscript/core-private/src/tenant.mjs +55 -0
  59. package/node_modules/@fastscript/core-private/src/typecheck.mjs +1464 -0
  60. package/node_modules/@fastscript/core-private/src/validate.mjs +22 -0
  61. package/node_modules/@fastscript/core-private/src/validation.mjs +88 -0
  62. package/node_modules/@fastscript/core-private/src/webhook.mjs +81 -0
  63. package/node_modules/@fastscript/core-private/src/worker.mjs +24 -0
  64. package/package.json +88 -8
  65. package/src/asset-optimizer.mjs +67 -0
  66. package/src/audit-log.mjs +50 -0
  67. package/src/auth.mjs +1 -115
  68. package/src/bench.mjs +20 -7
  69. package/src/build.mjs +1 -222
  70. package/src/cache.mjs +210 -20
  71. package/src/cli.mjs +29 -5
  72. package/src/compat.mjs +7 -1
  73. package/src/create.mjs +65 -11
  74. package/src/csp.mjs +26 -0
  75. package/src/db-cli.mjs +158 -18
  76. package/src/db-postgres-collection.mjs +110 -0
  77. package/src/deploy.mjs +1 -65
  78. package/src/docs-search.mjs +35 -0
  79. package/src/env.mjs +34 -5
  80. package/src/fs-diagnostics.mjs +70 -0
  81. package/src/fs-error-codes.mjs +126 -0
  82. package/src/fs-formatter.mjs +66 -0
  83. package/src/fs-linter.mjs +274 -0
  84. package/src/fs-normalize.mjs +17 -26
  85. package/src/fs-parser.mjs +1 -0
  86. package/src/generated/docs-search-index.mjs +3220 -0
  87. package/src/i18n.mjs +25 -0
  88. package/src/jobs.mjs +283 -32
  89. package/src/metrics.mjs +45 -0
  90. package/src/migration-wizard.mjs +16 -0
  91. package/src/module-loader.mjs +46 -0
  92. package/src/oauth-providers.mjs +103 -0
  93. package/src/plugins.mjs +194 -0
  94. package/src/retention.mjs +57 -0
  95. package/src/routes.mjs +178 -0
  96. package/src/scheduler.mjs +104 -0
  97. package/src/security.mjs +197 -19
  98. package/src/server-runtime.mjs +1 -339
  99. package/src/serverless-handler.mjs +20 -0
  100. package/src/session-policy.mjs +38 -0
  101. package/src/storage.mjs +1 -56
  102. package/src/style-system.mjs +461 -0
  103. package/src/tenant.mjs +55 -0
  104. package/src/typecheck.mjs +1 -0
  105. package/src/validate.mjs +5 -1
  106. package/src/validation.mjs +14 -5
  107. package/src/webhook.mjs +1 -71
  108. package/src/worker.mjs +23 -4
@@ -0,0 +1,1464 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { extname, join, resolve } from "node:path";
3
+ import { parseFastScript } from "./fs-parser.mjs";
4
+ import { resolveErrorMeta } from "./fs-error-codes.mjs";
5
+ import { inferRouteMeta, inferRouteParamTypes, sortRoutesByPriority } from "./routes.mjs";
6
+
7
+ const APP_DIR = resolve("app");
8
+ const PAGES_DIR = resolve("app/pages");
9
+ const OUT_DIR = resolve(".fastscript");
10
+ const TYPES_PATH = join(OUT_DIR, "route-params.d.ts");
11
+ const REPORT_PATH = join(OUT_DIR, "typecheck-report.json");
12
+
13
+ const T_ANY = Object.freeze({ kind: "any" });
14
+ const T_UNKNOWN = Object.freeze({ kind: "unknown" });
15
+ const T_VOID = Object.freeze({ kind: "void" });
16
+ const T_UNDEFINED = Object.freeze({ kind: "undefined" });
17
+ const T_NULL = Object.freeze({ kind: "null" });
18
+ const T_BOOLEAN = Object.freeze({ kind: "boolean" });
19
+ const T_NUMBER = Object.freeze({ kind: "number" });
20
+ const T_STRING = Object.freeze({ kind: "string" });
21
+ const T_OBJECT = Object.freeze({ kind: "object" });
22
+
23
+ let scopeCounter = 0;
24
+
25
+ function walk(dir) {
26
+ if (!existsSync(dir)) return [];
27
+ const out = [];
28
+ for (const entry of readdirSync(dir)) {
29
+ const full = join(dir, entry);
30
+ const st = statSync(full);
31
+ if (st.isDirectory()) out.push(...walk(full));
32
+ else if (st.isFile()) out.push(full);
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function createLineStarts(source) {
38
+ const text = String(source ?? "");
39
+ const out = [0];
40
+ for (let i = 0; i < text.length; i += 1) {
41
+ if (text[i] === "\n") out.push(i + 1);
42
+ }
43
+ return out;
44
+ }
45
+
46
+ function lineFromOffset(lineStarts, offset) {
47
+ let lo = 0;
48
+ let hi = lineStarts.length - 1;
49
+ while (lo <= hi) {
50
+ const mid = (lo + hi) >> 1;
51
+ const start = lineStarts[mid];
52
+ const next = lineStarts[mid + 1] ?? Number.POSITIVE_INFINITY;
53
+ if (offset < start) hi = mid - 1;
54
+ else if (offset >= next) lo = mid + 1;
55
+ else return { line: mid + 1, column: offset - start + 1 };
56
+ }
57
+ return { line: 1, column: 1 };
58
+ }
59
+
60
+ function spanFromNode(node, sourceLength = 0) {
61
+ const start = Math.max(0, Math.min(sourceLength, Number(node?.fsRange?.[0] ?? node?.start ?? 0)));
62
+ const end = Math.max(start, Math.min(sourceLength, Number(node?.fsRange?.[1] ?? node?.end ?? start)));
63
+ return { start, end };
64
+ }
65
+
66
+ function createDiagnostic({ file, source, lineStarts, code, message, severity, hint, span, related = [] }) {
67
+ const meta = resolveErrorMeta(code);
68
+ const sourceLength = source.length;
69
+ const start = Math.max(0, Math.min(sourceLength, Number(span?.start ?? 0)));
70
+ const end = Math.max(start, Math.min(sourceLength, Number(span?.end ?? start + 1)));
71
+ const loc = lineFromOffset(lineStarts, start);
72
+ return {
73
+ file,
74
+ code,
75
+ severity: severity || meta.severity || "error",
76
+ message: message || meta.message,
77
+ hint: hint || meta.hint || "",
78
+ span: { start, end },
79
+ line: loc.line,
80
+ column: loc.column,
81
+ related: related.map((entry) => {
82
+ const relStart = Math.max(0, Math.min(sourceLength, Number(entry.span?.start ?? 0)));
83
+ const relLoc = lineFromOffset(lineStarts, relStart);
84
+ return {
85
+ file: entry.file || file,
86
+ message: entry.message || "Related location",
87
+ line: relLoc.line,
88
+ column: relLoc.column,
89
+ span: {
90
+ start: relStart,
91
+ end: Math.max(relStart, Math.min(sourceLength, Number(entry.span?.end ?? relStart + 1))),
92
+ },
93
+ };
94
+ }),
95
+ };
96
+ }
97
+
98
+ function dedupeDiagnostics(diagnostics) {
99
+ const seen = new Set();
100
+ const out = [];
101
+ for (const diagnostic of diagnostics) {
102
+ const key = `${diagnostic.file}|${diagnostic.code}|${diagnostic.severity}|${diagnostic.message}|${diagnostic.span.start}|${diagnostic.span.end}`;
103
+ if (seen.has(key)) continue;
104
+ seen.add(key);
105
+ out.push(diagnostic);
106
+ }
107
+ out.sort((a, b) => {
108
+ if (a.file !== b.file) return a.file.localeCompare(b.file);
109
+ if (a.span.start !== b.span.start) return a.span.start - b.span.start;
110
+ if (a.code !== b.code) return a.code.localeCompare(b.code);
111
+ return a.message.localeCompare(b.message);
112
+ });
113
+ return out;
114
+ }
115
+
116
+ function makeFnType(params, returnType, minArgs = params.length, maxArgs = params.length) {
117
+ return { kind: "function", params, returnType: returnType || T_UNKNOWN, minArgs, maxArgs };
118
+ }
119
+
120
+ function makeArrayType(elementType = T_UNKNOWN) {
121
+ return { kind: "array", elementType };
122
+ }
123
+
124
+ function makeObjectType(properties = {}) {
125
+ return { kind: "object", properties: { ...properties } };
126
+ }
127
+
128
+ function typeFromLiteral(value) {
129
+ if (value === null) return T_NULL;
130
+ if (value === undefined) return T_UNDEFINED;
131
+ const t = typeof value;
132
+ if (t === "string") return T_STRING;
133
+ if (t === "number") return T_NUMBER;
134
+ if (t === "boolean") return T_BOOLEAN;
135
+ if (t === "bigint") return { kind: "bigint" };
136
+ return T_UNKNOWN;
137
+ }
138
+
139
+ function typeToString(type) {
140
+ if (!type) return "unknown";
141
+ if (type.kind === "union") return type.types.map(typeToString).join(" | ");
142
+ if (type.kind === "array") return `${typeToString(type.elementType)}[]`;
143
+ if (type.kind === "object") {
144
+ const entries = Object.entries(type.properties || {});
145
+ if (!entries.length) return "object";
146
+ return `{ ${entries.map(([k, v]) => `${k}: ${typeToString(v)}`).join("; ")} }`;
147
+ }
148
+ if (type.kind === "function") {
149
+ return `(${type.params.map(typeToString).join(", ")}) => ${typeToString(type.returnType)}`;
150
+ }
151
+ return type.kind;
152
+ }
153
+
154
+ function typeEquals(a, b) {
155
+ if (!a || !b) return false;
156
+ if (a.kind !== b.kind) return false;
157
+ if (a.kind === "array") return typeEquals(a.elementType, b.elementType);
158
+ if (a.kind === "function") {
159
+ if (a.params.length !== b.params.length) return false;
160
+ for (let i = 0; i < a.params.length; i += 1) {
161
+ if (!typeEquals(a.params[i], b.params[i])) return false;
162
+ }
163
+ return typeEquals(a.returnType, b.returnType);
164
+ }
165
+ if (a.kind === "object") {
166
+ const aKeys = Object.keys(a.properties || {}).sort();
167
+ const bKeys = Object.keys(b.properties || {}).sort();
168
+ if (aKeys.length !== bKeys.length) return false;
169
+ for (let i = 0; i < aKeys.length; i += 1) {
170
+ if (aKeys[i] !== bKeys[i]) return false;
171
+ if (!typeEquals(a.properties[aKeys[i]], b.properties[bKeys[i]])) return false;
172
+ }
173
+ return true;
174
+ }
175
+ if (a.kind === "union") {
176
+ if (a.types.length !== b.types.length) return false;
177
+ return a.types.every((candidate, index) => typeEquals(candidate, b.types[index]));
178
+ }
179
+ return true;
180
+ }
181
+
182
+ function unionTypes(types) {
183
+ const expanded = [];
184
+ for (const type of types) {
185
+ if (!type) continue;
186
+ if (type.kind === "union") expanded.push(...type.types);
187
+ else expanded.push(type);
188
+ }
189
+ if (!expanded.length) return T_UNKNOWN;
190
+ if (expanded.some((type) => type.kind === "any")) return T_ANY;
191
+
192
+ const unique = [];
193
+ for (const candidate of expanded) {
194
+ if (!unique.some((existing) => typeEquals(existing, candidate))) unique.push(candidate);
195
+ }
196
+ if (unique.length === 1) return unique[0];
197
+ return { kind: "union", types: unique.sort((a, b) => typeToString(a).localeCompare(typeToString(b))) };
198
+ }
199
+
200
+ function copyType(type) {
201
+ if (!type || typeof type !== "object") return T_UNKNOWN;
202
+ if (type.kind === "union") return unionTypes(type.types.map(copyType));
203
+ if (type.kind === "array") return makeArrayType(copyType(type.elementType));
204
+ if (type.kind === "object") {
205
+ const out = {};
206
+ for (const [key, value] of Object.entries(type.properties || {})) out[key] = copyType(value);
207
+ return makeObjectType(out);
208
+ }
209
+ if (type.kind === "function") return makeFnType(type.params.map(copyType), copyType(type.returnType), type.minArgs, type.maxArgs);
210
+ return { ...type };
211
+ }
212
+
213
+ function isAssignable(source, target) {
214
+ if (!source || !target) return true;
215
+ if (source.kind === "any" || target.kind === "any") return true;
216
+ if (source.kind === "unknown" || target.kind === "unknown") return true;
217
+ if (target.kind === "union") return target.types.some((candidate) => isAssignable(source, candidate));
218
+ if (source.kind === "union") return source.types.every((candidate) => isAssignable(candidate, target));
219
+ if (source.kind === target.kind) {
220
+ if (source.kind === "array") return isAssignable(source.elementType, target.elementType);
221
+ if (source.kind === "object") {
222
+ const targetProps = target.properties || {};
223
+ const sourceProps = source.properties || {};
224
+ for (const [key, targetType] of Object.entries(targetProps)) {
225
+ if (!(key in sourceProps)) return false;
226
+ if (!isAssignable(sourceProps[key], targetType)) return false;
227
+ }
228
+ return true;
229
+ }
230
+ if (source.kind === "function") {
231
+ if (source.minArgs > target.maxArgs) return false;
232
+ if (source.maxArgs < target.minArgs) return false;
233
+ return isAssignable(source.returnType, target.returnType);
234
+ }
235
+ return true;
236
+ }
237
+ if (source.kind === "null" && target.kind === "object") return true;
238
+ return false;
239
+ }
240
+
241
+ class Scope {
242
+ constructor(parent = null, kind = "block") {
243
+ this.id = ++scopeCounter;
244
+ this.parent = parent;
245
+ this.kind = kind;
246
+ this.symbols = new Map();
247
+ }
248
+
249
+ declare(symbol) {
250
+ this.symbols.set(symbol.name, symbol);
251
+ return symbol;
252
+ }
253
+
254
+ lookup(name) {
255
+ if (this.symbols.has(name)) return this.symbols.get(name);
256
+ if (this.parent) return this.parent.lookup(name);
257
+ return null;
258
+ }
259
+ }
260
+
261
+ function createSymbol({ name, type, kind, mutable, span, file, runtime = "universal", runtimes = null }) {
262
+ const normalizedRuntimes = Array.isArray(runtimes) && runtimes.length ? [...new Set(runtimes)] : [runtime || "universal"];
263
+ return { name, type: type || T_UNKNOWN, kind, mutable, span, file, runtime: normalizedRuntimes[0], runtimes: normalizedRuntimes };
264
+ }
265
+
266
+ function inferAllowedRuntimeContextsFromFile(file = "") {
267
+ const normalized = String(file).replace(/\\/g, "/").toLowerCase();
268
+ if (normalized.endsWith(".client.fs")) return ["universal", "browser"];
269
+ if (normalized.endsWith(".server.fs")) return ["universal", "server"];
270
+ if (normalized.endsWith(".edge.fs")) return ["universal", "edge"];
271
+ if (/(^|\/)(app\/)?api\//.test(normalized)) return ["universal", "server"];
272
+ if (/(^|\/)(app\/)?pages\//.test(normalized)) return ["universal", "browser", "server"];
273
+ return ["universal"];
274
+ }
275
+
276
+ function isRuntimeCompatible(symbolRuntimes = ["universal"], allowedContexts = ["universal"]) {
277
+ const runtimes = Array.isArray(symbolRuntimes) && symbolRuntimes.length ? symbolRuntimes : ["universal"];
278
+ const contexts = Array.isArray(allowedContexts) && allowedContexts.length ? allowedContexts : ["universal"];
279
+ if (runtimes.includes("universal")) return true;
280
+ return runtimes.some((runtime) => contexts.includes(runtime));
281
+ }
282
+
283
+ function collectPatternBindings(pattern, out = []) {
284
+ if (!pattern) return out;
285
+ switch (pattern.type) {
286
+ case "Identifier":
287
+ out.push({ name: pattern.name, node: pattern });
288
+ break;
289
+ case "AssignmentPattern":
290
+ collectPatternBindings(pattern.left, out);
291
+ break;
292
+ case "RestElement":
293
+ collectPatternBindings(pattern.argument, out);
294
+ break;
295
+ case "ArrayPattern":
296
+ for (const element of pattern.elements || []) collectPatternBindings(element, out);
297
+ break;
298
+ case "ObjectPattern":
299
+ for (const property of pattern.properties || []) {
300
+ if (property.type === "RestElement") collectPatternBindings(property.argument, out);
301
+ else collectPatternBindings(property.value || property.key, out);
302
+ }
303
+ break;
304
+ default:
305
+ break;
306
+ }
307
+ return out;
308
+ }
309
+
310
+ function declareBuiltins(scope, file) {
311
+ scope.declare(createSymbol({ name: "Number", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, 1) }));
312
+ scope.declare(createSymbol({ name: "String", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
313
+ scope.declare(createSymbol({ name: "Boolean", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_BOOLEAN, 1, 1) }));
314
+ scope.declare(createSymbol({ name: "Array", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
315
+ scope.declare(createSymbol({ name: "Object", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
316
+ scope.declare(createSymbol({ name: "JSON", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
317
+ scope.declare(createSymbol({ name: "Math", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
318
+ scope.declare(createSymbol({ name: "Date", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
319
+ scope.declare(createSymbol({ name: "Promise", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
320
+ scope.declare(createSymbol({ name: "Set", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
321
+ scope.declare(createSymbol({ name: "Map", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
322
+ scope.declare(createSymbol({ name: "Error", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
323
+ scope.declare(createSymbol({ name: "RegExp", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
324
+ scope.declare(createSymbol({ name: "Intl", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
325
+ scope.declare(createSymbol({ name: "URL", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_OBJECT, 1, Number.MAX_SAFE_INTEGER) }));
326
+ scope.declare(createSymbol({ name: "process", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
327
+ scope.declare(createSymbol({ name: "Buffer", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
328
+ scope.declare(createSymbol({ name: "__dirname", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_STRING, runtime: "server" }));
329
+ scope.declare(createSymbol({ name: "__filename", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_STRING, runtime: "server" }));
330
+ scope.declare(createSymbol({ name: "require", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_UNKNOWN, 1, Number.MAX_SAFE_INTEGER), runtime: "server" }));
331
+ scope.declare(createSymbol({ name: "module", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
332
+ scope.declare(createSymbol({ name: "exports", kind: "builtin", mutable: true, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "server" }));
333
+ scope.declare(createSymbol({ name: "globalThis", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
334
+ scope.declare(createSymbol({ name: "window", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
335
+ scope.declare(createSymbol({ name: "document", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
336
+ scope.declare(createSymbol({ name: "location", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
337
+ scope.declare(createSymbol({ name: "navigator", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker"] }));
338
+ scope.declare(createSymbol({ name: "history", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
339
+ scope.declare(createSymbol({ name: "localStorage", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
340
+ scope.declare(createSymbol({ name: "sessionStorage", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
341
+ scope.declare(createSymbol({ name: "indexedDB", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker"] }));
342
+ scope.declare(createSymbol({ name: "self", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtimes: ["browser", "worker", "edge"] }));
343
+ scope.declare(createSymbol({ name: "console", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
344
+ scope.declare(createSymbol({ name: "Request", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
345
+ scope.declare(createSymbol({ name: "Response", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
346
+ scope.declare(createSymbol({ name: "Headers", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
347
+ scope.declare(createSymbol({ name: "FormData", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
348
+ scope.declare(createSymbol({ name: "AbortController", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
349
+ scope.declare(createSymbol({ name: "AbortSignal", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
350
+ scope.declare(createSymbol({ name: "Blob", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
351
+ scope.declare(createSymbol({ name: "File", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
352
+ scope.declare(createSymbol({ name: "ReadableStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
353
+ scope.declare(createSymbol({ name: "WritableStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
354
+ scope.declare(createSymbol({ name: "TransformStream", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
355
+ scope.declare(createSymbol({ name: "crypto", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT }));
356
+ scope.declare(createSymbol({ name: "Event", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
357
+ scope.declare(createSymbol({ name: "CustomEvent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: T_OBJECT, runtime: "browser" }));
358
+ scope.declare(createSymbol({ name: "TextEncoder", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([], T_OBJECT, 0, 0) }));
359
+ scope.declare(createSymbol({ name: "TextDecoder", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([], T_OBJECT, 0, 0) }));
360
+ scope.declare(createSymbol({ name: "setTimeout", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, Number.MAX_SAFE_INTEGER) }));
361
+ scope.declare(createSymbol({ name: "clearTimeout", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
362
+ scope.declare(createSymbol({ name: "setInterval", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_NUMBER, 1, Number.MAX_SAFE_INTEGER) }));
363
+ scope.declare(createSymbol({ name: "clearInterval", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
364
+ scope.declare(createSymbol({ name: "queueMicrotask", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_VOID, 1, 1) }));
365
+ scope.declare(createSymbol({ name: "URLSearchParams", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_OBJECT, 1, 1) }));
366
+ scope.declare(createSymbol({ name: "fetch", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_UNKNOWN, 1, Number.MAX_SAFE_INTEGER) }));
367
+ scope.declare(createSymbol({ name: "atob", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
368
+ scope.declare(createSymbol({ name: "btoa", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
369
+ scope.declare(createSymbol({ name: "encodeURIComponent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
370
+ scope.declare(createSymbol({ name: "decodeURIComponent", kind: "builtin", mutable: false, span: { start: 0, end: 0 }, file, type: makeFnType([T_UNKNOWN], T_STRING, 1, 1) }));
371
+ }
372
+
373
+ function symbolSuggestion(name, scope) {
374
+ const candidates = [];
375
+ let cursor = scope;
376
+ while (cursor) {
377
+ for (const candidate of cursor.symbols.keys()) candidates.push(candidate);
378
+ cursor = cursor.parent;
379
+ }
380
+ const lowered = String(name || "").toLowerCase();
381
+ const hits = [...new Set(candidates)]
382
+ .filter((candidate) => candidate.toLowerCase().startsWith(lowered[0] || ""))
383
+ .slice(0, 3);
384
+ return hits.length ? hits.join(", ") : "";
385
+ }
386
+
387
+ function reportUnknownSymbol(identifier, scope, state) {
388
+ const span = spanFromNode(identifier, state.source.length);
389
+ const suggestion = symbolSuggestion(identifier.name, scope);
390
+ state.diagnostics.push(
391
+ createDiagnostic({
392
+ file: state.file,
393
+ source: state.source,
394
+ lineStarts: state.lineStarts,
395
+ code: "FS4101",
396
+ span,
397
+ message: `Unknown symbol \`${identifier.name}\`.`,
398
+ hint: suggestion ? `Did you mean: ${suggestion}` : resolveErrorMeta("FS4101").hint,
399
+ }),
400
+ );
401
+ }
402
+
403
+ function reportRuntimeScopeError(identifier, symbol, state) {
404
+ const span = spanFromNode(identifier, state.source.length);
405
+ const available = Array.isArray(symbol.runtimes) && symbol.runtimes.length ? symbol.runtimes : [symbol.runtime || "universal"];
406
+ const context = (state.allowedRuntimes || ["universal"]).join("|");
407
+ state.diagnostics.push(
408
+ createDiagnostic({
409
+ file: state.file,
410
+ source: state.source,
411
+ lineStarts: state.lineStarts,
412
+ code: "FS4201",
413
+ span,
414
+ message: `Symbol \`${identifier.name}\` is not available in ${context} context.`,
415
+ hint: `This symbol is only available in: ${available.join(", ")}`,
416
+ }),
417
+ );
418
+ }
419
+
420
+ function inferFunctionFromNode(node, scope, state) {
421
+ const params = [];
422
+ let required = 0;
423
+ for (const param of node.params || []) {
424
+ if (param.type === "AssignmentPattern") {
425
+ params.push(T_UNKNOWN);
426
+ continue;
427
+ }
428
+ required += 1;
429
+ params.push(T_UNKNOWN);
430
+ }
431
+
432
+ const fnScope = new Scope(scope, "function");
433
+ state.scopes.push(fnScope);
434
+ for (const param of node.params || []) {
435
+ const bindings = collectPatternBindings(param);
436
+ for (const binding of bindings) {
437
+ fnScope.declare(createSymbol({
438
+ name: binding.name,
439
+ type: T_UNKNOWN,
440
+ kind: "param",
441
+ mutable: true,
442
+ span: spanFromNode(binding.node, state.source.length),
443
+ file: state.file,
444
+ }));
445
+ }
446
+ }
447
+
448
+ const fnContext = { returns: [] };
449
+ if (node.body?.type === "BlockStatement") {
450
+ for (const statement of node.body.body || []) {
451
+ analyzeStatement(statement, fnScope, state, fnContext);
452
+ }
453
+ } else if (node.body) {
454
+ fnContext.returns.push(inferExpression(node.body, fnScope, state, fnContext));
455
+ }
456
+
457
+ const returnType = fnContext.returns.length ? unionTypes(fnContext.returns) : T_VOID;
458
+ return makeFnType(params, returnType, required, params.length);
459
+ }
460
+
461
+ function hoistDeclarations(body, scope, state) {
462
+ for (const statement of body || []) {
463
+ if (statement.type === "FunctionDeclaration" && statement.id?.name) {
464
+ scope.declare(createSymbol({
465
+ name: statement.id.name,
466
+ type: makeFnType(new Array(statement.params?.length || 0).fill(T_UNKNOWN), T_UNKNOWN),
467
+ kind: "function",
468
+ mutable: false,
469
+ span: spanFromNode(statement.id, state.source.length),
470
+ file: state.file,
471
+ }));
472
+ }
473
+
474
+ if (statement.type === "VariableDeclaration" && statement.kind === "var") {
475
+ for (const declaration of statement.declarations || []) {
476
+ const bindings = collectPatternBindings(declaration.id);
477
+ for (const binding of bindings) {
478
+ if (scope.symbols.has(binding.name)) continue;
479
+ scope.declare(createSymbol({
480
+ name: binding.name,
481
+ type: T_UNKNOWN,
482
+ kind: "var",
483
+ mutable: true,
484
+ span: spanFromNode(binding.node, state.source.length),
485
+ file: state.file,
486
+ }));
487
+ }
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ function inferExpression(node, scope, state, fnContext) {
494
+ if (!node) return T_UNKNOWN;
495
+
496
+ switch (node.type) {
497
+ case "Literal":
498
+ return typeFromLiteral(node.value);
499
+ case "TemplateLiteral":
500
+ for (const expression of node.expressions || []) inferExpression(expression, scope, state, fnContext);
501
+ return T_STRING;
502
+ case "Identifier": {
503
+ const symbol = scope.lookup(node.name);
504
+ if (!symbol) {
505
+ reportUnknownSymbol(node, scope, state);
506
+ return T_UNKNOWN;
507
+ }
508
+ if (!isRuntimeCompatible(symbol.runtimes, state.allowedRuntimes)) {
509
+ reportRuntimeScopeError(node, symbol, state);
510
+ }
511
+ return copyType(symbol.type);
512
+ }
513
+ case "ArrayExpression": {
514
+ const elementTypes = (node.elements || []).filter(Boolean).map((element) => inferExpression(element, scope, state, fnContext));
515
+ return makeArrayType(elementTypes.length ? unionTypes(elementTypes) : T_UNKNOWN);
516
+ }
517
+ case "ObjectExpression": {
518
+ const props = {};
519
+ for (const property of node.properties || []) {
520
+ if (property.type === "Property") {
521
+ let keyName = null;
522
+ if (property.computed) {
523
+ inferExpression(property.key, scope, state, fnContext);
524
+ } else if (property.key.type === "Identifier") {
525
+ keyName = property.key.name;
526
+ } else if (property.key.type === "Literal") {
527
+ keyName = String(property.key.value);
528
+ }
529
+ const valueType = inferExpression(property.value, scope, state, fnContext);
530
+ if (keyName) props[keyName] = valueType;
531
+ }
532
+ }
533
+ return makeObjectType(props);
534
+ }
535
+ case "UnaryExpression": {
536
+ const argType = inferExpression(node.argument, scope, state, fnContext);
537
+ if (["+", "-", "~"].includes(node.operator)) {
538
+ if (!isAssignable(argType, T_NUMBER)) {
539
+ state.diagnostics.push(createDiagnostic({
540
+ file: state.file,
541
+ source: state.source,
542
+ lineStarts: state.lineStarts,
543
+ code: "FS4107",
544
+ span: spanFromNode(node.argument, state.source.length),
545
+ message: `Operator \`${node.operator}\` expects a numeric operand, got ${typeToString(argType)}.`,
546
+ }));
547
+ }
548
+ return T_NUMBER;
549
+ }
550
+ if (node.operator === "!") return T_BOOLEAN;
551
+ if (node.operator === "typeof") return T_STRING;
552
+ if (node.operator === "void") return T_UNDEFINED;
553
+ return T_UNKNOWN;
554
+ }
555
+ case "BinaryExpression": {
556
+ const leftType = inferExpression(node.left, scope, state, fnContext);
557
+ const rightType = inferExpression(node.right, scope, state, fnContext);
558
+ if (node.operator === "+") {
559
+ if (leftType.kind === "string" || rightType.kind === "string") return T_STRING;
560
+ if (!isAssignable(leftType, T_NUMBER) || !isAssignable(rightType, T_NUMBER)) {
561
+ state.diagnostics.push(createDiagnostic({
562
+ file: state.file,
563
+ source: state.source,
564
+ lineStarts: state.lineStarts,
565
+ code: "FS4107",
566
+ span: spanFromNode(node, state.source.length),
567
+ message: `Operator \`+\` expected number or string-compatible operands, got ${typeToString(leftType)} and ${typeToString(rightType)}.`,
568
+ }));
569
+ }
570
+ return T_NUMBER;
571
+ }
572
+ if (["-", "*", "/", "%", "**", "<", ">", "<=", ">="].includes(node.operator)) {
573
+ if (!isAssignable(leftType, T_NUMBER) || !isAssignable(rightType, T_NUMBER)) {
574
+ state.diagnostics.push(createDiagnostic({
575
+ file: state.file,
576
+ source: state.source,
577
+ lineStarts: state.lineStarts,
578
+ code: "FS4107",
579
+ span: spanFromNode(node, state.source.length),
580
+ message: `Operator \`${node.operator}\` requires numeric operands, got ${typeToString(leftType)} and ${typeToString(rightType)}.`,
581
+ }));
582
+ }
583
+ return ["<", ">", "<=", ">="].includes(node.operator) ? T_BOOLEAN : T_NUMBER;
584
+ }
585
+ if (["==", "!=", "===", "!=="].includes(node.operator)) return T_BOOLEAN;
586
+ return T_UNKNOWN;
587
+ }
588
+ case "LogicalExpression": {
589
+ const leftType = inferExpression(node.left, scope, state, fnContext);
590
+ const rightType = inferExpression(node.right, scope, state, fnContext);
591
+ if (node.operator === "&&" || node.operator === "||" || node.operator === "??") return unionTypes([leftType, rightType]);
592
+ return T_UNKNOWN;
593
+ }
594
+ case "ConditionalExpression": {
595
+ inferExpression(node.test, scope, state, fnContext);
596
+ return unionTypes([
597
+ inferExpression(node.consequent, scope, state, fnContext),
598
+ inferExpression(node.alternate, scope, state, fnContext),
599
+ ]);
600
+ }
601
+ case "AssignmentExpression": {
602
+ const valueType = inferExpression(node.right, scope, state, fnContext);
603
+ if (node.left.type === "Identifier") {
604
+ const symbol = scope.lookup(node.left.name);
605
+ if (!symbol) {
606
+ reportUnknownSymbol(node.left, scope, state);
607
+ return valueType;
608
+ }
609
+ if (!symbol.mutable) {
610
+ state.diagnostics.push(createDiagnostic({
611
+ file: state.file,
612
+ source: state.source,
613
+ lineStarts: state.lineStarts,
614
+ code: "FS4102",
615
+ span: spanFromNode(node.left, state.source.length),
616
+ message: `Cannot assign to constant binding \`${node.left.name}\`.`,
617
+ related: [{ message: `\`${node.left.name}\` was declared here.`, span: symbol.span, file: symbol.file }],
618
+ }));
619
+ return valueType;
620
+ }
621
+ if (symbol.type.kind !== "unknown" && symbol.type.kind !== "any" && !isAssignable(valueType, symbol.type)) {
622
+ state.diagnostics.push(createDiagnostic({
623
+ file: state.file,
624
+ source: state.source,
625
+ lineStarts: state.lineStarts,
626
+ code: "FS4103",
627
+ span: spanFromNode(node.right, state.source.length),
628
+ message: `Cannot assign ${typeToString(valueType)} to \`${symbol.name}\` (${typeToString(symbol.type)}).`,
629
+ related: [{ message: `\`${symbol.name}\` declared with type ${typeToString(symbol.type)}.`, span: symbol.span, file: symbol.file }],
630
+ }));
631
+ } else if (symbol.type.kind === "unknown") {
632
+ symbol.type = valueType;
633
+ } else {
634
+ symbol.type = unionTypes([symbol.type, valueType]);
635
+ }
636
+ } else if (node.left.type === "MemberExpression") {
637
+ const objectType = inferExpression(node.left.object, scope, state, fnContext);
638
+ const propName = !node.left.computed && node.left.property?.type === "Identifier"
639
+ ? node.left.property.name
640
+ : (node.left.property?.type === "Literal" ? String(node.left.property.value) : null);
641
+ if (propName && objectType.kind === "object") {
642
+ const current = objectType.properties[propName];
643
+ objectType.properties[propName] = current ? unionTypes([current, valueType]) : valueType;
644
+ } else if (objectType.kind === "array" && propName === "length" && !isAssignable(valueType, T_NUMBER)) {
645
+ state.diagnostics.push(createDiagnostic({
646
+ file: state.file,
647
+ source: state.source,
648
+ lineStarts: state.lineStarts,
649
+ code: "FS4103",
650
+ span: spanFromNode(node.right, state.source.length),
651
+ message: `Cannot assign ${typeToString(valueType)} to array length (number).`,
652
+ }));
653
+ }
654
+ } else {
655
+ inferExpression(node.left, scope, state, fnContext);
656
+ }
657
+ return valueType;
658
+ }
659
+ case "UpdateExpression": {
660
+ if (node.argument.type === "Identifier") {
661
+ const symbol = scope.lookup(node.argument.name);
662
+ if (!symbol) reportUnknownSymbol(node.argument, scope, state);
663
+ else if (!symbol.mutable) {
664
+ state.diagnostics.push(createDiagnostic({
665
+ file: state.file,
666
+ source: state.source,
667
+ lineStarts: state.lineStarts,
668
+ code: "FS4102",
669
+ span: spanFromNode(node.argument, state.source.length),
670
+ message: `Cannot update constant binding \`${node.argument.name}\`.`,
671
+ related: [{ message: "Declared here.", span: symbol.span, file: symbol.file }],
672
+ }));
673
+ }
674
+ }
675
+ inferExpression(node.argument, scope, state, fnContext);
676
+ return T_NUMBER;
677
+ }
678
+ case "CallExpression": {
679
+ const argTypes = (node.arguments || []).map((arg) => inferExpression(arg, scope, state, fnContext));
680
+ if (node.callee?.type === "MemberExpression") {
681
+ const objectIdentifier = node.callee.object?.type === "Identifier" ? node.callee.object.name : null;
682
+ const staticPropName = !node.callee.computed && node.callee.property?.type === "Identifier"
683
+ ? node.callee.property.name
684
+ : (node.callee.property?.type === "Literal" ? String(node.callee.property.value) : null);
685
+ if (objectIdentifier && staticPropName) {
686
+ if (objectIdentifier === "Array" && staticPropName === "from") return makeArrayType(T_UNKNOWN);
687
+ if (objectIdentifier === "Array" && staticPropName === "isArray") return T_BOOLEAN;
688
+ if (objectIdentifier === "Array" && staticPropName === "of") return makeArrayType(argTypes.length ? unionTypes(argTypes) : T_UNKNOWN);
689
+ if (objectIdentifier === "Object" && staticPropName === "keys") return makeArrayType(T_STRING);
690
+ if (objectIdentifier === "Object" && staticPropName === "values") return makeArrayType(T_UNKNOWN);
691
+ if (objectIdentifier === "Object" && staticPropName === "entries") return makeArrayType(makeArrayType(T_UNKNOWN));
692
+ if (objectIdentifier === "Object" && staticPropName === "assign") return argTypes[0] || T_OBJECT;
693
+ if (objectIdentifier === "Object" && staticPropName === "fromEntries") return T_OBJECT;
694
+ if (objectIdentifier === "Object" && staticPropName === "hasOwn") return T_BOOLEAN;
695
+ if (objectIdentifier === "Promise" && staticPropName === "all") return T_OBJECT;
696
+ if (objectIdentifier === "Promise" && ["allSettled", "any", "race", "resolve", "reject"].includes(staticPropName)) return T_OBJECT;
697
+ if (objectIdentifier === "Date" && staticPropName === "now") return T_NUMBER;
698
+ if (objectIdentifier === "Date" && ["parse", "UTC"].includes(staticPropName)) return T_NUMBER;
699
+ if (objectIdentifier === "JSON" && staticPropName === "parse") return T_UNKNOWN;
700
+ if (objectIdentifier === "JSON" && staticPropName === "stringify") return T_STRING;
701
+ if (objectIdentifier === "Math" && staticPropName === "max") return T_NUMBER;
702
+ if (objectIdentifier === "Math" && ["min", "abs", "floor", "ceil", "round", "trunc", "sqrt", "pow", "sign"].includes(staticPropName)) return T_NUMBER;
703
+ if (objectIdentifier === "String" && ["fromCharCode", "fromCodePoint", "raw"].includes(staticPropName)) return T_STRING;
704
+ if (objectIdentifier === "Number" && ["isFinite", "isInteger", "isNaN", "isSafeInteger"].includes(staticPropName)) return T_BOOLEAN;
705
+ if (objectIdentifier === "Number" && ["parseFloat", "parseInt"].includes(staticPropName)) return T_NUMBER;
706
+ }
707
+
708
+ const objectType = inferExpression(node.callee.object, scope, state, fnContext);
709
+ const propName = !node.callee.computed && node.callee.property?.type === "Identifier"
710
+ ? node.callee.property.name
711
+ : (node.callee.property?.type === "Literal" ? String(node.callee.property.value) : null);
712
+ if (objectIdentifier && propName) {
713
+ if (objectIdentifier === "document" && ["querySelector", "getElementById", "createElement"].includes(propName)) return T_OBJECT;
714
+ if (objectIdentifier === "document" && ["querySelectorAll", "getElementsByClassName", "getElementsByTagName"].includes(propName)) return makeArrayType(T_OBJECT);
715
+ if (objectIdentifier === "document" && ["createTextNode", "createDocumentFragment"].includes(propName)) return T_OBJECT;
716
+ if (objectIdentifier === "history" && ["pushState", "replaceState", "back", "forward", "go"].includes(propName)) return T_VOID;
717
+ if (objectIdentifier === "localStorage" && propName === "getItem") return unionTypes([T_STRING, T_NULL]);
718
+ if (objectIdentifier === "localStorage" && ["setItem", "removeItem", "clear"].includes(propName)) return T_VOID;
719
+ if (objectIdentifier === "sessionStorage" && propName === "getItem") return unionTypes([T_STRING, T_NULL]);
720
+ if (objectIdentifier === "sessionStorage" && ["setItem", "removeItem", "clear"].includes(propName)) return T_VOID;
721
+ if (objectIdentifier === "window" && ["scrollTo", "scrollBy", "close", "open"].includes(propName)) return T_VOID;
722
+ if (objectIdentifier === "window" && propName === "addEventListener") return T_VOID;
723
+ if (objectIdentifier === "window" && propName === "removeEventListener") return T_VOID;
724
+ if (objectIdentifier === "window" && propName === "dispatchEvent") return T_BOOLEAN;
725
+ if (objectIdentifier === "location" && propName === "toString") return T_STRING;
726
+ }
727
+ if (objectType.kind === "array" && propName) {
728
+ if (propName === "push") {
729
+ for (let i = 0; i < argTypes.length; i += 1) {
730
+ if (!isAssignable(argTypes[i], objectType.elementType)) {
731
+ state.diagnostics.push(createDiagnostic({
732
+ file: state.file,
733
+ source: state.source,
734
+ lineStarts: state.lineStarts,
735
+ code: "FS4103",
736
+ span: spanFromNode(node.arguments[i], state.source.length),
737
+ message: `Array push expects ${typeToString(objectType.elementType)}, got ${typeToString(argTypes[i])}.`,
738
+ }));
739
+ }
740
+ }
741
+ return T_NUMBER;
742
+ }
743
+ if (propName === "map" && argTypes[0]?.kind === "function") return makeArrayType(argTypes[0].returnType || T_UNKNOWN);
744
+ if (propName === "map") return makeArrayType(T_UNKNOWN);
745
+ if (propName === "filter") return makeArrayType(objectType.elementType);
746
+ if (propName === "slice") return makeArrayType(objectType.elementType);
747
+ if (propName === "concat") return makeArrayType(objectType.elementType);
748
+ if (propName === "flat") return makeArrayType(T_UNKNOWN);
749
+ if (propName === "flatMap") return makeArrayType(T_UNKNOWN);
750
+ if (propName === "find") return unionTypes([objectType.elementType, T_UNDEFINED]);
751
+ if (propName === "findIndex") return T_NUMBER;
752
+ if (propName === "some") return T_BOOLEAN;
753
+ if (propName === "every") return T_BOOLEAN;
754
+ if (propName === "forEach") return T_VOID;
755
+ if (propName === "reduce") return argTypes.length >= 2 ? argTypes[1] : objectType.elementType;
756
+ if (propName === "join") return T_STRING;
757
+ if (propName === "includes") return T_BOOLEAN;
758
+ if (propName === "indexOf" || propName === "lastIndexOf") return T_NUMBER;
759
+ if (propName === "pop") return unionTypes([objectType.elementType, T_UNDEFINED]);
760
+ }
761
+ if (objectType.kind === "string" && propName) {
762
+ if (["toString", "toLocaleString", "trim", "trimStart", "trimEnd", "trimLeft", "trimRight", "toLowerCase", "toUpperCase", "toLocaleLowerCase", "toLocaleUpperCase", "slice", "substring", "concat", "padStart", "padEnd", "repeat", "replace", "replaceAll"].includes(propName)) return T_STRING;
763
+ if (["includes", "startsWith", "endsWith"].includes(propName)) return T_BOOLEAN;
764
+ if (["indexOf", "lastIndexOf", "charCodeAt", "codePointAt", "search", "localeCompare"].includes(propName)) return T_NUMBER;
765
+ if (["split", "match", "matchAll"].includes(propName)) return makeArrayType(T_UNKNOWN);
766
+ if (propName === "charAt") return T_STRING;
767
+ }
768
+ if (objectType.kind === "object" && propName) {
769
+ if (["addEventListener", "removeEventListener"].includes(propName)) return T_VOID;
770
+ if (propName === "dispatchEvent") return T_BOOLEAN;
771
+ if (["querySelector", "closest"].includes(propName)) return T_OBJECT;
772
+ if (propName === "querySelectorAll") return makeArrayType(T_OBJECT);
773
+ if (["appendChild", "removeChild", "replaceChild", "insertBefore", "insertAdjacentElement"].includes(propName)) return T_OBJECT;
774
+ if (propName === "insertAdjacentHTML") return T_VOID;
775
+ if (propName === "remove") return T_VOID;
776
+ if (["setAttribute", "removeAttribute"].includes(propName)) return T_VOID;
777
+ if (propName === "getAttribute") return unionTypes([T_STRING, T_NULL]);
778
+ if (propName === "hasAttribute") return T_BOOLEAN;
779
+ if (["matches", "contains", "toggle"].includes(propName)) return T_BOOLEAN;
780
+ if (["add", "delete"].includes(propName)) return T_OBJECT;
781
+ if (["get", "has"].includes(propName)) return unionTypes([T_UNKNOWN, T_BOOLEAN]);
782
+ if (["append", "set", "sort", "forEach", "submit", "reset"].includes(propName)) return T_VOID;
783
+ if (propName === "getAll") return makeArrayType(T_UNKNOWN);
784
+ if (["entries", "keys", "values"].includes(propName)) return makeArrayType(T_UNKNOWN);
785
+ }
786
+ }
787
+ const calleeType = inferExpression(node.callee, scope, state, fnContext);
788
+ if (calleeType.kind !== "function") {
789
+ if (calleeType.kind !== "unknown" && calleeType.kind !== "any") {
790
+ state.diagnostics.push(createDiagnostic({
791
+ file: state.file,
792
+ source: state.source,
793
+ lineStarts: state.lineStarts,
794
+ code: "FS4106",
795
+ span: spanFromNode(node.callee, state.source.length),
796
+ message: `Attempted to call value of type ${typeToString(calleeType)}.`,
797
+ }));
798
+ }
799
+ return T_UNKNOWN;
800
+ }
801
+ if (argTypes.length < calleeType.minArgs || argTypes.length > calleeType.maxArgs) {
802
+ const expected = calleeType.minArgs === calleeType.maxArgs ? `${calleeType.minArgs}` : `${calleeType.minArgs}-${calleeType.maxArgs}`;
803
+ state.diagnostics.push(createDiagnostic({
804
+ file: state.file,
805
+ source: state.source,
806
+ lineStarts: state.lineStarts,
807
+ code: "FS4104",
808
+ span: spanFromNode(node, state.source.length),
809
+ message: `Expected ${expected} argument(s), got ${argTypes.length}.`,
810
+ }));
811
+ }
812
+ for (let i = 0; i < Math.min(argTypes.length, calleeType.params.length); i += 1) {
813
+ if (!isAssignable(argTypes[i], calleeType.params[i])) {
814
+ state.diagnostics.push(createDiagnostic({
815
+ file: state.file,
816
+ source: state.source,
817
+ lineStarts: state.lineStarts,
818
+ code: "FS4103",
819
+ span: spanFromNode(node.arguments[i], state.source.length),
820
+ message: `Argument ${i + 1} expects ${typeToString(calleeType.params[i])}, got ${typeToString(argTypes[i])}.`,
821
+ }));
822
+ }
823
+ }
824
+ // Heuristic for common higher-order combinators:
825
+ // if a function returns a still-unknown function shape and receives a concrete function arg,
826
+ // propagate the argument's function type as the call result.
827
+ if (
828
+ calleeType.returnType?.kind === "function"
829
+ && argTypes[0]?.kind === "function"
830
+ && calleeType.returnType.params.every((param) => param?.kind === "unknown")
831
+ && calleeType.returnType.returnType?.kind === "unknown"
832
+ ) {
833
+ return copyType(argTypes[0]);
834
+ }
835
+ return copyType(calleeType.returnType);
836
+ }
837
+ case "FunctionExpression":
838
+ case "ArrowFunctionExpression":
839
+ return inferFunctionFromNode(node, scope, state);
840
+ case "MemberExpression":
841
+ {
842
+ const objectIdentifier = node.object?.type === "Identifier" ? node.object.name : null;
843
+ const objectType = inferExpression(node.object, scope, state, fnContext);
844
+ if (node.computed) {
845
+ const propType = inferExpression(node.property, scope, state, fnContext);
846
+ if (objectType.kind === "array" && isAssignable(propType, T_NUMBER)) return objectType.elementType;
847
+ return T_UNKNOWN;
848
+ }
849
+ const propName = node.property?.type === "Identifier" ? node.property.name : null;
850
+ if (!propName) return T_UNKNOWN;
851
+ if (objectIdentifier && objectType.kind === "object") {
852
+ if (objectIdentifier === "location" && ["href", "origin", "protocol", "host", "hostname", "port", "pathname", "search", "hash"].includes(propName)) return T_STRING;
853
+ if (objectIdentifier === "window" && ["innerWidth", "innerHeight"].includes(propName)) return T_NUMBER;
854
+ if (objectIdentifier === "window" && propName === "location") return T_OBJECT;
855
+ if (objectIdentifier === "window" && propName === "history") return T_OBJECT;
856
+ if (objectIdentifier === "window" && ["localStorage", "sessionStorage"].includes(propName)) return T_OBJECT;
857
+ if (objectIdentifier === "document" && ["body", "head", "documentElement"].includes(propName)) return T_OBJECT;
858
+ if (objectIdentifier === "document" && propName === "title") return T_STRING;
859
+ if (objectIdentifier === "document" && propName === "cookie") return T_STRING;
860
+ if (objectIdentifier === "document" && propName === "readyState") return T_STRING;
861
+ if (objectIdentifier === "history" && propName === "length") return T_NUMBER;
862
+ if (["localStorage", "sessionStorage"].includes(objectIdentifier) && propName === "length") return T_NUMBER;
863
+ }
864
+ if (objectType.kind === "array") {
865
+ if (propName === "length") return T_NUMBER;
866
+ return T_UNKNOWN;
867
+ }
868
+ if (objectType.kind === "string" && propName === "length") return T_NUMBER;
869
+ if (objectType.kind === "object" && objectType.properties?.[propName]) {
870
+ return copyType(objectType.properties[propName]);
871
+ }
872
+ return T_UNKNOWN;
873
+ }
874
+ case "AwaitExpression":
875
+ return inferExpression(node.argument, scope, state, fnContext);
876
+ case "NewExpression":
877
+ inferExpression(node.callee, scope, state, fnContext);
878
+ for (const arg of node.arguments || []) inferExpression(arg, scope, state, fnContext);
879
+ return T_OBJECT;
880
+ case "SequenceExpression":
881
+ if (!node.expressions?.length) return T_UNKNOWN;
882
+ for (let i = 0; i < node.expressions.length - 1; i += 1) inferExpression(node.expressions[i], scope, state, fnContext);
883
+ return inferExpression(node.expressions[node.expressions.length - 1], scope, state, fnContext);
884
+ case "ChainExpression":
885
+ return inferExpression(node.expression, scope, state, fnContext);
886
+ default:
887
+ return T_UNKNOWN;
888
+ }
889
+ }
890
+
891
+ function analyzeVariableDeclaration(node, scope, state, fnContext) {
892
+ function bindingTypeForPattern(pattern, valueType) {
893
+ if (!pattern) return valueType;
894
+ if (pattern.type === "Identifier") return valueType;
895
+ if (pattern.type === "AssignmentPattern") return bindingTypeForPattern(pattern.left, valueType);
896
+ if (pattern.type === "RestElement") return bindingTypeForPattern(pattern.argument, valueType);
897
+ if (pattern.type === "ArrayPattern") return valueType?.kind === "array" ? valueType.elementType || T_UNKNOWN : T_UNKNOWN;
898
+ if (pattern.type === "ObjectPattern") return T_UNKNOWN;
899
+ return valueType || T_UNKNOWN;
900
+ }
901
+
902
+ function declarePatternBindings(pattern, valueType, declarationKind) {
903
+ if (!pattern) return;
904
+ if (pattern.type === "Identifier") {
905
+ const existing = declarationKind === "var" ? scope.lookup(pattern.name) : scope.symbols.get(pattern.name);
906
+ if (existing && declarationKind !== "var") {
907
+ existing.type = unionTypes([existing.type, valueType || T_UNKNOWN]);
908
+ return;
909
+ }
910
+ scope.declare(createSymbol({
911
+ name: pattern.name,
912
+ type: valueType || T_UNKNOWN,
913
+ kind: declarationKind,
914
+ mutable: declarationKind !== "const",
915
+ span: spanFromNode(pattern, state.source.length),
916
+ file: state.file,
917
+ }));
918
+ return;
919
+ }
920
+ if (pattern.type === "AssignmentPattern") {
921
+ declarePatternBindings(pattern.left, valueType, declarationKind);
922
+ return;
923
+ }
924
+ if (pattern.type === "RestElement") {
925
+ declarePatternBindings(pattern.argument, valueType, declarationKind);
926
+ return;
927
+ }
928
+ if (pattern.type === "ArrayPattern") {
929
+ const elementType = valueType?.kind === "array" ? valueType.elementType || T_UNKNOWN : T_UNKNOWN;
930
+ for (const element of pattern.elements || []) {
931
+ declarePatternBindings(element, bindingTypeForPattern(element, elementType), declarationKind);
932
+ }
933
+ return;
934
+ }
935
+ if (pattern.type === "ObjectPattern") {
936
+ for (const property of pattern.properties || []) {
937
+ if (property.type === "RestElement") {
938
+ declarePatternBindings(property.argument, valueType?.kind === "object" ? valueType : T_UNKNOWN, declarationKind);
939
+ continue;
940
+ }
941
+ let keyName = null;
942
+ if (!property.computed && property.key?.type === "Identifier") keyName = property.key.name;
943
+ else if (property.key?.type === "Literal") keyName = String(property.key.value);
944
+ const propType = keyName && valueType?.kind === "object" ? valueType.properties?.[keyName] || T_UNKNOWN : T_UNKNOWN;
945
+ declarePatternBindings(property.value || property.key, bindingTypeForPattern(property.value || property.key, propType), declarationKind);
946
+ }
947
+ }
948
+ }
949
+
950
+ for (const declaration of node.declarations || []) {
951
+ const initType = declaration.init ? inferExpression(declaration.init, scope, state, fnContext) : T_UNKNOWN;
952
+ declarePatternBindings(declaration.id, initType, node.kind);
953
+ }
954
+ }
955
+
956
+ function analyzeStatement(node, scope, state, fnContext) {
957
+ if (!node) return;
958
+ switch (node.type) {
959
+ case "BlockStatement": {
960
+ const blockScope = new Scope(scope, "block");
961
+ state.scopes.push(blockScope);
962
+ for (const statement of node.body || []) analyzeStatement(statement, blockScope, state, fnContext);
963
+ break;
964
+ }
965
+ case "VariableDeclaration":
966
+ analyzeVariableDeclaration(node, scope, state, fnContext);
967
+ break;
968
+ case "ExpressionStatement":
969
+ inferExpression(node.expression, scope, state, fnContext);
970
+ break;
971
+ case "ReturnStatement":
972
+ if (fnContext) {
973
+ const returnType = node.argument ? inferExpression(node.argument, scope, state, fnContext) : T_VOID;
974
+ fnContext.returns.push(returnType);
975
+ }
976
+ break;
977
+ case "IfStatement":
978
+ inferExpression(node.test, scope, state, fnContext);
979
+ analyzeStatement(node.consequent, new Scope(scope, "if"), state, fnContext);
980
+ if (node.alternate) analyzeStatement(node.alternate, new Scope(scope, "else"), state, fnContext);
981
+ break;
982
+ case "ForStatement": {
983
+ const forScope = new Scope(scope, "for");
984
+ state.scopes.push(forScope);
985
+ if (node.init) {
986
+ if (node.init.type === "VariableDeclaration") analyzeVariableDeclaration(node.init, forScope, state, fnContext);
987
+ else inferExpression(node.init, forScope, state, fnContext);
988
+ }
989
+ if (node.test) inferExpression(node.test, forScope, state, fnContext);
990
+ if (node.update) inferExpression(node.update, forScope, state, fnContext);
991
+ analyzeStatement(node.body, forScope, state, fnContext);
992
+ break;
993
+ }
994
+ case "ForInStatement":
995
+ case "ForOfStatement": {
996
+ const forScope = new Scope(scope, "for");
997
+ state.scopes.push(forScope);
998
+ if (node.left?.type === "VariableDeclaration") analyzeVariableDeclaration(node.left, forScope, state, fnContext);
999
+ else if (node.left) inferExpression(node.left, forScope, state, fnContext);
1000
+ inferExpression(node.right, forScope, state, fnContext);
1001
+ analyzeStatement(node.body, forScope, state, fnContext);
1002
+ break;
1003
+ }
1004
+ case "WhileStatement":
1005
+ case "DoWhileStatement":
1006
+ inferExpression(node.test, scope, state, fnContext);
1007
+ analyzeStatement(node.body, new Scope(scope, "loop"), state, fnContext);
1008
+ break;
1009
+ case "FunctionDeclaration": {
1010
+ if (node.id?.name) {
1011
+ const symbol = scope.lookup(node.id.name) || scope.declare(createSymbol({
1012
+ name: node.id.name,
1013
+ type: T_UNKNOWN,
1014
+ kind: "function",
1015
+ mutable: false,
1016
+ span: spanFromNode(node.id, state.source.length),
1017
+ file: state.file,
1018
+ }));
1019
+ symbol.type = inferFunctionFromNode(node, scope, state);
1020
+ } else {
1021
+ inferFunctionFromNode(node, scope, state);
1022
+ }
1023
+ break;
1024
+ }
1025
+ case "ImportDeclaration":
1026
+ for (const specifier of node.specifiers || []) {
1027
+ const local = specifier.local;
1028
+ if (!local?.name) continue;
1029
+ scope.declare(createSymbol({
1030
+ name: local.name,
1031
+ type: T_UNKNOWN,
1032
+ kind: "import",
1033
+ mutable: false,
1034
+ span: spanFromNode(local, state.source.length),
1035
+ file: state.file,
1036
+ }));
1037
+ }
1038
+ break;
1039
+ case "ExportNamedDeclaration":
1040
+ case "ExportDefaultDeclaration":
1041
+ if (node.declaration) analyzeStatement(node.declaration, scope, state, fnContext);
1042
+ break;
1043
+ case "SwitchStatement":
1044
+ inferExpression(node.discriminant, scope, state, fnContext);
1045
+ for (const switchCase of node.cases || []) {
1046
+ if (switchCase.test) inferExpression(switchCase.test, scope, state, fnContext);
1047
+ for (const statement of switchCase.consequent || []) analyzeStatement(statement, new Scope(scope, "switch"), state, fnContext);
1048
+ }
1049
+ break;
1050
+ case "TryStatement":
1051
+ analyzeStatement(node.block, new Scope(scope, "try"), state, fnContext);
1052
+ if (node.handler) {
1053
+ const catchScope = new Scope(scope, "catch");
1054
+ state.scopes.push(catchScope);
1055
+ if (node.handler.param?.type === "Identifier") {
1056
+ catchScope.declare(createSymbol({
1057
+ name: node.handler.param.name,
1058
+ type: T_UNKNOWN,
1059
+ kind: "catch",
1060
+ mutable: true,
1061
+ span: spanFromNode(node.handler.param, state.source.length),
1062
+ file: state.file,
1063
+ }));
1064
+ }
1065
+ analyzeStatement(node.handler.body, catchScope, state, fnContext);
1066
+ }
1067
+ if (node.finalizer) analyzeStatement(node.finalizer, new Scope(scope, "finally"), state, fnContext);
1068
+ break;
1069
+ case "ThrowStatement":
1070
+ if (node.argument) inferExpression(node.argument, scope, state, fnContext);
1071
+ break;
1072
+ case "ClassDeclaration":
1073
+ if (node.id?.name) {
1074
+ scope.declare(createSymbol({
1075
+ name: node.id.name,
1076
+ type: T_OBJECT,
1077
+ kind: "class",
1078
+ mutable: false,
1079
+ span: spanFromNode(node.id, state.source.length),
1080
+ file: state.file,
1081
+ }));
1082
+ }
1083
+ break;
1084
+ default:
1085
+ break;
1086
+ }
1087
+ }
1088
+
1089
+ function analyzeFileTypes(file, source) {
1090
+ const lineStarts = createLineStarts(source);
1091
+ const parsed = parseFastScript(source, { file, mode: "lenient", recover: true });
1092
+ const diagnostics = [...parsed.diagnostics];
1093
+
1094
+ if (!parsed.estree || !Array.isArray(parsed.estree.body)) {
1095
+ return { file, diagnostics, scopes: [], symbols: [], astVersion: parsed.version };
1096
+ }
1097
+
1098
+ scopeCounter = 0;
1099
+ const rootScope = new Scope(null, "module");
1100
+ const state = { file, source, lineStarts, diagnostics, scopes: [rootScope], allowedRuntimes: inferAllowedRuntimeContextsFromFile(file) };
1101
+
1102
+ declareBuiltins(rootScope, file);
1103
+ hoistDeclarations(parsed.estree.body, rootScope, state);
1104
+ for (const statement of parsed.estree.body) analyzeStatement(statement, rootScope, state, null);
1105
+
1106
+ const symbols = [];
1107
+ for (const scope of state.scopes) {
1108
+ for (const symbol of scope.symbols.values()) {
1109
+ symbols.push({
1110
+ scopeId: scope.id,
1111
+ scopeKind: scope.kind,
1112
+ name: symbol.name,
1113
+ kind: symbol.kind,
1114
+ mutable: symbol.mutable,
1115
+ type: typeToString(symbol.type),
1116
+ span: symbol.span,
1117
+ });
1118
+ }
1119
+ }
1120
+
1121
+ return {
1122
+ file,
1123
+ diagnostics: dedupeDiagnostics(state.diagnostics),
1124
+ scopes: state.scopes.map((scope) => ({
1125
+ id: scope.id,
1126
+ kind: scope.kind,
1127
+ parentId: scope.parent?.id || null,
1128
+ symbols: [...scope.symbols.values()].map((symbol) => ({
1129
+ name: symbol.name,
1130
+ kind: symbol.kind,
1131
+ mutable: symbol.mutable,
1132
+ type: typeToString(symbol.type),
1133
+ span: symbol.span,
1134
+ })),
1135
+ })),
1136
+ symbols,
1137
+ astVersion: parsed.version,
1138
+ };
1139
+ }
1140
+
1141
+ function buildTypeFile(routes) {
1142
+ const lines = [];
1143
+ lines.push("/* auto-generated by fastscript typecheck */");
1144
+ lines.push("export type FastScriptRouteParams = {");
1145
+ for (const route of routes) {
1146
+ const params = inferRouteParamTypes(route.routePath, route.paramTypes);
1147
+ const body = Object.keys(params).length
1148
+ ? `{ ${Object.entries(params).map(([k, v]) => `${k}: ${v}`).join("; ")} }`
1149
+ : "{}";
1150
+ lines.push(` \"${route.routePath}\": ${body};`);
1151
+ }
1152
+ lines.push("};");
1153
+ lines.push("");
1154
+ lines.push("export type FastScriptRouteLoaderData = {");
1155
+ for (const route of routes) {
1156
+ lines.push(` \"${route.routePath}\": ${route.loaderDataType || "{}"};`);
1157
+ }
1158
+ lines.push("};");
1159
+ lines.push("");
1160
+ lines.push("export type FastScriptRouteContext<P extends keyof FastScriptRouteParams = keyof FastScriptRouteParams> = {");
1161
+ lines.push(" path: P;");
1162
+ lines.push(" params: FastScriptRouteParams[P];");
1163
+ lines.push(" data: FastScriptRouteLoaderData[P];");
1164
+ lines.push("};");
1165
+ lines.push("");
1166
+ return lines.join("\n");
1167
+ }
1168
+
1169
+ function routeConflicts(routes) {
1170
+ const seen = new Map();
1171
+ const conflicts = [];
1172
+ for (const route of routes) {
1173
+ const key = `${route.routePath}|${route.slot || "default"}`;
1174
+ const previous = seen.get(key);
1175
+ if (previous) {
1176
+ conflicts.push({
1177
+ code: "FS4001",
1178
+ severity: "error",
1179
+ message: `Duplicate route mapping: ${route.routePath} (${route.slot || "default"})`,
1180
+ file: route.file,
1181
+ line: 1,
1182
+ column: 1,
1183
+ span: { start: 0, end: 1 },
1184
+ related: [{ file: previous.file, line: 1, column: 1, message: "First route declared here.", span: { start: 0, end: 1 } }],
1185
+ });
1186
+ } else {
1187
+ seen.set(key, route);
1188
+ }
1189
+ }
1190
+ return conflicts;
1191
+ }
1192
+
1193
+ function usageHints(routes) {
1194
+ const hints = [];
1195
+ for (const route of routes) {
1196
+ let source = "";
1197
+ try {
1198
+ source = readFileSync(route.file, "utf8");
1199
+ } catch {
1200
+ source = "";
1201
+ }
1202
+ for (const param of route.params) {
1203
+ if (source.includes(`params.${param}`)) continue;
1204
+ hints.push({
1205
+ code: "FS4002",
1206
+ severity: "warning",
1207
+ message: `Route param \`${param}\` is declared but not referenced in ${route.file}.`,
1208
+ file: route.file,
1209
+ line: 1,
1210
+ column: 1,
1211
+ span: { start: 0, end: 1 },
1212
+ related: [],
1213
+ });
1214
+ }
1215
+ }
1216
+ return hints;
1217
+ }
1218
+
1219
+ function mergePropertyType(map, key, type) {
1220
+ const existing = map[key];
1221
+ if (!existing) {
1222
+ map[key] = type;
1223
+ return;
1224
+ }
1225
+ if (existing === type) return;
1226
+ const left = existing.split("|").map((part) => part.trim()).filter(Boolean);
1227
+ const right = String(type).split("|").map((part) => part.trim()).filter(Boolean);
1228
+ map[key] = [...new Set([...left, ...right])].sort().join(" | ");
1229
+ }
1230
+
1231
+ function astTypeLiteral(node) {
1232
+ if (!node) return "unknown";
1233
+ switch (node.type) {
1234
+ case "Literal":
1235
+ if (node.value === null) return "null";
1236
+ if (typeof node.value === "string") return "string";
1237
+ if (typeof node.value === "number") return "number";
1238
+ if (typeof node.value === "boolean") return "boolean";
1239
+ return "unknown";
1240
+ case "TemplateLiteral":
1241
+ return "string";
1242
+ case "ArrayExpression": {
1243
+ const itemTypes = (node.elements || []).filter(Boolean).map((entry) => astTypeLiteral(entry));
1244
+ const unique = [...new Set(itemTypes)];
1245
+ return `${(unique.length ? unique.join(" | ") : "unknown")}[]`;
1246
+ }
1247
+ case "ObjectExpression": {
1248
+ const props = [];
1249
+ for (const property of node.properties || []) {
1250
+ if (property.type !== "Property") continue;
1251
+ let key = null;
1252
+ if (!property.computed && property.key?.type === "Identifier") key = property.key.name;
1253
+ else if (property.key?.type === "Literal") key = String(property.key.value);
1254
+ if (!key) continue;
1255
+ props.push(`${key}: ${astTypeLiteral(property.value)}`);
1256
+ }
1257
+ if (!props.length) return "{}";
1258
+ return `{ ${props.join("; ")} }`;
1259
+ }
1260
+ case "Identifier":
1261
+ if (node.name === "undefined") return "undefined";
1262
+ return "unknown";
1263
+ case "UnaryExpression":
1264
+ if (node.operator === "!") return "boolean";
1265
+ if (node.operator === "typeof") return "string";
1266
+ if (["+", "-", "~"].includes(node.operator)) return "number";
1267
+ return astTypeLiteral(node.argument);
1268
+ case "BinaryExpression":
1269
+ if (["==", "!=", "===", "!==", "<", ">", "<=", ">="].includes(node.operator)) return "boolean";
1270
+ if (node.operator === "+" && (astTypeLiteral(node.left) === "string" || astTypeLiteral(node.right) === "string")) return "string";
1271
+ return "number";
1272
+ case "LogicalExpression":
1273
+ return `${astTypeLiteral(node.left)} | ${astTypeLiteral(node.right)}`;
1274
+ case "ConditionalExpression":
1275
+ return `${astTypeLiteral(node.consequent)} | ${astTypeLiteral(node.alternate)}`;
1276
+ case "ArrowFunctionExpression":
1277
+ case "FunctionExpression":
1278
+ return "function";
1279
+ case "NewExpression":
1280
+ return "object";
1281
+ default:
1282
+ return "unknown";
1283
+ }
1284
+ }
1285
+
1286
+ function collectReturnObjects(node, out = []) {
1287
+ if (!node || typeof node !== "object") return out;
1288
+ if (node.type === "ReturnStatement" && node.argument?.type === "ObjectExpression") {
1289
+ out.push(node.argument);
1290
+ }
1291
+ for (const value of Object.values(node)) {
1292
+ if (!value) continue;
1293
+ if (Array.isArray(value)) {
1294
+ for (const entry of value) collectReturnObjects(entry, out);
1295
+ } else if (typeof value === "object") {
1296
+ collectReturnObjects(value, out);
1297
+ }
1298
+ }
1299
+ return out;
1300
+ }
1301
+
1302
+ function inferRouteLoaderDataShape(file) {
1303
+ let source = "";
1304
+ try {
1305
+ source = readFileSync(file, "utf8");
1306
+ } catch {
1307
+ return { hasLoader: false, typeLiteral: "unknown", fields: {} };
1308
+ }
1309
+
1310
+ let ast = null;
1311
+ try {
1312
+ ast = parseFastScript(source, { file, mode: "lenient", recover: true });
1313
+ } catch {
1314
+ return { hasLoader: false, typeLiteral: "unknown", fields: {} };
1315
+ }
1316
+
1317
+ const body = ast?.estree?.body || [];
1318
+ let loadNode = null;
1319
+ for (const node of body) {
1320
+ if (node.type === "ExportNamedDeclaration") {
1321
+ const decl = node.declaration;
1322
+ if (decl?.type === "FunctionDeclaration" && decl.id?.name === "load") {
1323
+ loadNode = decl;
1324
+ break;
1325
+ }
1326
+ if (decl?.type === "VariableDeclaration") {
1327
+ for (const entry of decl.declarations || []) {
1328
+ if (entry.id?.type === "Identifier" && entry.id.name === "load" && entry.init && ["ArrowFunctionExpression", "FunctionExpression"].includes(entry.init.type)) {
1329
+ loadNode = entry.init;
1330
+ break;
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+ if (node.type === "FunctionDeclaration" && node.id?.name === "load") {
1336
+ loadNode = node;
1337
+ break;
1338
+ }
1339
+ }
1340
+
1341
+ if (!loadNode) return { hasLoader: false, typeLiteral: "{}", fields: {} };
1342
+
1343
+ const returns = collectReturnObjects(loadNode.body || loadNode, []);
1344
+ if (!returns.length) return { hasLoader: true, typeLiteral: "unknown", fields: {} };
1345
+
1346
+ const fields = {};
1347
+ for (const objectNode of returns) {
1348
+ for (const property of objectNode.properties || []) {
1349
+ if (property.type !== "Property") continue;
1350
+ let key = null;
1351
+ if (!property.computed && property.key?.type === "Identifier") key = property.key.name;
1352
+ else if (property.key?.type === "Literal") key = String(property.key.value);
1353
+ if (!key) continue;
1354
+ mergePropertyType(fields, key, astTypeLiteral(property.value));
1355
+ }
1356
+ }
1357
+
1358
+ const entries = Object.entries(fields);
1359
+ if (!entries.length) return { hasLoader: true, typeLiteral: "{}", fields };
1360
+ const typeLiteral = `{ ${entries.map(([key, value]) => `${key}: ${value}`).join("; ")} }`;
1361
+ return { hasLoader: true, typeLiteral, fields };
1362
+ }
1363
+
1364
+ function formatTypeDiagnostic(diagnostic) {
1365
+ const path = diagnostic.file || "<memory>";
1366
+ const base = `${path}:${diagnostic.line || 1}:${diagnostic.column || 1} ${diagnostic.code} ${diagnostic.severity || "error"} ${diagnostic.message}`;
1367
+ const hint = diagnostic.hint ? ` hint=${diagnostic.hint}` : "";
1368
+ const related = (diagnostic.related || [])
1369
+ .map((entry) => ` related=${entry.file}:${entry.line}:${entry.column} ${entry.message}`)
1370
+ .join("");
1371
+ return `${base}${hint}${related}`;
1372
+ }
1373
+
1374
+ function parseArgs(args) {
1375
+ const out = { mode: "fail", path: APP_DIR };
1376
+ for (let i = 0; i < args.length; i += 1) {
1377
+ if (args[i] === "--mode") out.mode = (args[i + 1] || out.mode).toLowerCase();
1378
+ if (args[i] === "--path") out.path = resolve(args[i + 1] || out.path);
1379
+ }
1380
+ return out;
1381
+ }
1382
+
1383
+ function summarizeSeverity(diagnostics) {
1384
+ const summary = { error: 0, warning: 0 };
1385
+ for (const diagnostic of diagnostics) {
1386
+ if (diagnostic.severity === "warning") summary.warning += 1;
1387
+ else summary.error += 1;
1388
+ }
1389
+ return summary;
1390
+ }
1391
+
1392
+ export async function runTypeCheck(args = []) {
1393
+ const options = parseArgs(args);
1394
+ if (!existsSync(options.path)) {
1395
+ throw new Error(`Missing typecheck path: ${options.path}`);
1396
+ }
1397
+
1398
+ const files = walk(options.path).filter((file) => extname(file) === ".fs");
1399
+ const fileReports = [];
1400
+ const diagnostics = [];
1401
+
1402
+ for (const file of files) {
1403
+ const source = readFileSync(file, "utf8");
1404
+ const report = analyzeFileTypes(file, source);
1405
+ fileReports.push(report);
1406
+ diagnostics.push(...report.diagnostics);
1407
+ }
1408
+
1409
+ const candidatePagesDir = join(options.path, "pages");
1410
+ const pagesDir = existsSync(candidatePagesDir) ? candidatePagesDir : PAGES_DIR;
1411
+ const pageFiles = walk(pagesDir).filter((file) => [".fs", ".js"].includes(extname(file)));
1412
+ const routes = pageFiles
1413
+ .filter((file) => !file.endsWith("_layout.fs") && !file.endsWith("_layout.js"))
1414
+ .filter((file) => !file.endsWith("404.fs") && !file.endsWith("404.js"))
1415
+ .map((file) => {
1416
+ const route = inferRouteMeta(file, pagesDir);
1417
+ const loader = inferRouteLoaderDataShape(file);
1418
+ return {
1419
+ ...route,
1420
+ hasLoader: loader.hasLoader,
1421
+ loaderDataType: loader.typeLiteral,
1422
+ loaderDataFields: loader.fields,
1423
+ };
1424
+ });
1425
+
1426
+ const sortedRoutes = sortRoutesByPriority(routes.map((route) => ({ ...route, path: route.routePath })))
1427
+ .map((route) => ({ ...route, routePath: route.path }));
1428
+
1429
+ diagnostics.push(...routeConflicts(sortedRoutes), ...usageHints(sortedRoutes));
1430
+ const dedupedDiagnostics = dedupeDiagnostics(diagnostics);
1431
+ const severity = summarizeSeverity(dedupedDiagnostics);
1432
+
1433
+ mkdirSync(OUT_DIR, { recursive: true });
1434
+ writeFileSync(TYPES_PATH, buildTypeFile(sortedRoutes), "utf8");
1435
+
1436
+ const report = {
1437
+ mode: options.mode,
1438
+ generatedAt: new Date().toISOString(),
1439
+ files: fileReports,
1440
+ routes: sortedRoutes,
1441
+ diagnostics: dedupedDiagnostics,
1442
+ summary: {
1443
+ files: files.length,
1444
+ routeCount: sortedRoutes.length,
1445
+ errors: severity.error,
1446
+ warnings: severity.warning,
1447
+ },
1448
+ };
1449
+
1450
+ writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2), "utf8");
1451
+
1452
+ for (const diagnostic of dedupedDiagnostics) {
1453
+ console.log(formatTypeDiagnostic(diagnostic));
1454
+ }
1455
+
1456
+ if (severity.error > 0 && options.mode !== "pass") {
1457
+ const error = new Error(`typecheck failed: ${severity.error} error(s), ${severity.warning} warning(s)`);
1458
+ error.status = 1;
1459
+ error.details = dedupedDiagnostics;
1460
+ throw error;
1461
+ }
1462
+
1463
+ console.log(`typecheck complete: files=${files.length}, routes=${sortedRoutes.length}, errors=${severity.error}, warnings=${severity.warning}, mode=${options.mode}`);
1464
+ }