@soda-gql/builder 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,15 +1,16 @@
1
- import { CanonicalIdSchema, buildAstPath, cachedFn, createCanonicalId, createCanonicalId as createCanonicalId$1, createCanonicalTracker, createCanonicalTracker as createCanonicalTracker$1, createOccurrenceTracker, createPathTracker, getPortableHasher, isExternalSpecifier, isRelativeSpecifier, normalizePath, resolveRelativeImportWithExistenceCheck, resolveRelativeImportWithReferences } from "@soda-gql/common";
1
+ import { CanonicalIdSchema, Effect, Effects, ParallelEffect, buildAstPath, cachedFn, createAsyncScheduler, createCanonicalId, createCanonicalId as createCanonicalId$1, createCanonicalTracker, createCanonicalTracker as createCanonicalTracker$1, createOccurrenceTracker, createPathTracker, createSyncScheduler, getPortableHasher, isExternalSpecifier, isRelativeSpecifier, normalizePath, resolveRelativeImportWithExistenceCheck, resolveRelativeImportWithReferences } from "@soda-gql/common";
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
2
3
  import { dirname, extname, join, normalize, resolve } from "node:path";
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import * as sandboxCore from "@soda-gql/core";
6
+ import { ComposedOperation, GqlElement, InlineOperation, Model, Slice } from "@soda-gql/core";
3
7
  import { z } from "zod";
4
8
  import { err, ok } from "neverthrow";
5
9
  import { createHash } from "node:crypto";
6
10
  import { parseSync, transformSync } from "@swc/core";
7
11
  import ts from "typescript";
8
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
12
  import fg from "fast-glob";
10
13
  import { Script, createContext } from "node:vm";
11
- import * as sandboxCore from "@soda-gql/core";
12
- import { ComposedOperation, GqlElement, InlineOperation, Model, Slice } from "@soda-gql/core";
13
14
  import * as sandboxRuntime from "@soda-gql/runtime";
14
15
 
15
16
  //#region packages/builder/src/internal/graphql-system.ts
@@ -40,7 +41,11 @@ const createGraphqlSystemIdentifyHelper = (config) => {
40
41
  const getCanonicalFileName = createGetCanonicalFileName(getUseCaseSensitiveFileNames());
41
42
  const toCanonical = (file) => {
42
43
  const resolved = resolve(file);
43
- return getCanonicalFileName(resolved);
44
+ try {
45
+ return getCanonicalFileName(realpathSync(resolved));
46
+ } catch {
47
+ return getCanonicalFileName(resolved);
48
+ }
44
49
  };
45
50
  const graphqlSystemPath = resolve(config.outdir, "index.ts");
46
51
  const canonicalGraphqlSystemPath = toCanonical(graphqlSystemPath);
@@ -68,6 +73,168 @@ const createGraphqlSystemIdentifyHelper = (config) => {
68
73
  };
69
74
  };
70
75
 
76
+ //#endregion
77
+ //#region packages/builder/src/scheduler/effects.ts
78
+ /**
79
+ * File read effect - reads a file from the filesystem.
80
+ * Works in both sync and async schedulers.
81
+ *
82
+ * @example
83
+ * const content = yield* new FileReadEffect("/path/to/file").run();
84
+ */
85
+ var FileReadEffect = class extends Effect {
86
+ constructor(path) {
87
+ super();
88
+ this.path = path;
89
+ }
90
+ _executeSync() {
91
+ return readFileSync(this.path, "utf-8");
92
+ }
93
+ _executeAsync() {
94
+ return readFile(this.path, "utf-8");
95
+ }
96
+ };
97
+ /**
98
+ * File stat effect - gets file stats from the filesystem.
99
+ * Works in both sync and async schedulers.
100
+ *
101
+ * @example
102
+ * const stats = yield* new FileStatEffect("/path/to/file").run();
103
+ */
104
+ var FileStatEffect = class extends Effect {
105
+ constructor(path) {
106
+ super();
107
+ this.path = path;
108
+ }
109
+ _executeSync() {
110
+ const stats = statSync(this.path);
111
+ return {
112
+ mtimeMs: stats.mtimeMs,
113
+ size: stats.size,
114
+ isFile: stats.isFile()
115
+ };
116
+ }
117
+ async _executeAsync() {
118
+ const stats = await stat(this.path);
119
+ return {
120
+ mtimeMs: stats.mtimeMs,
121
+ size: stats.size,
122
+ isFile: stats.isFile()
123
+ };
124
+ }
125
+ };
126
+ /**
127
+ * File read effect that returns null if file doesn't exist.
128
+ * Useful for discovery where missing files are expected.
129
+ */
130
+ var OptionalFileReadEffect = class extends Effect {
131
+ constructor(path) {
132
+ super();
133
+ this.path = path;
134
+ }
135
+ _executeSync() {
136
+ try {
137
+ return readFileSync(this.path, "utf-8");
138
+ } catch (error) {
139
+ if (error.code === "ENOENT") {
140
+ return null;
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+ async _executeAsync() {
146
+ try {
147
+ return await readFile(this.path, "utf-8");
148
+ } catch (error) {
149
+ if (error.code === "ENOENT") {
150
+ return null;
151
+ }
152
+ throw error;
153
+ }
154
+ }
155
+ };
156
+ /**
157
+ * File stat effect that returns null if file doesn't exist.
158
+ * Useful for discovery where missing files are expected.
159
+ */
160
+ var OptionalFileStatEffect = class extends Effect {
161
+ constructor(path) {
162
+ super();
163
+ this.path = path;
164
+ }
165
+ _executeSync() {
166
+ try {
167
+ const stats = statSync(this.path);
168
+ return {
169
+ mtimeMs: stats.mtimeMs,
170
+ size: stats.size,
171
+ isFile: stats.isFile()
172
+ };
173
+ } catch (error) {
174
+ if (error.code === "ENOENT") {
175
+ return null;
176
+ }
177
+ throw error;
178
+ }
179
+ }
180
+ async _executeAsync() {
181
+ try {
182
+ const stats = await stat(this.path);
183
+ return {
184
+ mtimeMs: stats.mtimeMs,
185
+ size: stats.size,
186
+ isFile: stats.isFile()
187
+ };
188
+ } catch (error) {
189
+ if (error.code === "ENOENT") {
190
+ return null;
191
+ }
192
+ throw error;
193
+ }
194
+ }
195
+ };
196
+ /**
197
+ * Element evaluation effect - evaluates a GqlElement using its generator.
198
+ * Supports both sync and async schedulers, enabling parallel element evaluation
199
+ * when using async scheduler.
200
+ *
201
+ * @example
202
+ * yield* new ElementEvaluationEffect(element).run();
203
+ */
204
+ var ElementEvaluationEffect = class extends Effect {
205
+ constructor(element) {
206
+ super();
207
+ this.element = element;
208
+ }
209
+ _executeSync() {
210
+ const generator = GqlElement.createEvaluationGenerator(this.element);
211
+ const result = generator.next();
212
+ while (!result.done) {
213
+ throw new Error("Async operation required during sync element evaluation");
214
+ }
215
+ }
216
+ async _executeAsync() {
217
+ const generator = GqlElement.createEvaluationGenerator(this.element);
218
+ let result = generator.next();
219
+ while (!result.done) {
220
+ await result.value;
221
+ result = generator.next();
222
+ }
223
+ }
224
+ };
225
+ /**
226
+ * Builder effect constructors.
227
+ * Extends the base Effects with file I/O operations and element evaluation.
228
+ */
229
+ const BuilderEffects = {
230
+ ...Effects,
231
+ readFile: (path) => new FileReadEffect(path),
232
+ stat: (path) => new FileStatEffect(path),
233
+ readFileOptional: (path) => new OptionalFileReadEffect(path),
234
+ statOptional: (path) => new OptionalFileStatEffect(path),
235
+ evaluateElement: (element) => new ElementEvaluationEffect(element)
236
+ };
237
+
71
238
  //#endregion
72
239
  //#region packages/builder/src/schemas/artifact.ts
73
240
  const BuilderArtifactElementMetadataSchema = z.object({
@@ -189,7 +356,8 @@ const aggregate = ({ analyses, elements }) => {
189
356
  operationName: element.element.operationName,
190
357
  document: element.element.document,
191
358
  variableNames: element.element.variableNames,
192
- projectionPathGraph: element.element.projectionPathGraph
359
+ projectionPathGraph: element.element.projectionPathGraph,
360
+ metadata: element.element.metadata
193
361
  };
194
362
  registry.set(definition.canonicalId, {
195
363
  id: definition.canonicalId,
@@ -207,7 +375,8 @@ const aggregate = ({ analyses, elements }) => {
207
375
  operationType: element.element.operationType,
208
376
  operationName: element.element.operationName,
209
377
  document: element.element.document,
210
- variableNames: element.element.variableNames
378
+ variableNames: element.element.variableNames,
379
+ metadata: element.element.metadata
211
380
  };
212
381
  registry.set(definition.canonicalId, {
213
382
  id: definition.canonicalId,
@@ -929,58 +1098,41 @@ const collectAllDefinitions$1 = ({ module, gqlIdentifiers, imports: _imports, ex
929
1098
  };
930
1099
  };
931
1100
  /**
932
- * Collect diagnostics (now empty since we support all definition types)
1101
+ * SWC adapter implementation.
1102
+ * The analyze method parses and collects all data in one pass,
1103
+ * ensuring the AST (Module) is released after analysis.
933
1104
  */
934
- const collectDiagnostics$1 = () => {
935
- return [];
936
- };
937
- /**
938
- * SWC adapter implementation
939
- */
940
- const swcAdapter = {
941
- parse(input) {
942
- const program = parseSync(input.source, {
943
- syntax: "typescript",
944
- tsx: input.filePath.endsWith(".tsx"),
945
- target: "es2022",
946
- decorators: false,
947
- dynamicImport: true
948
- });
949
- if (program.type !== "Module") {
950
- return null;
951
- }
952
- const swcModule = program;
953
- swcModule.__filePath = input.filePath;
954
- return swcModule;
955
- },
956
- collectGqlIdentifiers(file, helper) {
957
- return collectGqlIdentifiers(file, helper);
958
- },
959
- collectImports(file) {
960
- return collectImports$1(file);
961
- },
962
- collectExports(file) {
963
- return collectExports$1(file);
964
- },
965
- collectDefinitions(file, context) {
966
- const resolvePosition = toPositionResolver(context.source);
967
- const { definitions, handledCalls } = collectAllDefinitions$1({
968
- module: file,
969
- gqlIdentifiers: context.gqlIdentifiers,
970
- imports: context.imports,
971
- exports: context.exports,
972
- resolvePosition,
973
- source: context.source
974
- });
975
- return {
976
- definitions,
977
- handles: handledCalls
978
- };
979
- },
980
- collectDiagnostics(_file, _context) {
981
- return collectDiagnostics$1();
1105
+ const swcAdapter = { analyze(input, helper) {
1106
+ const program = parseSync(input.source, {
1107
+ syntax: "typescript",
1108
+ tsx: input.filePath.endsWith(".tsx"),
1109
+ target: "es2022",
1110
+ decorators: false,
1111
+ dynamicImport: true
1112
+ });
1113
+ if (program.type !== "Module") {
1114
+ return null;
982
1115
  }
983
- };
1116
+ const swcModule = program;
1117
+ swcModule.__filePath = input.filePath;
1118
+ const gqlIdentifiers = collectGqlIdentifiers(swcModule, helper);
1119
+ const imports = collectImports$1(swcModule);
1120
+ const exports = collectExports$1(swcModule);
1121
+ const resolvePosition = toPositionResolver(input.source);
1122
+ const { definitions } = collectAllDefinitions$1({
1123
+ module: swcModule,
1124
+ gqlIdentifiers,
1125
+ imports,
1126
+ exports,
1127
+ resolvePosition,
1128
+ source: input.source
1129
+ });
1130
+ return {
1131
+ imports,
1132
+ exports,
1133
+ definitions
1134
+ };
1135
+ } };
984
1136
 
985
1137
  //#endregion
986
1138
  //#region packages/builder/src/ast/adapters/typescript.ts
@@ -1313,42 +1465,26 @@ const collectAllDefinitions = ({ sourceFile, identifiers, exports }) => {
1313
1465
  };
1314
1466
  };
1315
1467
  /**
1316
- * Collect diagnostics (now empty since we support all definition types)
1317
- */
1318
- const collectDiagnostics = (_sourceFile, _identifiers, _handledCalls) => {
1319
- return [];
1320
- };
1321
- /**
1322
- * TypeScript adapter implementation
1468
+ * TypeScript adapter implementation.
1469
+ * The analyze method parses and collects all data in one pass,
1470
+ * ensuring the AST (ts.SourceFile) is released after analysis.
1323
1471
  */
1324
- const typescriptAdapter = {
1325
- parse(input) {
1326
- return createSourceFile(input.filePath, input.source);
1327
- },
1328
- collectGqlIdentifiers(file, helper) {
1329
- return collectGqlImports(file, helper);
1330
- },
1331
- collectImports(file) {
1332
- return collectImports(file);
1333
- },
1334
- collectExports(file) {
1335
- return collectExports(file);
1336
- },
1337
- collectDefinitions(file, context) {
1338
- const { definitions, handledCalls } = collectAllDefinitions({
1339
- sourceFile: file,
1340
- identifiers: context.gqlIdentifiers,
1341
- exports: context.exports
1342
- });
1343
- return {
1344
- definitions,
1345
- handles: handledCalls
1346
- };
1347
- },
1348
- collectDiagnostics(file, context) {
1349
- return collectDiagnostics(file, context.gqlIdentifiers, context.handledCalls);
1350
- }
1351
- };
1472
+ const typescriptAdapter = { analyze(input, helper) {
1473
+ const sourceFile = createSourceFile(input.filePath, input.source);
1474
+ const gqlIdentifiers = collectGqlImports(sourceFile, helper);
1475
+ const imports = collectImports(sourceFile);
1476
+ const exports = collectExports(sourceFile);
1477
+ const { definitions } = collectAllDefinitions({
1478
+ sourceFile,
1479
+ identifiers: gqlIdentifiers,
1480
+ exports
1481
+ });
1482
+ return {
1483
+ imports,
1484
+ exports,
1485
+ definitions
1486
+ };
1487
+ } };
1352
1488
 
1353
1489
  //#endregion
1354
1490
  //#region packages/builder/src/ast/core.ts
@@ -1363,38 +1499,22 @@ const typescriptAdapter = {
1363
1499
  const analyzeModuleCore = (input, adapter, graphqlHelper) => {
1364
1500
  const hasher = getPortableHasher();
1365
1501
  const signature = hasher.hash(input.source, "xxhash");
1366
- const file = adapter.parse(input);
1367
- if (!file) {
1502
+ const result = adapter.analyze(input, graphqlHelper);
1503
+ if (!result) {
1368
1504
  return {
1369
1505
  filePath: input.filePath,
1370
1506
  signature,
1371
1507
  definitions: [],
1372
- diagnostics: [],
1373
1508
  imports: [],
1374
1509
  exports: []
1375
1510
  };
1376
1511
  }
1377
- const gqlIdentifiers = adapter.collectGqlIdentifiers(file, graphqlHelper);
1378
- const imports = adapter.collectImports(file);
1379
- const exports = adapter.collectExports(file);
1380
- const { definitions, handles } = adapter.collectDefinitions(file, {
1381
- gqlIdentifiers,
1382
- imports,
1383
- exports,
1384
- source: input.source
1385
- });
1386
- const diagnostics = adapter.collectDiagnostics(file, {
1387
- gqlIdentifiers,
1388
- handledCalls: handles,
1389
- source: input.source
1390
- });
1391
1512
  return {
1392
1513
  filePath: input.filePath,
1393
1514
  signature,
1394
- definitions,
1395
- diagnostics,
1396
- imports,
1397
- exports
1515
+ definitions: result.definitions,
1516
+ imports: result.imports,
1517
+ exports: result.exports
1398
1518
  };
1399
1519
  };
1400
1520
 
@@ -1655,11 +1775,6 @@ const ModuleDefinitionSchema = z.object({
1655
1775
  loc: SourceLocationSchema,
1656
1776
  expression: z.string()
1657
1777
  });
1658
- const ModuleDiagnosticSchema = z.object({
1659
- code: z.literal("NON_TOP_LEVEL_DEFINITION"),
1660
- message: z.string(),
1661
- loc: SourceLocationSchema
1662
- });
1663
1778
  const ModuleImportSchema = z.object({
1664
1779
  source: z.string(),
1665
1780
  imported: z.string(),
@@ -1688,7 +1803,6 @@ const ModuleAnalysisSchema = z.object({
1688
1803
  filePath: z.string(),
1689
1804
  signature: z.string(),
1690
1805
  definitions: z.array(ModuleDefinitionSchema).readonly(),
1691
- diagnostics: z.array(ModuleDiagnosticSchema).readonly(),
1692
1806
  imports: z.array(ModuleImportSchema).readonly(),
1693
1807
  exports: z.array(ModuleExportSchema).readonly()
1694
1808
  });
@@ -1892,6 +2006,30 @@ function simpleHash(buffer) {
1892
2006
  return hash.toString(16);
1893
2007
  }
1894
2008
  /**
2009
+ * Compute fingerprint from pre-read file content and stats.
2010
+ * This is used by the generator-based discoverer which already has the content.
2011
+ *
2012
+ * @param path - Absolute path to file (for caching)
2013
+ * @param stats - File stats (mtimeMs, size)
2014
+ * @param content - File content as string
2015
+ * @returns FileFingerprint
2016
+ */
2017
+ function computeFingerprintFromContent(path, stats, content) {
2018
+ const cached = fingerprintCache.get(path);
2019
+ if (cached && cached.mtimeMs === stats.mtimeMs) {
2020
+ return cached;
2021
+ }
2022
+ const buffer = Buffer.from(content, "utf-8");
2023
+ const hash = computeHashSync(buffer);
2024
+ const fingerprint = {
2025
+ hash,
2026
+ sizeBytes: stats.size,
2027
+ mtimeMs: stats.mtimeMs
2028
+ };
2029
+ fingerprintCache.set(path, fingerprint);
2030
+ return fingerprint;
2031
+ }
2032
+ /**
1895
2033
  * Invalidate cached fingerprint for a specific path
1896
2034
  *
1897
2035
  * @param path - Absolute path to invalidate
@@ -1909,11 +2047,10 @@ function clearFingerprintCache() {
1909
2047
  //#endregion
1910
2048
  //#region packages/builder/src/discovery/discoverer.ts
1911
2049
  /**
1912
- * Discover and analyze all modules starting from entry points.
1913
- * Uses AST parsing instead of RegExp for reliable dependency extraction.
1914
- * Supports caching with fingerprint-based invalidation to skip re-parsing unchanged files.
2050
+ * Generator-based module discovery that yields effects for file I/O.
2051
+ * This allows the discovery process to be executed with either sync or async schedulers.
1915
2052
  */
1916
- const discoverModules = ({ entryPaths, astAnalyzer, incremental }) => {
2053
+ function* discoverModulesGen({ entryPaths, astAnalyzer, incremental }) {
1917
2054
  const snapshots = new Map();
1918
2055
  const stack = [...entryPaths];
1919
2056
  const changedFiles = incremental?.changedFiles ?? new Set();
@@ -1942,16 +2079,17 @@ const discoverModules = ({ entryPaths, astAnalyzer, incremental }) => {
1942
2079
  if (snapshots.has(filePath)) {
1943
2080
  continue;
1944
2081
  }
2082
+ let shouldReadFile = true;
1945
2083
  if (invalidatedSet.has(filePath)) {
1946
2084
  invalidateFingerprint(filePath);
1947
2085
  cacheSkips++;
1948
2086
  } else if (incremental) {
1949
2087
  const cached = incremental.cache.peek(filePath);
1950
2088
  if (cached) {
1951
- try {
1952
- const stats = statSync(filePath);
1953
- const mtimeMs = stats.mtimeMs;
1954
- const sizeBytes = stats.size;
2089
+ const stats$1 = yield* new OptionalFileStatEffect(filePath).run();
2090
+ if (stats$1) {
2091
+ const mtimeMs = stats$1.mtimeMs;
2092
+ const sizeBytes = stats$1.size;
1955
2093
  if (cached.fingerprint.mtimeMs === mtimeMs && cached.fingerprint.sizeBytes === sizeBytes) {
1956
2094
  snapshots.set(filePath, cached);
1957
2095
  cacheHits++;
@@ -1960,21 +2098,19 @@ const discoverModules = ({ entryPaths, astAnalyzer, incremental }) => {
1960
2098
  stack.push(dep.resolvedPath);
1961
2099
  }
1962
2100
  }
1963
- continue;
2101
+ shouldReadFile = false;
1964
2102
  }
1965
- } catch {}
2103
+ }
1966
2104
  }
1967
2105
  }
1968
- let source;
1969
- try {
1970
- source = readFileSync(filePath, "utf8");
1971
- } catch (error) {
1972
- if (error.code === "ENOENT") {
1973
- incremental?.cache.delete(filePath);
1974
- invalidateFingerprint(filePath);
1975
- continue;
1976
- }
1977
- return err(builderErrors.discoveryIOError(filePath, error instanceof Error ? error.message : String(error)));
2106
+ if (!shouldReadFile) {
2107
+ continue;
2108
+ }
2109
+ const source = yield* new OptionalFileReadEffect(filePath).run();
2110
+ if (source === null) {
2111
+ incremental?.cache.delete(filePath);
2112
+ invalidateFingerprint(filePath);
2113
+ continue;
1978
2114
  }
1979
2115
  const signature = createSourceHash(source);
1980
2116
  const analysis = astAnalyzer.analyze({
@@ -1988,11 +2124,8 @@ const discoverModules = ({ entryPaths, astAnalyzer, incremental }) => {
1988
2124
  stack.push(dep.resolvedPath);
1989
2125
  }
1990
2126
  }
1991
- const fingerprintResult = computeFingerprint(filePath);
1992
- if (fingerprintResult.isErr()) {
1993
- return err(builderErrors.discoveryIOError(filePath, `Failed to compute fingerprint: ${fingerprintResult.error.message}`));
1994
- }
1995
- const fingerprint = fingerprintResult.value;
2127
+ const stats = yield* new OptionalFileStatEffect(filePath).run();
2128
+ const fingerprint = computeFingerprintFromContent(filePath, stats, source);
1996
2129
  const snapshot = {
1997
2130
  filePath,
1998
2131
  normalizedFilePath: normalizePath(filePath),
@@ -2008,12 +2141,44 @@ const discoverModules = ({ entryPaths, astAnalyzer, incremental }) => {
2008
2141
  incremental.cache.store(snapshot);
2009
2142
  }
2010
2143
  }
2011
- return ok({
2144
+ return {
2012
2145
  snapshots: Array.from(snapshots.values()),
2013
2146
  cacheHits,
2014
2147
  cacheMisses,
2015
2148
  cacheSkips
2016
- });
2149
+ };
2150
+ }
2151
+ /**
2152
+ * Discover and analyze all modules starting from entry points.
2153
+ * Uses AST parsing instead of RegExp for reliable dependency extraction.
2154
+ * Supports caching with fingerprint-based invalidation to skip re-parsing unchanged files.
2155
+ *
2156
+ * This function uses the synchronous scheduler internally for backward compatibility.
2157
+ * For async execution with parallel file I/O, use discoverModulesGen with an async scheduler.
2158
+ */
2159
+ const discoverModules = (options) => {
2160
+ const scheduler = createSyncScheduler();
2161
+ const result = scheduler.run(() => discoverModulesGen(options));
2162
+ if (result.isErr()) {
2163
+ const error = result.error;
2164
+ return err(builderErrors.discoveryIOError("unknown", error.message));
2165
+ }
2166
+ return ok(result.value);
2167
+ };
2168
+ /**
2169
+ * Asynchronous version of discoverModules.
2170
+ * Uses async scheduler for parallel file I/O operations.
2171
+ *
2172
+ * This is useful for large codebases where parallel file operations can improve performance.
2173
+ */
2174
+ const discoverModulesAsync = async (options) => {
2175
+ const scheduler = createAsyncScheduler();
2176
+ const result = await scheduler.run(() => discoverModulesGen(options));
2177
+ if (result.isErr()) {
2178
+ const error = result.error;
2179
+ return err(builderErrors.discoveryIOError("unknown", error.message));
2180
+ }
2181
+ return ok(result.value);
2017
2182
  };
2018
2183
 
2019
2184
  //#endregion
@@ -2395,17 +2560,10 @@ const createIntermediateRegistry = ({ analyses } = {}) => {
2395
2560
  }
2396
2561
  return result;
2397
2562
  };
2398
- const evaluate = () => {
2399
- const evaluated = new Map();
2400
- const inProgress = new Set();
2401
- for (const filePath of modules.keys()) {
2402
- if (!evaluated.has(filePath)) {
2403
- evaluateModule(filePath, evaluated, inProgress);
2404
- }
2405
- }
2406
- for (const element of elements.values()) {
2407
- GqlElement.evaluate(element);
2408
- }
2563
+ /**
2564
+ * Build artifacts record from evaluated elements.
2565
+ */
2566
+ const buildArtifacts = () => {
2409
2567
  const artifacts = {};
2410
2568
  for (const [canonicalId, element] of elements.entries()) {
2411
2569
  if (element instanceof Model) {
@@ -2432,6 +2590,76 @@ const createIntermediateRegistry = ({ analyses } = {}) => {
2432
2590
  }
2433
2591
  return artifacts;
2434
2592
  };
2593
+ /**
2594
+ * Generator that evaluates all elements using the effect system.
2595
+ * Uses ParallelEffect to enable parallel evaluation in async mode.
2596
+ * In sync mode, ParallelEffect executes effects sequentially.
2597
+ */
2598
+ function* evaluateElementsGen() {
2599
+ const effects = Array.from(elements.values(), (element) => new ElementEvaluationEffect(element));
2600
+ if (effects.length > 0) {
2601
+ yield* new ParallelEffect(effects).run();
2602
+ }
2603
+ }
2604
+ /**
2605
+ * Synchronous evaluation - evaluates all modules and elements synchronously.
2606
+ * Throws if any element requires async operations (e.g., async metadata factory).
2607
+ */
2608
+ const evaluate = () => {
2609
+ const evaluated = new Map();
2610
+ const inProgress = new Set();
2611
+ for (const filePath of modules.keys()) {
2612
+ if (!evaluated.has(filePath)) {
2613
+ evaluateModule(filePath, evaluated, inProgress);
2614
+ }
2615
+ }
2616
+ const scheduler = createSyncScheduler();
2617
+ const result = scheduler.run(() => evaluateElementsGen());
2618
+ if (result.isErr()) {
2619
+ throw new Error(`Element evaluation failed: ${result.error.message}`);
2620
+ }
2621
+ return buildArtifacts();
2622
+ };
2623
+ /**
2624
+ * Asynchronous evaluation - evaluates all modules and elements with async support.
2625
+ * Supports async metadata factories and other async operations.
2626
+ */
2627
+ const evaluateAsync = async () => {
2628
+ const evaluated = new Map();
2629
+ const inProgress = new Set();
2630
+ for (const filePath of modules.keys()) {
2631
+ if (!evaluated.has(filePath)) {
2632
+ evaluateModule(filePath, evaluated, inProgress);
2633
+ }
2634
+ }
2635
+ const scheduler = createAsyncScheduler();
2636
+ const result = await scheduler.run(() => evaluateElementsGen());
2637
+ if (result.isErr()) {
2638
+ throw new Error(`Element evaluation failed: ${result.error.message}`);
2639
+ }
2640
+ return buildArtifacts();
2641
+ };
2642
+ /**
2643
+ * Evaluate all modules synchronously using trampoline.
2644
+ * This runs the module dependency resolution without element evaluation.
2645
+ * Call this before getElements() when using external scheduler control.
2646
+ */
2647
+ const evaluateModules = () => {
2648
+ const evaluated = new Map();
2649
+ const inProgress = new Set();
2650
+ for (const filePath of modules.keys()) {
2651
+ if (!evaluated.has(filePath)) {
2652
+ evaluateModule(filePath, evaluated, inProgress);
2653
+ }
2654
+ }
2655
+ };
2656
+ /**
2657
+ * Get all registered elements for external effect creation.
2658
+ * Call evaluateModules() first to ensure all modules have been evaluated.
2659
+ */
2660
+ const getElements = () => {
2661
+ return Array.from(elements.values());
2662
+ };
2435
2663
  const clear = () => {
2436
2664
  modules.clear();
2437
2665
  elements.clear();
@@ -2441,6 +2669,10 @@ const createIntermediateRegistry = ({ analyses } = {}) => {
2441
2669
  requestImport,
2442
2670
  addElement,
2443
2671
  evaluate,
2672
+ evaluateAsync,
2673
+ evaluateModules,
2674
+ getElements,
2675
+ buildArtifacts,
2444
2676
  clear
2445
2677
  };
2446
2678
  };
@@ -2574,7 +2806,11 @@ const generateIntermediateModules = function* ({ analyses, targetFiles, graphqlS
2574
2806
  };
2575
2807
  }
2576
2808
  };
2577
- const evaluateIntermediateModules = ({ intermediateModules, graphqlSystemPath, analyses }) => {
2809
+ /**
2810
+ * Set up VM context and run intermediate module scripts.
2811
+ * Returns the registry for evaluation.
2812
+ */
2813
+ const setupIntermediateModulesContext = ({ intermediateModules, graphqlSystemPath, analyses }) => {
2578
2814
  const registry = createIntermediateRegistry({ analyses });
2579
2815
  const gqlImportPath = resolveGraphqlSystemPath(graphqlSystemPath);
2580
2816
  const { gql } = executeGraphqlSystemModule(gqlImportPath);
@@ -2590,10 +2826,50 @@ const evaluateIntermediateModules = ({ intermediateModules, graphqlSystemPath, a
2590
2826
  throw error;
2591
2827
  }
2592
2828
  }
2829
+ return registry;
2830
+ };
2831
+ /**
2832
+ * Synchronous evaluation of intermediate modules.
2833
+ * Throws if any element requires async operations (e.g., async metadata factory).
2834
+ */
2835
+ const evaluateIntermediateModules = (input) => {
2836
+ const registry = setupIntermediateModulesContext(input);
2593
2837
  const elements = registry.evaluate();
2594
2838
  registry.clear();
2595
2839
  return elements;
2596
2840
  };
2841
+ /**
2842
+ * Asynchronous evaluation of intermediate modules.
2843
+ * Supports async metadata factories and other async operations.
2844
+ */
2845
+ const evaluateIntermediateModulesAsync = async (input) => {
2846
+ const registry = setupIntermediateModulesContext(input);
2847
+ const elements = await registry.evaluateAsync();
2848
+ registry.clear();
2849
+ return elements;
2850
+ };
2851
+ /**
2852
+ * Generator version of evaluateIntermediateModules for external scheduler control.
2853
+ * Yields effects for element evaluation, enabling unified scheduler at the root level.
2854
+ *
2855
+ * This function:
2856
+ * 1. Sets up the VM context and runs intermediate module scripts
2857
+ * 2. Runs synchronous module evaluation (trampoline - no I/O)
2858
+ * 3. Yields element evaluation effects via ParallelEffect
2859
+ * 4. Returns the artifacts record
2860
+ */
2861
+ function* evaluateIntermediateModulesGen(input) {
2862
+ const registry = setupIntermediateModulesContext(input);
2863
+ registry.evaluateModules();
2864
+ const elements = registry.getElements();
2865
+ const effects = elements.map((element) => new ElementEvaluationEffect(element));
2866
+ if (effects.length > 0) {
2867
+ yield* new ParallelEffect(effects).run();
2868
+ }
2869
+ const artifacts = registry.buildArtifacts();
2870
+ registry.clear();
2871
+ return artifacts;
2872
+ }
2597
2873
 
2598
2874
  //#endregion
2599
2875
  //#region packages/builder/src/tracker/file-tracker.ts
@@ -2667,13 +2943,19 @@ const isEmptyDiff = (diff) => {
2667
2943
 
2668
2944
  //#endregion
2669
2945
  //#region packages/builder/src/session/dependency-validation.ts
2670
- const validateModuleDependencies = ({ analyses }) => {
2946
+ const validateModuleDependencies = ({ analyses, graphqlSystemHelper }) => {
2671
2947
  for (const analysis of analyses.values()) {
2672
2948
  for (const { source, isTypeOnly } of analysis.imports) {
2673
2949
  if (isTypeOnly) {
2674
2950
  continue;
2675
2951
  }
2676
2952
  if (isRelativeSpecifier(source)) {
2953
+ if (graphqlSystemHelper.isGraphqlSystemImportSpecifier({
2954
+ filePath: analysis.filePath,
2955
+ specifier: source
2956
+ })) {
2957
+ continue;
2958
+ }
2677
2959
  const resolvedModule = resolveRelativeImportWithReferences({
2678
2960
  filePath: analysis.filePath,
2679
2961
  specifier: source,
@@ -2809,8 +3091,11 @@ const createBuilderSession = (options) => {
2809
3091
  evaluatorId
2810
3092
  }));
2811
3093
  const ensureFileTracker = cachedFn(() => createFileTracker());
2812
- const build = (options$1) => {
2813
- const force = options$1?.force ?? false;
3094
+ /**
3095
+ * Prepare build input. Shared between sync and async builds.
3096
+ * Returns either a skip result or the input for buildGen.
3097
+ */
3098
+ const prepareBuildInput = (force) => {
2814
3099
  const entryPathsResult = resolveEntryPaths(Array.from(entrypoints));
2815
3100
  if (entryPathsResult.isErr()) {
2816
3101
  return err(entryPathsResult.error);
@@ -2834,44 +3119,106 @@ const createBuilderSession = (options) => {
2834
3119
  return err(prepareResult.error);
2835
3120
  }
2836
3121
  if (prepareResult.value.type === "should-skip") {
2837
- return ok(prepareResult.value.data.artifact);
3122
+ return ok({
3123
+ type: "skip",
3124
+ artifact: prepareResult.value.data.artifact
3125
+ });
2838
3126
  }
2839
3127
  const { changedFiles, removedFiles } = prepareResult.value.data;
2840
- const discoveryCache = ensureDiscoveryCache();
2841
- const astAnalyzer = ensureAstAnalyzer();
2842
- const discoveryResult = discover({
2843
- discoveryCache,
2844
- astAnalyzer,
2845
- removedFiles,
2846
- changedFiles,
2847
- entryPaths,
2848
- previousModuleAdjacency: state.moduleAdjacency
3128
+ return ok({
3129
+ type: "build",
3130
+ input: {
3131
+ entryPaths,
3132
+ astAnalyzer: ensureAstAnalyzer(),
3133
+ discoveryCache: ensureDiscoveryCache(),
3134
+ changedFiles,
3135
+ removedFiles,
3136
+ previousModuleAdjacency: state.moduleAdjacency,
3137
+ previousIntermediateModules: state.intermediateModules,
3138
+ graphqlSystemPath: resolve(config.outdir, "index.ts"),
3139
+ graphqlHelper
3140
+ },
3141
+ currentScan
2849
3142
  });
2850
- if (discoveryResult.isErr()) {
2851
- return err(discoveryResult.error);
2852
- }
2853
- const { snapshots, analyses, currentModuleAdjacency, affectedFiles, stats } = discoveryResult.value;
2854
- const buildResult = buildDiscovered({
3143
+ };
3144
+ /**
3145
+ * Finalize build and update session state.
3146
+ */
3147
+ const finalizeBuild = (genResult, currentScan) => {
3148
+ const { snapshots, analyses, currentModuleAdjacency, intermediateModules, elements, stats } = genResult;
3149
+ const artifactResult = buildArtifact({
2855
3150
  analyses,
2856
- affectedFiles,
2857
- stats,
2858
- previousIntermediateModules: state.intermediateModules,
2859
- graphqlSystemPath: resolve(config.outdir, "index.ts")
3151
+ elements,
3152
+ stats
2860
3153
  });
2861
- if (buildResult.isErr()) {
2862
- return err(buildResult.error);
3154
+ if (artifactResult.isErr()) {
3155
+ return err(artifactResult.error);
2863
3156
  }
2864
- const { intermediateModules, artifact } = buildResult.value;
2865
3157
  state.gen++;
2866
3158
  state.snapshots = snapshots;
2867
3159
  state.moduleAdjacency = currentModuleAdjacency;
2868
- state.lastArtifact = artifact;
3160
+ state.lastArtifact = artifactResult.value;
2869
3161
  state.intermediateModules = intermediateModules;
2870
- tracker.update(currentScan);
2871
- return ok(artifact);
3162
+ ensureFileTracker().update(currentScan);
3163
+ return ok(artifactResult.value);
3164
+ };
3165
+ /**
3166
+ * Synchronous build using SyncScheduler.
3167
+ * Throws if any element requires async operations.
3168
+ */
3169
+ const build = (options$1) => {
3170
+ const prepResult = prepareBuildInput(options$1?.force ?? false);
3171
+ if (prepResult.isErr()) {
3172
+ return err(prepResult.error);
3173
+ }
3174
+ if (prepResult.value.type === "skip") {
3175
+ return ok(prepResult.value.artifact);
3176
+ }
3177
+ const { input, currentScan } = prepResult.value;
3178
+ const scheduler = createSyncScheduler();
3179
+ try {
3180
+ const result = scheduler.run(() => buildGen(input));
3181
+ if (result.isErr()) {
3182
+ return err(convertSchedulerError(result.error));
3183
+ }
3184
+ return finalizeBuild(result.value, currentScan);
3185
+ } catch (error) {
3186
+ if (error && typeof error === "object" && "code" in error) {
3187
+ return err(error);
3188
+ }
3189
+ throw error;
3190
+ }
3191
+ };
3192
+ /**
3193
+ * Asynchronous build using AsyncScheduler.
3194
+ * Supports async metadata factories and parallel element evaluation.
3195
+ */
3196
+ const buildAsync = async (options$1) => {
3197
+ const prepResult = prepareBuildInput(options$1?.force ?? false);
3198
+ if (prepResult.isErr()) {
3199
+ return err(prepResult.error);
3200
+ }
3201
+ if (prepResult.value.type === "skip") {
3202
+ return ok(prepResult.value.artifact);
3203
+ }
3204
+ const { input, currentScan } = prepResult.value;
3205
+ const scheduler = createAsyncScheduler();
3206
+ try {
3207
+ const result = await scheduler.run(() => buildGen(input));
3208
+ if (result.isErr()) {
3209
+ return err(convertSchedulerError(result.error));
3210
+ }
3211
+ return finalizeBuild(result.value, currentScan);
3212
+ } catch (error) {
3213
+ if (error && typeof error === "object" && "code" in error) {
3214
+ return err(error);
3215
+ }
3216
+ throw error;
3217
+ }
2872
3218
  };
2873
3219
  return {
2874
3220
  build,
3221
+ buildAsync,
2875
3222
  getGeneration: () => state.gen,
2876
3223
  getCurrentArtifact: () => state.lastArtifact,
2877
3224
  dispose: () => {
@@ -2897,13 +3244,18 @@ const prepare = (input) => {
2897
3244
  }
2898
3245
  });
2899
3246
  };
2900
- const discover = ({ discoveryCache, astAnalyzer, removedFiles, changedFiles, entryPaths, previousModuleAdjacency }) => {
3247
+ /**
3248
+ * Unified build generator that yields effects for file I/O and element evaluation.
3249
+ * This enables single scheduler control at the root level for both sync and async execution.
3250
+ */
3251
+ function* buildGen(input) {
3252
+ const { entryPaths, astAnalyzer, discoveryCache, changedFiles, removedFiles, previousModuleAdjacency, previousIntermediateModules, graphqlSystemPath, graphqlHelper } = input;
2901
3253
  const affectedFiles = collectAffectedFiles({
2902
3254
  changedFiles,
2903
3255
  removedFiles,
2904
3256
  previousModuleAdjacency
2905
3257
  });
2906
- const discoveryResult = discoverModules({
3258
+ const discoveryResult = yield* discoverModulesGen({
2907
3259
  entryPaths,
2908
3260
  astAnalyzer,
2909
3261
  incremental: {
@@ -2913,16 +3265,16 @@ const discover = ({ discoveryCache, astAnalyzer, removedFiles, changedFiles, ent
2913
3265
  affectedFiles
2914
3266
  }
2915
3267
  });
2916
- if (discoveryResult.isErr()) {
2917
- return err(discoveryResult.error);
2918
- }
2919
- const { cacheHits, cacheMisses, cacheSkips } = discoveryResult.value;
2920
- const snapshots = new Map(discoveryResult.value.snapshots.map((snapshot) => [snapshot.normalizedFilePath, snapshot]));
2921
- const analyses = new Map(discoveryResult.value.snapshots.map((snapshot) => [snapshot.normalizedFilePath, snapshot.analysis]));
2922
- const dependenciesValidationResult = validateModuleDependencies({ analyses });
3268
+ const { cacheHits, cacheMisses, cacheSkips } = discoveryResult;
3269
+ const snapshots = new Map(discoveryResult.snapshots.map((snapshot) => [snapshot.normalizedFilePath, snapshot]));
3270
+ const analyses = new Map(discoveryResult.snapshots.map((snapshot) => [snapshot.normalizedFilePath, snapshot.analysis]));
3271
+ const dependenciesValidationResult = validateModuleDependencies({
3272
+ analyses,
3273
+ graphqlSystemHelper: graphqlHelper
3274
+ });
2923
3275
  if (dependenciesValidationResult.isErr()) {
2924
3276
  const error = dependenciesValidationResult.error;
2925
- return err(builderErrors.graphMissingImport(error.chain[0], error.chain[1]));
3277
+ throw builderErrors.graphMissingImport(error.chain[0], error.chain[1]);
2926
3278
  }
2927
3279
  const currentModuleAdjacency = extractModuleAdjacency({ snapshots });
2928
3280
  const stats = {
@@ -2930,15 +3282,6 @@ const discover = ({ discoveryCache, astAnalyzer, removedFiles, changedFiles, ent
2930
3282
  misses: cacheMisses,
2931
3283
  skips: cacheSkips
2932
3284
  };
2933
- return ok({
2934
- snapshots,
2935
- analyses,
2936
- currentModuleAdjacency,
2937
- affectedFiles,
2938
- stats
2939
- });
2940
- };
2941
- const buildDiscovered = ({ analyses, affectedFiles, stats, previousIntermediateModules, graphqlSystemPath }) => {
2942
3285
  const intermediateModules = new Map(previousIntermediateModules);
2943
3286
  const targetFiles = new Set(affectedFiles);
2944
3287
  for (const filePath of analyses.keys()) {
@@ -2961,23 +3304,29 @@ const buildDiscovered = ({ analyses, affectedFiles, stats, previousIntermediateM
2961
3304
  })) {
2962
3305
  intermediateModules.set(intermediateModule.filePath, intermediateModule);
2963
3306
  }
2964
- const elements = evaluateIntermediateModules({
3307
+ const elements = yield* evaluateIntermediateModulesGen({
2965
3308
  intermediateModules,
2966
3309
  graphqlSystemPath,
2967
3310
  analyses
2968
3311
  });
2969
- const artifactResult = buildArtifact({
3312
+ return {
3313
+ snapshots,
2970
3314
  analyses,
3315
+ currentModuleAdjacency,
3316
+ intermediateModules,
2971
3317
  elements,
2972
3318
  stats
2973
- });
2974
- if (artifactResult.isErr()) {
2975
- return err(artifactResult.error);
3319
+ };
3320
+ }
3321
+ /**
3322
+ * Convert scheduler error to builder error.
3323
+ * If the cause is already a BuilderError, return it directly to preserve error codes.
3324
+ */
3325
+ const convertSchedulerError = (error) => {
3326
+ if (error.cause && typeof error.cause === "object" && "code" in error.cause) {
3327
+ return error.cause;
2976
3328
  }
2977
- return ok({
2978
- intermediateModules,
2979
- artifact: artifactResult.value
2980
- });
3329
+ return builderErrors.internalInvariant(error.message, "scheduler", error.cause);
2981
3330
  };
2982
3331
 
2983
3332
  //#endregion
@@ -3002,6 +3351,7 @@ const createBuilderService = ({ config, entrypointsOverride }) => {
3002
3351
  });
3003
3352
  return {
3004
3353
  build: (options) => session.build(options),
3354
+ buildAsync: (options) => session.buildAsync(options),
3005
3355
  getGeneration: () => session.getGeneration(),
3006
3356
  getCurrentArtifact: () => session.getCurrentArtifact(),
3007
3357
  dispose: () => session.dispose()
@@ -3009,5 +3359,5 @@ const createBuilderService = ({ config, entrypointsOverride }) => {
3009
3359
  };
3010
3360
 
3011
3361
  //#endregion
3012
- export { BuilderArtifactSchema, buildAstPath, createBuilderService, createBuilderSession, createCanonicalId, createCanonicalTracker, createGraphqlSystemIdentifyHelper, createOccurrenceTracker, createPathTracker };
3362
+ export { BuilderArtifactSchema, BuilderEffects, FileReadEffect, FileStatEffect, buildAstPath, collectAffectedFiles, createBuilderService, createBuilderSession, createCanonicalId, createCanonicalTracker, createGraphqlSystemIdentifyHelper, createOccurrenceTracker, createPathTracker, extractModuleAdjacency };
3013
3363
  //# sourceMappingURL=index.mjs.map