@soda-gql/typegen 0.11.26 → 0.12.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/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, extname, join, relative, resolve } from "node:path";
3
- import { builderErrors, createBuilderService, extractFieldSelections, loadSchemasFromBundle } from "@soda-gql/builder";
4
- import { calculateFieldsType, generateInputObjectType, generateInputType, generateInputTypeFromVarDefs, parseInputSpecifier } from "@soda-gql/core";
1
+ import { writeFile } from "node:fs/promises";
2
+ import { dirname, extname, join, normalize, relative, resolve } from "node:path";
3
+ import { builderErrors, createBuilderService, createGraphqlSystemIdentifyHelper, extractFieldSelections, loadSchemasFromBundle } from "@soda-gql/builder";
4
+ import { buildFieldsFromSelectionSet, calculateFieldsType, createSchemaIndexFromSchema, extractFragmentVariables, generateInputObjectType, generateInputType, generateInputTypeFromVarDefs, parseInputSpecifier, preprocessFragmentArgs } from "@soda-gql/core";
5
5
  import { Kind, parse } from "graphql";
6
6
  import { err, ok } from "neverthrow";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
- import { build } from "esbuild";
8
+ import fg from "fast-glob";
9
+ import { parseSync } from "@swc/core";
9
10
 
10
11
  //#region packages/typegen/src/emitter.ts
11
12
  /**
@@ -214,7 +215,7 @@ const generateInputObjectTypeDefinitions = (schema, schemaName, inputNames) => {
214
215
  depthOverrides,
215
216
  formatters
216
217
  });
217
- lines.push(`type Input_${schemaName}_${inputName} = ${typeString};`);
218
+ lines.push(`export type Input_${schemaName}_${inputName} = ${typeString};`);
218
219
  }
219
220
  return lines;
220
221
  };
@@ -234,7 +235,7 @@ const generateTypesCode = (grouped, schemas, injectsModulePath) => {
234
235
  " * @generated",
235
236
  " */",
236
237
  "",
237
- "import type { PrebuiltTypeRegistry } from \"@soda-gql/core\";"
238
+ "import type { AssertExtends, PrebuiltTypeRegistry } from \"@soda-gql/core\";"
238
239
  ];
239
240
  const scalarImports = schemaNames.map((name) => `scalar_${name}`).join(", ");
240
241
  lines.push(`import type { ${scalarImports} } from "${injectsModulePath}";`);
@@ -252,8 +253,16 @@ const generateTypesCode = (grouped, schemas, injectsModulePath) => {
252
253
  lines.push(...inputTypeLines);
253
254
  lines.push("");
254
255
  }
255
- const fragmentEntries = fragments.sort((a, b) => a.key.localeCompare(b.key)).map((f) => ` readonly "${f.key}": { readonly typename: "${f.typename}"; readonly input: ${f.inputType}; readonly output: ${f.outputType} };`);
256
- const operationEntries = operations.sort((a, b) => a.key.localeCompare(b.key)).map((o) => ` readonly "${o.key}": { readonly input: ${o.inputType}; readonly output: ${o.outputType} };`);
256
+ const deduplicatedFragments = new Map();
257
+ for (const f of fragments) {
258
+ deduplicatedFragments.set(f.key, f);
259
+ }
260
+ const fragmentEntries = Array.from(deduplicatedFragments.values()).sort((a, b) => a.key.localeCompare(b.key)).map((f) => ` readonly "${f.key}": { readonly typename: "${f.typename}"; readonly input: ${f.inputType}; readonly output: ${f.outputType} };`);
261
+ const deduplicatedOperations = new Map();
262
+ for (const o of operations) {
263
+ deduplicatedOperations.set(o.key, o);
264
+ }
265
+ const operationEntries = Array.from(deduplicatedOperations.values()).sort((a, b) => a.key.localeCompare(b.key)).map((o) => ` readonly "${o.key}": { readonly input: ${o.inputType}; readonly output: ${o.outputType} };`);
257
266
  lines.push(`export type PrebuiltTypes_${schemaName} = {`);
258
267
  lines.push(" readonly fragments: {");
259
268
  if (fragmentEntries.length > 0) {
@@ -266,12 +275,13 @@ const generateTypesCode = (grouped, schemas, injectsModulePath) => {
266
275
  }
267
276
  lines.push(" };");
268
277
  lines.push("};");
278
+ lines.push(`type _AssertPrebuiltTypes_${schemaName} = AssertExtends<PrebuiltTypes_${schemaName}, PrebuiltTypeRegistry>;`);
269
279
  lines.push("");
270
280
  }
271
281
  return lines.join("\n");
272
282
  };
273
283
  /**
274
- * Emit prebuilt types to the prebuilt/types.ts file.
284
+ * Emit prebuilt types to the types.prebuilt.ts file.
275
285
  *
276
286
  * This function uses a partial failure strategy: if type calculation fails for
277
287
  * individual elements (e.g., due to invalid field selections or missing schema
@@ -340,18 +350,6 @@ const typegenErrors = {
340
350
  code: "TYPEGEN_BUILD_FAILED",
341
351
  message,
342
352
  cause
343
- }),
344
- emitFailed: (path, message, cause) => ({
345
- code: "TYPEGEN_EMIT_FAILED",
346
- message,
347
- path,
348
- cause
349
- }),
350
- bundleFailed: (path, message, cause) => ({
351
- code: "TYPEGEN_BUNDLE_FAILED",
352
- message,
353
- path,
354
- cause
355
353
  })
356
354
  };
357
355
  /**
@@ -368,10 +366,6 @@ const formatTypegenError = (error) => {
368
366
  case "TYPEGEN_SCHEMA_LOAD_FAILED":
369
367
  lines.push(` Schemas: ${error.schemaNames.join(", ")}`);
370
368
  break;
371
- case "TYPEGEN_EMIT_FAILED":
372
- case "TYPEGEN_BUNDLE_FAILED":
373
- lines.push(` Path: ${error.path}`);
374
- break;
375
369
  }
376
370
  if ("cause" in error && error.cause) {
377
371
  lines.push(` Caused by: ${error.cause}`);
@@ -380,192 +374,461 @@ const formatTypegenError = (error) => {
380
374
  };
381
375
 
382
376
  //#endregion
383
- //#region packages/typegen/src/prebuilt-generator.ts
377
+ //#region packages/typegen/src/template-extractor.ts
378
+ const OPERATION_KINDS = new Set([
379
+ "query",
380
+ "mutation",
381
+ "subscription",
382
+ "fragment"
383
+ ]);
384
+ const isOperationKind = (value) => OPERATION_KINDS.has(value);
384
385
  /**
385
- * Generate the prebuilt index module code.
386
- *
387
- * Generates index.prebuilt.ts with builder-level type resolution.
388
- * Types are resolved at the fragment/operation builder level using TKey/TName,
389
- * eliminating the need for ResolvePrebuiltElement at the composer level.
386
+ * Parse TypeScript source with SWC, returning null on failure.
390
387
  */
391
- const generatePrebuiltModule = (schemas, options) => {
392
- const schemaNames = Array.from(schemas.keys());
393
- const injection = options.injection ?? new Map();
394
- const adapterImports = [];
395
- for (const name of schemaNames) {
396
- const config = injection.get(name);
397
- if (config?.hasAdapter) {
398
- adapterImports.push(`adapter_${name}`);
399
- }
400
- }
401
- const internalImports = schemaNames.flatMap((name) => [
402
- `__schema_${name}`,
403
- `__inputTypeMethods_${name}`,
404
- `__directiveMethods_${name}`
405
- ]);
406
- const genericTypes = `
388
+ const safeParseSync = (source, tsx) => {
389
+ try {
390
+ return parseSync(source, {
391
+ syntax: "typescript",
392
+ tsx,
393
+ decorators: false,
394
+ dynamicImport: true
395
+ });
396
+ } catch {
397
+ return null;
398
+ }
399
+ };
407
400
  /**
408
- * Generic field factory for type-erased field access.
409
- * Returns a callable for nested field builders. Primitive fields can be spread directly.
410
- * Runtime behavior differs but spread works for both: ...f.id() and ...f.user()(...)
411
- */
412
- type GenericFieldFactory = (
413
- ...args: unknown[]
414
- ) => (nest: (tools: GenericFieldsBuilderTools) => AnyFields) => AnyFields;
415
-
401
+ * Collect gql identifiers from import declarations.
402
+ * Finds imports like `import { gql } from "./graphql-system"`.
403
+ */
404
+ const collectGqlIdentifiers = (module, filePath, helper) => {
405
+ const identifiers = new Set();
406
+ for (const item of module.body) {
407
+ let declaration = null;
408
+ if (item.type === "ImportDeclaration") {
409
+ declaration = item;
410
+ } else if ("declaration" in item && item.declaration && item.declaration.type === "ImportDeclaration") {
411
+ declaration = item.declaration;
412
+ }
413
+ if (!declaration) {
414
+ continue;
415
+ }
416
+ if (!helper.isGraphqlSystemImportSpecifier({
417
+ filePath,
418
+ specifier: declaration.source.value
419
+ })) {
420
+ continue;
421
+ }
422
+ for (const specifier of declaration.specifiers ?? []) {
423
+ if (specifier.type === "ImportSpecifier") {
424
+ const imported = specifier.imported ? specifier.imported.value : specifier.local.value;
425
+ if (imported === "gql" && !specifier.imported) {
426
+ identifiers.add(specifier.local.value);
427
+ }
428
+ }
429
+ }
430
+ }
431
+ return identifiers;
432
+ };
416
433
  /**
417
- * Generic tools for fields builder callbacks.
418
- * Uses type-erased factory to allow any field access while maintaining strict mode compatibility.
419
- */
420
- type GenericFieldsBuilderTools = {
421
- readonly f: Record<string, GenericFieldFactory>;
422
- readonly $: Record<string, unknown>;
434
+ * Check if a call expression is a gql.{schemaName}(...) call.
435
+ * Returns the schema name if it is, null otherwise.
436
+ */
437
+ const getGqlCallSchemaName = (identifiers, call) => {
438
+ const callee = call.callee;
439
+ if (callee.type !== "MemberExpression") {
440
+ return null;
441
+ }
442
+ const member = callee;
443
+ if (member.object.type !== "Identifier" || !identifiers.has(member.object.value)) {
444
+ return null;
445
+ }
446
+ if (member.property.type !== "Identifier") {
447
+ return null;
448
+ }
449
+ const firstArg = call.arguments[0];
450
+ if (!firstArg?.expression || firstArg.expression.type !== "ArrowFunctionExpression") {
451
+ return null;
452
+ }
453
+ return member.property.value;
423
454
  };
424
- `;
425
- const contextTypes = schemaNames.map((name) => `
426
455
  /**
427
- * Resolve fragment types at builder level using TKey.
428
- * If TKey is a known key in PrebuiltTypes, return resolved types.
429
- * Otherwise, return PrebuiltEntryNotFound.
430
- */
431
- type ResolveFragmentAtBuilder_${name}<
432
- TKey extends string | undefined
433
- > = TKey extends keyof PrebuiltTypes_${name}["fragments"]
434
- ? Fragment<
435
- PrebuiltTypes_${name}["fragments"][TKey]["typename"],
436
- PrebuiltTypes_${name}["fragments"][TKey]["input"] extends infer TInput
437
- ? TInput extends void ? void : Partial<TInput & object>
438
- : void,
439
- Partial<AnyFields>,
440
- PrebuiltTypes_${name}["fragments"][TKey]["output"] & object
441
- >
442
- : TKey extends undefined
443
- ? Fragment<"(unknown)", PrebuiltEntryNotFound<"(undefined)", "fragment">, Partial<AnyFields>, PrebuiltEntryNotFound<"(undefined)", "fragment">>
444
- : Fragment<"(unknown)", PrebuiltEntryNotFound<TKey & string, "fragment">, Partial<AnyFields>, PrebuiltEntryNotFound<TKey & string, "fragment">>;
445
-
456
+ * Extract templates from a gql callback's arrow function body.
457
+ * Handles both expression bodies and block bodies with return statements.
458
+ */
459
+ const extractTemplatesFromCallback = (arrow, schemaName) => {
460
+ const templates = [];
461
+ const processExpression = (expr) => {
462
+ if (expr.type === "TaggedTemplateExpression") {
463
+ const tagged = expr;
464
+ extractFromTaggedTemplate(tagged, schemaName, templates);
465
+ return;
466
+ }
467
+ if (expr.type === "CallExpression") {
468
+ const call = expr;
469
+ if (call.callee.type === "TaggedTemplateExpression") {
470
+ extractFromTaggedTemplate(call.callee, schemaName, templates);
471
+ }
472
+ }
473
+ };
474
+ if (arrow.body.type !== "BlockStatement") {
475
+ processExpression(arrow.body);
476
+ return templates;
477
+ }
478
+ for (const stmt of arrow.body.stmts) {
479
+ if (stmt.type === "ReturnStatement" && stmt.argument) {
480
+ processExpression(stmt.argument);
481
+ }
482
+ }
483
+ return templates;
484
+ };
485
+ const extractFromTaggedTemplate = (tagged, schemaName, templates) => {
486
+ let kind;
487
+ let elementName;
488
+ let typeName;
489
+ if (tagged.tag.type === "Identifier") {
490
+ kind = tagged.tag.value;
491
+ } else if (tagged.tag.type === "CallExpression") {
492
+ const tagCall = tagged.tag;
493
+ if (tagCall.callee.type === "Identifier") {
494
+ kind = tagCall.callee.value;
495
+ } else {
496
+ return;
497
+ }
498
+ const firstArg = tagCall.arguments[0]?.expression;
499
+ if (firstArg?.type === "StringLiteral") {
500
+ elementName = firstArg.value;
501
+ }
502
+ const secondArg = tagCall.arguments[1]?.expression;
503
+ if (secondArg?.type === "StringLiteral") {
504
+ typeName = secondArg.value;
505
+ }
506
+ } else {
507
+ return;
508
+ }
509
+ if (!isOperationKind(kind)) {
510
+ return;
511
+ }
512
+ const { quasis, expressions } = tagged.template;
513
+ if (tagged.tag.type === "Identifier" && expressions.length > 0) {
514
+ return;
515
+ }
516
+ if (quasis.length === 0) {
517
+ return;
518
+ }
519
+ let content;
520
+ if (expressions.length === 0) {
521
+ const quasi = quasis[0];
522
+ if (!quasi) return;
523
+ content = quasi.cooked ?? quasi.raw;
524
+ } else {
525
+ const parts = [];
526
+ for (let i = 0; i < quasis.length; i++) {
527
+ const quasi = quasis[i];
528
+ if (!quasi) continue;
529
+ parts.push(quasi.cooked ?? quasi.raw);
530
+ if (i < expressions.length) {
531
+ parts.push(`__FRAG_SPREAD_${i}__`);
532
+ }
533
+ }
534
+ content = parts.join("");
535
+ }
536
+ templates.push({
537
+ schemaName,
538
+ kind,
539
+ content,
540
+ ...elementName !== undefined ? { elementName } : {},
541
+ ...typeName !== undefined ? { typeName } : {}
542
+ });
543
+ };
446
544
  /**
447
- * Resolve operation types at builder level using TName.
448
- */
449
- type ResolveOperationAtBuilder_${name}<
450
- TOperationType extends OperationType,
451
- TName extends string
452
- > = TName extends keyof PrebuiltTypes_${name}["operations"]
453
- ? Operation<
454
- TOperationType,
455
- TName,
456
- string[],
457
- PrebuiltTypes_${name}["operations"][TName]["input"] & AnyConstAssignableInput,
458
- Partial<AnyFields>,
459
- PrebuiltTypes_${name}["operations"][TName]["output"] & object
460
- >
461
- : Operation<
462
- TOperationType,
463
- TName,
464
- string[],
465
- PrebuiltEntryNotFound<TName, "operation">,
466
- Partial<AnyFields>,
467
- PrebuiltEntryNotFound<TName, "operation">
468
- >;
469
-
545
+ * Find the innermost gql call, unwrapping method chains like .attach().
546
+ */
547
+ const findGqlCall = (identifiers, node) => {
548
+ if (!node || node.type !== "CallExpression") {
549
+ return null;
550
+ }
551
+ const call = node;
552
+ if (getGqlCallSchemaName(identifiers, call) !== null) {
553
+ return call;
554
+ }
555
+ const callee = call.callee;
556
+ if (callee.type !== "MemberExpression") {
557
+ return null;
558
+ }
559
+ return findGqlCall(identifiers, callee.object);
560
+ };
470
561
  /**
471
- * Fragment builder that resolves types at builder level using TKey.
472
- */
473
- type PrebuiltFragmentBuilder_${name} = <TKey extends string | undefined = undefined>(
474
- options: {
475
- key?: TKey;
476
- fields: (tools: GenericFieldsBuilderTools) => AnyFields;
477
- variables?: Record<string, unknown>;
478
- metadata?: unknown;
479
- }
480
- ) => ResolveFragmentAtBuilder_${name}<TKey>;
481
-
562
+ * Walk AST to find gql calls and extract templates.
563
+ */
564
+ const walkAndExtract = (node, identifiers) => {
565
+ const templates = [];
566
+ const visit = (n) => {
567
+ if (!n || typeof n !== "object") {
568
+ return;
569
+ }
570
+ if ("type" in n && n.type === "CallExpression") {
571
+ const gqlCall = findGqlCall(identifiers, n);
572
+ if (gqlCall) {
573
+ const schemaName = getGqlCallSchemaName(identifiers, gqlCall);
574
+ if (schemaName) {
575
+ const arrow = gqlCall.arguments[0]?.expression;
576
+ templates.push(...extractTemplatesFromCallback(arrow, schemaName));
577
+ }
578
+ return;
579
+ }
580
+ }
581
+ if (Array.isArray(n)) {
582
+ for (const item of n) {
583
+ visit(item);
584
+ }
585
+ return;
586
+ }
587
+ for (const key of Object.keys(n)) {
588
+ if (key === "span" || key === "type") {
589
+ continue;
590
+ }
591
+ const value = n[key];
592
+ if (value && typeof value === "object") {
593
+ visit(value);
594
+ }
595
+ }
596
+ };
597
+ visit(node);
598
+ return templates;
599
+ };
482
600
  /**
483
- * Operation builder that resolves types at builder level using TName.
484
- */
485
- type PrebuiltOperationBuilder_${name}<TOperationType extends OperationType> = <TName extends string>(
486
- options: {
487
- name: TName;
488
- fields: (tools: GenericFieldsBuilderTools) => AnyFields;
489
- variables?: Record<string, unknown>;
490
- metadata?: unknown;
491
- }
492
- ) => ResolveOperationAtBuilder_${name}<TOperationType, TName>;
601
+ * Extract all tagged templates from a TypeScript source file.
602
+ *
603
+ * @param filePath - Absolute path to the source file (used for import resolution)
604
+ * @param source - TypeScript source code
605
+ * @param helper - GraphQL system identifier for resolving gql imports
606
+ * @returns Extracted templates and any warnings
607
+ */
608
+ const extractTemplatesFromSource = (filePath, source, helper) => {
609
+ const warnings = [];
610
+ const isTsx = filePath.endsWith(".tsx");
611
+ const program = safeParseSync(source, isTsx);
612
+ if (!program || program.type !== "Module") {
613
+ if (source.includes("gql")) {
614
+ warnings.push(`[typegen-extract] Failed to parse ${filePath}`);
615
+ }
616
+ return {
617
+ templates: [],
618
+ warnings
619
+ };
620
+ }
621
+ const gqlIdentifiers = collectGqlIdentifiers(program, filePath, helper);
622
+ if (gqlIdentifiers.size === 0) {
623
+ return {
624
+ templates: [],
625
+ warnings
626
+ };
627
+ }
628
+ return {
629
+ templates: walkAndExtract(program, gqlIdentifiers),
630
+ warnings
631
+ };
632
+ };
493
633
 
634
+ //#endregion
635
+ //#region packages/typegen/src/template-scanner.ts
494
636
  /**
495
- * Prebuilt context with builder-level type resolution for schema "${name}".
496
- */
497
- type PrebuiltContext_${name} = {
498
- readonly fragment: { [K: string]: PrebuiltFragmentBuilder_${name} };
499
- readonly query: { readonly operation: PrebuiltOperationBuilder_${name}<"query"> };
500
- readonly mutation: { readonly operation: PrebuiltOperationBuilder_${name}<"mutation"> };
501
- readonly subscription: { readonly operation: PrebuiltOperationBuilder_${name}<"subscription"> };
502
- readonly $var: unknown;
503
- readonly $dir: StandardDirectives;
504
- readonly $colocate: unknown;
505
- };`).join("\n");
506
- const gqlEntries = schemaNames.map((name) => {
507
- const config = injection.get(name);
508
- const adapterLine = config?.hasAdapter ? `,\n adapter: adapter_${name}` : "";
509
- return ` ${name}: createGqlElementComposer(
510
- __schema_${name} as AnyGraphqlSchema,
511
- {
512
- inputTypeMethods: __inputTypeMethods_${name},
513
- directiveMethods: __directiveMethods_${name}${adapterLine}
514
- }
515
- ) as unknown as GqlComposer_${name}`;
516
- });
517
- const injectsImportSpecifiers = adapterImports.length > 0 ? adapterImports.join(", ") : "";
518
- const injectsImportLine = injectsImportSpecifiers ? `import { ${injectsImportSpecifiers} } from "${options.injectsModulePath}";` : "";
519
- const indexCode = `\
637
+ * Source file scanner for tagged template extraction.
638
+ *
639
+ * Discovers source files from config include/exclude patterns,
640
+ * reads them, and extracts tagged templates using the template extractor.
641
+ *
642
+ * @module
643
+ */
520
644
  /**
521
- * Prebuilt GQL module with builder-level type resolution.
522
- *
523
- * Types are resolved at the fragment/operation builder level using TKey/TName,
524
- * not at the composer level. This enables proper typing for builder arguments
525
- * and eliminates the need for ResolvePrebuiltElement.
526
- *
527
- * @module
528
- * @generated by @soda-gql/typegen
529
- */
530
-
531
- import {
532
- createGqlElementComposer,
533
- type AnyConstAssignableInput,
534
- type AnyFields,
535
- type AnyGraphqlSchema,
536
- type Fragment,
537
- type Operation,
538
- type OperationType,
539
- type PrebuiltEntryNotFound,
540
- type StandardDirectives,
541
- } from "@soda-gql/core";
542
- ${injectsImportLine}
543
- import { ${internalImports.join(", ")} } from "${options.internalModulePath}";
544
- import type { ${schemaNames.map((name) => `PrebuiltTypes_${name}`).join(", ")} } from "./types.prebuilt";
545
- ${genericTypes}
546
- ${contextTypes}
547
-
548
- // Export context types for explicit annotation
549
- ${schemaNames.map((name) => `export type { PrebuiltContext_${name} };`).join("\n")}
550
-
551
- // Composer type - TResult already has resolved types from builders, no ResolvePrebuiltElement needed
552
- ${schemaNames.map((name) => `type GqlComposer_${name} = {
553
- <TResult>(composeElement: (context: PrebuiltContext_${name}) => TResult): TResult;
554
- readonly $schema: AnyGraphqlSchema;
555
- };`).join("\n")}
645
+ * Scan source files for tagged templates.
646
+ *
647
+ * Uses fast-glob to discover files matching include/exclude patterns,
648
+ * then extracts tagged templates from each file.
649
+ */
650
+ const scanSourceFiles = (options) => {
651
+ const { include, exclude, baseDir, helper } = options;
652
+ const warnings = [];
653
+ const ignorePatterns = exclude.map((pattern) => pattern.startsWith("!") ? pattern.slice(1) : pattern);
654
+ const matchedFiles = fg.sync(include, {
655
+ cwd: baseDir,
656
+ ignore: ignorePatterns,
657
+ onlyFiles: true,
658
+ absolute: true
659
+ });
660
+ const templates = new Map();
661
+ for (const filePath of matchedFiles) {
662
+ const normalizedPath = normalize(resolve(filePath)).replace(/\\/g, "/");
663
+ try {
664
+ const source = readFileSync(normalizedPath, "utf-8");
665
+ const { templates: extracted, warnings: extractionWarnings } = extractTemplatesFromSource(normalizedPath, source, helper);
666
+ warnings.push(...extractionWarnings);
667
+ if (extracted.length > 0) {
668
+ templates.set(normalizedPath, extracted);
669
+ }
670
+ } catch (error) {
671
+ const message = error instanceof Error ? error.message : String(error);
672
+ warnings.push(`[typegen-scan] Failed to read ${normalizedPath}: ${message}`);
673
+ }
674
+ }
675
+ return {
676
+ templates,
677
+ warnings
678
+ };
679
+ };
556
680
 
681
+ //#endregion
682
+ //#region packages/typegen/src/template-to-selections.ts
557
683
  /**
558
- * Prebuilt GQL composers with builder-level type resolution.
559
- *
560
- * These composers have the same runtime behavior as the base composers,
561
- * but their return types are resolved from the prebuilt type registry
562
- * at the builder level instead of using ResolvePrebuiltElement.
563
- */
564
- export const gql: { ${schemaNames.map((name) => `${name}: GqlComposer_${name}`).join("; ")} } = {
565
- ${gqlEntries.join(",\n")}
684
+ * Convert extracted templates into field selections for the emitter.
685
+ *
686
+ * @param templates - Templates extracted from source files, keyed by file path
687
+ * @param schemas - Loaded schema objects keyed by schema name
688
+ * @returns Map of canonical IDs to field selection data, plus any warnings
689
+ */
690
+ const convertTemplatesToSelections = (templates, schemas) => {
691
+ const selections = new Map();
692
+ const warnings = [];
693
+ const schemaIndexes = new Map(Object.entries(schemas).map(([name, schema]) => [name, createSchemaIndexFromSchema(schema)]));
694
+ for (const [filePath, fileTemplates] of templates) {
695
+ for (const template of fileTemplates) {
696
+ const schema = schemas[template.schemaName];
697
+ if (!schema) {
698
+ warnings.push(`[typegen-template] Unknown schema "${template.schemaName}" in ${filePath}`);
699
+ continue;
700
+ }
701
+ const schemaIndex = schemaIndexes.get(template.schemaName);
702
+ if (!schemaIndex) {
703
+ continue;
704
+ }
705
+ try {
706
+ if (template.kind === "fragment") {
707
+ const selection = convertFragmentTemplate(template, schema, filePath);
708
+ if (selection) {
709
+ selections.set(selection.id, selection.data);
710
+ }
711
+ } else {
712
+ const selection = convertOperationTemplate(template, schema, filePath);
713
+ if (selection) {
714
+ selections.set(selection.id, selection.data);
715
+ }
716
+ }
717
+ } catch (error) {
718
+ const message = error instanceof Error ? error.message : String(error);
719
+ warnings.push(`[typegen-template] Failed to process ${template.kind} in ${filePath}: ${message}`);
720
+ }
721
+ }
722
+ }
723
+ return {
724
+ selections,
725
+ warnings
726
+ };
566
727
  };
567
- `;
568
- return { indexCode };
728
+ /**
729
+ * Recursively filter out __FRAG_SPREAD_ placeholder nodes from a selection set.
730
+ * These placeholders are created by template-extractor for interpolated fragment references.
731
+ * buildFieldsFromSelectionSet would throw on them since no interpolationMap is available.
732
+ */
733
+ const filterPlaceholderSpreads = (selectionSet) => ({
734
+ ...selectionSet,
735
+ selections: selectionSet.selections.filter((sel) => !(sel.kind === Kind.FRAGMENT_SPREAD && sel.name.value.startsWith("__FRAG_SPREAD_"))).map((sel) => {
736
+ if (sel.kind === Kind.FIELD && sel.selectionSet) {
737
+ return {
738
+ ...sel,
739
+ selectionSet: filterPlaceholderSpreads(sel.selectionSet)
740
+ };
741
+ }
742
+ if (sel.kind === Kind.INLINE_FRAGMENT && sel.selectionSet) {
743
+ return {
744
+ ...sel,
745
+ selectionSet: filterPlaceholderSpreads(sel.selectionSet)
746
+ };
747
+ }
748
+ return sel;
749
+ })
750
+ });
751
+ /**
752
+ * Reconstruct full GraphQL source from an extracted template.
753
+ * For curried syntax (new), prepends the definition header from tag call arguments.
754
+ * For old syntax, returns content as-is.
755
+ */
756
+ const reconstructGraphql = (template) => {
757
+ if (template.elementName) {
758
+ if (template.kind === "fragment" && template.typeName) {
759
+ return `fragment ${template.elementName} on ${template.typeName} ${template.content}`;
760
+ }
761
+ return `${template.kind} ${template.elementName} ${template.content}`;
762
+ }
763
+ return template.content;
764
+ };
765
+ /**
766
+ * Convert a fragment template into FieldSelectionData.
767
+ */
768
+ const convertFragmentTemplate = (template, schema, filePath) => {
769
+ const schemaIndex = createSchemaIndexFromSchema(schema);
770
+ const graphqlSource = reconstructGraphql(template);
771
+ const variableDefinitions = extractFragmentVariables(graphqlSource, schemaIndex);
772
+ const { preprocessed } = preprocessFragmentArgs(graphqlSource);
773
+ const document = parse(preprocessed);
774
+ const fragDef = document.definitions.find((d) => d.kind === Kind.FRAGMENT_DEFINITION);
775
+ if (!fragDef || fragDef.kind !== Kind.FRAGMENT_DEFINITION) {
776
+ return null;
777
+ }
778
+ const fragmentName = fragDef.name.value;
779
+ const onType = fragDef.typeCondition.name.value;
780
+ const fields = buildFieldsFromSelectionSet(filterPlaceholderSpreads(fragDef.selectionSet), schema, onType);
781
+ const id = `${filePath}::${fragmentName}`;
782
+ return {
783
+ id,
784
+ data: {
785
+ type: "fragment",
786
+ schemaLabel: schema.label,
787
+ key: fragmentName,
788
+ typename: onType,
789
+ fields,
790
+ variableDefinitions
791
+ }
792
+ };
793
+ };
794
+ /**
795
+ * Convert an operation template into FieldSelectionData.
796
+ */
797
+ const convertOperationTemplate = (template, schema, filePath) => {
798
+ const graphqlSource = reconstructGraphql(template);
799
+ const document = parse(graphqlSource);
800
+ const opDef = document.definitions.find((d) => d.kind === Kind.OPERATION_DEFINITION);
801
+ if (!opDef || opDef.kind !== Kind.OPERATION_DEFINITION) {
802
+ return null;
803
+ }
804
+ const operationName = opDef.name?.value ?? "Anonymous";
805
+ const operationType = opDef.operation;
806
+ const rootTypeName = getRootTypeName(schema, operationType);
807
+ const fields = buildFieldsFromSelectionSet(filterPlaceholderSpreads(opDef.selectionSet), schema, rootTypeName);
808
+ const variableDefinitions = opDef.variableDefinitions ?? [];
809
+ const id = `${filePath}::${operationName}`;
810
+ return {
811
+ id,
812
+ data: {
813
+ type: "operation",
814
+ schemaLabel: schema.label,
815
+ operationName,
816
+ operationType,
817
+ fields,
818
+ variableDefinitions: [...variableDefinitions]
819
+ }
820
+ };
821
+ };
822
+ /**
823
+ * Get the root type name for an operation type from the schema.
824
+ */
825
+ const getRootTypeName = (schema, operationType) => {
826
+ switch (operationType) {
827
+ case "query": return schema.operations.query ?? "Query";
828
+ case "mutation": return schema.operations.mutation ?? "Mutation";
829
+ case "subscription": return schema.operations.subscription ?? "Subscription";
830
+ default: return "Query";
831
+ }
569
832
  };
570
833
 
571
834
  //#endregion
@@ -575,11 +838,10 @@ ${gqlEntries.join(",\n")}
575
838
  *
576
839
  * Orchestrates the prebuilt type generation process:
577
840
  * 1. Load schemas from generated CJS bundle
578
- * 2. Generate index.prebuilt.ts
579
- * 3. Build artifact to evaluate elements
580
- * 4. Extract field selections
841
+ * 2. Build artifact to evaluate elements
842
+ * 3. Extract field selections from builder
843
+ * 4. Scan source files for tagged templates and merge selections
581
844
  * 5. Emit types.prebuilt.ts
582
- * 6. Bundle prebuilt module
583
845
  *
584
846
  * @module
585
847
  */
@@ -621,58 +883,14 @@ const toImportSpecifier = (fromPath, targetPath, options) => {
621
883
  return `${withoutExt}${runtimeExt}`;
622
884
  };
623
885
  /**
624
- * Bundle the prebuilt module to CJS format.
625
- */
626
- const bundlePrebuiltModule = async (sourcePath) => {
627
- const sourceExt = extname(sourcePath);
628
- const baseName = sourcePath.slice(0, -sourceExt.length);
629
- const cjsPath = `${baseName}.cjs`;
630
- await build({
631
- entryPoints: [sourcePath],
632
- outfile: cjsPath,
633
- format: "cjs",
634
- platform: "node",
635
- bundle: true,
636
- external: ["@soda-gql/core", "@soda-gql/runtime"],
637
- sourcemap: false,
638
- minify: false,
639
- treeShaking: false
640
- });
641
- return { cjsPath };
642
- };
643
- /**
644
- * Write a TypeScript module to disk.
645
- */
646
- const writeModule = async (path, content) => {
647
- await mkdir(dirname(path), { recursive: true });
648
- await writeFile(path, content, "utf-8");
649
- };
650
- /**
651
- * Load GraphQL schema documents from schema paths.
652
- * This is needed for generatePrebuiltModule which expects DocumentNode.
653
- */
654
- const loadSchemaDocuments = (schemasConfig) => {
655
- const documents = new Map();
656
- for (const [name, schemaConfig] of Object.entries(schemasConfig)) {
657
- const schemaPaths = Array.isArray(schemaConfig.schema) ? schemaConfig.schema : [schemaConfig.schema];
658
- let combinedSource = "";
659
- for (const schemaPath of schemaPaths) {
660
- combinedSource += `${readFileSync(schemaPath, "utf-8")}\n`;
661
- }
662
- documents.set(name, parse(combinedSource));
663
- }
664
- return documents;
665
- };
666
- /**
667
886
  * Run the typegen process.
668
887
  *
669
888
  * This function:
670
889
  * 1. Loads schemas from the generated CJS bundle
671
- * 2. Generates index.prebuilt.ts using generatePrebuiltModule
672
- * 3. Creates a BuilderService and builds the artifact
673
- * 4. Extracts field selections from the artifact
890
+ * 2. Creates a BuilderService and builds the artifact
891
+ * 3. Extracts field selections from the artifact
892
+ * 4. Scans source files for tagged templates and merges selections
674
893
  * 5. Emits types.prebuilt.ts using emitPrebuiltTypes
675
- * 6. Bundles the prebuilt module
676
894
  *
677
895
  * @param options - Typegen options including config
678
896
  * @returns Result containing success data or error
@@ -691,24 +909,8 @@ const runTypegen = async (options) => {
691
909
  return err(typegenErrors.schemaLoadFailed(schemaNames, schemasResult.error));
692
910
  }
693
911
  const schemas = schemasResult.value;
694
- const schemaDocuments = loadSchemaDocuments(config.schemas);
695
- const prebuiltIndexPath = join(outdir, "index.prebuilt.ts");
696
- const internalModulePath = toImportSpecifier(prebuiltIndexPath, join(outdir, "_internal.ts"), importSpecifierOptions);
697
- const injectsModulePath = toImportSpecifier(prebuiltIndexPath, join(outdir, "_internal-injects.ts"), importSpecifierOptions);
698
- const injection = new Map();
699
- for (const [schemaName, schemaConfig] of Object.entries(config.schemas)) {
700
- injection.set(schemaName, { hasAdapter: !!schemaConfig.inject.adapter });
701
- }
702
- const prebuilt = generatePrebuiltModule(schemaDocuments, {
703
- internalModulePath,
704
- injectsModulePath,
705
- injection
706
- });
707
- try {
708
- await writeModule(prebuiltIndexPath, prebuilt.indexCode);
709
- } catch (error) {
710
- return err(typegenErrors.emitFailed(prebuiltIndexPath, `Failed to write prebuilt index: ${error instanceof Error ? error.message : String(error)}`, error));
711
- }
912
+ const prebuiltTypesPath = join(outdir, "types.prebuilt.ts");
913
+ const injectsModulePath = toImportSpecifier(prebuiltTypesPath, join(outdir, "_internal-injects.ts"), importSpecifierOptions);
712
914
  const builderService = createBuilderService({ config });
713
915
  const artifactResult = await builderService.buildAsync();
714
916
  if (artifactResult.isErr()) {
@@ -719,7 +921,38 @@ const runTypegen = async (options) => {
719
921
  return err(typegenErrors.buildFailed("No intermediate elements available after build", undefined));
720
922
  }
721
923
  const fieldSelectionsResult = extractFieldSelections(intermediateElements);
722
- const { selections: fieldSelections, warnings: extractWarnings } = fieldSelectionsResult;
924
+ const { selections: builderSelections, warnings: extractWarnings } = fieldSelectionsResult;
925
+ const graphqlHelper = createGraphqlSystemIdentifyHelper(config);
926
+ const scanResult = scanSourceFiles({
927
+ include: [...config.include],
928
+ exclude: [...config.exclude],
929
+ baseDir: config.baseDir,
930
+ helper: graphqlHelper
931
+ });
932
+ const templateSelections = convertTemplatesToSelections(scanResult.templates, schemas);
933
+ const extractFilePart = (id) => {
934
+ const filePart = id.split("::")[0] ?? "";
935
+ if (filePart.startsWith("/")) {
936
+ return relative(config.baseDir, filePart);
937
+ }
938
+ return filePart;
939
+ };
940
+ const extractElementName = (data) => data.type === "fragment" ? data.key : data.operationName;
941
+ const templateElements = new Set();
942
+ for (const [id, data] of templateSelections.selections) {
943
+ const name = extractElementName(data);
944
+ if (name) templateElements.add(`${extractFilePart(id)}::${name}`);
945
+ }
946
+ const fieldSelections = new Map();
947
+ for (const [id, data] of builderSelections) {
948
+ const name = extractElementName(data);
949
+ if (name && templateElements.has(`${extractFilePart(id)}::${name}`)) continue;
950
+ fieldSelections.set(id, data);
951
+ }
952
+ for (const [id, data] of templateSelections.selections) {
953
+ fieldSelections.set(id, data);
954
+ }
955
+ const scanWarnings = [...scanResult.warnings, ...templateSelections.warnings];
723
956
  const emitResult = await emitPrebuiltTypes({
724
957
  schemas,
725
958
  fieldSelections,
@@ -729,12 +962,7 @@ const runTypegen = async (options) => {
729
962
  if (emitResult.isErr()) {
730
963
  return err(emitResult.error);
731
964
  }
732
- const { path: prebuiltTypesPath, warnings: emitWarnings } = emitResult.value;
733
- try {
734
- await bundlePrebuiltModule(prebuiltIndexPath);
735
- } catch (error) {
736
- return err(typegenErrors.bundleFailed(prebuiltIndexPath, `Failed to bundle prebuilt module: ${error instanceof Error ? error.message : String(error)}`, error));
737
- }
965
+ const { warnings: emitWarnings } = emitResult.value;
738
966
  let fragmentCount = 0;
739
967
  let operationCount = 0;
740
968
  for (const selection of fieldSelections.values()) {
@@ -744,9 +972,12 @@ const runTypegen = async (options) => {
744
972
  operationCount++;
745
973
  }
746
974
  }
747
- const allWarnings = [...extractWarnings, ...emitWarnings];
975
+ const allWarnings = [
976
+ ...extractWarnings,
977
+ ...scanWarnings,
978
+ ...emitWarnings
979
+ ];
748
980
  return ok({
749
- prebuiltIndexPath,
750
981
  prebuiltTypesPath,
751
982
  fragmentCount,
752
983
  operationCount,
@@ -755,5 +986,5 @@ const runTypegen = async (options) => {
755
986
  };
756
987
 
757
988
  //#endregion
758
- export { emitPrebuiltTypes, formatTypegenError, generatePrebuiltModule, runTypegen, typegenErrors };
989
+ export { emitPrebuiltTypes, formatTypegenError, runTypegen, typegenErrors };
759
990
  //# sourceMappingURL=index.mjs.map