@soda-gql/builder 0.10.2 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.cjs +137 -3849
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -436
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +34 -436
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +122 -3806
- package/dist/index.mjs.map +1 -1
- package/dist/plugin-support.cjs +263 -0
- package/dist/plugin-support.cjs.map +1 -0
- package/dist/plugin-support.d.cts +308 -0
- package/dist/plugin-support.d.cts.map +1 -0
- package/dist/plugin-support.d.mts +308 -0
- package/dist/plugin-support.d.mts.map +1 -0
- package/dist/plugin-support.mjs +247 -0
- package/dist/plugin-support.mjs.map +1 -0
- package/dist/service-BIDYnmeU.d.cts +471 -0
- package/dist/service-BIDYnmeU.d.cts.map +1 -0
- package/dist/service-DdFZ_WpI.d.mts +471 -0
- package/dist/service-DdFZ_WpI.d.mts.map +1 -0
- package/dist/service-UG-_oJSl.mjs +4227 -0
- package/dist/service-UG-_oJSl.mjs.map +1 -0
- package/dist/service-nHDX5qIj.cjs +4384 -0
- package/dist/service-nHDX5qIj.cjs.map +1 -0
- package/package.json +12 -6
|
@@ -0,0 +1,4384 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
let node_fs = require("node:fs");
|
|
29
|
+
let node_fs_promises = require("node:fs/promises");
|
|
30
|
+
let neverthrow = require("neverthrow");
|
|
31
|
+
let zod = require("zod");
|
|
32
|
+
let node_crypto = require("node:crypto");
|
|
33
|
+
let node_path = require("node:path");
|
|
34
|
+
let node_vm = require("node:vm");
|
|
35
|
+
let __soda_gql_common = require("@soda-gql/common");
|
|
36
|
+
let __swc_core = require("@swc/core");
|
|
37
|
+
let __soda_gql_core = require("@soda-gql/core");
|
|
38
|
+
__soda_gql_core = __toESM(__soda_gql_core);
|
|
39
|
+
let __soda_gql_core_adapter = require("@soda-gql/core/adapter");
|
|
40
|
+
__soda_gql_core_adapter = __toESM(__soda_gql_core_adapter);
|
|
41
|
+
let __soda_gql_core_runtime = require("@soda-gql/core/runtime");
|
|
42
|
+
__soda_gql_core_runtime = __toESM(__soda_gql_core_runtime);
|
|
43
|
+
let __soda_gql_runtime = require("@soda-gql/runtime");
|
|
44
|
+
__soda_gql_runtime = __toESM(__soda_gql_runtime);
|
|
45
|
+
let typescript = require("typescript");
|
|
46
|
+
typescript = __toESM(typescript);
|
|
47
|
+
let fast_glob = require("fast-glob");
|
|
48
|
+
fast_glob = __toESM(fast_glob);
|
|
49
|
+
|
|
50
|
+
//#region packages/builder/src/schemas/artifact.ts
|
|
51
|
+
const BuilderArtifactElementMetadataSchema = zod.z.object({
|
|
52
|
+
sourcePath: zod.z.string(),
|
|
53
|
+
contentHash: zod.z.string()
|
|
54
|
+
});
|
|
55
|
+
const BuilderArtifactOperationSchema = zod.z.object({
|
|
56
|
+
id: zod.z.string(),
|
|
57
|
+
type: zod.z.literal("operation"),
|
|
58
|
+
metadata: BuilderArtifactElementMetadataSchema,
|
|
59
|
+
prebuild: zod.z.object({
|
|
60
|
+
operationType: zod.z.enum([
|
|
61
|
+
"query",
|
|
62
|
+
"mutation",
|
|
63
|
+
"subscription"
|
|
64
|
+
]),
|
|
65
|
+
operationName: zod.z.string(),
|
|
66
|
+
schemaLabel: zod.z.string(),
|
|
67
|
+
document: zod.z.unknown(),
|
|
68
|
+
variableNames: zod.z.array(zod.z.string())
|
|
69
|
+
})
|
|
70
|
+
});
|
|
71
|
+
const BuilderArtifactFragmentSchema = zod.z.object({
|
|
72
|
+
id: zod.z.string(),
|
|
73
|
+
type: zod.z.literal("fragment"),
|
|
74
|
+
metadata: BuilderArtifactElementMetadataSchema,
|
|
75
|
+
prebuild: zod.z.object({
|
|
76
|
+
typename: zod.z.string(),
|
|
77
|
+
key: zod.z.string().optional(),
|
|
78
|
+
schemaLabel: zod.z.string()
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
const BuilderArtifactElementSchema = zod.z.discriminatedUnion("type", [BuilderArtifactOperationSchema, BuilderArtifactFragmentSchema]);
|
|
82
|
+
const BuilderArtifactMetaSchema = zod.z.object({
|
|
83
|
+
version: zod.z.string(),
|
|
84
|
+
createdAt: zod.z.string()
|
|
85
|
+
});
|
|
86
|
+
const BuilderArtifactSchema = zod.z.object({
|
|
87
|
+
meta: BuilderArtifactMetaSchema.optional(),
|
|
88
|
+
elements: zod.z.record(zod.z.string(), BuilderArtifactElementSchema),
|
|
89
|
+
report: zod.z.object({
|
|
90
|
+
durationMs: zod.z.number(),
|
|
91
|
+
warnings: zod.z.array(zod.z.string()),
|
|
92
|
+
stats: zod.z.object({
|
|
93
|
+
hits: zod.z.number(),
|
|
94
|
+
misses: zod.z.number(),
|
|
95
|
+
skips: zod.z.number()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region packages/builder/src/artifact/loader.ts
|
|
102
|
+
/**
|
|
103
|
+
* Load a pre-built artifact from a JSON file asynchronously.
|
|
104
|
+
*
|
|
105
|
+
* @param path - Absolute path to the artifact JSON file
|
|
106
|
+
* @returns Result with the parsed artifact or an error
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const result = await loadArtifact("/path/to/artifact.json");
|
|
111
|
+
* if (result.isOk()) {
|
|
112
|
+
* const artifact = result.value;
|
|
113
|
+
* // Use artifact...
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
const loadArtifact = async (path) => {
|
|
118
|
+
if (!(0, node_fs.existsSync)(path)) {
|
|
119
|
+
return (0, neverthrow.err)({
|
|
120
|
+
code: "ARTIFACT_NOT_FOUND",
|
|
121
|
+
message: `Artifact file not found: ${path}`,
|
|
122
|
+
filePath: path
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
let content;
|
|
126
|
+
try {
|
|
127
|
+
content = await (0, node_fs_promises.readFile)(path, "utf-8");
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return (0, neverthrow.err)({
|
|
130
|
+
code: "ARTIFACT_NOT_FOUND",
|
|
131
|
+
message: `Failed to read artifact file: ${error instanceof Error ? error.message : String(error)}`,
|
|
132
|
+
filePath: path
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return parseAndValidateArtifact(content, path);
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Load a pre-built artifact from a JSON file synchronously.
|
|
139
|
+
*
|
|
140
|
+
* @param path - Absolute path to the artifact JSON file
|
|
141
|
+
* @returns Result with the parsed artifact or an error
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* const result = loadArtifactSync("/path/to/artifact.json");
|
|
146
|
+
* if (result.isOk()) {
|
|
147
|
+
* const artifact = result.value;
|
|
148
|
+
* // Use artifact...
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
const loadArtifactSync = (path) => {
|
|
153
|
+
if (!(0, node_fs.existsSync)(path)) {
|
|
154
|
+
return (0, neverthrow.err)({
|
|
155
|
+
code: "ARTIFACT_NOT_FOUND",
|
|
156
|
+
message: `Artifact file not found: ${path}`,
|
|
157
|
+
filePath: path
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
let content;
|
|
161
|
+
try {
|
|
162
|
+
content = (0, node_fs.readFileSync)(path, "utf-8");
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return (0, neverthrow.err)({
|
|
165
|
+
code: "ARTIFACT_NOT_FOUND",
|
|
166
|
+
message: `Failed to read artifact file: ${error instanceof Error ? error.message : String(error)}`,
|
|
167
|
+
filePath: path
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return parseAndValidateArtifact(content, path);
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Parse JSON content and validate against BuilderArtifactSchema.
|
|
174
|
+
*/
|
|
175
|
+
function parseAndValidateArtifact(content, filePath) {
|
|
176
|
+
let parsed;
|
|
177
|
+
try {
|
|
178
|
+
parsed = JSON.parse(content);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return (0, neverthrow.err)({
|
|
181
|
+
code: "ARTIFACT_PARSE_ERROR",
|
|
182
|
+
message: `Invalid JSON in artifact file: ${error instanceof Error ? error.message : String(error)}`,
|
|
183
|
+
filePath
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const validated = BuilderArtifactSchema.safeParse(parsed);
|
|
187
|
+
if (!validated.success) {
|
|
188
|
+
return (0, neverthrow.err)({
|
|
189
|
+
code: "ARTIFACT_VALIDATION_ERROR",
|
|
190
|
+
message: `Invalid artifact structure: ${validated.error.message}`,
|
|
191
|
+
filePath
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return (0, neverthrow.ok)(validated.data);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region packages/builder/src/ast/common/detection.ts
|
|
199
|
+
/**
|
|
200
|
+
* Create a diagnostic with appropriate severity
|
|
201
|
+
*/
|
|
202
|
+
const createDiagnostic = (config) => ({
|
|
203
|
+
code: config.code,
|
|
204
|
+
severity: getSeverity(config.code),
|
|
205
|
+
message: config.message,
|
|
206
|
+
location: config.location,
|
|
207
|
+
context: config.context
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* Get severity for a diagnostic code.
|
|
211
|
+
* - "error": Code will definitely not work
|
|
212
|
+
* - "warning": Code might work but is unsupported/unreliable
|
|
213
|
+
*/
|
|
214
|
+
const getSeverity = (code) => {
|
|
215
|
+
switch (code) {
|
|
216
|
+
case "MISSING_ARGUMENT":
|
|
217
|
+
case "INVALID_ARGUMENT_TYPE":
|
|
218
|
+
case "NON_MEMBER_CALLEE":
|
|
219
|
+
case "OPTIONAL_CHAINING":
|
|
220
|
+
case "SPREAD_ARGUMENT": return "error";
|
|
221
|
+
case "RENAMED_IMPORT":
|
|
222
|
+
case "STAR_IMPORT":
|
|
223
|
+
case "DEFAULT_IMPORT":
|
|
224
|
+
case "COMPUTED_PROPERTY":
|
|
225
|
+
case "DYNAMIC_CALLEE":
|
|
226
|
+
case "CLASS_PROPERTY":
|
|
227
|
+
case "EXTRA_ARGUMENTS": return "warning";
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* Diagnostic message templates for each code
|
|
232
|
+
*/
|
|
233
|
+
const diagnosticMessages = {
|
|
234
|
+
RENAMED_IMPORT: (ctx) => `Import alias "${ctx?.importedAs ?? "?"}" not recognized - use "import { gql } from ..."`,
|
|
235
|
+
STAR_IMPORT: (ctx) => `Namespace import "${ctx?.namespaceAlias ?? "?"}" not fully supported - use named import`,
|
|
236
|
+
DEFAULT_IMPORT: () => `Default import not supported - use "import { gql } from ..."`,
|
|
237
|
+
MISSING_ARGUMENT: () => `gql definition requires a factory function argument`,
|
|
238
|
+
INVALID_ARGUMENT_TYPE: (ctx) => `Expected arrow function, got ${ctx?.actualType ?? "unknown"}`,
|
|
239
|
+
NON_MEMBER_CALLEE: () => `Cannot call gql directly - use gql.schemaName(...)`,
|
|
240
|
+
COMPUTED_PROPERTY: () => `Computed property access not supported - use gql.schemaName(...)`,
|
|
241
|
+
DYNAMIC_CALLEE: () => `Dynamic callee expression not supported`,
|
|
242
|
+
OPTIONAL_CHAINING: () => `Optional chaining on gql not supported - use gql.schemaName(...) directly`,
|
|
243
|
+
EXTRA_ARGUMENTS: (ctx) => `gql definition only accepts one argument, ${ctx?.extraCount ?? "extra"} additional argument(s) ignored`,
|
|
244
|
+
SPREAD_ARGUMENT: () => `Spread arguments not supported - use arrow function directly`,
|
|
245
|
+
CLASS_PROPERTY: () => `Class property definitions may have inconsistent scope tracking`
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Create a diagnostic with a standard message
|
|
249
|
+
*/
|
|
250
|
+
const createStandardDiagnostic = (code, location, context) => {
|
|
251
|
+
return createDiagnostic({
|
|
252
|
+
code,
|
|
253
|
+
message: diagnosticMessages[code](context),
|
|
254
|
+
location,
|
|
255
|
+
context
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region packages/builder/src/errors.ts
|
|
261
|
+
/**
|
|
262
|
+
* Error constructor helpers for concise error creation.
|
|
263
|
+
*/
|
|
264
|
+
const builderErrors = {
|
|
265
|
+
entryNotFound: (entry, message) => ({
|
|
266
|
+
code: "ENTRY_NOT_FOUND",
|
|
267
|
+
message: message ?? `Entry not found: ${entry}`,
|
|
268
|
+
entry
|
|
269
|
+
}),
|
|
270
|
+
configNotFound: (path, message) => ({
|
|
271
|
+
code: "CONFIG_NOT_FOUND",
|
|
272
|
+
message: message ?? `Config file not found: ${path}`,
|
|
273
|
+
path
|
|
274
|
+
}),
|
|
275
|
+
configInvalid: (path, message, cause) => ({
|
|
276
|
+
code: "CONFIG_INVALID",
|
|
277
|
+
message,
|
|
278
|
+
path,
|
|
279
|
+
cause
|
|
280
|
+
}),
|
|
281
|
+
discoveryIOError: (path, message, errno, cause) => ({
|
|
282
|
+
code: "DISCOVERY_IO_ERROR",
|
|
283
|
+
message,
|
|
284
|
+
path,
|
|
285
|
+
errno,
|
|
286
|
+
cause
|
|
287
|
+
}),
|
|
288
|
+
fingerprintFailed: (filePath, message, cause) => ({
|
|
289
|
+
code: "FINGERPRINT_FAILED",
|
|
290
|
+
message,
|
|
291
|
+
filePath,
|
|
292
|
+
cause
|
|
293
|
+
}),
|
|
294
|
+
unsupportedAnalyzer: (analyzer, message) => ({
|
|
295
|
+
code: "UNSUPPORTED_ANALYZER",
|
|
296
|
+
message: message ?? `Unsupported analyzer: ${analyzer}`,
|
|
297
|
+
analyzer
|
|
298
|
+
}),
|
|
299
|
+
canonicalPathInvalid: (path, reason) => ({
|
|
300
|
+
code: "CANONICAL_PATH_INVALID",
|
|
301
|
+
message: `Invalid canonical path: ${path}${reason ? ` (${reason})` : ""}`,
|
|
302
|
+
path,
|
|
303
|
+
reason
|
|
304
|
+
}),
|
|
305
|
+
canonicalScopeMismatch: (expected, actual) => ({
|
|
306
|
+
code: "CANONICAL_SCOPE_MISMATCH",
|
|
307
|
+
message: `Scope mismatch: expected ${expected}, got ${actual}`,
|
|
308
|
+
expected,
|
|
309
|
+
actual
|
|
310
|
+
}),
|
|
311
|
+
graphCircularDependency: (chain) => ({
|
|
312
|
+
code: "GRAPH_CIRCULAR_DEPENDENCY",
|
|
313
|
+
message: `Circular dependency detected: ${chain.join(" → ")}`,
|
|
314
|
+
chain
|
|
315
|
+
}),
|
|
316
|
+
graphMissingImport: (importer, importee) => ({
|
|
317
|
+
code: "GRAPH_MISSING_IMPORT",
|
|
318
|
+
message: `Missing import: "${importer}" imports "${importee}" but it's not in the graph`,
|
|
319
|
+
importer,
|
|
320
|
+
importee
|
|
321
|
+
}),
|
|
322
|
+
docDuplicate: (name, sources) => ({
|
|
323
|
+
code: "DOC_DUPLICATE",
|
|
324
|
+
message: `Duplicate document name: ${name} found in ${sources.length} files`,
|
|
325
|
+
name,
|
|
326
|
+
sources
|
|
327
|
+
}),
|
|
328
|
+
writeFailed: (outPath, message, cause) => ({
|
|
329
|
+
code: "WRITE_FAILED",
|
|
330
|
+
message,
|
|
331
|
+
outPath,
|
|
332
|
+
cause
|
|
333
|
+
}),
|
|
334
|
+
cacheCorrupted: (message, cachePath, cause) => ({
|
|
335
|
+
code: "CACHE_CORRUPTED",
|
|
336
|
+
message,
|
|
337
|
+
cachePath,
|
|
338
|
+
cause
|
|
339
|
+
}),
|
|
340
|
+
runtimeModuleLoadFailed: (filePath, astPath, message, cause) => ({
|
|
341
|
+
code: "RUNTIME_MODULE_LOAD_FAILED",
|
|
342
|
+
message,
|
|
343
|
+
filePath,
|
|
344
|
+
astPath,
|
|
345
|
+
cause
|
|
346
|
+
}),
|
|
347
|
+
artifactRegistrationFailed: (elementId, reason) => ({
|
|
348
|
+
code: "ARTIFACT_REGISTRATION_FAILED",
|
|
349
|
+
message: `Failed to register artifact element ${elementId}: ${reason}`,
|
|
350
|
+
elementId,
|
|
351
|
+
reason
|
|
352
|
+
}),
|
|
353
|
+
elementEvaluationFailed: (modulePath, astPath, message, cause) => ({
|
|
354
|
+
code: "ELEMENT_EVALUATION_FAILED",
|
|
355
|
+
message,
|
|
356
|
+
modulePath,
|
|
357
|
+
astPath,
|
|
358
|
+
cause
|
|
359
|
+
}),
|
|
360
|
+
internalInvariant: (message, context, cause) => ({
|
|
361
|
+
code: "INTERNAL_INVARIANT",
|
|
362
|
+
message: `Internal invariant violated: ${message}`,
|
|
363
|
+
context,
|
|
364
|
+
cause
|
|
365
|
+
}),
|
|
366
|
+
schemaNotFound: (schemaLabel, canonicalId) => ({
|
|
367
|
+
code: "SCHEMA_NOT_FOUND",
|
|
368
|
+
message: `Schema not found for label "${schemaLabel}" (element: ${canonicalId})`,
|
|
369
|
+
schemaLabel,
|
|
370
|
+
canonicalId
|
|
371
|
+
})
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* Convenience helper to create an err Result from BuilderError.
|
|
375
|
+
*/
|
|
376
|
+
const builderErr = (error) => (0, neverthrow.err)(error);
|
|
377
|
+
/**
|
|
378
|
+
* Type guard for BuilderError.
|
|
379
|
+
*/
|
|
380
|
+
const isBuilderError = (error) => {
|
|
381
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string";
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* Format BuilderError for console output (human-readable).
|
|
385
|
+
*/
|
|
386
|
+
const formatBuilderError = (error) => {
|
|
387
|
+
const lines = [];
|
|
388
|
+
lines.push(`Error [${error.code}]: ${error.message}`);
|
|
389
|
+
switch (error.code) {
|
|
390
|
+
case "ENTRY_NOT_FOUND":
|
|
391
|
+
lines.push(` Entry: ${error.entry}`);
|
|
392
|
+
break;
|
|
393
|
+
case "CONFIG_NOT_FOUND":
|
|
394
|
+
case "CONFIG_INVALID":
|
|
395
|
+
lines.push(` Path: ${error.path}`);
|
|
396
|
+
if (error.code === "CONFIG_INVALID" && error.cause) {
|
|
397
|
+
lines.push(` Cause: ${error.cause}`);
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
case "DISCOVERY_IO_ERROR":
|
|
401
|
+
lines.push(` Path: ${error.path}`);
|
|
402
|
+
if (error.errno !== undefined) {
|
|
403
|
+
lines.push(` Errno: ${error.errno}`);
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
case "FINGERPRINT_FAILED":
|
|
407
|
+
lines.push(` File: ${error.filePath}`);
|
|
408
|
+
break;
|
|
409
|
+
case "CANONICAL_PATH_INVALID":
|
|
410
|
+
lines.push(` Path: ${error.path}`);
|
|
411
|
+
if (error.reason) {
|
|
412
|
+
lines.push(` Reason: ${error.reason}`);
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
case "CANONICAL_SCOPE_MISMATCH":
|
|
416
|
+
lines.push(` Expected: ${error.expected}`);
|
|
417
|
+
lines.push(` Actual: ${error.actual}`);
|
|
418
|
+
break;
|
|
419
|
+
case "GRAPH_CIRCULAR_DEPENDENCY":
|
|
420
|
+
lines.push(` Chain: ${error.chain.join(" → ")}`);
|
|
421
|
+
break;
|
|
422
|
+
case "GRAPH_MISSING_IMPORT":
|
|
423
|
+
lines.push(` Importer: ${error.importer}`);
|
|
424
|
+
lines.push(` Importee: ${error.importee}`);
|
|
425
|
+
break;
|
|
426
|
+
case "DOC_DUPLICATE":
|
|
427
|
+
lines.push(` Name: ${error.name}`);
|
|
428
|
+
lines.push(` Sources:\n ${error.sources.join("\n ")}`);
|
|
429
|
+
break;
|
|
430
|
+
case "WRITE_FAILED":
|
|
431
|
+
lines.push(` Output path: ${error.outPath}`);
|
|
432
|
+
break;
|
|
433
|
+
case "CACHE_CORRUPTED":
|
|
434
|
+
if (error.cachePath) {
|
|
435
|
+
lines.push(` Cache path: ${error.cachePath}`);
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
case "RUNTIME_MODULE_LOAD_FAILED":
|
|
439
|
+
lines.push(` File: ${error.filePath}`);
|
|
440
|
+
lines.push(` AST path: ${error.astPath}`);
|
|
441
|
+
break;
|
|
442
|
+
case "ARTIFACT_REGISTRATION_FAILED":
|
|
443
|
+
lines.push(` Element ID: ${error.elementId}`);
|
|
444
|
+
lines.push(` Reason: ${error.reason}`);
|
|
445
|
+
break;
|
|
446
|
+
case "ELEMENT_EVALUATION_FAILED":
|
|
447
|
+
lines.push(` at ${error.modulePath}`);
|
|
448
|
+
if (error.astPath) {
|
|
449
|
+
lines.push(` in ${error.astPath}`);
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
case "INTERNAL_INVARIANT":
|
|
453
|
+
if (error.context) {
|
|
454
|
+
lines.push(` Context: ${error.context}`);
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
case "SCHEMA_NOT_FOUND":
|
|
458
|
+
lines.push(` Schema label: ${error.schemaLabel}`);
|
|
459
|
+
lines.push(` Element: ${error.canonicalId}`);
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
if ("cause" in error && error.cause && !["CONFIG_INVALID"].includes(error.code)) {
|
|
463
|
+
lines.push(` Caused by: ${error.cause}`);
|
|
464
|
+
}
|
|
465
|
+
return lines.join("\n");
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* Assert unreachable code path (for exhaustiveness checks).
|
|
469
|
+
* This is the ONLY acceptable throw in builder code.
|
|
470
|
+
*/
|
|
471
|
+
const assertUnreachable = (value, context) => {
|
|
472
|
+
throw new Error(`Unreachable code path${context ? ` in ${context}` : ""}: received ${JSON.stringify(value)}`);
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region packages/builder/src/errors/formatter.ts
|
|
477
|
+
/**
|
|
478
|
+
* Hints for each error code to help users understand and fix issues.
|
|
479
|
+
*/
|
|
480
|
+
const errorHints = {
|
|
481
|
+
ELEMENT_EVALUATION_FAILED: "Check if all imported fragments are properly exported and included in entry patterns.",
|
|
482
|
+
GRAPH_CIRCULAR_DEPENDENCY: "Break the circular import by extracting shared types to a common module.",
|
|
483
|
+
GRAPH_MISSING_IMPORT: "Verify the import path exists and the module is included in entry patterns.",
|
|
484
|
+
RUNTIME_MODULE_LOAD_FAILED: "Ensure the module can be imported and all dependencies are installed.",
|
|
485
|
+
CONFIG_NOT_FOUND: "Create a soda-gql.config.ts file in your project root.",
|
|
486
|
+
CONFIG_INVALID: "Check your configuration file for syntax errors or invalid options.",
|
|
487
|
+
ENTRY_NOT_FOUND: "Verify the entry pattern matches your file structure.",
|
|
488
|
+
INTERNAL_INVARIANT: "This is an internal error. Please report it at https://github.com/soda-gql/soda-gql/issues"
|
|
489
|
+
};
|
|
490
|
+
/**
|
|
491
|
+
* Format a BuilderError into a structured FormattedError object.
|
|
492
|
+
*/
|
|
493
|
+
const formatBuilderErrorStructured = (error) => {
|
|
494
|
+
const base = {
|
|
495
|
+
code: error.code,
|
|
496
|
+
message: error.message,
|
|
497
|
+
hint: errorHints[error.code],
|
|
498
|
+
cause: "cause" in error ? error.cause : undefined
|
|
499
|
+
};
|
|
500
|
+
switch (error.code) {
|
|
501
|
+
case "ELEMENT_EVALUATION_FAILED": return {
|
|
502
|
+
...base,
|
|
503
|
+
location: {
|
|
504
|
+
modulePath: error.modulePath,
|
|
505
|
+
astPath: error.astPath || undefined
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
case "RUNTIME_MODULE_LOAD_FAILED": return {
|
|
509
|
+
...base,
|
|
510
|
+
location: {
|
|
511
|
+
modulePath: error.filePath,
|
|
512
|
+
astPath: error.astPath
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
case "GRAPH_MISSING_IMPORT": return {
|
|
516
|
+
...base,
|
|
517
|
+
relatedFiles: [error.importer, error.importee]
|
|
518
|
+
};
|
|
519
|
+
case "GRAPH_CIRCULAR_DEPENDENCY": return {
|
|
520
|
+
...base,
|
|
521
|
+
relatedFiles: error.chain
|
|
522
|
+
};
|
|
523
|
+
case "CONFIG_NOT_FOUND":
|
|
524
|
+
case "CONFIG_INVALID": return {
|
|
525
|
+
...base,
|
|
526
|
+
location: { modulePath: error.path }
|
|
527
|
+
};
|
|
528
|
+
case "FINGERPRINT_FAILED": return {
|
|
529
|
+
...base,
|
|
530
|
+
location: { modulePath: error.filePath }
|
|
531
|
+
};
|
|
532
|
+
case "DISCOVERY_IO_ERROR": return {
|
|
533
|
+
...base,
|
|
534
|
+
location: { modulePath: error.path }
|
|
535
|
+
};
|
|
536
|
+
default: return base;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
/**
|
|
540
|
+
* Format a BuilderError for CLI/stderr output with human-readable formatting.
|
|
541
|
+
* Includes location, hint, and related files when available.
|
|
542
|
+
*/
|
|
543
|
+
const formatBuilderErrorForCLI = (error) => {
|
|
544
|
+
const formatted = formatBuilderErrorStructured(error);
|
|
545
|
+
const lines = [];
|
|
546
|
+
lines.push(`Error [${formatted.code}]: ${formatted.message}`);
|
|
547
|
+
if (formatted.location) {
|
|
548
|
+
lines.push(` at ${formatted.location.modulePath}`);
|
|
549
|
+
if (formatted.location.astPath) {
|
|
550
|
+
lines.push(` in ${formatted.location.astPath}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (formatted.hint) {
|
|
554
|
+
lines.push("");
|
|
555
|
+
lines.push(` Hint: ${formatted.hint}`);
|
|
556
|
+
}
|
|
557
|
+
if (formatted.relatedFiles && formatted.relatedFiles.length > 0) {
|
|
558
|
+
lines.push("");
|
|
559
|
+
lines.push(" Related files:");
|
|
560
|
+
for (const file of formatted.relatedFiles) {
|
|
561
|
+
lines.push(` - ${file}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return lines.join("\n");
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
//#endregion
|
|
568
|
+
//#region packages/builder/src/scheduler/effects.ts
|
|
569
|
+
/**
|
|
570
|
+
* File read effect - reads a file from the filesystem.
|
|
571
|
+
* Works in both sync and async schedulers.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* const content = yield* new FileReadEffect("/path/to/file").run();
|
|
575
|
+
*/
|
|
576
|
+
var FileReadEffect = class extends __soda_gql_common.Effect {
|
|
577
|
+
constructor(path) {
|
|
578
|
+
super();
|
|
579
|
+
this.path = path;
|
|
580
|
+
}
|
|
581
|
+
_executeSync() {
|
|
582
|
+
return (0, node_fs.readFileSync)(this.path, "utf-8");
|
|
583
|
+
}
|
|
584
|
+
_executeAsync() {
|
|
585
|
+
return (0, node_fs_promises.readFile)(this.path, "utf-8");
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
/**
|
|
589
|
+
* File stat effect - gets file stats from the filesystem.
|
|
590
|
+
* Works in both sync and async schedulers.
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* const stats = yield* new FileStatEffect("/path/to/file").run();
|
|
594
|
+
*/
|
|
595
|
+
var FileStatEffect = class extends __soda_gql_common.Effect {
|
|
596
|
+
constructor(path) {
|
|
597
|
+
super();
|
|
598
|
+
this.path = path;
|
|
599
|
+
}
|
|
600
|
+
_executeSync() {
|
|
601
|
+
const stats = (0, node_fs.statSync)(this.path);
|
|
602
|
+
return {
|
|
603
|
+
mtimeMs: stats.mtimeMs,
|
|
604
|
+
size: stats.size,
|
|
605
|
+
isFile: stats.isFile()
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
async _executeAsync() {
|
|
609
|
+
const stats = await (0, node_fs_promises.stat)(this.path);
|
|
610
|
+
return {
|
|
611
|
+
mtimeMs: stats.mtimeMs,
|
|
612
|
+
size: stats.size,
|
|
613
|
+
isFile: stats.isFile()
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
/**
|
|
618
|
+
* File read effect that returns null if file doesn't exist.
|
|
619
|
+
* Useful for discovery where missing files are expected.
|
|
620
|
+
*/
|
|
621
|
+
var OptionalFileReadEffect = class extends __soda_gql_common.Effect {
|
|
622
|
+
constructor(path) {
|
|
623
|
+
super();
|
|
624
|
+
this.path = path;
|
|
625
|
+
}
|
|
626
|
+
_executeSync() {
|
|
627
|
+
try {
|
|
628
|
+
return (0, node_fs.readFileSync)(this.path, "utf-8");
|
|
629
|
+
} catch (error) {
|
|
630
|
+
if (error.code === "ENOENT") {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async _executeAsync() {
|
|
637
|
+
try {
|
|
638
|
+
return await (0, node_fs_promises.readFile)(this.path, "utf-8");
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (error.code === "ENOENT") {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
/**
|
|
648
|
+
* File stat effect that returns null if file doesn't exist.
|
|
649
|
+
* Useful for discovery where missing files are expected.
|
|
650
|
+
*/
|
|
651
|
+
var OptionalFileStatEffect = class extends __soda_gql_common.Effect {
|
|
652
|
+
constructor(path) {
|
|
653
|
+
super();
|
|
654
|
+
this.path = path;
|
|
655
|
+
}
|
|
656
|
+
_executeSync() {
|
|
657
|
+
try {
|
|
658
|
+
const stats = (0, node_fs.statSync)(this.path);
|
|
659
|
+
return {
|
|
660
|
+
mtimeMs: stats.mtimeMs,
|
|
661
|
+
size: stats.size,
|
|
662
|
+
isFile: stats.isFile()
|
|
663
|
+
};
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if (error.code === "ENOENT") {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async _executeAsync() {
|
|
672
|
+
try {
|
|
673
|
+
const stats = await (0, node_fs_promises.stat)(this.path);
|
|
674
|
+
return {
|
|
675
|
+
mtimeMs: stats.mtimeMs,
|
|
676
|
+
size: stats.size,
|
|
677
|
+
isFile: stats.isFile()
|
|
678
|
+
};
|
|
679
|
+
} catch (error) {
|
|
680
|
+
if (error.code === "ENOENT") {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
/**
|
|
688
|
+
* Element evaluation effect - evaluates a GqlElement using its generator.
|
|
689
|
+
* Supports both sync and async schedulers, enabling parallel element evaluation
|
|
690
|
+
* when using async scheduler.
|
|
691
|
+
*
|
|
692
|
+
* Wraps errors with module context for better debugging.
|
|
693
|
+
*
|
|
694
|
+
* @example
|
|
695
|
+
* yield* new ElementEvaluationEffect(element).run();
|
|
696
|
+
*/
|
|
697
|
+
var ElementEvaluationEffect = class extends __soda_gql_common.Effect {
|
|
698
|
+
constructor(element) {
|
|
699
|
+
super();
|
|
700
|
+
this.element = element;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Wrap an error with element context for better debugging.
|
|
704
|
+
*/
|
|
705
|
+
wrapError(error) {
|
|
706
|
+
const context = __soda_gql_core.GqlElement.getContext(this.element);
|
|
707
|
+
if (context) {
|
|
708
|
+
const { filePath, astPath } = (0, __soda_gql_common.parseCanonicalId)(context.canonicalId);
|
|
709
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
710
|
+
throw builderErrors.elementEvaluationFailed(filePath, astPath, message, error);
|
|
711
|
+
}
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
_executeSync() {
|
|
715
|
+
try {
|
|
716
|
+
const generator = __soda_gql_core.GqlElement.createEvaluationGenerator(this.element);
|
|
717
|
+
const result = generator.next();
|
|
718
|
+
while (!result.done) {
|
|
719
|
+
throw new Error("Async operation required during sync element evaluation");
|
|
720
|
+
}
|
|
721
|
+
} catch (error) {
|
|
722
|
+
this.wrapError(error);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async _executeAsync() {
|
|
726
|
+
try {
|
|
727
|
+
const generator = __soda_gql_core.GqlElement.createEvaluationGenerator(this.element);
|
|
728
|
+
let result = generator.next();
|
|
729
|
+
while (!result.done) {
|
|
730
|
+
await result.value;
|
|
731
|
+
result = generator.next();
|
|
732
|
+
}
|
|
733
|
+
} catch (error) {
|
|
734
|
+
this.wrapError(error);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
/**
|
|
739
|
+
* Builder effect constructors.
|
|
740
|
+
* Extends the base Effects with file I/O operations and element evaluation.
|
|
741
|
+
*/
|
|
742
|
+
const BuilderEffects = {
|
|
743
|
+
...__soda_gql_common.Effects,
|
|
744
|
+
readFile: (path) => new FileReadEffect(path),
|
|
745
|
+
stat: (path) => new FileStatEffect(path),
|
|
746
|
+
readFileOptional: (path) => new OptionalFileReadEffect(path),
|
|
747
|
+
statOptional: (path) => new OptionalFileStatEffect(path),
|
|
748
|
+
evaluateElement: (element) => new ElementEvaluationEffect(element)
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region packages/builder/src/vm/sandbox.ts
|
|
753
|
+
/**
|
|
754
|
+
* VM sandbox utilities for CJS bundle evaluation.
|
|
755
|
+
*
|
|
756
|
+
* Provides shared infrastructure for executing CommonJS modules
|
|
757
|
+
* in a sandboxed VM context with @soda-gql package mocking.
|
|
758
|
+
*
|
|
759
|
+
* @module
|
|
760
|
+
*/
|
|
761
|
+
/**
|
|
762
|
+
* Create a require function for the sandbox.
|
|
763
|
+
* Maps @soda-gql package imports to their actual modules.
|
|
764
|
+
*/
|
|
765
|
+
const createSandboxRequire = () => (path) => {
|
|
766
|
+
if (path === "@soda-gql/core") return __soda_gql_core;
|
|
767
|
+
if (path === "@soda-gql/core/adapter") return __soda_gql_core_adapter;
|
|
768
|
+
if (path === "@soda-gql/core/runtime") return __soda_gql_core_runtime;
|
|
769
|
+
if (path === "@soda-gql/runtime") return __soda_gql_runtime;
|
|
770
|
+
throw new Error(`Unknown module: ${path}`);
|
|
771
|
+
};
|
|
772
|
+
/**
|
|
773
|
+
* Create a VM sandbox for executing CJS bundles.
|
|
774
|
+
*
|
|
775
|
+
* Sets up:
|
|
776
|
+
* - require() handler for @soda-gql packages
|
|
777
|
+
* - module.exports and exports pointing to the same object
|
|
778
|
+
* - __dirname, __filename for path resolution
|
|
779
|
+
* - global and globalThis pointing to the sandbox itself
|
|
780
|
+
*
|
|
781
|
+
* @param modulePath - Absolute path to the module being executed
|
|
782
|
+
* @param additionalContext - Optional additional context properties
|
|
783
|
+
* @returns Configured sandbox object
|
|
784
|
+
*/
|
|
785
|
+
const createSandbox = (modulePath, additionalContext) => {
|
|
786
|
+
const moduleExports = {};
|
|
787
|
+
const sandbox = {
|
|
788
|
+
require: createSandboxRequire(),
|
|
789
|
+
module: { exports: moduleExports },
|
|
790
|
+
exports: moduleExports,
|
|
791
|
+
__dirname: (0, node_path.resolve)(modulePath, ".."),
|
|
792
|
+
__filename: modulePath,
|
|
793
|
+
global: undefined,
|
|
794
|
+
globalThis: undefined,
|
|
795
|
+
...additionalContext
|
|
796
|
+
};
|
|
797
|
+
sandbox.global = sandbox;
|
|
798
|
+
sandbox.globalThis = sandbox;
|
|
799
|
+
return sandbox;
|
|
800
|
+
};
|
|
801
|
+
/**
|
|
802
|
+
* Execute CJS code in a sandbox and return the exports.
|
|
803
|
+
*
|
|
804
|
+
* Note: Reads from sandbox.module.exports because esbuild CJS output
|
|
805
|
+
* reassigns module.exports via __toCommonJS(), replacing the original object.
|
|
806
|
+
*
|
|
807
|
+
* @param code - The CJS code to execute
|
|
808
|
+
* @param modulePath - Absolute path to the module (for error messages)
|
|
809
|
+
* @param additionalContext - Optional additional context properties
|
|
810
|
+
* @returns The module's exports object
|
|
811
|
+
*/
|
|
812
|
+
const executeSandbox = (code, modulePath, additionalContext) => {
|
|
813
|
+
const sandbox = createSandbox(modulePath, additionalContext);
|
|
814
|
+
const context = (0, node_vm.createContext)(sandbox);
|
|
815
|
+
new node_vm.Script(code, { filename: modulePath }).runInContext(context);
|
|
816
|
+
return sandbox.module.exports;
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region packages/builder/src/intermediate-module/codegen.ts
|
|
821
|
+
const formatFactory = (expression) => {
|
|
822
|
+
const trimmed = expression.trim();
|
|
823
|
+
if (!trimmed.includes("\n")) {
|
|
824
|
+
return trimmed;
|
|
825
|
+
}
|
|
826
|
+
const lines = trimmed.split("\n").map((line) => line.trimEnd());
|
|
827
|
+
const indented = lines.map((line, index) => index === 0 ? line : ` ${line}`).join("\n");
|
|
828
|
+
return `(\n ${indented}\n )`;
|
|
829
|
+
};
|
|
830
|
+
const buildTree = (definitions) => {
|
|
831
|
+
const roots = new Map();
|
|
832
|
+
definitions.forEach((definition) => {
|
|
833
|
+
const parts = definition.astPath.split(".");
|
|
834
|
+
const expressionText = definition.expression.trim();
|
|
835
|
+
if (parts.length === 1) {
|
|
836
|
+
const rootName = parts[0];
|
|
837
|
+
if (rootName) {
|
|
838
|
+
roots.set(rootName, {
|
|
839
|
+
expression: expressionText,
|
|
840
|
+
canonicalId: definition.canonicalId,
|
|
841
|
+
children: new Map()
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
const rootName = parts[0];
|
|
846
|
+
if (!rootName) return;
|
|
847
|
+
let root = roots.get(rootName);
|
|
848
|
+
if (!root) {
|
|
849
|
+
root = { children: new Map() };
|
|
850
|
+
roots.set(rootName, root);
|
|
851
|
+
}
|
|
852
|
+
let current = root;
|
|
853
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
854
|
+
const part = parts[i];
|
|
855
|
+
if (!part) continue;
|
|
856
|
+
let child = current.children.get(part);
|
|
857
|
+
if (!child) {
|
|
858
|
+
child = { children: new Map() };
|
|
859
|
+
current.children.set(part, child);
|
|
860
|
+
}
|
|
861
|
+
current = child;
|
|
862
|
+
}
|
|
863
|
+
const leafName = parts[parts.length - 1];
|
|
864
|
+
if (leafName) {
|
|
865
|
+
current.children.set(leafName, {
|
|
866
|
+
expression: expressionText,
|
|
867
|
+
canonicalId: definition.canonicalId,
|
|
868
|
+
children: new Map()
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
return roots;
|
|
874
|
+
};
|
|
875
|
+
/**
|
|
876
|
+
* Check if a string is a valid JavaScript identifier
|
|
877
|
+
*/
|
|
878
|
+
const isValidIdentifier = (name) => {
|
|
879
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !isReservedWord(name);
|
|
880
|
+
};
|
|
881
|
+
/**
|
|
882
|
+
* Check if a string is a JavaScript reserved word
|
|
883
|
+
*/
|
|
884
|
+
const isReservedWord = (name) => {
|
|
885
|
+
const reserved = new Set([
|
|
886
|
+
"break",
|
|
887
|
+
"case",
|
|
888
|
+
"catch",
|
|
889
|
+
"class",
|
|
890
|
+
"const",
|
|
891
|
+
"continue",
|
|
892
|
+
"debugger",
|
|
893
|
+
"default",
|
|
894
|
+
"delete",
|
|
895
|
+
"do",
|
|
896
|
+
"else",
|
|
897
|
+
"export",
|
|
898
|
+
"extends",
|
|
899
|
+
"finally",
|
|
900
|
+
"for",
|
|
901
|
+
"function",
|
|
902
|
+
"if",
|
|
903
|
+
"import",
|
|
904
|
+
"in",
|
|
905
|
+
"instanceof",
|
|
906
|
+
"new",
|
|
907
|
+
"return",
|
|
908
|
+
"super",
|
|
909
|
+
"switch",
|
|
910
|
+
"this",
|
|
911
|
+
"throw",
|
|
912
|
+
"try",
|
|
913
|
+
"typeof",
|
|
914
|
+
"var",
|
|
915
|
+
"void",
|
|
916
|
+
"while",
|
|
917
|
+
"with",
|
|
918
|
+
"yield",
|
|
919
|
+
"let",
|
|
920
|
+
"static",
|
|
921
|
+
"enum",
|
|
922
|
+
"await",
|
|
923
|
+
"implements",
|
|
924
|
+
"interface",
|
|
925
|
+
"package",
|
|
926
|
+
"private",
|
|
927
|
+
"protected",
|
|
928
|
+
"public"
|
|
929
|
+
]);
|
|
930
|
+
return reserved.has(name);
|
|
931
|
+
};
|
|
932
|
+
/**
|
|
933
|
+
* Format a key for use in an object literal
|
|
934
|
+
* Invalid identifiers are quoted, valid ones are not
|
|
935
|
+
*/
|
|
936
|
+
const formatObjectKey = (key) => {
|
|
937
|
+
return isValidIdentifier(key) ? key : `"${key}"`;
|
|
938
|
+
};
|
|
939
|
+
const renderTreeNode = (node, indent) => {
|
|
940
|
+
if (node.expression && node.children.size === 0 && node.canonicalId) {
|
|
941
|
+
const expr = formatFactory(node.expression);
|
|
942
|
+
return `registry.addElement("${node.canonicalId}", () => ${expr})`;
|
|
943
|
+
}
|
|
944
|
+
const indentStr = " ".repeat(indent);
|
|
945
|
+
const entries = Array.from(node.children.entries()).map(([key, child]) => {
|
|
946
|
+
const value = renderTreeNode(child, indent + 1);
|
|
947
|
+
const formattedKey = formatObjectKey(key);
|
|
948
|
+
return `${indentStr} ${formattedKey}: ${value},`;
|
|
949
|
+
});
|
|
950
|
+
if (entries.length === 0) {
|
|
951
|
+
return "{}";
|
|
952
|
+
}
|
|
953
|
+
return `{\n${entries.join("\n")}\n${indentStr}}`;
|
|
954
|
+
};
|
|
955
|
+
const buildNestedObject = (definition) => {
|
|
956
|
+
const tree = buildTree(definition);
|
|
957
|
+
const declarations = [];
|
|
958
|
+
const returnEntries = [];
|
|
959
|
+
tree.forEach((node, rootName) => {
|
|
960
|
+
if (node.children.size > 0) {
|
|
961
|
+
const objectLiteral = renderTreeNode(node, 2);
|
|
962
|
+
declarations.push(` const ${rootName} = ${objectLiteral};`);
|
|
963
|
+
returnEntries.push(rootName);
|
|
964
|
+
} else if (node.expression && node.canonicalId) {
|
|
965
|
+
const expr = formatFactory(node.expression);
|
|
966
|
+
declarations.push(` const ${rootName} = registry.addElement("${node.canonicalId}", () => ${expr});`);
|
|
967
|
+
returnEntries.push(rootName);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
const returnStatement = returnEntries.length > 0 ? ` return {\n${returnEntries.map((name) => ` ${name},`).join("\n")}\n };` : " return {};";
|
|
971
|
+
if (declarations.length === 0) {
|
|
972
|
+
return returnStatement;
|
|
973
|
+
}
|
|
974
|
+
return `${declarations.join("\n")}\n${returnStatement}`;
|
|
975
|
+
};
|
|
976
|
+
/**
|
|
977
|
+
* Render import statements for the intermediate module using ModuleSummary.
|
|
978
|
+
* Only includes imports from modules that have gql exports.
|
|
979
|
+
*/
|
|
980
|
+
const renderImportStatements = ({ filePath, analysis, analyses, graphqlSystemPath }) => {
|
|
981
|
+
const importLines = [];
|
|
982
|
+
const importedRootNames = new Set();
|
|
983
|
+
const namespaceImports = new Set();
|
|
984
|
+
const importsByFile = new Map();
|
|
985
|
+
analysis.imports.forEach((imp) => {
|
|
986
|
+
if (imp.isTypeOnly) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (!imp.source.startsWith(".")) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const resolvedPath = (0, __soda_gql_common.resolveRelativeImportWithReferences)({
|
|
993
|
+
filePath,
|
|
994
|
+
specifier: imp.source,
|
|
995
|
+
references: analyses
|
|
996
|
+
});
|
|
997
|
+
if (!resolvedPath) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (resolvedPath === graphqlSystemPath) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const imports = importsByFile.get(resolvedPath) ?? [];
|
|
1004
|
+
imports.push(imp);
|
|
1005
|
+
importsByFile.set(resolvedPath, imports);
|
|
1006
|
+
});
|
|
1007
|
+
importsByFile.forEach((imports, filePath$1) => {
|
|
1008
|
+
const namespaceImport = imports.find((imp) => imp.kind === "namespace");
|
|
1009
|
+
if (namespaceImport) {
|
|
1010
|
+
importLines.push(` const ${namespaceImport.local} = yield registry.requestImport("${filePath$1}");`);
|
|
1011
|
+
namespaceImports.add(namespaceImport.local);
|
|
1012
|
+
importedRootNames.add(namespaceImport.local);
|
|
1013
|
+
} else {
|
|
1014
|
+
const rootNames = new Set();
|
|
1015
|
+
imports.forEach((imp) => {
|
|
1016
|
+
if (imp.kind === "named" || imp.kind === "default") {
|
|
1017
|
+
rootNames.add(imp.local);
|
|
1018
|
+
importedRootNames.add(imp.local);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
if (rootNames.size > 0) {
|
|
1022
|
+
const destructured = Array.from(rootNames).sort().join(", ");
|
|
1023
|
+
importLines.push(` const { ${destructured} } = yield registry.requestImport("${filePath$1}");`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
return {
|
|
1028
|
+
imports: importLines.length > 0 ? `${importLines.join("\n")}` : "",
|
|
1029
|
+
importedRootNames,
|
|
1030
|
+
namespaceImports
|
|
1031
|
+
};
|
|
1032
|
+
};
|
|
1033
|
+
const renderRegistryBlock = ({ filePath, analysis, analyses, graphqlSystemPath }) => {
|
|
1034
|
+
const { imports } = renderImportStatements({
|
|
1035
|
+
filePath,
|
|
1036
|
+
analysis,
|
|
1037
|
+
analyses,
|
|
1038
|
+
graphqlSystemPath
|
|
1039
|
+
});
|
|
1040
|
+
return [
|
|
1041
|
+
`registry.setModule("${filePath}", function*() {`,
|
|
1042
|
+
imports,
|
|
1043
|
+
"",
|
|
1044
|
+
buildNestedObject(analysis.definitions),
|
|
1045
|
+
"});"
|
|
1046
|
+
].join("\n");
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region packages/builder/src/intermediate-module/registry.ts
|
|
1051
|
+
const createIntermediateRegistry = ({ analyses } = {}) => {
|
|
1052
|
+
const modules = new Map();
|
|
1053
|
+
const elements = new Map();
|
|
1054
|
+
const setModule = (filePath, factory) => {
|
|
1055
|
+
modules.set(filePath, factory);
|
|
1056
|
+
};
|
|
1057
|
+
/**
|
|
1058
|
+
* Creates an import request to be yielded by module generators.
|
|
1059
|
+
* Usage: `const { foo } = yield registry.requestImport("/path/to/module");`
|
|
1060
|
+
*/
|
|
1061
|
+
const requestImport = (filePath) => ({
|
|
1062
|
+
kind: "import",
|
|
1063
|
+
filePath
|
|
1064
|
+
});
|
|
1065
|
+
const addElement = (canonicalId, factory) => {
|
|
1066
|
+
const builder = factory();
|
|
1067
|
+
__soda_gql_core.GqlElement.setContext(builder, { canonicalId });
|
|
1068
|
+
elements.set(canonicalId, builder);
|
|
1069
|
+
return builder;
|
|
1070
|
+
};
|
|
1071
|
+
/**
|
|
1072
|
+
* Evaluate a single module and its dependencies using trampoline.
|
|
1073
|
+
* Returns the cached result or evaluates and caches if not yet evaluated.
|
|
1074
|
+
*/
|
|
1075
|
+
const evaluateModule = (filePath, evaluated, inProgress) => {
|
|
1076
|
+
const cached = evaluated.get(filePath);
|
|
1077
|
+
if (cached) {
|
|
1078
|
+
return cached;
|
|
1079
|
+
}
|
|
1080
|
+
const stack = [];
|
|
1081
|
+
const factory = modules.get(filePath);
|
|
1082
|
+
if (!factory) {
|
|
1083
|
+
throw new Error(`Module not found or yet to be registered: ${filePath}`);
|
|
1084
|
+
}
|
|
1085
|
+
stack.push({
|
|
1086
|
+
filePath,
|
|
1087
|
+
generator: factory()
|
|
1088
|
+
});
|
|
1089
|
+
let frame;
|
|
1090
|
+
while (frame = stack[stack.length - 1]) {
|
|
1091
|
+
inProgress.add(frame.filePath);
|
|
1092
|
+
const result$1 = frame.resolvedDependency !== undefined ? frame.generator.next(frame.resolvedDependency) : frame.generator.next();
|
|
1093
|
+
frame.resolvedDependency = undefined;
|
|
1094
|
+
if (result$1.done) {
|
|
1095
|
+
evaluated.set(frame.filePath, result$1.value);
|
|
1096
|
+
inProgress.delete(frame.filePath);
|
|
1097
|
+
stack.pop();
|
|
1098
|
+
const parentFrame = stack[stack.length - 1];
|
|
1099
|
+
if (parentFrame) {
|
|
1100
|
+
parentFrame.resolvedDependency = result$1.value;
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
const request = result$1.value;
|
|
1104
|
+
if (request.kind === "import") {
|
|
1105
|
+
const depPath = request.filePath;
|
|
1106
|
+
const depCached = evaluated.get(depPath);
|
|
1107
|
+
if (depCached) {
|
|
1108
|
+
frame.resolvedDependency = depCached;
|
|
1109
|
+
} else {
|
|
1110
|
+
if (inProgress.has(depPath)) {
|
|
1111
|
+
if (analyses) {
|
|
1112
|
+
const currentAnalysis = analyses.get(frame.filePath);
|
|
1113
|
+
const targetAnalysis = analyses.get(depPath);
|
|
1114
|
+
const currentHasGql = currentAnalysis && currentAnalysis.definitions.length > 0;
|
|
1115
|
+
const targetHasGql = targetAnalysis && targetAnalysis.definitions.length > 0;
|
|
1116
|
+
if (!currentHasGql || !targetHasGql) {
|
|
1117
|
+
frame.resolvedDependency = {};
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
throw new Error(`Circular dependency detected: ${depPath}`);
|
|
1122
|
+
}
|
|
1123
|
+
const depFactory = modules.get(depPath);
|
|
1124
|
+
if (!depFactory) {
|
|
1125
|
+
throw new Error(`Module not found or yet to be registered: ${depPath}`);
|
|
1126
|
+
}
|
|
1127
|
+
stack.push({
|
|
1128
|
+
filePath: depPath,
|
|
1129
|
+
generator: depFactory()
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const result = evaluated.get(filePath);
|
|
1136
|
+
if (!result) {
|
|
1137
|
+
throw new Error(`Module evaluation failed: ${filePath}`);
|
|
1138
|
+
}
|
|
1139
|
+
return result;
|
|
1140
|
+
};
|
|
1141
|
+
/**
|
|
1142
|
+
* Build artifacts record from evaluated elements.
|
|
1143
|
+
*/
|
|
1144
|
+
const buildArtifacts = () => {
|
|
1145
|
+
const artifacts = {};
|
|
1146
|
+
for (const [canonicalId, element] of elements.entries()) {
|
|
1147
|
+
if (element instanceof __soda_gql_core.Fragment) {
|
|
1148
|
+
artifacts[canonicalId] = {
|
|
1149
|
+
type: "fragment",
|
|
1150
|
+
element
|
|
1151
|
+
};
|
|
1152
|
+
} else if (element instanceof __soda_gql_core.Operation) {
|
|
1153
|
+
artifacts[canonicalId] = {
|
|
1154
|
+
type: "operation",
|
|
1155
|
+
element
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return artifacts;
|
|
1160
|
+
};
|
|
1161
|
+
/**
|
|
1162
|
+
* Generator that evaluates all elements using the effect system.
|
|
1163
|
+
* Uses ParallelEffect to enable parallel evaluation in async mode.
|
|
1164
|
+
* In sync mode, ParallelEffect executes effects sequentially.
|
|
1165
|
+
*/
|
|
1166
|
+
function* evaluateElementsGen() {
|
|
1167
|
+
const effects = Array.from(elements.values(), (element) => new ElementEvaluationEffect(element));
|
|
1168
|
+
if (effects.length > 0) {
|
|
1169
|
+
yield* new __soda_gql_common.ParallelEffect(effects).run();
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Synchronous evaluation - evaluates all modules and elements synchronously.
|
|
1174
|
+
* Throws if any element requires async operations (e.g., async metadata factory).
|
|
1175
|
+
*/
|
|
1176
|
+
const evaluate = () => {
|
|
1177
|
+
const evaluated = new Map();
|
|
1178
|
+
const inProgress = new Set();
|
|
1179
|
+
for (const filePath of modules.keys()) {
|
|
1180
|
+
if (!evaluated.has(filePath)) {
|
|
1181
|
+
evaluateModule(filePath, evaluated, inProgress);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const scheduler = (0, __soda_gql_common.createSyncScheduler)();
|
|
1185
|
+
const result = scheduler.run(() => evaluateElementsGen());
|
|
1186
|
+
if (result.isErr()) {
|
|
1187
|
+
throw new Error(`Element evaluation failed: ${result.error.message}`);
|
|
1188
|
+
}
|
|
1189
|
+
return buildArtifacts();
|
|
1190
|
+
};
|
|
1191
|
+
/**
|
|
1192
|
+
* Asynchronous evaluation - evaluates all modules and elements with async support.
|
|
1193
|
+
* Supports async metadata factories and other async operations.
|
|
1194
|
+
*/
|
|
1195
|
+
const evaluateAsync = async () => {
|
|
1196
|
+
const evaluated = new Map();
|
|
1197
|
+
const inProgress = new Set();
|
|
1198
|
+
for (const filePath of modules.keys()) {
|
|
1199
|
+
if (!evaluated.has(filePath)) {
|
|
1200
|
+
evaluateModule(filePath, evaluated, inProgress);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const scheduler = (0, __soda_gql_common.createAsyncScheduler)();
|
|
1204
|
+
const result = await scheduler.run(() => evaluateElementsGen());
|
|
1205
|
+
if (result.isErr()) {
|
|
1206
|
+
throw new Error(`Element evaluation failed: ${result.error.message}`);
|
|
1207
|
+
}
|
|
1208
|
+
return buildArtifacts();
|
|
1209
|
+
};
|
|
1210
|
+
/**
|
|
1211
|
+
* Evaluate all modules synchronously using trampoline.
|
|
1212
|
+
* This runs the module dependency resolution without element evaluation.
|
|
1213
|
+
* Call this before getElements() when using external scheduler control.
|
|
1214
|
+
*/
|
|
1215
|
+
const evaluateModules = () => {
|
|
1216
|
+
const evaluated = new Map();
|
|
1217
|
+
const inProgress = new Set();
|
|
1218
|
+
for (const filePath of modules.keys()) {
|
|
1219
|
+
if (!evaluated.has(filePath)) {
|
|
1220
|
+
evaluateModule(filePath, evaluated, inProgress);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
/**
|
|
1225
|
+
* Get all registered elements for external effect creation.
|
|
1226
|
+
* Call evaluateModules() first to ensure all modules have been evaluated.
|
|
1227
|
+
*/
|
|
1228
|
+
const getElements = () => {
|
|
1229
|
+
return Array.from(elements.values());
|
|
1230
|
+
};
|
|
1231
|
+
const clear = () => {
|
|
1232
|
+
modules.clear();
|
|
1233
|
+
elements.clear();
|
|
1234
|
+
};
|
|
1235
|
+
return {
|
|
1236
|
+
setModule,
|
|
1237
|
+
requestImport,
|
|
1238
|
+
addElement,
|
|
1239
|
+
evaluate,
|
|
1240
|
+
evaluateAsync,
|
|
1241
|
+
evaluateModules,
|
|
1242
|
+
getElements,
|
|
1243
|
+
buildArtifacts,
|
|
1244
|
+
clear
|
|
1245
|
+
};
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
//#endregion
|
|
1249
|
+
//#region packages/builder/src/intermediate-module/evaluation.ts
|
|
1250
|
+
const transpile = ({ filePath, sourceCode }) => {
|
|
1251
|
+
try {
|
|
1252
|
+
const result = (0, __swc_core.transformSync)(sourceCode, {
|
|
1253
|
+
filename: `${filePath}.ts`,
|
|
1254
|
+
jsc: {
|
|
1255
|
+
parser: {
|
|
1256
|
+
syntax: "typescript",
|
|
1257
|
+
tsx: false
|
|
1258
|
+
},
|
|
1259
|
+
target: "es2022"
|
|
1260
|
+
},
|
|
1261
|
+
module: { type: "es6" },
|
|
1262
|
+
sourceMaps: false,
|
|
1263
|
+
minify: false
|
|
1264
|
+
});
|
|
1265
|
+
return (0, neverthrow.ok)(result.code);
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1268
|
+
return (0, neverthrow.err)({
|
|
1269
|
+
code: "RUNTIME_MODULE_LOAD_FAILED",
|
|
1270
|
+
filePath,
|
|
1271
|
+
astPath: "",
|
|
1272
|
+
message: `SWC transpilation failed: ${message}`
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
/**
|
|
1277
|
+
* Resolve graphql system path to the bundled CJS file.
|
|
1278
|
+
* Accepts both .ts (for backward compatibility) and .cjs paths.
|
|
1279
|
+
* Maps .ts to sibling .cjs file if it exists.
|
|
1280
|
+
*/
|
|
1281
|
+
function resolveGraphqlSystemPath(configPath) {
|
|
1282
|
+
const ext = (0, node_path.extname)(configPath);
|
|
1283
|
+
if (ext === ".cjs") {
|
|
1284
|
+
return (0, node_path.resolve)(process.cwd(), configPath);
|
|
1285
|
+
}
|
|
1286
|
+
if (ext === ".ts") {
|
|
1287
|
+
const basePath = configPath.slice(0, -3);
|
|
1288
|
+
const cjsPath = `${basePath}.cjs`;
|
|
1289
|
+
const resolvedCjsPath = (0, node_path.resolve)(process.cwd(), cjsPath);
|
|
1290
|
+
if ((0, node_fs.existsSync)(resolvedCjsPath)) {
|
|
1291
|
+
return resolvedCjsPath;
|
|
1292
|
+
}
|
|
1293
|
+
return (0, node_path.resolve)(process.cwd(), configPath);
|
|
1294
|
+
}
|
|
1295
|
+
return (0, node_path.resolve)(process.cwd(), configPath);
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Bundle and execute GraphQL system module using rspack + memfs.
|
|
1299
|
+
* Creates a self-contained bundle that can run in VM context.
|
|
1300
|
+
* This is cached per session to avoid re-bundling.
|
|
1301
|
+
*/
|
|
1302
|
+
let cachedGql = null;
|
|
1303
|
+
let cachedModulePath = null;
|
|
1304
|
+
/**
|
|
1305
|
+
* Clear the cached gql module.
|
|
1306
|
+
* Call this between test runs to ensure clean state.
|
|
1307
|
+
* @internal - exported for testing purposes only
|
|
1308
|
+
*/
|
|
1309
|
+
const __clearGqlCache = () => {
|
|
1310
|
+
cachedGql = null;
|
|
1311
|
+
cachedModulePath = null;
|
|
1312
|
+
};
|
|
1313
|
+
function executeGraphqlSystemModule(modulePath) {
|
|
1314
|
+
if (cachedModulePath === modulePath && cachedGql !== null) {
|
|
1315
|
+
return { gql: cachedGql };
|
|
1316
|
+
}
|
|
1317
|
+
const bundledCode = (0, node_fs.readFileSync)(modulePath, "utf-8");
|
|
1318
|
+
const sandbox = createSandbox(modulePath);
|
|
1319
|
+
new node_vm.Script(bundledCode, { filename: modulePath }).runInNewContext(sandbox);
|
|
1320
|
+
const finalExports = sandbox.module.exports;
|
|
1321
|
+
const exportedGql = finalExports.gql ?? finalExports.default;
|
|
1322
|
+
if (exportedGql === undefined) {
|
|
1323
|
+
throw new Error(`No 'gql' export found in GraphQL system module: ${modulePath}`);
|
|
1324
|
+
}
|
|
1325
|
+
cachedGql = exportedGql;
|
|
1326
|
+
cachedModulePath = modulePath;
|
|
1327
|
+
return { gql: cachedGql };
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Build intermediate modules from dependency graph.
|
|
1331
|
+
* Each intermediate module corresponds to one source file.
|
|
1332
|
+
*/
|
|
1333
|
+
const generateIntermediateModules = function* ({ analyses, targetFiles, graphqlSystemPath }) {
|
|
1334
|
+
for (const filePath of targetFiles) {
|
|
1335
|
+
const analysis = analyses.get(filePath);
|
|
1336
|
+
if (!analysis) {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const sourceCode = renderRegistryBlock({
|
|
1340
|
+
filePath,
|
|
1341
|
+
analysis,
|
|
1342
|
+
analyses,
|
|
1343
|
+
graphqlSystemPath
|
|
1344
|
+
});
|
|
1345
|
+
if (process.env.DEBUG_INTERMEDIATE_MODULE) {
|
|
1346
|
+
console.log("=== Intermediate module source ===");
|
|
1347
|
+
console.log("FilePath:", filePath);
|
|
1348
|
+
console.log("Definitions:", analysis.definitions.map((d) => d.astPath));
|
|
1349
|
+
console.log("Source code:\n", sourceCode);
|
|
1350
|
+
console.log("=================================");
|
|
1351
|
+
}
|
|
1352
|
+
const transpiledCodeResult = transpile({
|
|
1353
|
+
filePath,
|
|
1354
|
+
sourceCode
|
|
1355
|
+
});
|
|
1356
|
+
if (transpiledCodeResult.isErr()) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
const transpiledCode = transpiledCodeResult.value;
|
|
1360
|
+
const script = new node_vm.Script(transpiledCode);
|
|
1361
|
+
const hash = (0, node_crypto.createHash)("sha1");
|
|
1362
|
+
hash.update(transpiledCode);
|
|
1363
|
+
const contentHash = hash.digest("hex");
|
|
1364
|
+
const canonicalIds = analysis.definitions.map((definition) => definition.canonicalId);
|
|
1365
|
+
yield {
|
|
1366
|
+
filePath,
|
|
1367
|
+
canonicalIds,
|
|
1368
|
+
sourceCode,
|
|
1369
|
+
transpiledCode,
|
|
1370
|
+
contentHash,
|
|
1371
|
+
script
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
/**
|
|
1376
|
+
* Set up VM context and run intermediate module scripts.
|
|
1377
|
+
* Returns the registry for evaluation.
|
|
1378
|
+
*/
|
|
1379
|
+
const setupIntermediateModulesContext = ({ intermediateModules, graphqlSystemPath, analyses }) => {
|
|
1380
|
+
const registry = createIntermediateRegistry({ analyses });
|
|
1381
|
+
const gqlImportPath = resolveGraphqlSystemPath(graphqlSystemPath);
|
|
1382
|
+
const { gql } = executeGraphqlSystemModule(gqlImportPath);
|
|
1383
|
+
const vmContext = (0, node_vm.createContext)({
|
|
1384
|
+
gql,
|
|
1385
|
+
registry
|
|
1386
|
+
});
|
|
1387
|
+
for (const { script, filePath } of intermediateModules.values()) {
|
|
1388
|
+
try {
|
|
1389
|
+
script.runInContext(vmContext);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
console.error(`Error evaluating intermediate module ${filePath}:`, error);
|
|
1392
|
+
throw error;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return registry;
|
|
1396
|
+
};
|
|
1397
|
+
/**
|
|
1398
|
+
* Synchronous evaluation of intermediate modules.
|
|
1399
|
+
* Throws if any element requires async operations (e.g., async metadata factory).
|
|
1400
|
+
*/
|
|
1401
|
+
const evaluateIntermediateModules = (input) => {
|
|
1402
|
+
const registry = setupIntermediateModulesContext(input);
|
|
1403
|
+
const elements = registry.evaluate();
|
|
1404
|
+
registry.clear();
|
|
1405
|
+
return elements;
|
|
1406
|
+
};
|
|
1407
|
+
/**
|
|
1408
|
+
* Asynchronous evaluation of intermediate modules.
|
|
1409
|
+
* Supports async metadata factories and other async operations.
|
|
1410
|
+
*/
|
|
1411
|
+
const evaluateIntermediateModulesAsync = async (input) => {
|
|
1412
|
+
const registry = setupIntermediateModulesContext(input);
|
|
1413
|
+
const elements = await registry.evaluateAsync();
|
|
1414
|
+
registry.clear();
|
|
1415
|
+
return elements;
|
|
1416
|
+
};
|
|
1417
|
+
/**
|
|
1418
|
+
* Generator version of evaluateIntermediateModules for external scheduler control.
|
|
1419
|
+
* Yields effects for element evaluation, enabling unified scheduler at the root level.
|
|
1420
|
+
*
|
|
1421
|
+
* This function:
|
|
1422
|
+
* 1. Sets up the VM context and runs intermediate module scripts
|
|
1423
|
+
* 2. Runs synchronous module evaluation (trampoline - no I/O)
|
|
1424
|
+
* 3. Yields element evaluation effects via ParallelEffect
|
|
1425
|
+
* 4. Returns the artifacts record
|
|
1426
|
+
*/
|
|
1427
|
+
function* evaluateIntermediateModulesGen(input) {
|
|
1428
|
+
const registry = setupIntermediateModulesContext(input);
|
|
1429
|
+
registry.evaluateModules();
|
|
1430
|
+
const elements = registry.getElements();
|
|
1431
|
+
const effects = elements.map((element) => new ElementEvaluationEffect(element));
|
|
1432
|
+
if (effects.length > 0) {
|
|
1433
|
+
yield* new __soda_gql_common.ParallelEffect(effects).run();
|
|
1434
|
+
}
|
|
1435
|
+
const artifacts = registry.buildArtifacts();
|
|
1436
|
+
registry.clear();
|
|
1437
|
+
return artifacts;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
//#endregion
|
|
1441
|
+
//#region packages/builder/src/internal/graphql-system.ts
|
|
1442
|
+
/**
|
|
1443
|
+
* Helper for identifying graphql-system files and import specifiers.
|
|
1444
|
+
* Provides robust detection across symlinks, case-insensitive filesystems, and user-defined aliases.
|
|
1445
|
+
*/
|
|
1446
|
+
/**
|
|
1447
|
+
* Create a canonical file name getter based on platform.
|
|
1448
|
+
* On case-sensitive filesystems, paths are returned as-is.
|
|
1449
|
+
* On case-insensitive filesystems, paths are lowercased for comparison.
|
|
1450
|
+
*/
|
|
1451
|
+
const createGetCanonicalFileName = (useCaseSensitiveFileNames) => {
|
|
1452
|
+
return useCaseSensitiveFileNames ? (path) => path : (path) => path.toLowerCase();
|
|
1453
|
+
};
|
|
1454
|
+
/**
|
|
1455
|
+
* Detect if the filesystem is case-sensitive.
|
|
1456
|
+
* We assume Unix-like systems are case-sensitive, and Windows is not.
|
|
1457
|
+
*/
|
|
1458
|
+
const getUseCaseSensitiveFileNames = () => {
|
|
1459
|
+
return process.platform !== "win32";
|
|
1460
|
+
};
|
|
1461
|
+
/**
|
|
1462
|
+
* Create a GraphqlSystemIdentifyHelper from the resolved config.
|
|
1463
|
+
* Uses canonical path comparison to handle casing, symlinks, and aliases.
|
|
1464
|
+
*/
|
|
1465
|
+
const createGraphqlSystemIdentifyHelper = (config) => {
|
|
1466
|
+
const getCanonicalFileName = createGetCanonicalFileName(getUseCaseSensitiveFileNames());
|
|
1467
|
+
const toCanonical = (file) => {
|
|
1468
|
+
const resolved = (0, node_path.resolve)(file);
|
|
1469
|
+
try {
|
|
1470
|
+
return getCanonicalFileName((0, node_fs.realpathSync)(resolved));
|
|
1471
|
+
} catch {
|
|
1472
|
+
return getCanonicalFileName(resolved);
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
const graphqlSystemPath = (0, node_path.resolve)(config.outdir, "index.ts");
|
|
1476
|
+
const canonicalGraphqlSystemPath = toCanonical(graphqlSystemPath);
|
|
1477
|
+
const canonicalAliases = new Set(config.graphqlSystemAliases.map((alias) => alias));
|
|
1478
|
+
return {
|
|
1479
|
+
isGraphqlSystemFile: ({ filePath }) => {
|
|
1480
|
+
return toCanonical(filePath) === canonicalGraphqlSystemPath;
|
|
1481
|
+
},
|
|
1482
|
+
isGraphqlSystemImportSpecifier: ({ filePath, specifier }) => {
|
|
1483
|
+
if (canonicalAliases.has(specifier)) {
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1486
|
+
if (!specifier.startsWith(".")) {
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
const resolved = (0, __soda_gql_common.resolveRelativeImportWithExistenceCheck)({
|
|
1490
|
+
filePath,
|
|
1491
|
+
specifier
|
|
1492
|
+
});
|
|
1493
|
+
if (!resolved) {
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
return toCanonical(resolved) === canonicalGraphqlSystemPath;
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
//#endregion
|
|
1502
|
+
//#region packages/builder/src/artifact/aggregate.ts
|
|
1503
|
+
const canonicalToFilePath$1 = (canonicalId) => canonicalId.split("::")[0] ?? canonicalId;
|
|
1504
|
+
const computeContentHash = (prebuild) => {
|
|
1505
|
+
const hash = (0, node_crypto.createHash)("sha1");
|
|
1506
|
+
hash.update(JSON.stringify(prebuild));
|
|
1507
|
+
return hash.digest("hex");
|
|
1508
|
+
};
|
|
1509
|
+
const emitRegistrationError = (definition, message) => ({
|
|
1510
|
+
code: "RUNTIME_MODULE_LOAD_FAILED",
|
|
1511
|
+
filePath: canonicalToFilePath$1(definition.canonicalId),
|
|
1512
|
+
astPath: definition.astPath,
|
|
1513
|
+
message
|
|
1514
|
+
});
|
|
1515
|
+
const aggregate = ({ analyses, elements }) => {
|
|
1516
|
+
const registry = new Map();
|
|
1517
|
+
for (const analysis of analyses.values()) {
|
|
1518
|
+
for (const definition of analysis.definitions) {
|
|
1519
|
+
const element = elements[definition.canonicalId];
|
|
1520
|
+
if (!element) {
|
|
1521
|
+
const availableIds = Object.keys(elements).join(", ");
|
|
1522
|
+
const message = `ARTIFACT_NOT_FOUND_IN_RUNTIME_MODULE: ${definition.canonicalId}\nAvailable: ${availableIds}`;
|
|
1523
|
+
return (0, neverthrow.err)(emitRegistrationError(definition, message));
|
|
1524
|
+
}
|
|
1525
|
+
if (registry.has(definition.canonicalId)) {
|
|
1526
|
+
return (0, neverthrow.err)(emitRegistrationError(definition, `ARTIFACT_ALREADY_REGISTERED`));
|
|
1527
|
+
}
|
|
1528
|
+
const metadata = {
|
|
1529
|
+
sourcePath: analysis.filePath ?? canonicalToFilePath$1(definition.canonicalId),
|
|
1530
|
+
contentHash: ""
|
|
1531
|
+
};
|
|
1532
|
+
if (element.type === "fragment") {
|
|
1533
|
+
const prebuild = {
|
|
1534
|
+
typename: element.element.typename,
|
|
1535
|
+
key: element.element.key,
|
|
1536
|
+
schemaLabel: element.element.schemaLabel
|
|
1537
|
+
};
|
|
1538
|
+
registry.set(definition.canonicalId, {
|
|
1539
|
+
id: definition.canonicalId,
|
|
1540
|
+
type: "fragment",
|
|
1541
|
+
prebuild,
|
|
1542
|
+
metadata: {
|
|
1543
|
+
...metadata,
|
|
1544
|
+
contentHash: computeContentHash(prebuild)
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (element.type === "operation") {
|
|
1550
|
+
const prebuild = {
|
|
1551
|
+
operationType: element.element.operationType,
|
|
1552
|
+
operationName: element.element.operationName,
|
|
1553
|
+
schemaLabel: element.element.schemaLabel,
|
|
1554
|
+
document: element.element.document,
|
|
1555
|
+
variableNames: element.element.variableNames,
|
|
1556
|
+
metadata: element.element.metadata
|
|
1557
|
+
};
|
|
1558
|
+
registry.set(definition.canonicalId, {
|
|
1559
|
+
id: definition.canonicalId,
|
|
1560
|
+
type: "operation",
|
|
1561
|
+
prebuild,
|
|
1562
|
+
metadata: {
|
|
1563
|
+
...metadata,
|
|
1564
|
+
contentHash: computeContentHash(prebuild)
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
return (0, neverthrow.err)(emitRegistrationError(definition, "UNKNOWN_ARTIFACT_KIND"));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return (0, neverthrow.ok)(registry);
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
//#endregion
|
|
1576
|
+
//#region packages/builder/src/artifact/issue-handler.ts
|
|
1577
|
+
const canonicalToFilePath = (canonicalId) => canonicalId.split("::")[0] ?? canonicalId;
|
|
1578
|
+
const checkIssues = ({ elements }) => {
|
|
1579
|
+
const operationNames = new Set();
|
|
1580
|
+
for (const [canonicalId, { type, element }] of Object.entries(elements)) {
|
|
1581
|
+
if (type !== "operation") {
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (operationNames.has(element.operationName)) {
|
|
1585
|
+
const sources = [canonicalToFilePath(canonicalId)];
|
|
1586
|
+
return (0, neverthrow.err)({
|
|
1587
|
+
code: "DOC_DUPLICATE",
|
|
1588
|
+
message: `Duplicate document name: ${element.operationName}`,
|
|
1589
|
+
name: element.operationName,
|
|
1590
|
+
sources
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
operationNames.add(element.operationName);
|
|
1594
|
+
}
|
|
1595
|
+
return (0, neverthrow.ok)([]);
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
//#endregion
|
|
1599
|
+
//#region packages/builder/src/artifact/builder.ts
|
|
1600
|
+
const buildArtifact = ({ elements, analyses, stats: cache }) => {
|
|
1601
|
+
const issuesResult = checkIssues({ elements });
|
|
1602
|
+
if (issuesResult.isErr()) {
|
|
1603
|
+
return (0, neverthrow.err)(issuesResult.error);
|
|
1604
|
+
}
|
|
1605
|
+
const warnings = issuesResult.value;
|
|
1606
|
+
const aggregationResult = aggregate({
|
|
1607
|
+
analyses,
|
|
1608
|
+
elements
|
|
1609
|
+
});
|
|
1610
|
+
if (aggregationResult.isErr()) {
|
|
1611
|
+
return (0, neverthrow.err)(aggregationResult.error);
|
|
1612
|
+
}
|
|
1613
|
+
return (0, neverthrow.ok)({
|
|
1614
|
+
elements: Object.fromEntries(aggregationResult.value.entries()),
|
|
1615
|
+
report: {
|
|
1616
|
+
durationMs: 0,
|
|
1617
|
+
warnings,
|
|
1618
|
+
stats: cache
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
//#endregion
|
|
1624
|
+
//#region packages/builder/src/ast/common/scope.ts
|
|
1625
|
+
/**
|
|
1626
|
+
* Build AST path from scope stack
|
|
1627
|
+
*/
|
|
1628
|
+
const buildAstPath = (stack) => {
|
|
1629
|
+
return stack.map((frame) => frame.nameSegment).join(".");
|
|
1630
|
+
};
|
|
1631
|
+
/**
|
|
1632
|
+
* Create an occurrence tracker for disambiguating anonymous/duplicate scopes.
|
|
1633
|
+
*/
|
|
1634
|
+
const createOccurrenceTracker = () => {
|
|
1635
|
+
const occurrenceCounters = new Map();
|
|
1636
|
+
return { getNextOccurrence(key) {
|
|
1637
|
+
const current = occurrenceCounters.get(key) ?? 0;
|
|
1638
|
+
occurrenceCounters.set(key, current + 1);
|
|
1639
|
+
return current;
|
|
1640
|
+
} };
|
|
1641
|
+
};
|
|
1642
|
+
/**
|
|
1643
|
+
* Create a path uniqueness tracker to ensure AST paths are unique.
|
|
1644
|
+
*/
|
|
1645
|
+
const createPathTracker = () => {
|
|
1646
|
+
const usedPaths = new Set();
|
|
1647
|
+
return { ensureUniquePath(basePath) {
|
|
1648
|
+
let path = basePath;
|
|
1649
|
+
let suffix = 0;
|
|
1650
|
+
while (usedPaths.has(path)) {
|
|
1651
|
+
suffix++;
|
|
1652
|
+
path = `${basePath}$${suffix}`;
|
|
1653
|
+
}
|
|
1654
|
+
usedPaths.add(path);
|
|
1655
|
+
return path;
|
|
1656
|
+
} };
|
|
1657
|
+
};
|
|
1658
|
+
/**
|
|
1659
|
+
* Create an export bindings map from module exports.
|
|
1660
|
+
* Maps local variable names to their exported names.
|
|
1661
|
+
*/
|
|
1662
|
+
const createExportBindingsMap = (exports$1) => {
|
|
1663
|
+
const exportBindings = new Map();
|
|
1664
|
+
exports$1.forEach((exp) => {
|
|
1665
|
+
if (exp.kind === "named" && exp.local && !exp.isTypeOnly) {
|
|
1666
|
+
exportBindings.set(exp.local, exp.exported);
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
return exportBindings;
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
//#endregion
|
|
1673
|
+
//#region packages/builder/src/ast/adapters/swc.ts
|
|
1674
|
+
/**
|
|
1675
|
+
* SWC adapter for the analyzer core.
|
|
1676
|
+
* Implements parser-specific logic using the SWC parser.
|
|
1677
|
+
*/
|
|
1678
|
+
const collectImports$1 = (module$1) => {
|
|
1679
|
+
const imports = [];
|
|
1680
|
+
const handle = (declaration) => {
|
|
1681
|
+
const source = declaration.source.value;
|
|
1682
|
+
declaration.specifiers?.forEach((specifier) => {
|
|
1683
|
+
if (specifier.type === "ImportSpecifier") {
|
|
1684
|
+
imports.push({
|
|
1685
|
+
source,
|
|
1686
|
+
local: specifier.local.value,
|
|
1687
|
+
kind: "named",
|
|
1688
|
+
isTypeOnly: Boolean(specifier.isTypeOnly)
|
|
1689
|
+
});
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
1693
|
+
imports.push({
|
|
1694
|
+
source,
|
|
1695
|
+
local: specifier.local.value,
|
|
1696
|
+
kind: "namespace",
|
|
1697
|
+
isTypeOnly: false
|
|
1698
|
+
});
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
1702
|
+
imports.push({
|
|
1703
|
+
source,
|
|
1704
|
+
local: specifier.local.value,
|
|
1705
|
+
kind: "default",
|
|
1706
|
+
isTypeOnly: false
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
};
|
|
1711
|
+
module$1.body.forEach((item) => {
|
|
1712
|
+
if (item.type === "ImportDeclaration") {
|
|
1713
|
+
handle(item);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
if ("declaration" in item && item.declaration && "type" in item.declaration && item.declaration.type === "ImportDeclaration") {
|
|
1717
|
+
handle(item.declaration);
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
return imports;
|
|
1721
|
+
};
|
|
1722
|
+
const collectExports$1 = (module$1) => {
|
|
1723
|
+
const exports$1 = [];
|
|
1724
|
+
const handle = (declaration) => {
|
|
1725
|
+
if (declaration.type === "ExportDeclaration") {
|
|
1726
|
+
if (declaration.declaration.type === "VariableDeclaration") {
|
|
1727
|
+
declaration.declaration.declarations.forEach((decl) => {
|
|
1728
|
+
if (decl.id.type === "Identifier") {
|
|
1729
|
+
exports$1.push({
|
|
1730
|
+
kind: "named",
|
|
1731
|
+
exported: decl.id.value,
|
|
1732
|
+
local: decl.id.value,
|
|
1733
|
+
isTypeOnly: false
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
if (declaration.declaration.type === "FunctionDeclaration") {
|
|
1739
|
+
const ident = declaration.declaration.identifier;
|
|
1740
|
+
if (ident) {
|
|
1741
|
+
exports$1.push({
|
|
1742
|
+
kind: "named",
|
|
1743
|
+
exported: ident.value,
|
|
1744
|
+
local: ident.value,
|
|
1745
|
+
isTypeOnly: false
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (declaration.type === "ExportNamedDeclaration") {
|
|
1752
|
+
const source = declaration.source?.value;
|
|
1753
|
+
declaration.specifiers?.forEach((specifier) => {
|
|
1754
|
+
if (specifier.type !== "ExportSpecifier") {
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const exported = specifier.exported ? specifier.exported.value : specifier.orig.value;
|
|
1758
|
+
const local = specifier.orig.value;
|
|
1759
|
+
if (source) {
|
|
1760
|
+
exports$1.push({
|
|
1761
|
+
kind: "reexport",
|
|
1762
|
+
exported,
|
|
1763
|
+
local,
|
|
1764
|
+
source,
|
|
1765
|
+
isTypeOnly: Boolean(specifier.isTypeOnly)
|
|
1766
|
+
});
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
exports$1.push({
|
|
1770
|
+
kind: "named",
|
|
1771
|
+
exported,
|
|
1772
|
+
local,
|
|
1773
|
+
isTypeOnly: Boolean(specifier.isTypeOnly)
|
|
1774
|
+
});
|
|
1775
|
+
});
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (declaration.type === "ExportAllDeclaration") {
|
|
1779
|
+
exports$1.push({
|
|
1780
|
+
kind: "reexport",
|
|
1781
|
+
exported: "*",
|
|
1782
|
+
source: declaration.source.value,
|
|
1783
|
+
isTypeOnly: false
|
|
1784
|
+
});
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (declaration.type === "ExportDefaultDeclaration" || declaration.type === "ExportDefaultExpression") {
|
|
1788
|
+
exports$1.push({
|
|
1789
|
+
kind: "named",
|
|
1790
|
+
exported: "default",
|
|
1791
|
+
local: "default",
|
|
1792
|
+
isTypeOnly: false
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
module$1.body.forEach((item) => {
|
|
1797
|
+
if (item.type === "ExportDeclaration" || item.type === "ExportNamedDeclaration" || item.type === "ExportAllDeclaration" || item.type === "ExportDefaultDeclaration" || item.type === "ExportDefaultExpression") {
|
|
1798
|
+
handle(item);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if ("declaration" in item && item.declaration) {
|
|
1802
|
+
const declaration = item.declaration;
|
|
1803
|
+
if (declaration.type === "ExportDeclaration" || declaration.type === "ExportNamedDeclaration" || declaration.type === "ExportAllDeclaration" || declaration.type === "ExportDefaultDeclaration" || declaration.type === "ExportDefaultExpression") {
|
|
1804
|
+
handle(declaration);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
return exports$1;
|
|
1809
|
+
};
|
|
1810
|
+
const collectGqlIdentifiers = (module$1, helper) => {
|
|
1811
|
+
const identifiers = new Set();
|
|
1812
|
+
module$1.body.forEach((item) => {
|
|
1813
|
+
const declaration = item.type === "ImportDeclaration" ? item : "declaration" in item && item.declaration && item.declaration.type === "ImportDeclaration" ? item.declaration : null;
|
|
1814
|
+
if (!declaration) {
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (!helper.isGraphqlSystemImportSpecifier({
|
|
1818
|
+
filePath: module$1.__filePath,
|
|
1819
|
+
specifier: declaration.source.value
|
|
1820
|
+
})) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
declaration.specifiers?.forEach((specifier) => {
|
|
1824
|
+
if (specifier.type === "ImportSpecifier") {
|
|
1825
|
+
const imported = specifier.imported ? specifier.imported.value : specifier.local.value;
|
|
1826
|
+
if (imported === "gql" && !specifier.imported) {
|
|
1827
|
+
identifiers.add(specifier.local.value);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
});
|
|
1832
|
+
return identifiers;
|
|
1833
|
+
};
|
|
1834
|
+
const isGqlCall = (identifiers, call) => {
|
|
1835
|
+
const callee = call.callee;
|
|
1836
|
+
if (callee.type !== "MemberExpression") {
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
if (callee.object.type !== "Identifier") {
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
if (!identifiers.has(callee.object.value)) {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
if (callee.property.type !== "Identifier") {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
const firstArg = call.arguments[0];
|
|
1849
|
+
if (!firstArg?.expression || firstArg.expression.type !== "ArrowFunctionExpression") {
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
return true;
|
|
1853
|
+
};
|
|
1854
|
+
/**
|
|
1855
|
+
* Unwrap method chains (like .attach()) to find the underlying gql call.
|
|
1856
|
+
* Returns the innermost CallExpression that is a valid gql definition call.
|
|
1857
|
+
*/
|
|
1858
|
+
const unwrapMethodChains$1 = (identifiers, node) => {
|
|
1859
|
+
if (!node || node.type !== "CallExpression") {
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
if (isGqlCall(identifiers, node)) {
|
|
1863
|
+
return node;
|
|
1864
|
+
}
|
|
1865
|
+
const callee = node.callee;
|
|
1866
|
+
if (callee.type !== "MemberExpression") {
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
return unwrapMethodChains$1(identifiers, callee.object);
|
|
1870
|
+
};
|
|
1871
|
+
const collectAllDefinitions$1 = ({ module: module$1, gqlIdentifiers, imports: _imports, exports: exports$1, source }) => {
|
|
1872
|
+
const getPropertyName$1 = (property) => {
|
|
1873
|
+
if (!property) {
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
if (property.type === "Identifier") {
|
|
1877
|
+
return property.value;
|
|
1878
|
+
}
|
|
1879
|
+
if (property.type === "StringLiteral" || property.type === "NumericLiteral") {
|
|
1880
|
+
return property.value;
|
|
1881
|
+
}
|
|
1882
|
+
return null;
|
|
1883
|
+
};
|
|
1884
|
+
const pending = [];
|
|
1885
|
+
const handledCalls = [];
|
|
1886
|
+
const exportBindings = createExportBindingsMap(exports$1);
|
|
1887
|
+
const tracker = (0, __soda_gql_common.createCanonicalTracker)({
|
|
1888
|
+
filePath: module$1.__filePath,
|
|
1889
|
+
getExportName: (localName) => exportBindings.get(localName)
|
|
1890
|
+
});
|
|
1891
|
+
const anonymousCounters = new Map();
|
|
1892
|
+
const getAnonymousName = (kind) => {
|
|
1893
|
+
const count = anonymousCounters.get(kind) ?? 0;
|
|
1894
|
+
anonymousCounters.set(kind, count + 1);
|
|
1895
|
+
return `_${kind}_${count}`;
|
|
1896
|
+
};
|
|
1897
|
+
const withScope = (stack, segment, kind, stableKey, callback) => {
|
|
1898
|
+
const handle = tracker.enterScope({
|
|
1899
|
+
segment,
|
|
1900
|
+
kind,
|
|
1901
|
+
stableKey
|
|
1902
|
+
});
|
|
1903
|
+
try {
|
|
1904
|
+
const frame = {
|
|
1905
|
+
nameSegment: segment,
|
|
1906
|
+
kind
|
|
1907
|
+
};
|
|
1908
|
+
return callback([...stack, frame]);
|
|
1909
|
+
} finally {
|
|
1910
|
+
tracker.exitScope(handle);
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
const expressionFromCall = (call) => {
|
|
1914
|
+
const spanOffset = module$1.__spanOffset;
|
|
1915
|
+
let start = call.span.start - spanOffset;
|
|
1916
|
+
const end = call.span.end - spanOffset;
|
|
1917
|
+
if (start > 0 && source[start] === "q" && source[start - 1] === "g" && source.slice(start, start + 3) === "ql.") {
|
|
1918
|
+
start -= 1;
|
|
1919
|
+
}
|
|
1920
|
+
const raw = source.slice(start, end);
|
|
1921
|
+
const marker = raw.indexOf("gql");
|
|
1922
|
+
const expression = marker >= 0 ? raw.slice(marker) : raw;
|
|
1923
|
+
return expression.replace(/\s*;\s*$/, "");
|
|
1924
|
+
};
|
|
1925
|
+
const isInClassProperty = (stack) => stack.some((frame, i) => frame.kind === "property" && stack[i - 1]?.kind === "class");
|
|
1926
|
+
const visit = (node, stack) => {
|
|
1927
|
+
if (!node || typeof node !== "object") {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (node.type === "CallExpression") {
|
|
1931
|
+
const gqlCall = unwrapMethodChains$1(gqlIdentifiers, node);
|
|
1932
|
+
if (gqlCall && !isInClassProperty(stack)) {
|
|
1933
|
+
const needsAnonymousScope = tracker.currentDepth() === 0;
|
|
1934
|
+
let anonymousScopeHandle;
|
|
1935
|
+
if (needsAnonymousScope) {
|
|
1936
|
+
const anonymousName = getAnonymousName("anonymous");
|
|
1937
|
+
anonymousScopeHandle = tracker.enterScope({
|
|
1938
|
+
segment: anonymousName,
|
|
1939
|
+
kind: "expression",
|
|
1940
|
+
stableKey: "anonymous"
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
try {
|
|
1944
|
+
const { astPath } = tracker.registerDefinition();
|
|
1945
|
+
const isTopLevel = stack.length === 1;
|
|
1946
|
+
let isExported = false;
|
|
1947
|
+
let exportBinding;
|
|
1948
|
+
if (isTopLevel && stack[0]) {
|
|
1949
|
+
const topLevelName = stack[0].nameSegment;
|
|
1950
|
+
if (exportBindings.has(topLevelName)) {
|
|
1951
|
+
isExported = true;
|
|
1952
|
+
exportBinding = exportBindings.get(topLevelName);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
handledCalls.push(node);
|
|
1956
|
+
pending.push({
|
|
1957
|
+
astPath,
|
|
1958
|
+
isTopLevel,
|
|
1959
|
+
isExported,
|
|
1960
|
+
exportBinding,
|
|
1961
|
+
expression: expressionFromCall(gqlCall)
|
|
1962
|
+
});
|
|
1963
|
+
} finally {
|
|
1964
|
+
if (anonymousScopeHandle) {
|
|
1965
|
+
tracker.exitScope(anonymousScopeHandle);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (node.type === "VariableDeclaration") {
|
|
1972
|
+
node.declarations?.forEach((decl) => {
|
|
1973
|
+
if (decl.id?.type === "Identifier") {
|
|
1974
|
+
const varName = decl.id.value;
|
|
1975
|
+
if (decl.init) {
|
|
1976
|
+
withScope(stack, varName, "variable", `var:${varName}`, (newStack) => {
|
|
1977
|
+
visit(decl.init, newStack);
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
} else if (decl.init) {
|
|
1981
|
+
visit(decl.init, stack);
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (node.type === "FunctionDeclaration") {
|
|
1987
|
+
const funcName = node.identifier?.value ?? getAnonymousName("function");
|
|
1988
|
+
if (node.body) {
|
|
1989
|
+
withScope(stack, funcName, "function", `func:${funcName}`, (newStack) => {
|
|
1990
|
+
visit(node.body, newStack);
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
if (node.type === "ArrowFunctionExpression") {
|
|
1996
|
+
const arrowName = getAnonymousName("arrow");
|
|
1997
|
+
if (node.body) {
|
|
1998
|
+
withScope(stack, arrowName, "function", "arrow", (newStack) => {
|
|
1999
|
+
visit(node.body, newStack);
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (node.type === "FunctionExpression") {
|
|
2005
|
+
const funcName = node.identifier?.value ?? getAnonymousName("function");
|
|
2006
|
+
if (node.body) {
|
|
2007
|
+
withScope(stack, funcName, "function", `func:${funcName}`, (newStack) => {
|
|
2008
|
+
visit(node.body, newStack);
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
if (node.type === "ClassDeclaration") {
|
|
2014
|
+
const className = node.identifier?.value ?? getAnonymousName("class");
|
|
2015
|
+
withScope(stack, className, "class", `class:${className}`, (classStack) => {
|
|
2016
|
+
node.body?.forEach((member) => {
|
|
2017
|
+
if (member.type === "ClassMethod" || member.type === "ClassProperty") {
|
|
2018
|
+
const memberName = member.key?.value ?? null;
|
|
2019
|
+
if (memberName) {
|
|
2020
|
+
const memberKind = member.type === "ClassMethod" ? "method" : "property";
|
|
2021
|
+
withScope(classStack, memberName, memberKind, `member:${className}.${memberName}`, (memberStack) => {
|
|
2022
|
+
if (member.type === "ClassMethod" && member.function?.body) {
|
|
2023
|
+
visit(member.function.body, memberStack);
|
|
2024
|
+
} else if (member.type === "ClassProperty" && member.value) {
|
|
2025
|
+
visit(member.value, memberStack);
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
if (node.type === "KeyValueProperty") {
|
|
2035
|
+
const propName = getPropertyName$1(node.key);
|
|
2036
|
+
if (propName) {
|
|
2037
|
+
withScope(stack, propName, "property", `prop:${propName}`, (newStack) => {
|
|
2038
|
+
visit(node.value, newStack);
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (Array.isArray(node)) {
|
|
2044
|
+
for (const child of node) {
|
|
2045
|
+
visit(child, stack);
|
|
2046
|
+
}
|
|
2047
|
+
} else {
|
|
2048
|
+
for (const value of Object.values(node)) {
|
|
2049
|
+
if (Array.isArray(value)) {
|
|
2050
|
+
for (const child of value) {
|
|
2051
|
+
visit(child, stack);
|
|
2052
|
+
}
|
|
2053
|
+
} else if (value && typeof value === "object") {
|
|
2054
|
+
visit(value, stack);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
module$1.body.forEach((statement) => {
|
|
2060
|
+
visit(statement, []);
|
|
2061
|
+
});
|
|
2062
|
+
const definitions = pending.map((item) => ({
|
|
2063
|
+
canonicalId: (0, __soda_gql_common.createCanonicalId)(module$1.__filePath, item.astPath),
|
|
2064
|
+
astPath: item.astPath,
|
|
2065
|
+
isTopLevel: item.isTopLevel,
|
|
2066
|
+
isExported: item.isExported,
|
|
2067
|
+
exportBinding: item.exportBinding,
|
|
2068
|
+
expression: item.expression
|
|
2069
|
+
}));
|
|
2070
|
+
return {
|
|
2071
|
+
definitions,
|
|
2072
|
+
handledCalls
|
|
2073
|
+
};
|
|
2074
|
+
};
|
|
2075
|
+
/**
|
|
2076
|
+
* Get location from an SWC node span
|
|
2077
|
+
*/
|
|
2078
|
+
const getLocation$1 = (module$1, span) => {
|
|
2079
|
+
const start = span.start - module$1.__spanOffset;
|
|
2080
|
+
const end = span.end - module$1.__spanOffset;
|
|
2081
|
+
return {
|
|
2082
|
+
start,
|
|
2083
|
+
end
|
|
2084
|
+
};
|
|
2085
|
+
};
|
|
2086
|
+
/**
|
|
2087
|
+
* Collect diagnostics for invalid import patterns from graphql-system
|
|
2088
|
+
*/
|
|
2089
|
+
const collectImportDiagnostics$1 = (module$1, helper) => {
|
|
2090
|
+
const diagnostics = [];
|
|
2091
|
+
module$1.body.forEach((item) => {
|
|
2092
|
+
const declaration = item.type === "ImportDeclaration" ? item : "declaration" in item && item.declaration && "type" in item.declaration && item.declaration.type === "ImportDeclaration" ? item.declaration : null;
|
|
2093
|
+
if (!declaration) {
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
if (!helper.isGraphqlSystemImportSpecifier({
|
|
2097
|
+
filePath: module$1.__filePath,
|
|
2098
|
+
specifier: declaration.source.value
|
|
2099
|
+
})) {
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
declaration.specifiers?.forEach((specifier) => {
|
|
2103
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
2104
|
+
diagnostics.push(createStandardDiagnostic("DEFAULT_IMPORT", getLocation$1(module$1, specifier.span), undefined));
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
2108
|
+
diagnostics.push(createStandardDiagnostic("STAR_IMPORT", getLocation$1(module$1, specifier.span), { namespaceAlias: specifier.local.value }));
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
if (specifier.type === "ImportSpecifier") {
|
|
2112
|
+
const imported = specifier.imported ? specifier.imported.value : specifier.local.value;
|
|
2113
|
+
if (imported === "gql" && specifier.imported) {
|
|
2114
|
+
diagnostics.push(createStandardDiagnostic("RENAMED_IMPORT", getLocation$1(module$1, specifier.span), { importedAs: specifier.local.value }));
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
});
|
|
2119
|
+
return diagnostics;
|
|
2120
|
+
};
|
|
2121
|
+
/**
|
|
2122
|
+
* Check if a node contains a reference to any gql identifier
|
|
2123
|
+
*/
|
|
2124
|
+
const containsGqlIdentifier$1 = (node, identifiers) => {
|
|
2125
|
+
if (!node || typeof node !== "object") {
|
|
2126
|
+
return false;
|
|
2127
|
+
}
|
|
2128
|
+
if (node.type === "Identifier" && identifiers.has(node.value)) {
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
for (const value of Object.values(node)) {
|
|
2132
|
+
if (Array.isArray(value)) {
|
|
2133
|
+
for (const child of value) {
|
|
2134
|
+
if (containsGqlIdentifier$1(child, identifiers)) {
|
|
2135
|
+
return true;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
} else if (value && typeof value === "object") {
|
|
2139
|
+
if (containsGqlIdentifier$1(value, identifiers)) {
|
|
2140
|
+
return true;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
return false;
|
|
2145
|
+
};
|
|
2146
|
+
/**
|
|
2147
|
+
* Get the type name of an argument for error messages
|
|
2148
|
+
*/
|
|
2149
|
+
const getArgumentType$1 = (node) => {
|
|
2150
|
+
if (!node) return "undefined";
|
|
2151
|
+
switch (node.type) {
|
|
2152
|
+
case "StringLiteral": return "string";
|
|
2153
|
+
case "NumericLiteral": return "number";
|
|
2154
|
+
case "ObjectExpression": return "object";
|
|
2155
|
+
case "ArrayExpression": return "array";
|
|
2156
|
+
case "FunctionExpression": return "function";
|
|
2157
|
+
case "NullLiteral": return "null";
|
|
2158
|
+
case "BooleanLiteral": return "boolean";
|
|
2159
|
+
default: return "unknown";
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
/**
|
|
2163
|
+
* Collect diagnostics for invalid gql call patterns
|
|
2164
|
+
*/
|
|
2165
|
+
const collectCallDiagnostics$1 = (module$1, gqlIdentifiers) => {
|
|
2166
|
+
const diagnostics = [];
|
|
2167
|
+
const visit = (node) => {
|
|
2168
|
+
if (!node || typeof node !== "object") {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
if (node.type === "CallExpression") {
|
|
2172
|
+
const diagnostic = checkCallExpression$1(module$1, node, gqlIdentifiers);
|
|
2173
|
+
if (diagnostic) {
|
|
2174
|
+
diagnostics.push(diagnostic);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
for (const value of Object.values(node)) {
|
|
2178
|
+
if (Array.isArray(value)) {
|
|
2179
|
+
for (const child of value) {
|
|
2180
|
+
visit(child);
|
|
2181
|
+
}
|
|
2182
|
+
} else if (value && typeof value === "object") {
|
|
2183
|
+
visit(value);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
module$1.body.forEach(visit);
|
|
2188
|
+
return diagnostics;
|
|
2189
|
+
};
|
|
2190
|
+
/**
|
|
2191
|
+
* Check a call expression for invalid gql patterns
|
|
2192
|
+
*/
|
|
2193
|
+
const checkCallExpression$1 = (module$1, call, gqlIdentifiers) => {
|
|
2194
|
+
const callee = call.callee;
|
|
2195
|
+
if (callee.type === "Identifier" && gqlIdentifiers.has(callee.value)) {
|
|
2196
|
+
return createStandardDiagnostic("NON_MEMBER_CALLEE", getLocation$1(module$1, call.span), undefined);
|
|
2197
|
+
}
|
|
2198
|
+
if (callee.type === "OptionalChainingExpression") {
|
|
2199
|
+
const base = callee.base;
|
|
2200
|
+
if (base?.type === "MemberExpression") {
|
|
2201
|
+
const object$1 = base.object;
|
|
2202
|
+
if (object$1?.type === "Identifier" && gqlIdentifiers.has(object$1.value)) {
|
|
2203
|
+
return createStandardDiagnostic("OPTIONAL_CHAINING", getLocation$1(module$1, call.span), undefined);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
if (callee.type !== "MemberExpression") {
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
2211
|
+
const object = callee.object;
|
|
2212
|
+
const property = callee.property;
|
|
2213
|
+
if (property?.type === "Computed" && object?.type === "Identifier" && gqlIdentifiers.has(object.value)) {
|
|
2214
|
+
return createStandardDiagnostic("COMPUTED_PROPERTY", getLocation$1(module$1, call.span), undefined);
|
|
2215
|
+
}
|
|
2216
|
+
if (object?.type !== "Identifier") {
|
|
2217
|
+
if (containsGqlIdentifier$1(object, gqlIdentifiers)) {
|
|
2218
|
+
return createStandardDiagnostic("DYNAMIC_CALLEE", getLocation$1(module$1, call.span), undefined);
|
|
2219
|
+
}
|
|
2220
|
+
return null;
|
|
2221
|
+
}
|
|
2222
|
+
if (!gqlIdentifiers.has(object.value)) {
|
|
2223
|
+
return null;
|
|
2224
|
+
}
|
|
2225
|
+
if (!call.arguments || call.arguments.length === 0) {
|
|
2226
|
+
return createStandardDiagnostic("MISSING_ARGUMENT", getLocation$1(module$1, call.span), undefined);
|
|
2227
|
+
}
|
|
2228
|
+
const firstArg = call.arguments[0];
|
|
2229
|
+
const firstArgAny = firstArg;
|
|
2230
|
+
if (firstArgAny?.spread) {
|
|
2231
|
+
return createStandardDiagnostic("SPREAD_ARGUMENT", getLocation$1(module$1, call.span), undefined);
|
|
2232
|
+
}
|
|
2233
|
+
const expression = firstArgAny?.expression;
|
|
2234
|
+
if (expression && expression.type !== "ArrowFunctionExpression") {
|
|
2235
|
+
const actualType = getArgumentType$1(expression);
|
|
2236
|
+
return createStandardDiagnostic("INVALID_ARGUMENT_TYPE", getLocation$1(module$1, call.span), { actualType });
|
|
2237
|
+
}
|
|
2238
|
+
if (call.arguments.length > 1) {
|
|
2239
|
+
const extraCount = call.arguments.length - 1;
|
|
2240
|
+
return createStandardDiagnostic("EXTRA_ARGUMENTS", getLocation$1(module$1, call.span), { extraCount: String(extraCount) });
|
|
2241
|
+
}
|
|
2242
|
+
return null;
|
|
2243
|
+
};
|
|
2244
|
+
/**
|
|
2245
|
+
* Collect diagnostics for gql calls in class properties
|
|
2246
|
+
*/
|
|
2247
|
+
const collectClassPropertyDiagnostics$1 = (module$1, gqlIdentifiers) => {
|
|
2248
|
+
const diagnostics = [];
|
|
2249
|
+
const containsGqlCall = (node) => {
|
|
2250
|
+
if (!node || typeof node !== "object") {
|
|
2251
|
+
return false;
|
|
2252
|
+
}
|
|
2253
|
+
if (node.type === "CallExpression" && isGqlCall(gqlIdentifiers, node)) {
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
for (const value of Object.values(node)) {
|
|
2257
|
+
if (Array.isArray(value)) {
|
|
2258
|
+
for (const child of value) {
|
|
2259
|
+
if (containsGqlCall(child)) {
|
|
2260
|
+
return true;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
} else if (value && typeof value === "object") {
|
|
2264
|
+
if (containsGqlCall(value)) {
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return false;
|
|
2270
|
+
};
|
|
2271
|
+
const visit = (node) => {
|
|
2272
|
+
if (!node || typeof node !== "object") {
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (node.type === "ClassDeclaration") {
|
|
2276
|
+
node.body?.forEach((member) => {
|
|
2277
|
+
if (member.type === "ClassProperty" && member.value) {
|
|
2278
|
+
if (containsGqlCall(member.value)) {
|
|
2279
|
+
diagnostics.push(createStandardDiagnostic("CLASS_PROPERTY", getLocation$1(module$1, member.span), undefined));
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
for (const value of Object.values(node)) {
|
|
2285
|
+
if (Array.isArray(value)) {
|
|
2286
|
+
for (const child of value) {
|
|
2287
|
+
visit(child);
|
|
2288
|
+
}
|
|
2289
|
+
} else if (value && typeof value === "object") {
|
|
2290
|
+
visit(value);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
module$1.body.forEach(visit);
|
|
2295
|
+
return diagnostics;
|
|
2296
|
+
};
|
|
2297
|
+
/**
|
|
2298
|
+
* SWC adapter implementation.
|
|
2299
|
+
* The analyze method parses and collects all data in one pass,
|
|
2300
|
+
* ensuring the AST (Module) is released after analysis.
|
|
2301
|
+
*/
|
|
2302
|
+
const swcAdapter = { analyze(input, helper) {
|
|
2303
|
+
const program = (0, __swc_core.parseSync)(input.source, {
|
|
2304
|
+
syntax: "typescript",
|
|
2305
|
+
tsx: input.filePath.endsWith(".tsx"),
|
|
2306
|
+
target: "es2022",
|
|
2307
|
+
decorators: false,
|
|
2308
|
+
dynamicImport: true
|
|
2309
|
+
});
|
|
2310
|
+
if (program.type !== "Module") {
|
|
2311
|
+
return null;
|
|
2312
|
+
}
|
|
2313
|
+
const spanOffset = program.span.end - input.source.length + 1;
|
|
2314
|
+
const swcModule = program;
|
|
2315
|
+
swcModule.__filePath = input.filePath;
|
|
2316
|
+
swcModule.__spanOffset = spanOffset;
|
|
2317
|
+
const gqlIdentifiers = collectGqlIdentifiers(swcModule, helper);
|
|
2318
|
+
const imports = collectImports$1(swcModule);
|
|
2319
|
+
const exports$1 = collectExports$1(swcModule);
|
|
2320
|
+
const { definitions } = collectAllDefinitions$1({
|
|
2321
|
+
module: swcModule,
|
|
2322
|
+
gqlIdentifiers,
|
|
2323
|
+
imports,
|
|
2324
|
+
exports: exports$1,
|
|
2325
|
+
source: input.source
|
|
2326
|
+
});
|
|
2327
|
+
const diagnostics = [
|
|
2328
|
+
...collectImportDiagnostics$1(swcModule, helper),
|
|
2329
|
+
...collectCallDiagnostics$1(swcModule, gqlIdentifiers),
|
|
2330
|
+
...collectClassPropertyDiagnostics$1(swcModule, gqlIdentifiers)
|
|
2331
|
+
];
|
|
2332
|
+
return {
|
|
2333
|
+
imports,
|
|
2334
|
+
exports: exports$1,
|
|
2335
|
+
definitions,
|
|
2336
|
+
diagnostics
|
|
2337
|
+
};
|
|
2338
|
+
} };
|
|
2339
|
+
|
|
2340
|
+
//#endregion
|
|
2341
|
+
//#region packages/builder/src/ast/adapters/typescript.ts
|
|
2342
|
+
/**
|
|
2343
|
+
* TypeScript adapter for the analyzer core.
|
|
2344
|
+
* Implements parser-specific logic using the TypeScript compiler API.
|
|
2345
|
+
*/
|
|
2346
|
+
const createSourceFile = (filePath, source) => {
|
|
2347
|
+
const scriptKind = (0, node_path.extname)(filePath) === ".tsx" ? typescript.default.ScriptKind.TSX : typescript.default.ScriptKind.TS;
|
|
2348
|
+
return typescript.default.createSourceFile(filePath, source, typescript.default.ScriptTarget.ES2022, true, scriptKind);
|
|
2349
|
+
};
|
|
2350
|
+
const collectGqlImports = (sourceFile, helper) => {
|
|
2351
|
+
const identifiers = new Set();
|
|
2352
|
+
sourceFile.statements.forEach((statement) => {
|
|
2353
|
+
if (!typescript.default.isImportDeclaration(statement) || !statement.importClause) {
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
const moduleText = statement.moduleSpecifier.text;
|
|
2357
|
+
if (!helper.isGraphqlSystemImportSpecifier({
|
|
2358
|
+
filePath: sourceFile.fileName,
|
|
2359
|
+
specifier: moduleText
|
|
2360
|
+
})) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
if (statement.importClause.namedBindings && typescript.default.isNamedImports(statement.importClause.namedBindings)) {
|
|
2364
|
+
statement.importClause.namedBindings.elements.forEach((element) => {
|
|
2365
|
+
const imported = element.propertyName ? element.propertyName.text : element.name.text;
|
|
2366
|
+
if (imported === "gql" && !element.propertyName) {
|
|
2367
|
+
identifiers.add(element.name.text);
|
|
2368
|
+
}
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
});
|
|
2372
|
+
return identifiers;
|
|
2373
|
+
};
|
|
2374
|
+
const collectImports = (sourceFile) => {
|
|
2375
|
+
const imports = [];
|
|
2376
|
+
sourceFile.statements.forEach((statement) => {
|
|
2377
|
+
if (!typescript.default.isImportDeclaration(statement) || !statement.importClause) {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
const moduleText = statement.moduleSpecifier.text;
|
|
2381
|
+
const { importClause } = statement;
|
|
2382
|
+
if (importClause.name) {
|
|
2383
|
+
imports.push({
|
|
2384
|
+
source: moduleText,
|
|
2385
|
+
local: importClause.name.text,
|
|
2386
|
+
kind: "default",
|
|
2387
|
+
isTypeOnly: Boolean(importClause.isTypeOnly)
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
const { namedBindings } = importClause;
|
|
2391
|
+
if (!namedBindings) {
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (typescript.default.isNamespaceImport(namedBindings)) {
|
|
2395
|
+
imports.push({
|
|
2396
|
+
source: moduleText,
|
|
2397
|
+
local: namedBindings.name.text,
|
|
2398
|
+
kind: "namespace",
|
|
2399
|
+
isTypeOnly: Boolean(importClause.isTypeOnly)
|
|
2400
|
+
});
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
namedBindings.elements.forEach((element) => {
|
|
2404
|
+
imports.push({
|
|
2405
|
+
source: moduleText,
|
|
2406
|
+
local: element.name.text,
|
|
2407
|
+
kind: "named",
|
|
2408
|
+
isTypeOnly: Boolean(importClause.isTypeOnly || element.isTypeOnly)
|
|
2409
|
+
});
|
|
2410
|
+
});
|
|
2411
|
+
});
|
|
2412
|
+
return imports;
|
|
2413
|
+
};
|
|
2414
|
+
const collectExports = (sourceFile) => {
|
|
2415
|
+
const exports$1 = [];
|
|
2416
|
+
sourceFile.statements.forEach((statement) => {
|
|
2417
|
+
if (typescript.default.isExportDeclaration(statement)) {
|
|
2418
|
+
const moduleSpecifier = statement.moduleSpecifier ? statement.moduleSpecifier.text : undefined;
|
|
2419
|
+
if (statement.exportClause && typescript.default.isNamedExports(statement.exportClause)) {
|
|
2420
|
+
statement.exportClause.elements.forEach((element) => {
|
|
2421
|
+
if (moduleSpecifier) {
|
|
2422
|
+
exports$1.push({
|
|
2423
|
+
kind: "reexport",
|
|
2424
|
+
exported: element.name.text,
|
|
2425
|
+
local: element.propertyName ? element.propertyName.text : undefined,
|
|
2426
|
+
source: moduleSpecifier,
|
|
2427
|
+
isTypeOnly: Boolean(statement.isTypeOnly || element.isTypeOnly)
|
|
2428
|
+
});
|
|
2429
|
+
} else {
|
|
2430
|
+
exports$1.push({
|
|
2431
|
+
kind: "named",
|
|
2432
|
+
exported: element.name.text,
|
|
2433
|
+
local: element.propertyName ? element.propertyName.text : element.name.text,
|
|
2434
|
+
isTypeOnly: Boolean(statement.isTypeOnly || element.isTypeOnly)
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
if (moduleSpecifier) {
|
|
2441
|
+
exports$1.push({
|
|
2442
|
+
kind: "reexport",
|
|
2443
|
+
exported: "*",
|
|
2444
|
+
source: moduleSpecifier,
|
|
2445
|
+
isTypeOnly: Boolean(statement.isTypeOnly)
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
if (typescript.default.isExportAssignment(statement)) {
|
|
2451
|
+
exports$1.push({
|
|
2452
|
+
kind: "named",
|
|
2453
|
+
exported: "default",
|
|
2454
|
+
local: "default",
|
|
2455
|
+
isTypeOnly: false
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
if (typescript.default.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === typescript.default.SyntaxKind.ExportKeyword)) {
|
|
2459
|
+
statement.declarationList.declarations.forEach((declaration) => {
|
|
2460
|
+
if (typescript.default.isIdentifier(declaration.name)) {
|
|
2461
|
+
exports$1.push({
|
|
2462
|
+
kind: "named",
|
|
2463
|
+
exported: declaration.name.text,
|
|
2464
|
+
local: declaration.name.text,
|
|
2465
|
+
isTypeOnly: false
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
if (typescript.default.isFunctionDeclaration(statement) && statement.modifiers?.some((modifier) => modifier.kind === typescript.default.SyntaxKind.ExportKeyword) && statement.name) {
|
|
2471
|
+
exports$1.push({
|
|
2472
|
+
kind: "named",
|
|
2473
|
+
exported: statement.name.text,
|
|
2474
|
+
local: statement.name.text,
|
|
2475
|
+
isTypeOnly: false
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
return exports$1;
|
|
2480
|
+
};
|
|
2481
|
+
/**
|
|
2482
|
+
* Unwrap NonNullExpression nodes to get the underlying expression.
|
|
2483
|
+
* Handles cases like `gql!` or `gql!!` by recursively unwrapping.
|
|
2484
|
+
*/
|
|
2485
|
+
const unwrapNonNullExpression = (node) => {
|
|
2486
|
+
if (typescript.default.isNonNullExpression(node)) {
|
|
2487
|
+
return unwrapNonNullExpression(node.expression);
|
|
2488
|
+
}
|
|
2489
|
+
return node;
|
|
2490
|
+
};
|
|
2491
|
+
const isGqlDefinitionCall = (identifiers, callExpression) => {
|
|
2492
|
+
const expression = callExpression.expression;
|
|
2493
|
+
if (!typescript.default.isPropertyAccessExpression(expression)) {
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2496
|
+
const baseExpr = unwrapNonNullExpression(expression.expression);
|
|
2497
|
+
if (!typescript.default.isIdentifier(baseExpr) || !identifiers.has(baseExpr.text)) {
|
|
2498
|
+
return false;
|
|
2499
|
+
}
|
|
2500
|
+
const [factory] = callExpression.arguments;
|
|
2501
|
+
if (!factory || !typescript.default.isArrowFunction(factory)) {
|
|
2502
|
+
return false;
|
|
2503
|
+
}
|
|
2504
|
+
return true;
|
|
2505
|
+
};
|
|
2506
|
+
/**
|
|
2507
|
+
* Unwrap method chains (like .attach()) to find the underlying gql call.
|
|
2508
|
+
* Returns the innermost CallExpression that is a valid gql definition call.
|
|
2509
|
+
*/
|
|
2510
|
+
const unwrapMethodChains = (identifiers, node) => {
|
|
2511
|
+
if (!typescript.default.isCallExpression(node)) {
|
|
2512
|
+
return null;
|
|
2513
|
+
}
|
|
2514
|
+
if (isGqlDefinitionCall(identifiers, node)) {
|
|
2515
|
+
return node;
|
|
2516
|
+
}
|
|
2517
|
+
const expression = node.expression;
|
|
2518
|
+
if (!typescript.default.isPropertyAccessExpression(expression)) {
|
|
2519
|
+
return null;
|
|
2520
|
+
}
|
|
2521
|
+
return unwrapMethodChains(identifiers, expression.expression);
|
|
2522
|
+
};
|
|
2523
|
+
/**
|
|
2524
|
+
* Get property name from AST node
|
|
2525
|
+
*/
|
|
2526
|
+
const getPropertyName = (name) => {
|
|
2527
|
+
if (typescript.default.isIdentifier(name)) {
|
|
2528
|
+
return name.text;
|
|
2529
|
+
}
|
|
2530
|
+
if (typescript.default.isStringLiteral(name) || typescript.default.isNumericLiteral(name)) {
|
|
2531
|
+
return name.text;
|
|
2532
|
+
}
|
|
2533
|
+
return null;
|
|
2534
|
+
};
|
|
2535
|
+
/**
|
|
2536
|
+
* Collect all gql definitions (exported, non-exported, top-level, nested)
|
|
2537
|
+
*/
|
|
2538
|
+
const collectAllDefinitions = ({ sourceFile, identifiers, exports: exports$1 }) => {
|
|
2539
|
+
const pending = [];
|
|
2540
|
+
const handledCalls = [];
|
|
2541
|
+
const exportBindings = createExportBindingsMap(exports$1);
|
|
2542
|
+
const tracker = (0, __soda_gql_common.createCanonicalTracker)({
|
|
2543
|
+
filePath: sourceFile.fileName,
|
|
2544
|
+
getExportName: (localName) => exportBindings.get(localName)
|
|
2545
|
+
});
|
|
2546
|
+
const anonymousCounters = new Map();
|
|
2547
|
+
const getAnonymousName = (kind) => {
|
|
2548
|
+
const count = anonymousCounters.get(kind) ?? 0;
|
|
2549
|
+
anonymousCounters.set(kind, count + 1);
|
|
2550
|
+
return `_${kind}_${count}`;
|
|
2551
|
+
};
|
|
2552
|
+
const withScope = (stack, segment, kind, stableKey, callback) => {
|
|
2553
|
+
const handle = tracker.enterScope({
|
|
2554
|
+
segment,
|
|
2555
|
+
kind,
|
|
2556
|
+
stableKey
|
|
2557
|
+
});
|
|
2558
|
+
try {
|
|
2559
|
+
const frame = {
|
|
2560
|
+
nameSegment: segment,
|
|
2561
|
+
kind
|
|
2562
|
+
};
|
|
2563
|
+
return callback([...stack, frame]);
|
|
2564
|
+
} finally {
|
|
2565
|
+
tracker.exitScope(handle);
|
|
2566
|
+
}
|
|
2567
|
+
};
|
|
2568
|
+
const isInClassProperty = (stack) => stack.some((frame, i) => frame.kind === "property" && stack[i - 1]?.kind === "class");
|
|
2569
|
+
const visit = (node, stack) => {
|
|
2570
|
+
if (typescript.default.isCallExpression(node)) {
|
|
2571
|
+
const gqlCall = unwrapMethodChains(identifiers, node);
|
|
2572
|
+
if (gqlCall && !isInClassProperty(stack)) {
|
|
2573
|
+
const needsAnonymousScope = tracker.currentDepth() === 0;
|
|
2574
|
+
let anonymousScopeHandle;
|
|
2575
|
+
if (needsAnonymousScope) {
|
|
2576
|
+
const anonymousName = getAnonymousName("anonymous");
|
|
2577
|
+
anonymousScopeHandle = tracker.enterScope({
|
|
2578
|
+
segment: anonymousName,
|
|
2579
|
+
kind: "expression",
|
|
2580
|
+
stableKey: "anonymous"
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
try {
|
|
2584
|
+
const { astPath } = tracker.registerDefinition();
|
|
2585
|
+
const isTopLevel = stack.length === 1;
|
|
2586
|
+
let isExported = false;
|
|
2587
|
+
let exportBinding;
|
|
2588
|
+
if (isTopLevel && stack[0]) {
|
|
2589
|
+
const topLevelName = stack[0].nameSegment;
|
|
2590
|
+
if (exportBindings.has(topLevelName)) {
|
|
2591
|
+
isExported = true;
|
|
2592
|
+
exportBinding = exportBindings.get(topLevelName);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
handledCalls.push(node);
|
|
2596
|
+
pending.push({
|
|
2597
|
+
astPath,
|
|
2598
|
+
isTopLevel,
|
|
2599
|
+
isExported,
|
|
2600
|
+
exportBinding,
|
|
2601
|
+
expression: gqlCall.getText(sourceFile)
|
|
2602
|
+
});
|
|
2603
|
+
} finally {
|
|
2604
|
+
if (anonymousScopeHandle) {
|
|
2605
|
+
tracker.exitScope(anonymousScopeHandle);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (typescript.default.isVariableDeclaration(node) && node.name && typescript.default.isIdentifier(node.name)) {
|
|
2612
|
+
const varName = node.name.text;
|
|
2613
|
+
if (node.initializer) {
|
|
2614
|
+
const next = node.initializer;
|
|
2615
|
+
withScope(stack, varName, "variable", `var:${varName}`, (newStack) => {
|
|
2616
|
+
visit(next, newStack);
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (typescript.default.isFunctionDeclaration(node)) {
|
|
2622
|
+
const funcName = node.name?.text ?? getAnonymousName("function");
|
|
2623
|
+
if (node.body) {
|
|
2624
|
+
const next = node.body;
|
|
2625
|
+
withScope(stack, funcName, "function", `func:${funcName}`, (newStack) => {
|
|
2626
|
+
typescript.default.forEachChild(next, (child) => visit(child, newStack));
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
if (typescript.default.isArrowFunction(node)) {
|
|
2632
|
+
const arrowName = getAnonymousName("arrow");
|
|
2633
|
+
withScope(stack, arrowName, "function", "arrow", (newStack) => {
|
|
2634
|
+
if (typescript.default.isBlock(node.body)) {
|
|
2635
|
+
typescript.default.forEachChild(node.body, (child) => visit(child, newStack));
|
|
2636
|
+
} else {
|
|
2637
|
+
visit(node.body, newStack);
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (typescript.default.isFunctionExpression(node)) {
|
|
2643
|
+
const funcName = node.name?.text ?? getAnonymousName("function");
|
|
2644
|
+
if (node.body) {
|
|
2645
|
+
withScope(stack, funcName, "function", `func:${funcName}`, (newStack) => {
|
|
2646
|
+
typescript.default.forEachChild(node.body, (child) => visit(child, newStack));
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
if (typescript.default.isClassDeclaration(node)) {
|
|
2652
|
+
const className = node.name?.text ?? getAnonymousName("class");
|
|
2653
|
+
withScope(stack, className, "class", `class:${className}`, (classStack) => {
|
|
2654
|
+
node.members.forEach((member) => {
|
|
2655
|
+
if (typescript.default.isMethodDeclaration(member) || typescript.default.isPropertyDeclaration(member)) {
|
|
2656
|
+
const memberName = member.name && typescript.default.isIdentifier(member.name) ? member.name.text : null;
|
|
2657
|
+
if (memberName) {
|
|
2658
|
+
const memberKind = typescript.default.isMethodDeclaration(member) ? "method" : "property";
|
|
2659
|
+
withScope(classStack, memberName, memberKind, `member:${className}.${memberName}`, (memberStack) => {
|
|
2660
|
+
if (typescript.default.isMethodDeclaration(member) && member.body) {
|
|
2661
|
+
typescript.default.forEachChild(member.body, (child) => visit(child, memberStack));
|
|
2662
|
+
} else if (typescript.default.isPropertyDeclaration(member) && member.initializer) {
|
|
2663
|
+
visit(member.initializer, memberStack);
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
});
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
if (typescript.default.isPropertyAssignment(node)) {
|
|
2673
|
+
const propName = getPropertyName(node.name);
|
|
2674
|
+
if (propName) {
|
|
2675
|
+
withScope(stack, propName, "property", `prop:${propName}`, (newStack) => {
|
|
2676
|
+
visit(node.initializer, newStack);
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
typescript.default.forEachChild(node, (child) => visit(child, stack));
|
|
2682
|
+
};
|
|
2683
|
+
sourceFile.statements.forEach((statement) => {
|
|
2684
|
+
visit(statement, []);
|
|
2685
|
+
});
|
|
2686
|
+
const definitions = pending.map((item) => ({
|
|
2687
|
+
canonicalId: (0, __soda_gql_common.createCanonicalId)(sourceFile.fileName, item.astPath),
|
|
2688
|
+
astPath: item.astPath,
|
|
2689
|
+
isTopLevel: item.isTopLevel,
|
|
2690
|
+
isExported: item.isExported,
|
|
2691
|
+
exportBinding: item.exportBinding,
|
|
2692
|
+
expression: item.expression
|
|
2693
|
+
}));
|
|
2694
|
+
return {
|
|
2695
|
+
definitions,
|
|
2696
|
+
handledCalls
|
|
2697
|
+
};
|
|
2698
|
+
};
|
|
2699
|
+
/**
|
|
2700
|
+
* Get location from a TypeScript node
|
|
2701
|
+
*/
|
|
2702
|
+
const getLocation = (sourceFile, node) => {
|
|
2703
|
+
const start = node.getStart(sourceFile);
|
|
2704
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
|
|
2705
|
+
return {
|
|
2706
|
+
start,
|
|
2707
|
+
end: node.getEnd(),
|
|
2708
|
+
line: line + 1,
|
|
2709
|
+
column: character + 1
|
|
2710
|
+
};
|
|
2711
|
+
};
|
|
2712
|
+
/**
|
|
2713
|
+
* Collect diagnostics for invalid import patterns from graphql-system
|
|
2714
|
+
*/
|
|
2715
|
+
const collectImportDiagnostics = (sourceFile, helper) => {
|
|
2716
|
+
const diagnostics = [];
|
|
2717
|
+
sourceFile.statements.forEach((statement) => {
|
|
2718
|
+
if (!typescript.default.isImportDeclaration(statement) || !statement.importClause) {
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
const moduleText = statement.moduleSpecifier.text;
|
|
2722
|
+
if (!helper.isGraphqlSystemImportSpecifier({
|
|
2723
|
+
filePath: sourceFile.fileName,
|
|
2724
|
+
specifier: moduleText
|
|
2725
|
+
})) {
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
const { importClause } = statement;
|
|
2729
|
+
if (importClause.name) {
|
|
2730
|
+
diagnostics.push(createStandardDiagnostic("DEFAULT_IMPORT", getLocation(sourceFile, importClause.name), undefined));
|
|
2731
|
+
}
|
|
2732
|
+
const { namedBindings } = importClause;
|
|
2733
|
+
if (!namedBindings) {
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
if (typescript.default.isNamespaceImport(namedBindings)) {
|
|
2737
|
+
diagnostics.push(createStandardDiagnostic("STAR_IMPORT", getLocation(sourceFile, namedBindings), { namespaceAlias: namedBindings.name.text }));
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
namedBindings.elements.forEach((element) => {
|
|
2741
|
+
const imported = element.propertyName ? element.propertyName.text : element.name.text;
|
|
2742
|
+
if (imported === "gql" && element.propertyName) {
|
|
2743
|
+
diagnostics.push(createStandardDiagnostic("RENAMED_IMPORT", getLocation(sourceFile, element), { importedAs: element.name.text }));
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
});
|
|
2747
|
+
return diagnostics;
|
|
2748
|
+
};
|
|
2749
|
+
/**
|
|
2750
|
+
* Check if a node contains a reference to any gql identifier
|
|
2751
|
+
*/
|
|
2752
|
+
const containsGqlIdentifier = (node, identifiers) => {
|
|
2753
|
+
if (typescript.default.isIdentifier(node) && identifiers.has(node.text)) {
|
|
2754
|
+
return true;
|
|
2755
|
+
}
|
|
2756
|
+
let found = false;
|
|
2757
|
+
typescript.default.forEachChild(node, (child) => {
|
|
2758
|
+
if (containsGqlIdentifier(child, identifiers)) {
|
|
2759
|
+
found = true;
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
return found;
|
|
2763
|
+
};
|
|
2764
|
+
/**
|
|
2765
|
+
* Get the type name of an argument for error messages
|
|
2766
|
+
*/
|
|
2767
|
+
const getArgumentType = (node) => {
|
|
2768
|
+
if (typescript.default.isStringLiteral(node)) return "string";
|
|
2769
|
+
if (typescript.default.isNumericLiteral(node)) return "number";
|
|
2770
|
+
if (typescript.default.isObjectLiteralExpression(node)) return "object";
|
|
2771
|
+
if (typescript.default.isArrayLiteralExpression(node)) return "array";
|
|
2772
|
+
if (typescript.default.isFunctionExpression(node)) return "function";
|
|
2773
|
+
if (node.kind === typescript.default.SyntaxKind.NullKeyword) return "null";
|
|
2774
|
+
if (node.kind === typescript.default.SyntaxKind.UndefinedKeyword) return "undefined";
|
|
2775
|
+
if (node.kind === typescript.default.SyntaxKind.TrueKeyword || node.kind === typescript.default.SyntaxKind.FalseKeyword) return "boolean";
|
|
2776
|
+
return "unknown";
|
|
2777
|
+
};
|
|
2778
|
+
/**
|
|
2779
|
+
* Collect diagnostics for invalid gql call patterns
|
|
2780
|
+
*/
|
|
2781
|
+
const collectCallDiagnostics = (sourceFile, gqlIdentifiers) => {
|
|
2782
|
+
const diagnostics = [];
|
|
2783
|
+
const visit = (node) => {
|
|
2784
|
+
if (typescript.default.isCallExpression(node)) {
|
|
2785
|
+
const diagnostic = checkCallExpression(sourceFile, node, gqlIdentifiers);
|
|
2786
|
+
if (diagnostic) {
|
|
2787
|
+
diagnostics.push(diagnostic);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
typescript.default.forEachChild(node, visit);
|
|
2791
|
+
};
|
|
2792
|
+
sourceFile.statements.forEach(visit);
|
|
2793
|
+
return diagnostics;
|
|
2794
|
+
};
|
|
2795
|
+
/**
|
|
2796
|
+
* Check a call expression for invalid gql patterns
|
|
2797
|
+
*/
|
|
2798
|
+
const checkCallExpression = (sourceFile, call, gqlIdentifiers) => {
|
|
2799
|
+
const { expression } = call;
|
|
2800
|
+
if (typescript.default.isIdentifier(expression) && gqlIdentifiers.has(expression.text)) {
|
|
2801
|
+
return createStandardDiagnostic("NON_MEMBER_CALLEE", getLocation(sourceFile, call), undefined);
|
|
2802
|
+
}
|
|
2803
|
+
if (typescript.default.isElementAccessExpression(expression)) {
|
|
2804
|
+
const baseExpr$1 = unwrapNonNullExpression(expression.expression);
|
|
2805
|
+
if (typescript.default.isIdentifier(baseExpr$1) && gqlIdentifiers.has(baseExpr$1.text)) {
|
|
2806
|
+
return createStandardDiagnostic("COMPUTED_PROPERTY", getLocation(sourceFile, call), undefined);
|
|
2807
|
+
}
|
|
2808
|
+
return null;
|
|
2809
|
+
}
|
|
2810
|
+
if (!typescript.default.isPropertyAccessExpression(expression)) {
|
|
2811
|
+
return null;
|
|
2812
|
+
}
|
|
2813
|
+
const baseExpr = unwrapNonNullExpression(expression.expression);
|
|
2814
|
+
if (expression.questionDotToken) {
|
|
2815
|
+
if (typescript.default.isIdentifier(baseExpr) && gqlIdentifiers.has(baseExpr.text)) {
|
|
2816
|
+
return createStandardDiagnostic("OPTIONAL_CHAINING", getLocation(sourceFile, call), undefined);
|
|
2817
|
+
}
|
|
2818
|
+
return null;
|
|
2819
|
+
}
|
|
2820
|
+
if (!typescript.default.isIdentifier(baseExpr)) {
|
|
2821
|
+
if (containsGqlIdentifier(expression.expression, gqlIdentifiers)) {
|
|
2822
|
+
return createStandardDiagnostic("DYNAMIC_CALLEE", getLocation(sourceFile, call), undefined);
|
|
2823
|
+
}
|
|
2824
|
+
return null;
|
|
2825
|
+
}
|
|
2826
|
+
if (!gqlIdentifiers.has(baseExpr.text)) {
|
|
2827
|
+
return null;
|
|
2828
|
+
}
|
|
2829
|
+
if (call.arguments.length === 0) {
|
|
2830
|
+
return createStandardDiagnostic("MISSING_ARGUMENT", getLocation(sourceFile, call), undefined);
|
|
2831
|
+
}
|
|
2832
|
+
const firstArg = call.arguments[0];
|
|
2833
|
+
if (firstArg && typescript.default.isSpreadElement(firstArg)) {
|
|
2834
|
+
return createStandardDiagnostic("SPREAD_ARGUMENT", getLocation(sourceFile, call), undefined);
|
|
2835
|
+
}
|
|
2836
|
+
if (firstArg && !typescript.default.isArrowFunction(firstArg)) {
|
|
2837
|
+
const actualType = getArgumentType(firstArg);
|
|
2838
|
+
return createStandardDiagnostic("INVALID_ARGUMENT_TYPE", getLocation(sourceFile, call), { actualType });
|
|
2839
|
+
}
|
|
2840
|
+
if (call.arguments.length > 1) {
|
|
2841
|
+
const extraCount = call.arguments.length - 1;
|
|
2842
|
+
return createStandardDiagnostic("EXTRA_ARGUMENTS", getLocation(sourceFile, call), { extraCount: String(extraCount) });
|
|
2843
|
+
}
|
|
2844
|
+
return null;
|
|
2845
|
+
};
|
|
2846
|
+
/**
|
|
2847
|
+
* Collect diagnostics for gql calls in class properties
|
|
2848
|
+
*/
|
|
2849
|
+
const collectClassPropertyDiagnostics = (sourceFile, gqlIdentifiers) => {
|
|
2850
|
+
const diagnostics = [];
|
|
2851
|
+
const containsGqlCall = (node) => {
|
|
2852
|
+
if (typescript.default.isCallExpression(node) && isGqlDefinitionCall(gqlIdentifiers, node)) {
|
|
2853
|
+
return true;
|
|
2854
|
+
}
|
|
2855
|
+
let found = false;
|
|
2856
|
+
typescript.default.forEachChild(node, (child) => {
|
|
2857
|
+
if (containsGqlCall(child)) {
|
|
2858
|
+
found = true;
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2861
|
+
return found;
|
|
2862
|
+
};
|
|
2863
|
+
const visit = (node) => {
|
|
2864
|
+
if (typescript.default.isClassDeclaration(node)) {
|
|
2865
|
+
node.members.forEach((member) => {
|
|
2866
|
+
if (typescript.default.isPropertyDeclaration(member) && member.initializer) {
|
|
2867
|
+
if (containsGqlCall(member.initializer)) {
|
|
2868
|
+
diagnostics.push(createStandardDiagnostic("CLASS_PROPERTY", getLocation(sourceFile, member), undefined));
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
typescript.default.forEachChild(node, visit);
|
|
2874
|
+
};
|
|
2875
|
+
sourceFile.statements.forEach(visit);
|
|
2876
|
+
return diagnostics;
|
|
2877
|
+
};
|
|
2878
|
+
/**
|
|
2879
|
+
* TypeScript adapter implementation.
|
|
2880
|
+
* The analyze method parses and collects all data in one pass,
|
|
2881
|
+
* ensuring the AST (ts.SourceFile) is released after analysis.
|
|
2882
|
+
*/
|
|
2883
|
+
const typescriptAdapter = { analyze(input, helper) {
|
|
2884
|
+
const sourceFile = createSourceFile(input.filePath, input.source);
|
|
2885
|
+
const gqlIdentifiers = collectGqlImports(sourceFile, helper);
|
|
2886
|
+
const imports = collectImports(sourceFile);
|
|
2887
|
+
const exports$1 = collectExports(sourceFile);
|
|
2888
|
+
const { definitions } = collectAllDefinitions({
|
|
2889
|
+
sourceFile,
|
|
2890
|
+
identifiers: gqlIdentifiers,
|
|
2891
|
+
exports: exports$1
|
|
2892
|
+
});
|
|
2893
|
+
const diagnostics = [
|
|
2894
|
+
...collectImportDiagnostics(sourceFile, helper),
|
|
2895
|
+
...collectCallDiagnostics(sourceFile, gqlIdentifiers),
|
|
2896
|
+
...collectClassPropertyDiagnostics(sourceFile, gqlIdentifiers)
|
|
2897
|
+
];
|
|
2898
|
+
return {
|
|
2899
|
+
imports,
|
|
2900
|
+
exports: exports$1,
|
|
2901
|
+
definitions,
|
|
2902
|
+
diagnostics
|
|
2903
|
+
};
|
|
2904
|
+
} };
|
|
2905
|
+
|
|
2906
|
+
//#endregion
|
|
2907
|
+
//#region packages/builder/src/ast/core.ts
|
|
2908
|
+
/**
|
|
2909
|
+
* Core analyzer logic that orchestrates the analysis pipeline.
|
|
2910
|
+
* Adapters (TypeScript, SWC, etc.) implement the adapter interface to plug into this pipeline.
|
|
2911
|
+
*/
|
|
2912
|
+
/**
|
|
2913
|
+
* Core analyzer function that orchestrates the analysis pipeline.
|
|
2914
|
+
* Adapters implement the AnalyzerAdapter interface to provide parser-specific logic.
|
|
2915
|
+
*/
|
|
2916
|
+
const analyzeModuleCore = (input, adapter, graphqlHelper) => {
|
|
2917
|
+
const hasher = (0, __soda_gql_common.getPortableHasher)();
|
|
2918
|
+
const signature = hasher.hash(input.source, "xxhash");
|
|
2919
|
+
const result = adapter.analyze(input, graphqlHelper);
|
|
2920
|
+
if (!result) {
|
|
2921
|
+
return {
|
|
2922
|
+
filePath: input.filePath,
|
|
2923
|
+
signature,
|
|
2924
|
+
definitions: [],
|
|
2925
|
+
imports: [],
|
|
2926
|
+
exports: [],
|
|
2927
|
+
diagnostics: []
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
return {
|
|
2931
|
+
filePath: input.filePath,
|
|
2932
|
+
signature,
|
|
2933
|
+
definitions: result.definitions,
|
|
2934
|
+
imports: result.imports,
|
|
2935
|
+
exports: result.exports,
|
|
2936
|
+
diagnostics: result.diagnostics
|
|
2937
|
+
};
|
|
2938
|
+
};
|
|
2939
|
+
|
|
2940
|
+
//#endregion
|
|
2941
|
+
//#region packages/builder/src/ast/index.ts
|
|
2942
|
+
const createAstAnalyzer = ({ analyzer, graphqlHelper }) => {
|
|
2943
|
+
const analyze = (input) => {
|
|
2944
|
+
if (analyzer === "ts") {
|
|
2945
|
+
return analyzeModuleCore(input, typescriptAdapter, graphqlHelper);
|
|
2946
|
+
}
|
|
2947
|
+
if (analyzer === "swc") {
|
|
2948
|
+
return analyzeModuleCore(input, swcAdapter, graphqlHelper);
|
|
2949
|
+
}
|
|
2950
|
+
return assertUnreachable(analyzer, "createAstAnalyzer");
|
|
2951
|
+
};
|
|
2952
|
+
return {
|
|
2953
|
+
type: analyzer,
|
|
2954
|
+
analyze
|
|
2955
|
+
};
|
|
2956
|
+
};
|
|
2957
|
+
|
|
2958
|
+
//#endregion
|
|
2959
|
+
//#region packages/builder/src/cache/memory-cache.ts
|
|
2960
|
+
const sanitizeSegment = (segment) => segment.replace(/[\\/]/g, "_");
|
|
2961
|
+
const toNamespaceKey = (segments) => segments.map(sanitizeSegment).join("/");
|
|
2962
|
+
const toEntryKey = (key) => {
|
|
2963
|
+
const hasher = (0, __soda_gql_common.getPortableHasher)();
|
|
2964
|
+
return hasher.hash(key, "xxhash");
|
|
2965
|
+
};
|
|
2966
|
+
const PERSISTENCE_VERSION = "v1";
|
|
2967
|
+
/**
|
|
2968
|
+
* Validate persisted data structure.
|
|
2969
|
+
* Uses simple validation to detect corruption without strict schema.
|
|
2970
|
+
*/
|
|
2971
|
+
const isValidPersistedData = (data) => {
|
|
2972
|
+
if (typeof data !== "object" || data === null) return false;
|
|
2973
|
+
const record = data;
|
|
2974
|
+
if (typeof record.version !== "string") return false;
|
|
2975
|
+
if (typeof record.storage !== "object" || record.storage === null) return false;
|
|
2976
|
+
for (const value of Object.values(record.storage)) {
|
|
2977
|
+
if (!Array.isArray(value)) return false;
|
|
2978
|
+
for (const entry of value) {
|
|
2979
|
+
if (!Array.isArray(entry) || entry.length !== 2) return false;
|
|
2980
|
+
if (typeof entry[0] !== "string") return false;
|
|
2981
|
+
const envelope = entry[1];
|
|
2982
|
+
if (typeof envelope !== "object" || envelope === null) return false;
|
|
2983
|
+
const env = envelope;
|
|
2984
|
+
if (typeof env.key !== "string" || typeof env.version !== "string") return false;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return true;
|
|
2988
|
+
};
|
|
2989
|
+
const createMemoryCache = ({ prefix = [], persistence } = {}) => {
|
|
2990
|
+
const storage = new Map();
|
|
2991
|
+
if (persistence?.enabled) {
|
|
2992
|
+
try {
|
|
2993
|
+
if ((0, node_fs.existsSync)(persistence.filePath)) {
|
|
2994
|
+
const content = (0, node_fs.readFileSync)(persistence.filePath, "utf-8");
|
|
2995
|
+
let parsed;
|
|
2996
|
+
try {
|
|
2997
|
+
parsed = JSON.parse(content);
|
|
2998
|
+
} catch {
|
|
2999
|
+
console.warn(`[cache] Corrupt cache file (invalid JSON), starting fresh: ${persistence.filePath}`);
|
|
3000
|
+
try {
|
|
3001
|
+
(0, node_fs.unlinkSync)(persistence.filePath);
|
|
3002
|
+
} catch {}
|
|
3003
|
+
parsed = null;
|
|
3004
|
+
}
|
|
3005
|
+
if (parsed) {
|
|
3006
|
+
if (!isValidPersistedData(parsed)) {
|
|
3007
|
+
console.warn(`[cache] Corrupt cache file (invalid structure), starting fresh: ${persistence.filePath}`);
|
|
3008
|
+
try {
|
|
3009
|
+
(0, node_fs.unlinkSync)(persistence.filePath);
|
|
3010
|
+
} catch {}
|
|
3011
|
+
} else if (parsed.version === PERSISTENCE_VERSION) {
|
|
3012
|
+
for (const [namespaceKey, entries] of Object.entries(parsed.storage)) {
|
|
3013
|
+
const namespaceMap = new Map();
|
|
3014
|
+
for (const [hashedKey, envelope] of entries) {
|
|
3015
|
+
namespaceMap.set(hashedKey, envelope);
|
|
3016
|
+
}
|
|
3017
|
+
storage.set(namespaceKey, namespaceMap);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
console.warn(`[cache] Failed to load cache from ${persistence.filePath}:`, error);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
const getOrCreateNamespace = (namespaceKey) => {
|
|
3027
|
+
let namespace = storage.get(namespaceKey);
|
|
3028
|
+
if (!namespace) {
|
|
3029
|
+
namespace = new Map();
|
|
3030
|
+
storage.set(namespaceKey, namespace);
|
|
3031
|
+
}
|
|
3032
|
+
return namespace;
|
|
3033
|
+
};
|
|
3034
|
+
return {
|
|
3035
|
+
createStore: ({ namespace, schema, version = "v1" }) => {
|
|
3036
|
+
const namespaceKey = toNamespaceKey([...prefix, ...namespace]);
|
|
3037
|
+
const envelopeSchema = zod.z.object({
|
|
3038
|
+
key: zod.z.string(),
|
|
3039
|
+
version: zod.z.string(),
|
|
3040
|
+
value: schema
|
|
3041
|
+
});
|
|
3042
|
+
const resolveEntryKey = (key) => toEntryKey(key);
|
|
3043
|
+
const validateEnvelope = (raw) => {
|
|
3044
|
+
const parsed = envelopeSchema.safeParse(raw);
|
|
3045
|
+
if (!parsed.success) {
|
|
3046
|
+
return null;
|
|
3047
|
+
}
|
|
3048
|
+
if (parsed.data.version !== version) {
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
return parsed.data;
|
|
3052
|
+
};
|
|
3053
|
+
const load = (key) => {
|
|
3054
|
+
const namespaceStore = storage.get(namespaceKey);
|
|
3055
|
+
if (!namespaceStore) {
|
|
3056
|
+
return null;
|
|
3057
|
+
}
|
|
3058
|
+
const entryKey = resolveEntryKey(key);
|
|
3059
|
+
const raw = namespaceStore.get(entryKey);
|
|
3060
|
+
if (!raw) {
|
|
3061
|
+
return null;
|
|
3062
|
+
}
|
|
3063
|
+
const envelope = validateEnvelope(raw);
|
|
3064
|
+
if (!envelope || envelope.key !== key) {
|
|
3065
|
+
namespaceStore.delete(entryKey);
|
|
3066
|
+
return null;
|
|
3067
|
+
}
|
|
3068
|
+
return envelope.value;
|
|
3069
|
+
};
|
|
3070
|
+
const store = (key, value) => {
|
|
3071
|
+
const namespaceStore = getOrCreateNamespace(namespaceKey);
|
|
3072
|
+
const entryKey = resolveEntryKey(key);
|
|
3073
|
+
const envelope = {
|
|
3074
|
+
key,
|
|
3075
|
+
version,
|
|
3076
|
+
value
|
|
3077
|
+
};
|
|
3078
|
+
namespaceStore.set(entryKey, envelope);
|
|
3079
|
+
};
|
|
3080
|
+
const deleteEntry = (key) => {
|
|
3081
|
+
const namespaceStore = storage.get(namespaceKey);
|
|
3082
|
+
if (!namespaceStore) {
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
const entryKey = resolveEntryKey(key);
|
|
3086
|
+
namespaceStore.delete(entryKey);
|
|
3087
|
+
};
|
|
3088
|
+
function* iterateEntries() {
|
|
3089
|
+
const namespaceStore = storage.get(namespaceKey);
|
|
3090
|
+
if (!namespaceStore) {
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
for (const raw of namespaceStore.values()) {
|
|
3094
|
+
const envelope = validateEnvelope(raw);
|
|
3095
|
+
if (!envelope) {
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
3098
|
+
yield [envelope.key, envelope.value];
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
const clear = () => {
|
|
3102
|
+
const namespaceStore = storage.get(namespaceKey);
|
|
3103
|
+
if (namespaceStore) {
|
|
3104
|
+
namespaceStore.clear();
|
|
3105
|
+
}
|
|
3106
|
+
};
|
|
3107
|
+
const size = () => {
|
|
3108
|
+
let count = 0;
|
|
3109
|
+
for (const _ of iterateEntries()) {
|
|
3110
|
+
count += 1;
|
|
3111
|
+
}
|
|
3112
|
+
return count;
|
|
3113
|
+
};
|
|
3114
|
+
return {
|
|
3115
|
+
load,
|
|
3116
|
+
store,
|
|
3117
|
+
delete: deleteEntry,
|
|
3118
|
+
entries: iterateEntries,
|
|
3119
|
+
clear,
|
|
3120
|
+
size
|
|
3121
|
+
};
|
|
3122
|
+
},
|
|
3123
|
+
clearAll: () => {
|
|
3124
|
+
storage.clear();
|
|
3125
|
+
},
|
|
3126
|
+
save: () => {
|
|
3127
|
+
if (!persistence?.enabled) {
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
try {
|
|
3131
|
+
const serialized = {};
|
|
3132
|
+
for (const [namespaceKey, namespaceMap] of storage.entries()) {
|
|
3133
|
+
serialized[namespaceKey] = Array.from(namespaceMap.entries());
|
|
3134
|
+
}
|
|
3135
|
+
const data = {
|
|
3136
|
+
version: PERSISTENCE_VERSION,
|
|
3137
|
+
storage: serialized
|
|
3138
|
+
};
|
|
3139
|
+
const fs = (0, __soda_gql_common.getPortableFS)();
|
|
3140
|
+
fs.writeFileSyncAtomic(persistence.filePath, JSON.stringify(data));
|
|
3141
|
+
} catch (error) {
|
|
3142
|
+
console.warn(`[cache] Failed to save cache to ${persistence.filePath}:`, error);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
};
|
|
3147
|
+
|
|
3148
|
+
//#endregion
|
|
3149
|
+
//#region packages/builder/src/cache/entity-cache.ts
|
|
3150
|
+
/**
|
|
3151
|
+
* Abstract base class for entity caches.
|
|
3152
|
+
* Provides common caching functionality with signature-based eviction.
|
|
3153
|
+
*/
|
|
3154
|
+
var EntityCache = class {
|
|
3155
|
+
cacheStore;
|
|
3156
|
+
keyNormalizer;
|
|
3157
|
+
constructor(options) {
|
|
3158
|
+
this.cacheStore = options.factory.createStore({
|
|
3159
|
+
namespace: [...options.namespace],
|
|
3160
|
+
schema: options.schema,
|
|
3161
|
+
version: options.version
|
|
3162
|
+
});
|
|
3163
|
+
this.keyNormalizer = options.keyNormalizer ?? __soda_gql_common.normalizePath;
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Normalize a key for consistent cache lookups.
|
|
3167
|
+
*/
|
|
3168
|
+
normalizeKey(key) {
|
|
3169
|
+
return this.keyNormalizer(key);
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Load raw value from cache without signature validation.
|
|
3173
|
+
*/
|
|
3174
|
+
loadRaw(key) {
|
|
3175
|
+
return this.cacheStore.load(key);
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* Store raw value to cache.
|
|
3179
|
+
*/
|
|
3180
|
+
storeRaw(key, value) {
|
|
3181
|
+
this.cacheStore.store(key, value);
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Delete an entry from the cache.
|
|
3185
|
+
*/
|
|
3186
|
+
delete(key) {
|
|
3187
|
+
const normalizedKey = this.normalizeKey(key);
|
|
3188
|
+
this.cacheStore.delete(normalizedKey);
|
|
3189
|
+
}
|
|
3190
|
+
/**
|
|
3191
|
+
* Get all cached entries.
|
|
3192
|
+
* Subclasses should override this to provide custom iteration.
|
|
3193
|
+
*/
|
|
3194
|
+
*baseEntries() {
|
|
3195
|
+
for (const [, value] of this.cacheStore.entries()) {
|
|
3196
|
+
yield value;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Clear all entries from the cache.
|
|
3201
|
+
*/
|
|
3202
|
+
clear() {
|
|
3203
|
+
this.cacheStore.clear();
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Get the number of entries in the cache.
|
|
3207
|
+
*/
|
|
3208
|
+
size() {
|
|
3209
|
+
return this.cacheStore.size();
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
|
|
3213
|
+
//#endregion
|
|
3214
|
+
//#region packages/builder/src/schemas/cache.ts
|
|
3215
|
+
const ModuleDefinitionSchema = zod.z.object({
|
|
3216
|
+
canonicalId: __soda_gql_common.CanonicalIdSchema,
|
|
3217
|
+
astPath: zod.z.string(),
|
|
3218
|
+
isTopLevel: zod.z.boolean(),
|
|
3219
|
+
isExported: zod.z.boolean(),
|
|
3220
|
+
exportBinding: zod.z.string().optional(),
|
|
3221
|
+
expression: zod.z.string()
|
|
3222
|
+
});
|
|
3223
|
+
const ModuleImportSchema = zod.z.object({
|
|
3224
|
+
source: zod.z.string(),
|
|
3225
|
+
local: zod.z.string(),
|
|
3226
|
+
kind: zod.z.enum([
|
|
3227
|
+
"named",
|
|
3228
|
+
"namespace",
|
|
3229
|
+
"default"
|
|
3230
|
+
]),
|
|
3231
|
+
isTypeOnly: zod.z.boolean()
|
|
3232
|
+
});
|
|
3233
|
+
const ModuleExportSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
|
|
3234
|
+
kind: zod.z.literal("named"),
|
|
3235
|
+
exported: zod.z.string(),
|
|
3236
|
+
local: zod.z.string(),
|
|
3237
|
+
source: zod.z.undefined().optional(),
|
|
3238
|
+
isTypeOnly: zod.z.boolean()
|
|
3239
|
+
}), zod.z.object({
|
|
3240
|
+
kind: zod.z.literal("reexport"),
|
|
3241
|
+
exported: zod.z.string(),
|
|
3242
|
+
source: zod.z.string(),
|
|
3243
|
+
local: zod.z.string().optional(),
|
|
3244
|
+
isTypeOnly: zod.z.boolean()
|
|
3245
|
+
})]);
|
|
3246
|
+
const DiagnosticCodeSchema = zod.z.enum([
|
|
3247
|
+
"RENAMED_IMPORT",
|
|
3248
|
+
"STAR_IMPORT",
|
|
3249
|
+
"DEFAULT_IMPORT",
|
|
3250
|
+
"MISSING_ARGUMENT",
|
|
3251
|
+
"INVALID_ARGUMENT_TYPE",
|
|
3252
|
+
"NON_MEMBER_CALLEE",
|
|
3253
|
+
"COMPUTED_PROPERTY",
|
|
3254
|
+
"DYNAMIC_CALLEE",
|
|
3255
|
+
"OPTIONAL_CHAINING",
|
|
3256
|
+
"EXTRA_ARGUMENTS",
|
|
3257
|
+
"SPREAD_ARGUMENT",
|
|
3258
|
+
"CLASS_PROPERTY"
|
|
3259
|
+
]);
|
|
3260
|
+
const DiagnosticLocationSchema = zod.z.object({
|
|
3261
|
+
start: zod.z.number(),
|
|
3262
|
+
end: zod.z.number(),
|
|
3263
|
+
line: zod.z.number().optional(),
|
|
3264
|
+
column: zod.z.number().optional()
|
|
3265
|
+
});
|
|
3266
|
+
const ModuleDiagnosticSchema = zod.z.object({
|
|
3267
|
+
code: DiagnosticCodeSchema,
|
|
3268
|
+
severity: zod.z.enum(["error", "warning"]),
|
|
3269
|
+
message: zod.z.string(),
|
|
3270
|
+
location: DiagnosticLocationSchema,
|
|
3271
|
+
context: zod.z.record(zod.z.string(), zod.z.string()).readonly().optional()
|
|
3272
|
+
});
|
|
3273
|
+
const ModuleAnalysisSchema = zod.z.object({
|
|
3274
|
+
filePath: zod.z.string(),
|
|
3275
|
+
signature: zod.z.string(),
|
|
3276
|
+
definitions: zod.z.array(ModuleDefinitionSchema).readonly(),
|
|
3277
|
+
imports: zod.z.array(ModuleImportSchema).readonly(),
|
|
3278
|
+
exports: zod.z.array(ModuleExportSchema).readonly(),
|
|
3279
|
+
diagnostics: zod.z.array(ModuleDiagnosticSchema).readonly()
|
|
3280
|
+
});
|
|
3281
|
+
|
|
3282
|
+
//#endregion
|
|
3283
|
+
//#region packages/builder/src/schemas/discovery.ts
|
|
3284
|
+
const FileFingerprintSchema = zod.z.object({
|
|
3285
|
+
hash: zod.z.string(),
|
|
3286
|
+
sizeBytes: zod.z.number(),
|
|
3287
|
+
mtimeMs: zod.z.number()
|
|
3288
|
+
});
|
|
3289
|
+
const DiscoveredDependencySchema = zod.z.object({
|
|
3290
|
+
specifier: zod.z.string(),
|
|
3291
|
+
resolvedPath: zod.z.string().nullable(),
|
|
3292
|
+
isExternal: zod.z.boolean()
|
|
3293
|
+
});
|
|
3294
|
+
const DiscoverySnapshotSchema = zod.z.object({
|
|
3295
|
+
filePath: zod.z.string(),
|
|
3296
|
+
normalizedFilePath: zod.z.string(),
|
|
3297
|
+
signature: zod.z.string(),
|
|
3298
|
+
fingerprint: FileFingerprintSchema,
|
|
3299
|
+
analyzer: zod.z.string(),
|
|
3300
|
+
createdAtMs: zod.z.number(),
|
|
3301
|
+
analysis: ModuleAnalysisSchema,
|
|
3302
|
+
dependencies: zod.z.array(DiscoveredDependencySchema).readonly()
|
|
3303
|
+
});
|
|
3304
|
+
|
|
3305
|
+
//#endregion
|
|
3306
|
+
//#region packages/builder/src/discovery/cache.ts
|
|
3307
|
+
const DISCOVERY_CACHE_VERSION = "discovery-cache/v3";
|
|
3308
|
+
var JsonDiscoveryCache = class extends EntityCache {
|
|
3309
|
+
constructor(options) {
|
|
3310
|
+
const namespace = [
|
|
3311
|
+
...options.namespacePrefix ?? ["discovery"],
|
|
3312
|
+
options.analyzer,
|
|
3313
|
+
options.evaluatorId
|
|
3314
|
+
];
|
|
3315
|
+
super({
|
|
3316
|
+
factory: options.factory,
|
|
3317
|
+
namespace,
|
|
3318
|
+
schema: DiscoverySnapshotSchema,
|
|
3319
|
+
version: options.version ?? DISCOVERY_CACHE_VERSION
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
load(filePath, expectedSignature) {
|
|
3323
|
+
const key = this.normalizeKey(filePath);
|
|
3324
|
+
const snapshot = this.loadRaw(key);
|
|
3325
|
+
if (!snapshot) {
|
|
3326
|
+
return null;
|
|
3327
|
+
}
|
|
3328
|
+
if (snapshot.signature !== expectedSignature) {
|
|
3329
|
+
this.delete(filePath);
|
|
3330
|
+
return null;
|
|
3331
|
+
}
|
|
3332
|
+
return snapshot;
|
|
3333
|
+
}
|
|
3334
|
+
peek(filePath) {
|
|
3335
|
+
const key = this.normalizeKey(filePath);
|
|
3336
|
+
return this.loadRaw(key);
|
|
3337
|
+
}
|
|
3338
|
+
store(snapshot) {
|
|
3339
|
+
const key = this.normalizeKey(snapshot.filePath);
|
|
3340
|
+
this.storeRaw(key, snapshot);
|
|
3341
|
+
}
|
|
3342
|
+
entries() {
|
|
3343
|
+
return this.baseEntries();
|
|
3344
|
+
}
|
|
3345
|
+
};
|
|
3346
|
+
const createDiscoveryCache = (options) => new JsonDiscoveryCache(options);
|
|
3347
|
+
|
|
3348
|
+
//#endregion
|
|
3349
|
+
//#region packages/builder/src/discovery/common.ts
|
|
3350
|
+
/**
|
|
3351
|
+
* Extract all unique dependencies (relative + external) from the analysis.
|
|
3352
|
+
* Resolves local specifiers immediately so discovery only traverses once.
|
|
3353
|
+
*/
|
|
3354
|
+
const extractModuleDependencies = (analysis) => {
|
|
3355
|
+
const dependencies = new Map();
|
|
3356
|
+
const addDependency = (specifier) => {
|
|
3357
|
+
if (dependencies.has(specifier)) {
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
const isExternal = (0, __soda_gql_common.isExternalSpecifier)(specifier);
|
|
3361
|
+
const resolvedPath = isExternal ? null : (0, __soda_gql_common.resolveRelativeImportWithExistenceCheck)({
|
|
3362
|
+
filePath: analysis.filePath,
|
|
3363
|
+
specifier
|
|
3364
|
+
});
|
|
3365
|
+
dependencies.set(specifier, {
|
|
3366
|
+
specifier,
|
|
3367
|
+
resolvedPath,
|
|
3368
|
+
isExternal
|
|
3369
|
+
});
|
|
3370
|
+
};
|
|
3371
|
+
for (const imp of analysis.imports) {
|
|
3372
|
+
addDependency(imp.source);
|
|
3373
|
+
}
|
|
3374
|
+
for (const exp of analysis.exports) {
|
|
3375
|
+
if (exp.kind === "reexport") {
|
|
3376
|
+
addDependency(exp.source);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
return Array.from(dependencies.values());
|
|
3380
|
+
};
|
|
3381
|
+
const createSourceHash = (source) => {
|
|
3382
|
+
const hasher = (0, __soda_gql_common.getPortableHasher)();
|
|
3383
|
+
return hasher.hash(source, "xxhash");
|
|
3384
|
+
};
|
|
3385
|
+
|
|
3386
|
+
//#endregion
|
|
3387
|
+
//#region packages/builder/src/discovery/fingerprint.ts
|
|
3388
|
+
/**
|
|
3389
|
+
* In-memory fingerprint cache keyed by absolute path
|
|
3390
|
+
*/
|
|
3391
|
+
const fingerprintCache = new Map();
|
|
3392
|
+
/**
|
|
3393
|
+
* Lazy-loaded xxhash instance
|
|
3394
|
+
*/
|
|
3395
|
+
let xxhashInstance = null;
|
|
3396
|
+
/**
|
|
3397
|
+
* Lazily load xxhash-wasm instance
|
|
3398
|
+
*/
|
|
3399
|
+
async function getXXHash() {
|
|
3400
|
+
if (xxhashInstance === null) {
|
|
3401
|
+
const { default: xxhash } = await import("xxhash-wasm");
|
|
3402
|
+
xxhashInstance = await xxhash();
|
|
3403
|
+
}
|
|
3404
|
+
return xxhashInstance;
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* Compute file fingerprint with memoization.
|
|
3408
|
+
* Uses mtime to short-circuit re-hashing unchanged files.
|
|
3409
|
+
*
|
|
3410
|
+
* @param path - Absolute path to file
|
|
3411
|
+
* @returns Result containing FileFingerprint or FingerprintError
|
|
3412
|
+
*/
|
|
3413
|
+
function computeFingerprint(path) {
|
|
3414
|
+
try {
|
|
3415
|
+
const stats = (0, node_fs.statSync)(path);
|
|
3416
|
+
if (!stats.isFile()) {
|
|
3417
|
+
return (0, neverthrow.err)({
|
|
3418
|
+
code: "NOT_A_FILE",
|
|
3419
|
+
path,
|
|
3420
|
+
message: `Path is not a file: ${path}`
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
const mtimeMs = stats.mtimeMs;
|
|
3424
|
+
const cached = fingerprintCache.get(path);
|
|
3425
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
3426
|
+
return (0, neverthrow.ok)(cached);
|
|
3427
|
+
}
|
|
3428
|
+
const contents = (0, node_fs.readFileSync)(path);
|
|
3429
|
+
const sizeBytes = stats.size;
|
|
3430
|
+
const hash = computeHashSync(contents);
|
|
3431
|
+
const fingerprint = {
|
|
3432
|
+
hash,
|
|
3433
|
+
sizeBytes,
|
|
3434
|
+
mtimeMs
|
|
3435
|
+
};
|
|
3436
|
+
fingerprintCache.set(path, fingerprint);
|
|
3437
|
+
return (0, neverthrow.ok)(fingerprint);
|
|
3438
|
+
} catch (error) {
|
|
3439
|
+
if (error.code === "ENOENT") {
|
|
3440
|
+
return (0, neverthrow.err)({
|
|
3441
|
+
code: "FILE_NOT_FOUND",
|
|
3442
|
+
path,
|
|
3443
|
+
message: `File not found: ${path}`
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
return (0, neverthrow.err)({
|
|
3447
|
+
code: "READ_ERROR",
|
|
3448
|
+
path,
|
|
3449
|
+
message: `Failed to read file: ${error}`
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Compute hash synchronously.
|
|
3455
|
+
* For first call, uses simple string hash as fallback.
|
|
3456
|
+
* Subsequent calls will use xxhash after async initialization.
|
|
3457
|
+
*/
|
|
3458
|
+
function computeHashSync(contents) {
|
|
3459
|
+
if (xxhashInstance !== null) {
|
|
3460
|
+
const hash = xxhashInstance.h64Raw(new Uint8Array(contents));
|
|
3461
|
+
return hash.toString(16);
|
|
3462
|
+
}
|
|
3463
|
+
void getXXHash();
|
|
3464
|
+
return simpleHash(contents);
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Simple hash function for initial calls before xxhash loads
|
|
3468
|
+
*/
|
|
3469
|
+
function simpleHash(buffer) {
|
|
3470
|
+
let hash = 0;
|
|
3471
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
3472
|
+
const byte = buffer[i];
|
|
3473
|
+
if (byte !== undefined) {
|
|
3474
|
+
hash = (hash << 5) - hash + byte;
|
|
3475
|
+
hash = hash & hash;
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
return hash.toString(16);
|
|
3479
|
+
}
|
|
3480
|
+
/**
|
|
3481
|
+
* Compute fingerprint from pre-read file content and stats.
|
|
3482
|
+
* This is used by the generator-based discoverer which already has the content.
|
|
3483
|
+
*
|
|
3484
|
+
* @param path - Absolute path to file (for caching)
|
|
3485
|
+
* @param stats - File stats (mtimeMs, size)
|
|
3486
|
+
* @param content - File content as string
|
|
3487
|
+
* @returns FileFingerprint
|
|
3488
|
+
*/
|
|
3489
|
+
function computeFingerprintFromContent(path, stats, content) {
|
|
3490
|
+
const cached = fingerprintCache.get(path);
|
|
3491
|
+
if (cached && cached.mtimeMs === stats.mtimeMs) {
|
|
3492
|
+
return cached;
|
|
3493
|
+
}
|
|
3494
|
+
const buffer = Buffer.from(content, "utf-8");
|
|
3495
|
+
const hash = computeHashSync(buffer);
|
|
3496
|
+
const fingerprint = {
|
|
3497
|
+
hash,
|
|
3498
|
+
sizeBytes: stats.size,
|
|
3499
|
+
mtimeMs: stats.mtimeMs
|
|
3500
|
+
};
|
|
3501
|
+
fingerprintCache.set(path, fingerprint);
|
|
3502
|
+
return fingerprint;
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Invalidate cached fingerprint for a specific path
|
|
3506
|
+
*
|
|
3507
|
+
* @param path - Absolute path to invalidate
|
|
3508
|
+
*/
|
|
3509
|
+
function invalidateFingerprint(path) {
|
|
3510
|
+
fingerprintCache.delete(path);
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Clear all cached fingerprints
|
|
3514
|
+
*/
|
|
3515
|
+
function clearFingerprintCache() {
|
|
3516
|
+
fingerprintCache.clear();
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
//#endregion
|
|
3520
|
+
//#region packages/builder/src/discovery/discoverer.ts
|
|
3521
|
+
/**
|
|
3522
|
+
* Generator-based module discovery that yields effects for file I/O.
|
|
3523
|
+
* This allows the discovery process to be executed with either sync or async schedulers.
|
|
3524
|
+
*/
|
|
3525
|
+
function* discoverModulesGen({ entryPaths, astAnalyzer, incremental }) {
|
|
3526
|
+
const snapshots = new Map();
|
|
3527
|
+
const stack = [...entryPaths];
|
|
3528
|
+
const changedFiles = incremental?.changedFiles ?? new Set();
|
|
3529
|
+
const removedFiles = incremental?.removedFiles ?? new Set();
|
|
3530
|
+
const affectedFiles = incremental?.affectedFiles ?? new Set();
|
|
3531
|
+
const invalidatedSet = new Set([
|
|
3532
|
+
...changedFiles,
|
|
3533
|
+
...removedFiles,
|
|
3534
|
+
...affectedFiles
|
|
3535
|
+
]);
|
|
3536
|
+
let cacheHits = 0;
|
|
3537
|
+
let cacheMisses = 0;
|
|
3538
|
+
let cacheSkips = 0;
|
|
3539
|
+
if (incremental) {
|
|
3540
|
+
for (const filePath of removedFiles) {
|
|
3541
|
+
incremental.cache.delete(filePath);
|
|
3542
|
+
invalidateFingerprint(filePath);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
while (stack.length > 0) {
|
|
3546
|
+
const rawFilePath = stack.pop();
|
|
3547
|
+
if (!rawFilePath) {
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
const filePath = (0, __soda_gql_common.normalizePath)(rawFilePath);
|
|
3551
|
+
if (snapshots.has(filePath)) {
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
let shouldReadFile = true;
|
|
3555
|
+
if (invalidatedSet.has(filePath)) {
|
|
3556
|
+
invalidateFingerprint(filePath);
|
|
3557
|
+
cacheSkips++;
|
|
3558
|
+
} else if (incremental) {
|
|
3559
|
+
const cached = incremental.cache.peek(filePath);
|
|
3560
|
+
if (cached) {
|
|
3561
|
+
const stats$1 = yield* new OptionalFileStatEffect(filePath).run();
|
|
3562
|
+
if (stats$1) {
|
|
3563
|
+
const mtimeMs = stats$1.mtimeMs;
|
|
3564
|
+
const sizeBytes = stats$1.size;
|
|
3565
|
+
if (cached.fingerprint.mtimeMs === mtimeMs && cached.fingerprint.sizeBytes === sizeBytes) {
|
|
3566
|
+
snapshots.set(filePath, cached);
|
|
3567
|
+
cacheHits++;
|
|
3568
|
+
for (const dep of cached.dependencies) {
|
|
3569
|
+
if (dep.resolvedPath && !snapshots.has(dep.resolvedPath)) {
|
|
3570
|
+
stack.push(dep.resolvedPath);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
shouldReadFile = false;
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
if (!shouldReadFile) {
|
|
3579
|
+
continue;
|
|
3580
|
+
}
|
|
3581
|
+
const source = yield* new OptionalFileReadEffect(filePath).run();
|
|
3582
|
+
if (source === null) {
|
|
3583
|
+
incremental?.cache.delete(filePath);
|
|
3584
|
+
invalidateFingerprint(filePath);
|
|
3585
|
+
continue;
|
|
3586
|
+
}
|
|
3587
|
+
const signature = createSourceHash(source);
|
|
3588
|
+
const analysis = astAnalyzer.analyze({
|
|
3589
|
+
filePath,
|
|
3590
|
+
source
|
|
3591
|
+
});
|
|
3592
|
+
cacheMisses++;
|
|
3593
|
+
const dependencies = extractModuleDependencies(analysis);
|
|
3594
|
+
for (const dep of dependencies) {
|
|
3595
|
+
if (!dep.isExternal && dep.resolvedPath && !snapshots.has(dep.resolvedPath)) {
|
|
3596
|
+
stack.push(dep.resolvedPath);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
const stats = yield* new OptionalFileStatEffect(filePath).run();
|
|
3600
|
+
const fingerprint = computeFingerprintFromContent(filePath, stats, source);
|
|
3601
|
+
const snapshot = {
|
|
3602
|
+
filePath,
|
|
3603
|
+
normalizedFilePath: (0, __soda_gql_common.normalizePath)(filePath),
|
|
3604
|
+
signature,
|
|
3605
|
+
fingerprint,
|
|
3606
|
+
analyzer: astAnalyzer.type,
|
|
3607
|
+
createdAtMs: Date.now(),
|
|
3608
|
+
analysis,
|
|
3609
|
+
dependencies
|
|
3610
|
+
};
|
|
3611
|
+
snapshots.set(filePath, snapshot);
|
|
3612
|
+
if (incremental) {
|
|
3613
|
+
incremental.cache.store(snapshot);
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
return {
|
|
3617
|
+
snapshots: Array.from(snapshots.values()),
|
|
3618
|
+
cacheHits,
|
|
3619
|
+
cacheMisses,
|
|
3620
|
+
cacheSkips
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
/**
|
|
3624
|
+
* Discover and analyze all modules starting from entry points.
|
|
3625
|
+
* Uses AST parsing instead of RegExp for reliable dependency extraction.
|
|
3626
|
+
* Supports caching with fingerprint-based invalidation to skip re-parsing unchanged files.
|
|
3627
|
+
*
|
|
3628
|
+
* This function uses the synchronous scheduler internally for backward compatibility.
|
|
3629
|
+
* For async execution with parallel file I/O, use discoverModulesGen with an async scheduler.
|
|
3630
|
+
*/
|
|
3631
|
+
const discoverModules = (options) => {
|
|
3632
|
+
const scheduler = (0, __soda_gql_common.createSyncScheduler)();
|
|
3633
|
+
const result = scheduler.run(() => discoverModulesGen(options));
|
|
3634
|
+
if (result.isErr()) {
|
|
3635
|
+
const error = result.error;
|
|
3636
|
+
return (0, neverthrow.err)(builderErrors.discoveryIOError("unknown", error.message));
|
|
3637
|
+
}
|
|
3638
|
+
return (0, neverthrow.ok)(result.value);
|
|
3639
|
+
};
|
|
3640
|
+
/**
|
|
3641
|
+
* Asynchronous version of discoverModules.
|
|
3642
|
+
* Uses async scheduler for parallel file I/O operations.
|
|
3643
|
+
*
|
|
3644
|
+
* This is useful for large codebases where parallel file operations can improve performance.
|
|
3645
|
+
*/
|
|
3646
|
+
const discoverModulesAsync = async (options) => {
|
|
3647
|
+
const scheduler = (0, __soda_gql_common.createAsyncScheduler)();
|
|
3648
|
+
const result = await scheduler.run(() => discoverModulesGen(options));
|
|
3649
|
+
if (result.isErr()) {
|
|
3650
|
+
const error = result.error;
|
|
3651
|
+
return (0, neverthrow.err)(builderErrors.discoveryIOError("unknown", error.message));
|
|
3652
|
+
}
|
|
3653
|
+
return (0, neverthrow.ok)(result.value);
|
|
3654
|
+
};
|
|
3655
|
+
|
|
3656
|
+
//#endregion
|
|
3657
|
+
//#region packages/builder/src/utils/glob.ts
|
|
3658
|
+
/**
|
|
3659
|
+
* Cross-runtime glob pattern matching abstraction
|
|
3660
|
+
* Provides a unified interface for glob operations across Bun and Node.js
|
|
3661
|
+
*/
|
|
3662
|
+
/**
|
|
3663
|
+
* Scan files matching glob patterns from the given directory.
|
|
3664
|
+
* Supports negation patterns (e.g., "!**\/excluded.ts") when using fast-glob.
|
|
3665
|
+
*
|
|
3666
|
+
* @param patterns - Glob pattern(s). Can be a single pattern or array of patterns.
|
|
3667
|
+
* @param cwd - Working directory (defaults to process.cwd())
|
|
3668
|
+
* @returns Array of matched file paths (relative to cwd)
|
|
3669
|
+
*/
|
|
3670
|
+
const scanGlob = (patterns, cwd = process.cwd()) => {
|
|
3671
|
+
const patternArray = Array.isArray(patterns) ? patterns : [patterns];
|
|
3672
|
+
const hasNegation = patternArray.some((p) => p.startsWith("!"));
|
|
3673
|
+
if (typeof Bun !== "undefined" && Bun.Glob && !hasNegation) {
|
|
3674
|
+
const { Glob } = Bun;
|
|
3675
|
+
const results = new Set();
|
|
3676
|
+
for (const pattern of patternArray) {
|
|
3677
|
+
const glob = new Glob(pattern);
|
|
3678
|
+
for (const match of glob.scanSync(cwd)) {
|
|
3679
|
+
results.add(match);
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
return Array.from(results);
|
|
3683
|
+
}
|
|
3684
|
+
return fast_glob.default.sync(patternArray, {
|
|
3685
|
+
cwd,
|
|
3686
|
+
dot: true,
|
|
3687
|
+
onlyFiles: true
|
|
3688
|
+
});
|
|
3689
|
+
};
|
|
3690
|
+
|
|
3691
|
+
//#endregion
|
|
3692
|
+
//#region packages/builder/src/discovery/entry-paths.ts
|
|
3693
|
+
/**
|
|
3694
|
+
* Resolve entry file paths from glob patterns or direct paths.
|
|
3695
|
+
* Used by the discovery system to find entry points for module traversal.
|
|
3696
|
+
* All paths are normalized to POSIX format for consistent cache key matching.
|
|
3697
|
+
* Uses Node.js normalize() + backslash replacement to match normalizePath from @soda-gql/common.
|
|
3698
|
+
*
|
|
3699
|
+
* @param entries - Include patterns (glob or direct paths). Supports negation patterns (e.g., "!./path/to/exclude.ts")
|
|
3700
|
+
*/
|
|
3701
|
+
const resolveEntryPaths = (entries) => {
|
|
3702
|
+
const directPaths = [];
|
|
3703
|
+
const globPatterns = [];
|
|
3704
|
+
for (const entry of entries) {
|
|
3705
|
+
if (entry.startsWith("!")) {
|
|
3706
|
+
globPatterns.push(entry);
|
|
3707
|
+
continue;
|
|
3708
|
+
}
|
|
3709
|
+
const absolute = (0, node_path.resolve)(entry);
|
|
3710
|
+
if ((0, node_fs.existsSync)(absolute)) {
|
|
3711
|
+
directPaths.push((0, node_path.normalize)(absolute).replace(/\\/g, "/"));
|
|
3712
|
+
} else {
|
|
3713
|
+
globPatterns.push(entry);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
const globMatches = globPatterns.length > 0 ? scanGlob(globPatterns, process.cwd()).map((match) => {
|
|
3717
|
+
return (0, node_path.normalize)((0, node_path.resolve)(match)).replace(/\\/g, "/");
|
|
3718
|
+
}) : [];
|
|
3719
|
+
const resolvedPaths = [...directPaths, ...globMatches];
|
|
3720
|
+
if (resolvedPaths.length === 0) {
|
|
3721
|
+
return (0, neverthrow.err)({
|
|
3722
|
+
code: "ENTRY_NOT_FOUND",
|
|
3723
|
+
message: `No entry files matched ${entries.join(", ")}`,
|
|
3724
|
+
entry: entries.join(", ")
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
return (0, neverthrow.ok)(resolvedPaths);
|
|
3728
|
+
};
|
|
3729
|
+
|
|
3730
|
+
//#endregion
|
|
3731
|
+
//#region packages/builder/src/tracker/file-tracker.ts
|
|
3732
|
+
/**
|
|
3733
|
+
* Create a file tracker that maintains in-memory state for change detection.
|
|
3734
|
+
*
|
|
3735
|
+
* The tracker keeps file metadata (mtime, size) in memory during the process lifetime
|
|
3736
|
+
* and detects which files have been added, updated, or removed. State is scoped to
|
|
3737
|
+
* the process and does not persist across restarts.
|
|
3738
|
+
*/
|
|
3739
|
+
const createFileTracker = () => {
|
|
3740
|
+
let currentScan = { files: new Map() };
|
|
3741
|
+
let nextScan = null;
|
|
3742
|
+
const scan = (extraPaths) => {
|
|
3743
|
+
const allPathsToScan = new Set([...extraPaths, ...currentScan.files.keys()]);
|
|
3744
|
+
const files = new Map();
|
|
3745
|
+
for (const path of allPathsToScan) {
|
|
3746
|
+
try {
|
|
3747
|
+
const normalized = (0, __soda_gql_common.normalizePath)(path);
|
|
3748
|
+
const stats = (0, node_fs.statSync)(normalized);
|
|
3749
|
+
files.set(normalized, {
|
|
3750
|
+
mtimeMs: stats.mtimeMs,
|
|
3751
|
+
size: stats.size
|
|
3752
|
+
});
|
|
3753
|
+
} catch {}
|
|
3754
|
+
}
|
|
3755
|
+
nextScan = { files };
|
|
3756
|
+
return (0, neverthrow.ok)(nextScan);
|
|
3757
|
+
};
|
|
3758
|
+
const detectChanges = () => {
|
|
3759
|
+
const previous = currentScan;
|
|
3760
|
+
const current = nextScan ?? currentScan;
|
|
3761
|
+
const added = new Set();
|
|
3762
|
+
const updated = new Set();
|
|
3763
|
+
const removed = new Set();
|
|
3764
|
+
for (const [path, currentMetadata] of current.files) {
|
|
3765
|
+
const previousMetadata = previous.files.get(path);
|
|
3766
|
+
if (!previousMetadata) {
|
|
3767
|
+
added.add(path);
|
|
3768
|
+
} else if (previousMetadata.mtimeMs !== currentMetadata.mtimeMs || previousMetadata.size !== currentMetadata.size) {
|
|
3769
|
+
updated.add(path);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
for (const path of previous.files.keys()) {
|
|
3773
|
+
if (!current.files.has(path)) {
|
|
3774
|
+
removed.add(path);
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
return {
|
|
3778
|
+
added,
|
|
3779
|
+
updated,
|
|
3780
|
+
removed
|
|
3781
|
+
};
|
|
3782
|
+
};
|
|
3783
|
+
const update = (scan$1) => {
|
|
3784
|
+
currentScan = scan$1;
|
|
3785
|
+
nextScan = null;
|
|
3786
|
+
};
|
|
3787
|
+
return {
|
|
3788
|
+
scan,
|
|
3789
|
+
detectChanges,
|
|
3790
|
+
update
|
|
3791
|
+
};
|
|
3792
|
+
};
|
|
3793
|
+
/**
|
|
3794
|
+
* Check if a file diff is empty (no changes detected).
|
|
3795
|
+
*/
|
|
3796
|
+
const isEmptyDiff = (diff) => {
|
|
3797
|
+
return diff.added.size === 0 && diff.updated.size === 0 && diff.removed.size === 0;
|
|
3798
|
+
};
|
|
3799
|
+
|
|
3800
|
+
//#endregion
|
|
3801
|
+
//#region packages/builder/src/session/dependency-validation.ts
|
|
3802
|
+
const validateModuleDependencies = ({ analyses, graphqlSystemHelper }) => {
|
|
3803
|
+
for (const analysis of analyses.values()) {
|
|
3804
|
+
for (const { source, isTypeOnly } of analysis.imports) {
|
|
3805
|
+
if (isTypeOnly) {
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
if ((0, __soda_gql_common.isRelativeSpecifier)(source)) {
|
|
3809
|
+
if (graphqlSystemHelper.isGraphqlSystemImportSpecifier({
|
|
3810
|
+
filePath: analysis.filePath,
|
|
3811
|
+
specifier: source
|
|
3812
|
+
})) {
|
|
3813
|
+
continue;
|
|
3814
|
+
}
|
|
3815
|
+
const resolvedModule = (0, __soda_gql_common.resolveRelativeImportWithReferences)({
|
|
3816
|
+
filePath: analysis.filePath,
|
|
3817
|
+
specifier: source,
|
|
3818
|
+
references: analyses
|
|
3819
|
+
});
|
|
3820
|
+
if (!resolvedModule) {
|
|
3821
|
+
return (0, neverthrow.err)({
|
|
3822
|
+
code: "MISSING_IMPORT",
|
|
3823
|
+
chain: [analysis.filePath, source]
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
return (0, neverthrow.ok)(null);
|
|
3830
|
+
};
|
|
3831
|
+
|
|
3832
|
+
//#endregion
|
|
3833
|
+
//#region packages/builder/src/session/module-adjacency.ts
|
|
3834
|
+
/**
|
|
3835
|
+
* Extract module-level adjacency from dependency graph.
|
|
3836
|
+
* Returns Map of file path -> set of files that import it.
|
|
3837
|
+
* All paths are normalized to POSIX format for consistent cache key matching.
|
|
3838
|
+
*/
|
|
3839
|
+
const extractModuleAdjacency = ({ snapshots }) => {
|
|
3840
|
+
const importsByModule = new Map();
|
|
3841
|
+
for (const snapshot of snapshots.values()) {
|
|
3842
|
+
const { normalizedFilePath, dependencies, analysis } = snapshot;
|
|
3843
|
+
const imports = new Set();
|
|
3844
|
+
for (const { resolvedPath } of dependencies) {
|
|
3845
|
+
if (resolvedPath && resolvedPath !== normalizedFilePath && snapshots.has(resolvedPath)) {
|
|
3846
|
+
imports.add(resolvedPath);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
if (dependencies.length === 0 && analysis.imports.length > 0) {
|
|
3850
|
+
for (const imp of analysis.imports) {
|
|
3851
|
+
if (imp.isTypeOnly) {
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
const resolved = (0, __soda_gql_common.resolveRelativeImportWithReferences)({
|
|
3855
|
+
filePath: normalizedFilePath,
|
|
3856
|
+
specifier: imp.source,
|
|
3857
|
+
references: snapshots
|
|
3858
|
+
});
|
|
3859
|
+
if (resolved) {
|
|
3860
|
+
imports.add(resolved);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
if (imports.size > 0) {
|
|
3865
|
+
importsByModule.set(normalizedFilePath, imports);
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
const adjacency = new Map();
|
|
3869
|
+
for (const [importer, imports] of importsByModule) {
|
|
3870
|
+
for (const imported of imports) {
|
|
3871
|
+
if (!adjacency.has(imported)) {
|
|
3872
|
+
adjacency.set(imported, new Set());
|
|
3873
|
+
}
|
|
3874
|
+
adjacency.get(imported)?.add(importer);
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
for (const modulePath of snapshots.keys()) {
|
|
3878
|
+
if (!adjacency.has(modulePath)) {
|
|
3879
|
+
adjacency.set(modulePath, new Set());
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
return adjacency;
|
|
3883
|
+
};
|
|
3884
|
+
/**
|
|
3885
|
+
* Collect all modules affected by changes, including transitive dependents.
|
|
3886
|
+
* Uses BFS to traverse module adjacency graph.
|
|
3887
|
+
* All paths are already normalized from extractModuleAdjacency.
|
|
3888
|
+
*/
|
|
3889
|
+
const collectAffectedFiles = (input) => {
|
|
3890
|
+
const { changedFiles, removedFiles, previousModuleAdjacency } = input;
|
|
3891
|
+
const affected = new Set([...changedFiles, ...removedFiles]);
|
|
3892
|
+
const queue = [...changedFiles];
|
|
3893
|
+
const visited = new Set(changedFiles);
|
|
3894
|
+
while (queue.length > 0) {
|
|
3895
|
+
const current = queue.shift();
|
|
3896
|
+
if (!current) break;
|
|
3897
|
+
const dependents = previousModuleAdjacency.get(current);
|
|
3898
|
+
if (dependents) {
|
|
3899
|
+
for (const dependent of dependents) {
|
|
3900
|
+
if (!visited.has(dependent)) {
|
|
3901
|
+
visited.add(dependent);
|
|
3902
|
+
affected.add(dependent);
|
|
3903
|
+
queue.push(dependent);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
return affected;
|
|
3909
|
+
};
|
|
3910
|
+
|
|
3911
|
+
//#endregion
|
|
3912
|
+
//#region packages/builder/src/session/builder-session.ts
|
|
3913
|
+
/**
|
|
3914
|
+
* Singleton state for beforeExit handler registration.
|
|
3915
|
+
* Ensures only one handler is registered regardless of how many sessions are created.
|
|
3916
|
+
*/
|
|
3917
|
+
const exitHandlerState = {
|
|
3918
|
+
registered: false,
|
|
3919
|
+
factories: new Set()
|
|
3920
|
+
};
|
|
3921
|
+
/**
|
|
3922
|
+
* Register a cache factory for save on process exit.
|
|
3923
|
+
* Uses singleton pattern to prevent multiple handler registrations.
|
|
3924
|
+
*/
|
|
3925
|
+
const registerExitHandler = (cacheFactory) => {
|
|
3926
|
+
exitHandlerState.factories.add(cacheFactory);
|
|
3927
|
+
if (!exitHandlerState.registered) {
|
|
3928
|
+
exitHandlerState.registered = true;
|
|
3929
|
+
process.on("beforeExit", () => {
|
|
3930
|
+
for (const factory of exitHandlerState.factories) {
|
|
3931
|
+
factory.save();
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
};
|
|
3936
|
+
/**
|
|
3937
|
+
* Unregister a cache factory from the exit handler.
|
|
3938
|
+
*/
|
|
3939
|
+
const unregisterExitHandler = (cacheFactory) => {
|
|
3940
|
+
exitHandlerState.factories.delete(cacheFactory);
|
|
3941
|
+
};
|
|
3942
|
+
/**
|
|
3943
|
+
* Reset exit handler state for testing.
|
|
3944
|
+
* @internal
|
|
3945
|
+
*/
|
|
3946
|
+
const __resetExitHandlerForTests = () => {
|
|
3947
|
+
exitHandlerState.registered = false;
|
|
3948
|
+
exitHandlerState.factories.clear();
|
|
3949
|
+
};
|
|
3950
|
+
/**
|
|
3951
|
+
* Create a new builder session.
|
|
3952
|
+
*
|
|
3953
|
+
* The session maintains in-memory state across builds to enable incremental processing.
|
|
3954
|
+
*/
|
|
3955
|
+
const createBuilderSession = (options) => {
|
|
3956
|
+
const config = options.config;
|
|
3957
|
+
const evaluatorId = options.evaluatorId ?? "default";
|
|
3958
|
+
const entrypoints = new Set(options.entrypointsOverride ?? config.include);
|
|
3959
|
+
const state = {
|
|
3960
|
+
gen: 0,
|
|
3961
|
+
snapshots: new Map(),
|
|
3962
|
+
moduleAdjacency: new Map(),
|
|
3963
|
+
intermediateModules: new Map(),
|
|
3964
|
+
lastArtifact: null,
|
|
3965
|
+
lastIntermediateElements: null
|
|
3966
|
+
};
|
|
3967
|
+
const cacheFactory = createMemoryCache({
|
|
3968
|
+
prefix: ["builder"],
|
|
3969
|
+
persistence: {
|
|
3970
|
+
enabled: true,
|
|
3971
|
+
filePath: (0, node_path.join)(process.cwd(), "node_modules", ".cache", "soda-gql", "builder", "cache.json")
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
registerExitHandler(cacheFactory);
|
|
3975
|
+
const graphqlHelper = createGraphqlSystemIdentifyHelper(config);
|
|
3976
|
+
const ensureAstAnalyzer = (0, __soda_gql_common.cachedFn)(() => createAstAnalyzer({
|
|
3977
|
+
analyzer: config.analyzer,
|
|
3978
|
+
graphqlHelper
|
|
3979
|
+
}));
|
|
3980
|
+
const ensureDiscoveryCache = (0, __soda_gql_common.cachedFn)(() => createDiscoveryCache({
|
|
3981
|
+
factory: cacheFactory,
|
|
3982
|
+
analyzer: config.analyzer,
|
|
3983
|
+
evaluatorId
|
|
3984
|
+
}));
|
|
3985
|
+
const ensureFileTracker = (0, __soda_gql_common.cachedFn)(() => createFileTracker());
|
|
3986
|
+
/**
|
|
3987
|
+
* Prepare build input. Shared between sync and async builds.
|
|
3988
|
+
* Returns either a skip result or the input for buildGen.
|
|
3989
|
+
*/
|
|
3990
|
+
const prepareBuildInput = (force) => {
|
|
3991
|
+
const entryPathsResult = resolveEntryPaths(Array.from(entrypoints));
|
|
3992
|
+
if (entryPathsResult.isErr()) {
|
|
3993
|
+
return (0, neverthrow.err)(entryPathsResult.error);
|
|
3994
|
+
}
|
|
3995
|
+
const entryPaths = entryPathsResult.value;
|
|
3996
|
+
const tracker = ensureFileTracker();
|
|
3997
|
+
const scanResult = tracker.scan(entryPaths);
|
|
3998
|
+
if (scanResult.isErr()) {
|
|
3999
|
+
const trackerError = scanResult.error;
|
|
4000
|
+
return (0, neverthrow.err)(builderErrors.discoveryIOError(trackerError.type === "scan-failed" ? trackerError.path : "unknown", `Failed to scan files: ${trackerError.message}`));
|
|
4001
|
+
}
|
|
4002
|
+
const currentScan = scanResult.value;
|
|
4003
|
+
const diff = tracker.detectChanges();
|
|
4004
|
+
const prepareResult = prepare({
|
|
4005
|
+
diff,
|
|
4006
|
+
entryPaths,
|
|
4007
|
+
lastArtifact: state.lastArtifact,
|
|
4008
|
+
force
|
|
4009
|
+
});
|
|
4010
|
+
if (prepareResult.isErr()) {
|
|
4011
|
+
return (0, neverthrow.err)(prepareResult.error);
|
|
4012
|
+
}
|
|
4013
|
+
if (prepareResult.value.type === "should-skip") {
|
|
4014
|
+
return (0, neverthrow.ok)({
|
|
4015
|
+
type: "skip",
|
|
4016
|
+
artifact: prepareResult.value.data.artifact
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
const { changedFiles, removedFiles } = prepareResult.value.data;
|
|
4020
|
+
return (0, neverthrow.ok)({
|
|
4021
|
+
type: "build",
|
|
4022
|
+
input: {
|
|
4023
|
+
entryPaths,
|
|
4024
|
+
astAnalyzer: ensureAstAnalyzer(),
|
|
4025
|
+
discoveryCache: ensureDiscoveryCache(),
|
|
4026
|
+
changedFiles,
|
|
4027
|
+
removedFiles,
|
|
4028
|
+
previousModuleAdjacency: state.moduleAdjacency,
|
|
4029
|
+
previousIntermediateModules: state.intermediateModules,
|
|
4030
|
+
graphqlSystemPath: (0, node_path.resolve)(config.outdir, "index.ts"),
|
|
4031
|
+
graphqlHelper
|
|
4032
|
+
},
|
|
4033
|
+
currentScan
|
|
4034
|
+
});
|
|
4035
|
+
};
|
|
4036
|
+
/**
|
|
4037
|
+
* Finalize build and update session state.
|
|
4038
|
+
*/
|
|
4039
|
+
const finalizeBuild = (genResult, currentScan) => {
|
|
4040
|
+
const { snapshots, analyses, currentModuleAdjacency, intermediateModules, elements, stats } = genResult;
|
|
4041
|
+
const artifactResult = buildArtifact({
|
|
4042
|
+
analyses,
|
|
4043
|
+
elements,
|
|
4044
|
+
stats
|
|
4045
|
+
});
|
|
4046
|
+
if (artifactResult.isErr()) {
|
|
4047
|
+
return (0, neverthrow.err)(artifactResult.error);
|
|
4048
|
+
}
|
|
4049
|
+
state.gen++;
|
|
4050
|
+
state.snapshots = snapshots;
|
|
4051
|
+
state.moduleAdjacency = currentModuleAdjacency;
|
|
4052
|
+
state.lastArtifact = artifactResult.value;
|
|
4053
|
+
state.intermediateModules = intermediateModules;
|
|
4054
|
+
state.lastIntermediateElements = elements;
|
|
4055
|
+
ensureFileTracker().update(currentScan);
|
|
4056
|
+
return (0, neverthrow.ok)(artifactResult.value);
|
|
4057
|
+
};
|
|
4058
|
+
/**
|
|
4059
|
+
* Synchronous build using SyncScheduler.
|
|
4060
|
+
* Throws if any element requires async operations.
|
|
4061
|
+
*/
|
|
4062
|
+
const build = (options$1) => {
|
|
4063
|
+
const prepResult = prepareBuildInput(options$1?.force ?? false);
|
|
4064
|
+
if (prepResult.isErr()) {
|
|
4065
|
+
return (0, neverthrow.err)(prepResult.error);
|
|
4066
|
+
}
|
|
4067
|
+
if (prepResult.value.type === "skip") {
|
|
4068
|
+
return (0, neverthrow.ok)(prepResult.value.artifact);
|
|
4069
|
+
}
|
|
4070
|
+
const { input, currentScan } = prepResult.value;
|
|
4071
|
+
const scheduler = (0, __soda_gql_common.createSyncScheduler)();
|
|
4072
|
+
try {
|
|
4073
|
+
const result = scheduler.run(() => buildGen(input));
|
|
4074
|
+
if (result.isErr()) {
|
|
4075
|
+
return (0, neverthrow.err)(convertSchedulerError(result.error));
|
|
4076
|
+
}
|
|
4077
|
+
return finalizeBuild(result.value, currentScan);
|
|
4078
|
+
} catch (error) {
|
|
4079
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
4080
|
+
return (0, neverthrow.err)(error);
|
|
4081
|
+
}
|
|
4082
|
+
throw error;
|
|
4083
|
+
}
|
|
4084
|
+
};
|
|
4085
|
+
/**
|
|
4086
|
+
* Asynchronous build using AsyncScheduler.
|
|
4087
|
+
* Supports async metadata factories and parallel element evaluation.
|
|
4088
|
+
*/
|
|
4089
|
+
const buildAsync = async (options$1) => {
|
|
4090
|
+
const prepResult = prepareBuildInput(options$1?.force ?? false);
|
|
4091
|
+
if (prepResult.isErr()) {
|
|
4092
|
+
return (0, neverthrow.err)(prepResult.error);
|
|
4093
|
+
}
|
|
4094
|
+
if (prepResult.value.type === "skip") {
|
|
4095
|
+
return (0, neverthrow.ok)(prepResult.value.artifact);
|
|
4096
|
+
}
|
|
4097
|
+
const { input, currentScan } = prepResult.value;
|
|
4098
|
+
const scheduler = (0, __soda_gql_common.createAsyncScheduler)();
|
|
4099
|
+
try {
|
|
4100
|
+
const result = await scheduler.run(() => buildGen(input));
|
|
4101
|
+
if (result.isErr()) {
|
|
4102
|
+
return (0, neverthrow.err)(convertSchedulerError(result.error));
|
|
4103
|
+
}
|
|
4104
|
+
return finalizeBuild(result.value, currentScan);
|
|
4105
|
+
} catch (error) {
|
|
4106
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
4107
|
+
return (0, neverthrow.err)(error);
|
|
4108
|
+
}
|
|
4109
|
+
throw error;
|
|
4110
|
+
}
|
|
4111
|
+
};
|
|
4112
|
+
return {
|
|
4113
|
+
build,
|
|
4114
|
+
buildAsync,
|
|
4115
|
+
getGeneration: () => state.gen,
|
|
4116
|
+
getCurrentArtifact: () => state.lastArtifact,
|
|
4117
|
+
getIntermediateElements: () => state.lastIntermediateElements,
|
|
4118
|
+
dispose: () => {
|
|
4119
|
+
cacheFactory.save();
|
|
4120
|
+
unregisterExitHandler(cacheFactory);
|
|
4121
|
+
}
|
|
4122
|
+
};
|
|
4123
|
+
};
|
|
4124
|
+
const prepare = (input) => {
|
|
4125
|
+
const { diff, lastArtifact, force } = input;
|
|
4126
|
+
const changedFiles = new Set([...diff.added, ...diff.updated]);
|
|
4127
|
+
const removedFiles = diff.removed;
|
|
4128
|
+
if (!force && isEmptyDiff(diff) && lastArtifact) {
|
|
4129
|
+
return (0, neverthrow.ok)({
|
|
4130
|
+
type: "should-skip",
|
|
4131
|
+
data: { artifact: lastArtifact }
|
|
4132
|
+
});
|
|
4133
|
+
}
|
|
4134
|
+
return (0, neverthrow.ok)({
|
|
4135
|
+
type: "should-build",
|
|
4136
|
+
data: {
|
|
4137
|
+
changedFiles,
|
|
4138
|
+
removedFiles
|
|
4139
|
+
}
|
|
4140
|
+
});
|
|
4141
|
+
};
|
|
4142
|
+
/**
|
|
4143
|
+
* Unified build generator that yields effects for file I/O and element evaluation.
|
|
4144
|
+
* This enables single scheduler control at the root level for both sync and async execution.
|
|
4145
|
+
*/
|
|
4146
|
+
function* buildGen(input) {
|
|
4147
|
+
const { entryPaths, astAnalyzer, discoveryCache, changedFiles, removedFiles, previousModuleAdjacency, previousIntermediateModules, graphqlSystemPath, graphqlHelper } = input;
|
|
4148
|
+
const affectedFiles = collectAffectedFiles({
|
|
4149
|
+
changedFiles,
|
|
4150
|
+
removedFiles,
|
|
4151
|
+
previousModuleAdjacency
|
|
4152
|
+
});
|
|
4153
|
+
const discoveryResult = yield* discoverModulesGen({
|
|
4154
|
+
entryPaths,
|
|
4155
|
+
astAnalyzer,
|
|
4156
|
+
incremental: {
|
|
4157
|
+
cache: discoveryCache,
|
|
4158
|
+
changedFiles,
|
|
4159
|
+
removedFiles,
|
|
4160
|
+
affectedFiles
|
|
4161
|
+
}
|
|
4162
|
+
});
|
|
4163
|
+
const { cacheHits, cacheMisses, cacheSkips } = discoveryResult;
|
|
4164
|
+
const snapshots = new Map(discoveryResult.snapshots.map((snapshot) => [snapshot.normalizedFilePath, snapshot]));
|
|
4165
|
+
const analyses = new Map(discoveryResult.snapshots.map((snapshot) => [snapshot.normalizedFilePath, snapshot.analysis]));
|
|
4166
|
+
const dependenciesValidationResult = validateModuleDependencies({
|
|
4167
|
+
analyses,
|
|
4168
|
+
graphqlSystemHelper: graphqlHelper
|
|
4169
|
+
});
|
|
4170
|
+
if (dependenciesValidationResult.isErr()) {
|
|
4171
|
+
const error = dependenciesValidationResult.error;
|
|
4172
|
+
throw builderErrors.graphMissingImport(error.chain[0], error.chain[1]);
|
|
4173
|
+
}
|
|
4174
|
+
const currentModuleAdjacency = extractModuleAdjacency({ snapshots });
|
|
4175
|
+
const stats = {
|
|
4176
|
+
hits: cacheHits,
|
|
4177
|
+
misses: cacheMisses,
|
|
4178
|
+
skips: cacheSkips
|
|
4179
|
+
};
|
|
4180
|
+
const intermediateModules = new Map(previousIntermediateModules);
|
|
4181
|
+
const targetFiles = new Set(affectedFiles);
|
|
4182
|
+
for (const filePath of analyses.keys()) {
|
|
4183
|
+
if (!previousIntermediateModules.has(filePath)) {
|
|
4184
|
+
targetFiles.add(filePath);
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
if (targetFiles.size === 0) {
|
|
4188
|
+
for (const filePath of analyses.keys()) {
|
|
4189
|
+
targetFiles.add(filePath);
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
for (const targetFilePath of targetFiles) {
|
|
4193
|
+
intermediateModules.delete(targetFilePath);
|
|
4194
|
+
}
|
|
4195
|
+
for (const intermediateModule of generateIntermediateModules({
|
|
4196
|
+
analyses,
|
|
4197
|
+
targetFiles,
|
|
4198
|
+
graphqlSystemPath
|
|
4199
|
+
})) {
|
|
4200
|
+
intermediateModules.set(intermediateModule.filePath, intermediateModule);
|
|
4201
|
+
}
|
|
4202
|
+
const elements = yield* evaluateIntermediateModulesGen({
|
|
4203
|
+
intermediateModules,
|
|
4204
|
+
graphqlSystemPath,
|
|
4205
|
+
analyses
|
|
4206
|
+
});
|
|
4207
|
+
return {
|
|
4208
|
+
snapshots,
|
|
4209
|
+
analyses,
|
|
4210
|
+
currentModuleAdjacency,
|
|
4211
|
+
intermediateModules,
|
|
4212
|
+
elements,
|
|
4213
|
+
stats
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
/**
|
|
4217
|
+
* Convert scheduler error to builder error.
|
|
4218
|
+
* If the cause is already a BuilderError, return it directly to preserve error codes.
|
|
4219
|
+
*/
|
|
4220
|
+
const convertSchedulerError = (error) => {
|
|
4221
|
+
if (error.cause && typeof error.cause === "object" && "code" in error.cause) {
|
|
4222
|
+
return error.cause;
|
|
4223
|
+
}
|
|
4224
|
+
return builderErrors.internalInvariant(error.message, "scheduler", error.cause);
|
|
4225
|
+
};
|
|
4226
|
+
|
|
4227
|
+
//#endregion
|
|
4228
|
+
//#region packages/builder/src/service.ts
|
|
4229
|
+
/**
|
|
4230
|
+
* Create a builder service instance with session support.
|
|
4231
|
+
*
|
|
4232
|
+
* The service maintains a long-lived session for incremental builds.
|
|
4233
|
+
* File changes are automatically detected using an internal file tracker.
|
|
4234
|
+
* First build() call initializes the session, subsequent calls perform
|
|
4235
|
+
* incremental builds based on detected file changes.
|
|
4236
|
+
*
|
|
4237
|
+
* Note: Empty entry arrays will produce ENTRY_NOT_FOUND errors at build time.
|
|
4238
|
+
*
|
|
4239
|
+
* @param config - Builder configuration including entry patterns, analyzer, mode, and optional debugDir
|
|
4240
|
+
* @returns BuilderService instance
|
|
4241
|
+
*/
|
|
4242
|
+
const createBuilderService = ({ config, entrypointsOverride }) => {
|
|
4243
|
+
const session = createBuilderSession({
|
|
4244
|
+
config,
|
|
4245
|
+
entrypointsOverride
|
|
4246
|
+
});
|
|
4247
|
+
return {
|
|
4248
|
+
build: (options) => session.build(options),
|
|
4249
|
+
buildAsync: (options) => session.buildAsync(options),
|
|
4250
|
+
getGeneration: () => session.getGeneration(),
|
|
4251
|
+
getCurrentArtifact: () => session.getCurrentArtifact(),
|
|
4252
|
+
getIntermediateElements: () => session.getIntermediateElements(),
|
|
4253
|
+
dispose: () => session.dispose()
|
|
4254
|
+
};
|
|
4255
|
+
};
|
|
4256
|
+
|
|
4257
|
+
//#endregion
|
|
4258
|
+
Object.defineProperty(exports, 'BuilderArtifactSchema', {
|
|
4259
|
+
enumerable: true,
|
|
4260
|
+
get: function () {
|
|
4261
|
+
return BuilderArtifactSchema;
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
Object.defineProperty(exports, 'BuilderEffects', {
|
|
4265
|
+
enumerable: true,
|
|
4266
|
+
get: function () {
|
|
4267
|
+
return BuilderEffects;
|
|
4268
|
+
}
|
|
4269
|
+
});
|
|
4270
|
+
Object.defineProperty(exports, 'FileReadEffect', {
|
|
4271
|
+
enumerable: true,
|
|
4272
|
+
get: function () {
|
|
4273
|
+
return FileReadEffect;
|
|
4274
|
+
}
|
|
4275
|
+
});
|
|
4276
|
+
Object.defineProperty(exports, 'FileStatEffect', {
|
|
4277
|
+
enumerable: true,
|
|
4278
|
+
get: function () {
|
|
4279
|
+
return FileStatEffect;
|
|
4280
|
+
}
|
|
4281
|
+
});
|
|
4282
|
+
Object.defineProperty(exports, '__clearGqlCache', {
|
|
4283
|
+
enumerable: true,
|
|
4284
|
+
get: function () {
|
|
4285
|
+
return __clearGqlCache;
|
|
4286
|
+
}
|
|
4287
|
+
});
|
|
4288
|
+
Object.defineProperty(exports, '__toESM', {
|
|
4289
|
+
enumerable: true,
|
|
4290
|
+
get: function () {
|
|
4291
|
+
return __toESM;
|
|
4292
|
+
}
|
|
4293
|
+
});
|
|
4294
|
+
Object.defineProperty(exports, 'builderErrors', {
|
|
4295
|
+
enumerable: true,
|
|
4296
|
+
get: function () {
|
|
4297
|
+
return builderErrors;
|
|
4298
|
+
}
|
|
4299
|
+
});
|
|
4300
|
+
Object.defineProperty(exports, 'collectAffectedFiles', {
|
|
4301
|
+
enumerable: true,
|
|
4302
|
+
get: function () {
|
|
4303
|
+
return collectAffectedFiles;
|
|
4304
|
+
}
|
|
4305
|
+
});
|
|
4306
|
+
Object.defineProperty(exports, 'createBuilderService', {
|
|
4307
|
+
enumerable: true,
|
|
4308
|
+
get: function () {
|
|
4309
|
+
return createBuilderService;
|
|
4310
|
+
}
|
|
4311
|
+
});
|
|
4312
|
+
Object.defineProperty(exports, 'createBuilderSession', {
|
|
4313
|
+
enumerable: true,
|
|
4314
|
+
get: function () {
|
|
4315
|
+
return createBuilderSession;
|
|
4316
|
+
}
|
|
4317
|
+
});
|
|
4318
|
+
Object.defineProperty(exports, 'createDiagnostic', {
|
|
4319
|
+
enumerable: true,
|
|
4320
|
+
get: function () {
|
|
4321
|
+
return createDiagnostic;
|
|
4322
|
+
}
|
|
4323
|
+
});
|
|
4324
|
+
Object.defineProperty(exports, 'createGraphqlSystemIdentifyHelper', {
|
|
4325
|
+
enumerable: true,
|
|
4326
|
+
get: function () {
|
|
4327
|
+
return createGraphqlSystemIdentifyHelper;
|
|
4328
|
+
}
|
|
4329
|
+
});
|
|
4330
|
+
Object.defineProperty(exports, 'createStandardDiagnostic', {
|
|
4331
|
+
enumerable: true,
|
|
4332
|
+
get: function () {
|
|
4333
|
+
return createStandardDiagnostic;
|
|
4334
|
+
}
|
|
4335
|
+
});
|
|
4336
|
+
Object.defineProperty(exports, 'diagnosticMessages', {
|
|
4337
|
+
enumerable: true,
|
|
4338
|
+
get: function () {
|
|
4339
|
+
return diagnosticMessages;
|
|
4340
|
+
}
|
|
4341
|
+
});
|
|
4342
|
+
Object.defineProperty(exports, 'executeSandbox', {
|
|
4343
|
+
enumerable: true,
|
|
4344
|
+
get: function () {
|
|
4345
|
+
return executeSandbox;
|
|
4346
|
+
}
|
|
4347
|
+
});
|
|
4348
|
+
Object.defineProperty(exports, 'extractModuleAdjacency', {
|
|
4349
|
+
enumerable: true,
|
|
4350
|
+
get: function () {
|
|
4351
|
+
return extractModuleAdjacency;
|
|
4352
|
+
}
|
|
4353
|
+
});
|
|
4354
|
+
Object.defineProperty(exports, 'formatBuilderErrorForCLI', {
|
|
4355
|
+
enumerable: true,
|
|
4356
|
+
get: function () {
|
|
4357
|
+
return formatBuilderErrorForCLI;
|
|
4358
|
+
}
|
|
4359
|
+
});
|
|
4360
|
+
Object.defineProperty(exports, 'formatBuilderErrorStructured', {
|
|
4361
|
+
enumerable: true,
|
|
4362
|
+
get: function () {
|
|
4363
|
+
return formatBuilderErrorStructured;
|
|
4364
|
+
}
|
|
4365
|
+
});
|
|
4366
|
+
Object.defineProperty(exports, 'getSeverity', {
|
|
4367
|
+
enumerable: true,
|
|
4368
|
+
get: function () {
|
|
4369
|
+
return getSeverity;
|
|
4370
|
+
}
|
|
4371
|
+
});
|
|
4372
|
+
Object.defineProperty(exports, 'loadArtifact', {
|
|
4373
|
+
enumerable: true,
|
|
4374
|
+
get: function () {
|
|
4375
|
+
return loadArtifact;
|
|
4376
|
+
}
|
|
4377
|
+
});
|
|
4378
|
+
Object.defineProperty(exports, 'loadArtifactSync', {
|
|
4379
|
+
enumerable: true,
|
|
4380
|
+
get: function () {
|
|
4381
|
+
return loadArtifactSync;
|
|
4382
|
+
}
|
|
4383
|
+
});
|
|
4384
|
+
//# sourceMappingURL=service-nHDX5qIj.cjs.map
|