@soda-gql/builder 0.8.0 → 0.8.2

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.cjs CHANGED
@@ -1804,25 +1804,41 @@ const collectAllDefinitions$1 = ({ module: module$1, gqlIdentifiers, imports: _i
1804
1804
  if (node.type === "CallExpression") {
1805
1805
  const gqlCall = unwrapMethodChains$1(gqlIdentifiers, node);
1806
1806
  if (gqlCall) {
1807
- const { astPath } = tracker.registerDefinition();
1808
- const isTopLevel = stack.length === 1;
1809
- let isExported = false;
1810
- let exportBinding;
1811
- if (isTopLevel && stack[0]) {
1812
- const topLevelName = stack[0].nameSegment;
1813
- if (exportBindings.has(topLevelName)) {
1814
- isExported = true;
1815
- exportBinding = exportBindings.get(topLevelName);
1807
+ const needsAnonymousScope = tracker.currentDepth() === 0;
1808
+ let anonymousScopeHandle;
1809
+ if (needsAnonymousScope) {
1810
+ const anonymousName = getAnonymousName("anonymous");
1811
+ anonymousScopeHandle = tracker.enterScope({
1812
+ segment: anonymousName,
1813
+ kind: "expression",
1814
+ stableKey: "anonymous"
1815
+ });
1816
+ }
1817
+ try {
1818
+ const { astPath } = tracker.registerDefinition();
1819
+ const isTopLevel = stack.length === 1;
1820
+ let isExported = false;
1821
+ let exportBinding;
1822
+ if (isTopLevel && stack[0]) {
1823
+ const topLevelName = stack[0].nameSegment;
1824
+ if (exportBindings.has(topLevelName)) {
1825
+ isExported = true;
1826
+ exportBinding = exportBindings.get(topLevelName);
1827
+ }
1828
+ }
1829
+ handledCalls.push(node);
1830
+ pending.push({
1831
+ astPath,
1832
+ isTopLevel,
1833
+ isExported,
1834
+ exportBinding,
1835
+ expression: expressionFromCall(gqlCall)
1836
+ });
1837
+ } finally {
1838
+ if (anonymousScopeHandle) {
1839
+ tracker.exitScope(anonymousScopeHandle);
1816
1840
  }
1817
1841
  }
1818
- handledCalls.push(node);
1819
- pending.push({
1820
- astPath,
1821
- isTopLevel,
1822
- isExported,
1823
- exportBinding,
1824
- expression: expressionFromCall(gqlCall)
1825
- });
1826
1842
  return;
1827
1843
  }
1828
1844
  }
@@ -1835,6 +1851,8 @@ const collectAllDefinitions$1 = ({ module: module$1, gqlIdentifiers, imports: _i
1835
1851
  visit(decl.init, newStack);
1836
1852
  });
1837
1853
  }
1854
+ } else if (decl.init) {
1855
+ visit(decl.init, stack);
1838
1856
  }
1839
1857
  });
1840
1858
  return;
@@ -2186,25 +2204,41 @@ const collectAllDefinitions = ({ sourceFile, identifiers, exports: exports$1 })
2186
2204
  if (typescript.default.isCallExpression(node)) {
2187
2205
  const gqlCall = unwrapMethodChains(identifiers, node);
2188
2206
  if (gqlCall) {
2189
- const { astPath } = tracker.registerDefinition();
2190
- const isTopLevel = stack.length === 1;
2191
- let isExported = false;
2192
- let exportBinding;
2193
- if (isTopLevel && stack[0]) {
2194
- const topLevelName = stack[0].nameSegment;
2195
- if (exportBindings.has(topLevelName)) {
2196
- isExported = true;
2197
- exportBinding = exportBindings.get(topLevelName);
2207
+ const needsAnonymousScope = tracker.currentDepth() === 0;
2208
+ let anonymousScopeHandle;
2209
+ if (needsAnonymousScope) {
2210
+ const anonymousName = getAnonymousName("anonymous");
2211
+ anonymousScopeHandle = tracker.enterScope({
2212
+ segment: anonymousName,
2213
+ kind: "expression",
2214
+ stableKey: "anonymous"
2215
+ });
2216
+ }
2217
+ try {
2218
+ const { astPath } = tracker.registerDefinition();
2219
+ const isTopLevel = stack.length === 1;
2220
+ let isExported = false;
2221
+ let exportBinding;
2222
+ if (isTopLevel && stack[0]) {
2223
+ const topLevelName = stack[0].nameSegment;
2224
+ if (exportBindings.has(topLevelName)) {
2225
+ isExported = true;
2226
+ exportBinding = exportBindings.get(topLevelName);
2227
+ }
2228
+ }
2229
+ handledCalls.push(node);
2230
+ pending.push({
2231
+ astPath,
2232
+ isTopLevel,
2233
+ isExported,
2234
+ exportBinding,
2235
+ expression: gqlCall.getText(sourceFile)
2236
+ });
2237
+ } finally {
2238
+ if (anonymousScopeHandle) {
2239
+ tracker.exitScope(anonymousScopeHandle);
2198
2240
  }
2199
2241
  }
2200
- handledCalls.push(node);
2201
- pending.push({
2202
- astPath,
2203
- isTopLevel,
2204
- isExported,
2205
- exportBinding,
2206
- expression: gqlCall.getText(sourceFile)
2207
- });
2208
2242
  return;
2209
2243
  }
2210
2244
  }
@@ -2377,20 +2411,58 @@ const toEntryKey = (key) => {
2377
2411
  return hasher.hash(key, "xxhash");
2378
2412
  };
2379
2413
  const PERSISTENCE_VERSION = "v1";
2414
+ /**
2415
+ * Validate persisted data structure.
2416
+ * Uses simple validation to detect corruption without strict schema.
2417
+ */
2418
+ const isValidPersistedData = (data) => {
2419
+ if (typeof data !== "object" || data === null) return false;
2420
+ const record = data;
2421
+ if (typeof record.version !== "string") return false;
2422
+ if (typeof record.storage !== "object" || record.storage === null) return false;
2423
+ for (const value of Object.values(record.storage)) {
2424
+ if (!Array.isArray(value)) return false;
2425
+ for (const entry of value) {
2426
+ if (!Array.isArray(entry) || entry.length !== 2) return false;
2427
+ if (typeof entry[0] !== "string") return false;
2428
+ const envelope = entry[1];
2429
+ if (typeof envelope !== "object" || envelope === null) return false;
2430
+ const env = envelope;
2431
+ if (typeof env.key !== "string" || typeof env.version !== "string") return false;
2432
+ }
2433
+ }
2434
+ return true;
2435
+ };
2380
2436
  const createMemoryCache = ({ prefix = [], persistence } = {}) => {
2381
2437
  const storage = new Map();
2382
2438
  if (persistence?.enabled) {
2383
2439
  try {
2384
2440
  if ((0, node_fs.existsSync)(persistence.filePath)) {
2385
2441
  const content = (0, node_fs.readFileSync)(persistence.filePath, "utf-8");
2386
- const data = JSON.parse(content);
2387
- if (data.version === PERSISTENCE_VERSION && data.storage) {
2388
- for (const [namespaceKey, entries] of Object.entries(data.storage)) {
2389
- const namespaceMap = new Map();
2390
- for (const [hashedKey, envelope] of entries) {
2391
- namespaceMap.set(hashedKey, envelope);
2442
+ let parsed;
2443
+ try {
2444
+ parsed = JSON.parse(content);
2445
+ } catch {
2446
+ console.warn(`[cache] Corrupt cache file (invalid JSON), starting fresh: ${persistence.filePath}`);
2447
+ try {
2448
+ (0, node_fs.unlinkSync)(persistence.filePath);
2449
+ } catch {}
2450
+ parsed = null;
2451
+ }
2452
+ if (parsed) {
2453
+ if (!isValidPersistedData(parsed)) {
2454
+ console.warn(`[cache] Corrupt cache file (invalid structure), starting fresh: ${persistence.filePath}`);
2455
+ try {
2456
+ (0, node_fs.unlinkSync)(persistence.filePath);
2457
+ } catch {}
2458
+ } else if (parsed.version === PERSISTENCE_VERSION) {
2459
+ for (const [namespaceKey, entries] of Object.entries(parsed.storage)) {
2460
+ const namespaceMap = new Map();
2461
+ for (const [hashedKey, envelope] of entries) {
2462
+ namespaceMap.set(hashedKey, envelope);
2463
+ }
2464
+ storage.set(namespaceKey, namespaceMap);
2392
2465
  }
2393
- storage.set(namespaceKey, namespaceMap);
2394
2466
  }
2395
2467
  }
2396
2468
  }
@@ -2511,11 +2583,8 @@ const createMemoryCache = ({ prefix = [], persistence } = {}) => {
2511
2583
  version: PERSISTENCE_VERSION,
2512
2584
  storage: serialized
2513
2585
  };
2514
- const dir = (0, node_path.dirname)(persistence.filePath);
2515
- if (!(0, node_fs.existsSync)(dir)) {
2516
- (0, node_fs.mkdirSync)(dir, { recursive: true });
2517
- }
2518
- (0, node_fs.writeFileSync)(persistence.filePath, JSON.stringify(data), "utf-8");
2586
+ const fs = (0, __soda_gql_common.getPortableFS)();
2587
+ fs.writeFileSyncAtomic(persistence.filePath, JSON.stringify(data));
2519
2588
  } catch (error) {
2520
2589
  console.warn(`[cache] Failed to save cache to ${persistence.filePath}:`, error);
2521
2590
  }
@@ -3244,6 +3313,43 @@ const collectAffectedFiles = (input) => {
3244
3313
  //#endregion
3245
3314
  //#region packages/builder/src/session/builder-session.ts
3246
3315
  /**
3316
+ * Singleton state for beforeExit handler registration.
3317
+ * Ensures only one handler is registered regardless of how many sessions are created.
3318
+ */
3319
+ const exitHandlerState = {
3320
+ registered: false,
3321
+ factories: new Set()
3322
+ };
3323
+ /**
3324
+ * Register a cache factory for save on process exit.
3325
+ * Uses singleton pattern to prevent multiple handler registrations.
3326
+ */
3327
+ const registerExitHandler = (cacheFactory) => {
3328
+ exitHandlerState.factories.add(cacheFactory);
3329
+ if (!exitHandlerState.registered) {
3330
+ exitHandlerState.registered = true;
3331
+ process.on("beforeExit", () => {
3332
+ for (const factory of exitHandlerState.factories) {
3333
+ factory.save();
3334
+ }
3335
+ });
3336
+ }
3337
+ };
3338
+ /**
3339
+ * Unregister a cache factory from the exit handler.
3340
+ */
3341
+ const unregisterExitHandler = (cacheFactory) => {
3342
+ exitHandlerState.factories.delete(cacheFactory);
3343
+ };
3344
+ /**
3345
+ * Reset exit handler state for testing.
3346
+ * @internal
3347
+ */
3348
+ const __resetExitHandlerForTests = () => {
3349
+ exitHandlerState.registered = false;
3350
+ exitHandlerState.factories.clear();
3351
+ };
3352
+ /**
3247
3353
  * Create a new builder session.
3248
3354
  *
3249
3355
  * The session maintains in-memory state across builds to enable incremental processing.
@@ -3266,9 +3372,7 @@ const createBuilderSession = (options) => {
3266
3372
  filePath: (0, node_path.join)(process.cwd(), "node_modules", ".cache", "soda-gql", "builder", "cache.json")
3267
3373
  }
3268
3374
  });
3269
- process.on("beforeExit", () => {
3270
- cacheFactory.save();
3271
- });
3375
+ registerExitHandler(cacheFactory);
3272
3376
  const graphqlHelper = createGraphqlSystemIdentifyHelper(config);
3273
3377
  const ensureAstAnalyzer = (0, __soda_gql_common.cachedFn)(() => createAstAnalyzer({
3274
3378
  analyzer: config.analyzer,
@@ -3412,6 +3516,7 @@ const createBuilderSession = (options) => {
3412
3516
  getCurrentArtifact: () => state.lastArtifact,
3413
3517
  dispose: () => {
3414
3518
  cacheFactory.save();
3519
+ unregisterExitHandler(cacheFactory);
3415
3520
  }
3416
3521
  };
3417
3522
  };