@soda-gql/common 0.12.5 → 0.13.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-DeJfMmkU.d.mts.map +1 -1
- package/dist/template-extraction.cjs +55 -8
- package/dist/template-extraction.cjs.map +1 -1
- package/dist/template-extraction.d.cts +18 -5
- package/dist/template-extraction.d.cts.map +1 -1
- package/dist/template-extraction.d.mts +18 -5
- package/dist/template-extraction.d.mts.map +1 -1
- package/dist/template-extraction.mjs +55 -9
- package/dist/template-extraction.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-DeJfMmkU.d.mts","names":[],"sources":["../src/zod/schema-helper.ts"],"sourcesContent":[],"mappings":";;;KAGY,qBAAqB,CAAA,CAAE,QAAQ;KAE/B,2CAFA,MAEiD,OAFxC,KAEoD,SAF9B,CAEwC,
|
|
1
|
+
{"version":3,"file":"index-DeJfMmkU.d.mts","names":[],"sources":["../src/zod/schema-helper.ts"],"sourcesContent":[],"mappings":";;;KAGY,qBAAqB,CAAA,CAAE,QAAQ;KAE/B,2CAFA,MAEiD,OAFxC,KAEoD,SAF9B,CAEwC,OAFhD,CAEwD,CAFxD,CAAA,CAAA,EAEnC;AAA6D,iBAE7C,eAF6C,CAAA,gBAAA,MAAA,CAAA,CAAA,CAAA,EAAA,CAAA,eAGpC,QAHoC,CAG3B,OAH2B,CAGnB,OAHmB,CAAA,CAAA,CAAA,CAAA,KAAA,EAGD,MAHC,GAAA,QAGgB,OAHhB,CAAA,MAG8B,MAH9B,EAAA,MAG4C,OAH5C,CAAA,GAAA,KAAA,EAAA,EAAA,GAG8D,CAAA,CAAA,SAH9D,CAG8D,MAH9D,GAAA,QAGY,OAHZ,CAAA,MAGY,MAHZ,EAAA,MAGY,OAHZ,CAAA,GAAA,KAAA,EAAA,SAAA,KAAA,EAAA,GAAA,kBAAA,MAGY,CAHZ,GAGY,CAHZ,CAGY,CAHZ,CAAA,EAAA,GAAA,KAAA,EAGY,CAAA,CAAA,IAAA,CAAA,OAHZ,CAAA"}
|
|
@@ -211,22 +211,48 @@ const detectBaseIndent = (tsSource, contentStartOffset) => {
|
|
|
211
211
|
return tsSource.slice(lineStart, i);
|
|
212
212
|
};
|
|
213
213
|
/**
|
|
214
|
+
* Convert graphql-js 2-space indentation to the target indent unit.
|
|
215
|
+
* Strips leading 2-space groups and replaces with equivalent indent units.
|
|
216
|
+
*/
|
|
217
|
+
const convertGraphqlIndent = (line, indentUnit) => {
|
|
218
|
+
if (indentUnit === " ") return line;
|
|
219
|
+
let level = 0;
|
|
220
|
+
let pos = 0;
|
|
221
|
+
while (pos + 1 < line.length && line[pos] === " " && line[pos + 1] === " ") {
|
|
222
|
+
level++;
|
|
223
|
+
pos += 2;
|
|
224
|
+
}
|
|
225
|
+
if (level === 0) return line;
|
|
226
|
+
return indentUnit.repeat(level) + line.slice(pos);
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
214
229
|
* Re-indent formatted GraphQL to match the embedding context.
|
|
215
230
|
*
|
|
216
|
-
*
|
|
217
|
-
* -
|
|
218
|
-
*
|
|
231
|
+
* **Inline templates** (content does NOT start with `\n`):
|
|
232
|
+
* - Line 0 (`{`): no prefix — appears right after backtick
|
|
233
|
+
* - Lines 1..N: graphql-js indentation only (converted to target unit)
|
|
234
|
+
*
|
|
235
|
+
* **Block templates** (content starts with `\n`):
|
|
236
|
+
* - All lines: `baseIndent + indentUnit` prefix + converted graphql indent
|
|
237
|
+
* - Trailing: `\n` + `baseIndent`
|
|
219
238
|
*/
|
|
220
|
-
const reindent = (formatted, baseIndent, originalContent) => {
|
|
239
|
+
const reindent = (formatted, baseIndent, originalContent, indentUnit = " ") => {
|
|
221
240
|
const trimmedFormatted = formatted.trim();
|
|
222
241
|
if (!originalContent.includes("\n") && !trimmedFormatted.includes("\n")) {
|
|
223
242
|
return trimmedFormatted;
|
|
224
243
|
}
|
|
225
|
-
const indent = `${baseIndent} `;
|
|
226
244
|
const lines = trimmedFormatted.split("\n");
|
|
227
|
-
const indentedLines = lines.map((line) => line.trim() === "" ? "" : indent + line);
|
|
228
245
|
const startsWithNewline = originalContent.startsWith("\n");
|
|
229
|
-
const endsWithNewline = originalContent
|
|
246
|
+
const endsWithNewline = /\n\s*$/.test(originalContent);
|
|
247
|
+
const indentedLines = lines.map((line, i) => {
|
|
248
|
+
if (line.trim() === "") return "";
|
|
249
|
+
const converted = convertGraphqlIndent(line, indentUnit);
|
|
250
|
+
if (!startsWithNewline) {
|
|
251
|
+
if (i === 0) return converted;
|
|
252
|
+
return baseIndent + converted;
|
|
253
|
+
}
|
|
254
|
+
return `${baseIndent}${indentUnit}${converted}`;
|
|
255
|
+
});
|
|
230
256
|
let result = indentedLines.join("\n");
|
|
231
257
|
if (startsWithNewline) {
|
|
232
258
|
result = `\n${result}`;
|
|
@@ -236,6 +262,25 @@ const reindent = (formatted, baseIndent, originalContent) => {
|
|
|
236
262
|
}
|
|
237
263
|
return result;
|
|
238
264
|
};
|
|
265
|
+
/**
|
|
266
|
+
* Detect the indentation unit used in a TypeScript source file.
|
|
267
|
+
*
|
|
268
|
+
* Uses the smallest indentation width found as the indent unit.
|
|
269
|
+
* In files with embedded templates (e.g., graphql in backticks),
|
|
270
|
+
* template content lines are at `baseIndent + graphqlIndent`,
|
|
271
|
+
* so their absolute indent is always >= the file's indent unit.
|
|
272
|
+
*/
|
|
273
|
+
const detectIndentUnit = (tsSource) => {
|
|
274
|
+
let minSpaceIndent = Infinity;
|
|
275
|
+
for (const line of tsSource.split("\n")) {
|
|
276
|
+
if (line.length === 0 || line.trimStart().length === 0) continue;
|
|
277
|
+
if (line[0] === " ") return " ";
|
|
278
|
+
const match = line.match(/^( +)\S/);
|
|
279
|
+
if (match) minSpaceIndent = Math.min(minSpaceIndent, match[1].length);
|
|
280
|
+
}
|
|
281
|
+
if (minSpaceIndent === Infinity || minSpaceIndent <= 1) return " ";
|
|
282
|
+
return " ".repeat(minSpaceIndent);
|
|
283
|
+
};
|
|
239
284
|
const GRAPHQL_KEYWORDS = new Set([
|
|
240
285
|
"query",
|
|
241
286
|
"mutation",
|
|
@@ -305,6 +350,7 @@ const unwrapFormattedContent = (formatted, prefixPattern) => {
|
|
|
305
350
|
* @returns Array of edits to apply to the source (sorted by position, not yet applied)
|
|
306
351
|
*/
|
|
307
352
|
const formatTemplatesInSource = (templates, tsSource, formatGraphql) => {
|
|
353
|
+
const indentUnit = detectIndentUnit(tsSource);
|
|
308
354
|
const edits = [];
|
|
309
355
|
for (const template of templates) {
|
|
310
356
|
if (!template.contentRange) continue;
|
|
@@ -327,7 +373,7 @@ const formatTemplatesInSource = (templates, tsSource, formatGraphql) => {
|
|
|
327
373
|
continue;
|
|
328
374
|
}
|
|
329
375
|
const baseIndent = detectBaseIndent(tsSource, template.contentRange.start);
|
|
330
|
-
const reindented = reindent(unwrapped, baseIndent, template.content);
|
|
376
|
+
const reindented = reindent(unwrapped, baseIndent, template.content, indentUnit);
|
|
331
377
|
if (reindented === template.content) {
|
|
332
378
|
continue;
|
|
333
379
|
}
|
|
@@ -344,6 +390,7 @@ const formatTemplatesInSource = (templates, tsSource, formatGraphql) => {
|
|
|
344
390
|
exports.OPERATION_KINDS = OPERATION_KINDS;
|
|
345
391
|
exports.buildGraphqlWrapper = buildGraphqlWrapper;
|
|
346
392
|
exports.detectBaseIndent = detectBaseIndent;
|
|
393
|
+
exports.detectIndentUnit = detectIndentUnit;
|
|
347
394
|
exports.extractFromTaggedTemplate = extractFromTaggedTemplate;
|
|
348
395
|
exports.extractTemplatesFromCallback = extractTemplatesFromCallback;
|
|
349
396
|
exports.findGqlCall = findGqlCall;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-extraction.cjs","names":["templates: ExtractedTemplate[]","kind: string","elementName: string | undefined","typeName: string | undefined","expressionRanges: { start: number; end: number }[]","parts: string[]","template: ExtractedTemplate","prefix","prefixPattern","edits: TemplateFormatEdit[]","formatted: string"],"sources":["../src/template-extraction/extract.ts","../src/template-extraction/format.ts"],"sourcesContent":["/**\n * Template extraction from TypeScript source using SWC AST.\n *\n * Based on the typegen extractor (superset): supports both bare-tag\n * (`query\\`...\\``) and curried (`query(\"Name\")\\`...\\``) syntax.\n *\n * Position tracking is optional — pass spanOffset + converter to\n * populate contentRange on extracted templates.\n *\n * @module\n */\n\n// Re-export for convenience — SWC types are type-only imports\nimport type { ArrowFunctionExpression, CallExpression, MemberExpression, Node, TaggedTemplateExpression } from \"@swc/types\";\nimport type { SwcSpanConverter } from \"../utils/swc-span\";\nimport type { ExtractedTemplate, ExtractedTemplateWithPosition, OperationKind } from \"./types\";\n\nexport const OPERATION_KINDS = new Set<string>([\"query\", \"mutation\", \"subscription\", \"fragment\"]);\n\nexport const isOperationKind = (value: string): value is OperationKind => OPERATION_KINDS.has(value);\n\n/** Optional position tracking context for extraction. */\nexport type PositionTrackingContext = {\n readonly spanOffset: number;\n readonly converter: SwcSpanConverter;\n};\n\n/**\n * Check if a call expression is a gql.{schemaName}(...) call.\n * Returns the schema name if it is, null otherwise.\n */\nexport const getGqlCallSchemaName = (identifiers: ReadonlySet<string>, call: CallExpression): string | null => {\n const callee = call.callee;\n if (callee.type !== \"MemberExpression\") {\n return null;\n }\n\n const member = callee as MemberExpression;\n if (member.object.type !== \"Identifier\" || !identifiers.has(member.object.value)) {\n return null;\n }\n\n if (member.property.type !== \"Identifier\") {\n return null;\n }\n\n const firstArg = call.arguments[0];\n if (!firstArg?.expression || firstArg.expression.type !== \"ArrowFunctionExpression\") {\n return null;\n }\n\n return member.property.value;\n};\n\n/**\n * Extract templates from a gql callback's arrow function body.\n * Handles both expression bodies and block bodies with return statements.\n */\nexport const extractTemplatesFromCallback = (\n arrow: ArrowFunctionExpression,\n schemaName: string,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[] => {\n const templates: ExtractedTemplate[] = [];\n\n const processExpression = (expr: Node): void => {\n // Direct tagged template: query(\"Name\")`...`\n if (expr.type === \"TaggedTemplateExpression\") {\n const tagged = expr as unknown as TaggedTemplateExpression;\n extractFromTaggedTemplate(tagged, schemaName, templates, positionCtx);\n return;\n }\n\n // Metadata chaining: query(\"Name\")`...`({ metadata: {} })\n if (expr.type === \"CallExpression\") {\n const call = expr as unknown as CallExpression;\n if (call.callee.type === \"TaggedTemplateExpression\") {\n extractFromTaggedTemplate(call.callee as TaggedTemplateExpression, schemaName, templates, positionCtx);\n }\n }\n };\n\n // Expression body: ({ query }) => query(\"Name\")`...`\n if (arrow.body.type !== \"BlockStatement\") {\n processExpression(arrow.body);\n return templates;\n }\n\n // Block body: ({ query }) => { return query(\"Name\")`...`; }\n for (const stmt of arrow.body.stmts) {\n if (stmt.type === \"ReturnStatement\" && stmt.argument) {\n processExpression(stmt.argument);\n }\n }\n\n return templates;\n};\n\n/**\n * Extract a single template from a tagged template expression.\n * Supports both bare-tag (Identifier) and curried (CallExpression) tag forms.\n */\nexport const extractFromTaggedTemplate = (\n tagged: TaggedTemplateExpression,\n schemaName: string,\n templates: ExtractedTemplate[],\n positionCtx?: PositionTrackingContext,\n): void => {\n // Tag can be:\n // - CallExpression: query(\"name\")`...` or fragment(\"name\", \"type\")`...` (curried syntax)\n // - Identifier: legacy bare-tag form (skipped if it contains interpolations)\n let kind: string;\n let elementName: string | undefined;\n let typeName: string | undefined;\n\n if (tagged.tag.type === \"Identifier\") {\n kind = tagged.tag.value;\n } else if (tagged.tag.type === \"CallExpression\") {\n const tagCall = tagged.tag as CallExpression;\n if (tagCall.callee.type === \"Identifier\") {\n kind = tagCall.callee.value;\n } else {\n return;\n }\n // Extract elementName and typeName from call arguments\n const firstArg = tagCall.arguments[0]?.expression;\n if (firstArg?.type === \"StringLiteral\") {\n elementName = (firstArg as { value: string }).value;\n }\n const secondArg = tagCall.arguments[1]?.expression;\n if (secondArg?.type === \"StringLiteral\") {\n typeName = (secondArg as { value: string }).value;\n }\n } else {\n return;\n }\n\n if (!isOperationKind(kind)) {\n return;\n }\n\n const { quasis, expressions } = tagged.template;\n\n // For legacy Identifier tag, skip templates with interpolations\n if (tagged.tag.type === \"Identifier\" && expressions.length > 0) {\n return;\n }\n\n if (quasis.length === 0) {\n return;\n }\n\n // Build content and optionally track position\n let contentStart = -1;\n let contentEnd = -1;\n const expressionRanges: { start: number; end: number }[] = [];\n\n const parts: string[] = [];\n for (let i = 0; i < quasis.length; i++) {\n const quasi = quasis[i];\n if (!quasi) continue;\n\n if (positionCtx) {\n const quasiStart = positionCtx.converter.byteOffsetToCharIndex(quasi.span.start - positionCtx.spanOffset);\n const quasiEnd = positionCtx.converter.byteOffsetToCharIndex(quasi.span.end - positionCtx.spanOffset);\n if (contentStart === -1) contentStart = quasiStart;\n contentEnd = quasiEnd;\n }\n\n parts.push(quasi.cooked ?? quasi.raw);\n if (i < expressions.length) {\n parts.push(`__FRAG_SPREAD_${i}__`);\n if (positionCtx) {\n // All SWC AST nodes have span; cast needed because Expression union type doesn't expose it uniformly\n const expr = expressions[i] as unknown as { span: { start: number; end: number } };\n const exprStart = positionCtx.converter.byteOffsetToCharIndex(expr.span.start - positionCtx.spanOffset);\n const exprEnd = positionCtx.converter.byteOffsetToCharIndex(expr.span.end - positionCtx.spanOffset);\n expressionRanges.push({ start: exprStart, end: exprEnd });\n }\n }\n }\n const content = parts.join(\"\");\n\n const template: ExtractedTemplate = {\n schemaName,\n kind,\n content,\n ...(elementName !== undefined ? { elementName } : {}),\n ...(typeName !== undefined ? { typeName } : {}),\n ...(positionCtx && contentStart !== -1 && contentEnd !== -1\n ? { contentRange: { start: contentStart, end: contentEnd } }\n : {}),\n ...(expressionRanges.length > 0 ? { expressionRanges } : {}),\n };\n\n templates.push(template);\n};\n\n/**\n * Find the innermost gql call, unwrapping method chains like .attach().\n */\nexport const findGqlCall = (identifiers: ReadonlySet<string>, node: Node): CallExpression | null => {\n if (!node || node.type !== \"CallExpression\") {\n return null;\n }\n\n const call = node as unknown as CallExpression;\n if (getGqlCallSchemaName(identifiers, call) !== null) {\n return call;\n }\n\n const callee = call.callee;\n if (callee.type !== \"MemberExpression\") {\n return null;\n }\n\n return findGqlCall(identifiers, callee.object as unknown as Node);\n};\n\n/**\n * Walk AST to find gql calls and extract templates.\n */\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx: PositionTrackingContext,\n): ExtractedTemplateWithPosition[];\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[];\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[] {\n const templates: ExtractedTemplate[] = [];\n\n const visit = (n: Node | ReadonlyArray<Node> | Record<string, unknown>): void => {\n if (!n || typeof n !== \"object\") {\n return;\n }\n\n if (\"type\" in n && n.type === \"CallExpression\") {\n const gqlCall = findGqlCall(identifiers, n as Node);\n if (gqlCall) {\n const schemaName = getGqlCallSchemaName(identifiers, gqlCall);\n if (schemaName) {\n const arrow = gqlCall.arguments[0]?.expression as ArrowFunctionExpression;\n templates.push(...extractTemplatesFromCallback(arrow, schemaName, positionCtx));\n }\n return; // Don't recurse into gql calls\n }\n }\n\n // Recurse into all array and object properties\n if (Array.isArray(n)) {\n for (const item of n) {\n visit(item as Node);\n }\n return;\n }\n\n for (const key of Object.keys(n)) {\n if (key === \"span\" || key === \"type\") {\n continue;\n }\n const value = (n as Record<string, unknown>)[key];\n if (value && typeof value === \"object\") {\n visit(value as Node);\n }\n }\n };\n\n visit(node);\n return templates;\n}\n","/**\n * GraphQL template formatting utilities.\n *\n * Pure string operations — no dependency on `graphql` package.\n * Consumers provide their own format function (e.g., graphql-js parse/print).\n *\n * @module\n */\n\nimport type { ExtractedTemplate, TemplateFormatEdit } from \"./types\";\n\n/** A function that formats GraphQL source text. */\nexport type FormatGraphqlFn = (source: string) => string;\n\n/**\n * Detect the base indentation for a template by looking at the line\n * containing the opening backtick.\n */\nexport const detectBaseIndent = (tsSource: string, contentStartOffset: number): string => {\n // Find the start of the line containing contentStartOffset\n let lineStart = contentStartOffset;\n while (lineStart > 0 && tsSource.charCodeAt(lineStart - 1) !== 10) {\n lineStart--;\n }\n\n // Extract leading whitespace from that line\n let i = lineStart;\n while (i < tsSource.length && (tsSource.charCodeAt(i) === 32 || tsSource.charCodeAt(i) === 9)) {\n i++;\n }\n\n return tsSource.slice(lineStart, i);\n};\n\n/**\n * Re-indent formatted GraphQL to match the embedding context.\n *\n * - If original was single-line and formatted is single-line, keep as-is\n * - Otherwise, apply base indent + one level to each line, preserving\n * original leading/trailing newline pattern\n */\nexport const reindent = (formatted: string, baseIndent: string, originalContent: string): string => {\n const trimmedFormatted = formatted.trim();\n\n // If original was single-line and formatted is also single-line, keep it\n if (!originalContent.includes(\"\\n\") && !trimmedFormatted.includes(\"\\n\")) {\n return trimmedFormatted;\n }\n\n // For multi-line: use the indentation pattern from the original content\n const indent = `${baseIndent} `; // add one level of indentation\n const lines = trimmedFormatted.split(\"\\n\");\n const indentedLines = lines.map((line) => (line.trim() === \"\" ? \"\" : indent + line));\n\n // Match original leading/trailing newline pattern\n const startsWithNewline = originalContent.startsWith(\"\\n\");\n const endsWithNewline = originalContent.endsWith(\"\\n\");\n\n let result = indentedLines.join(\"\\n\");\n if (startsWithNewline) {\n result = `\\n${result}`;\n }\n if (endsWithNewline) {\n result = `${result}\\n${baseIndent}`;\n }\n\n return result;\n};\n\nconst GRAPHQL_KEYWORDS = new Set([\"query\", \"mutation\", \"subscription\", \"fragment\"]);\n\n/**\n * Reconstruct a full GraphQL document from template content + metadata.\n *\n * For curried syntax, the template `content` is only the body (e.g., `{ user { id } }`).\n * GraphQL parsers need a full document (e.g., `query GetUser { user { id } }`).\n *\n * For bare-tag syntax, the content already starts with a keyword and is a full document.\n *\n * @returns The wrapped source and a regex pattern to strip the synthetic prefix after formatting\n */\nexport const buildGraphqlWrapper = (template: ExtractedTemplate): { wrapped: string; prefixPattern: RegExp | null } => {\n const content = template.content.trimStart();\n const firstWord = content.split(/[\\s({]/)[0] ?? \"\";\n\n // If content already starts with a GraphQL keyword, it's a full document (bare-tag)\n if (GRAPHQL_KEYWORDS.has(firstWord)) {\n return { wrapped: template.content, prefixPattern: null };\n }\n\n // Curried syntax — reconstruct the header\n if (template.elementName) {\n if (template.kind === \"fragment\" && template.typeName) {\n const prefix = `fragment ${template.elementName} on ${template.typeName} `;\n const prefixPattern = new RegExp(`^fragment\\\\s+${template.elementName}\\\\s+on\\\\s+${template.typeName}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n }\n const prefix = `${template.kind} ${template.elementName} `;\n const prefixPattern = new RegExp(`^${template.kind}\\\\s+${template.elementName}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n }\n\n // No elementName — try wrapping with just the kind\n const prefix = `${template.kind} `;\n const prefixPattern = new RegExp(`^${template.kind}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n};\n\n/**\n * Strip the reconstructed prefix from formatted output to get back the template body.\n * Uses regex pattern matching to handle whitespace normalization by the formatter\n * (e.g., `query Foo ($id: ID!)` → `query Foo($id: ID!)`).\n */\nexport const unwrapFormattedContent = (formatted: string, prefixPattern: RegExp | null): string => {\n if (!prefixPattern) return formatted;\n const match = formatted.match(prefixPattern);\n if (!match) return formatted;\n return formatted.slice(match[0].length);\n};\n\n/**\n * Format GraphQL templates within TypeScript source.\n *\n * Applies the given format function to each template's content, handles\n * wrapper reconstruction for curried syntax, and re-indents the result\n * to match the TypeScript embedding context.\n *\n * @returns Array of edits to apply to the source (sorted by position, not yet applied)\n */\nexport const formatTemplatesInSource = (\n templates: readonly ExtractedTemplate[],\n tsSource: string,\n formatGraphql: FormatGraphqlFn,\n): readonly TemplateFormatEdit[] => {\n const edits: TemplateFormatEdit[] = [];\n\n for (const template of templates) {\n if (!template.contentRange) continue;\n\n // Wrap the content for formatting\n const { wrapped, prefixPattern } = buildGraphqlWrapper(template);\n\n let formatted: string;\n try {\n formatted = formatGraphql(wrapped);\n } catch {\n continue;\n }\n\n // Unwrap the prefix\n let unwrapped = unwrapFormattedContent(formatted, prefixPattern);\n\n // Restore interpolation expressions: replace __FRAG_SPREAD_N__ with original ${...} syntax\n if (template.expressionRanges && template.expressionRanges.length > 0) {\n for (let i = 0; i < template.expressionRanges.length; i++) {\n const range = template.expressionRanges[i]!;\n const exprText = tsSource.slice(range.start, range.end);\n unwrapped = unwrapped.replace(`__FRAG_SPREAD_${i}__`, `\\${${exprText}}`);\n }\n }\n\n // Fast path: skip if formatter produces identical output\n if (unwrapped === template.content) {\n continue;\n }\n\n // Detect base indentation from the TS source\n const baseIndent = detectBaseIndent(tsSource, template.contentRange.start);\n\n // Re-indent the formatted output\n const reindented = reindent(unwrapped, baseIndent, template.content);\n\n // Skip if no changes after re-indentation\n if (reindented === template.content) {\n continue;\n }\n\n edits.push({\n start: template.contentRange.start,\n end: template.contentRange.end,\n newText: reindented,\n });\n }\n\n return edits;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"template-extraction.cjs","names":["templates: ExtractedTemplate[]","kind: string","elementName: string | undefined","typeName: string | undefined","expressionRanges: { start: number; end: number }[]","parts: string[]","template: ExtractedTemplate","prefix","prefixPattern","edits: TemplateFormatEdit[]","formatted: string"],"sources":["../src/template-extraction/extract.ts","../src/template-extraction/format.ts"],"sourcesContent":["/**\n * Template extraction from TypeScript source using SWC AST.\n *\n * Based on the typegen extractor (superset): supports both bare-tag\n * (`query\\`...\\``) and curried (`query(\"Name\")\\`...\\``) syntax.\n *\n * Position tracking is optional — pass spanOffset + converter to\n * populate contentRange on extracted templates.\n *\n * @module\n */\n\n// Re-export for convenience — SWC types are type-only imports\nimport type { ArrowFunctionExpression, CallExpression, MemberExpression, Node, TaggedTemplateExpression } from \"@swc/types\";\nimport type { SwcSpanConverter } from \"../utils/swc-span\";\nimport type { ExtractedTemplate, ExtractedTemplateWithPosition, OperationKind } from \"./types\";\n\nexport const OPERATION_KINDS = new Set<string>([\"query\", \"mutation\", \"subscription\", \"fragment\"]);\n\nexport const isOperationKind = (value: string): value is OperationKind => OPERATION_KINDS.has(value);\n\n/** Optional position tracking context for extraction. */\nexport type PositionTrackingContext = {\n readonly spanOffset: number;\n readonly converter: SwcSpanConverter;\n};\n\n/**\n * Check if a call expression is a gql.{schemaName}(...) call.\n * Returns the schema name if it is, null otherwise.\n */\nexport const getGqlCallSchemaName = (identifiers: ReadonlySet<string>, call: CallExpression): string | null => {\n const callee = call.callee;\n if (callee.type !== \"MemberExpression\") {\n return null;\n }\n\n const member = callee as MemberExpression;\n if (member.object.type !== \"Identifier\" || !identifiers.has(member.object.value)) {\n return null;\n }\n\n if (member.property.type !== \"Identifier\") {\n return null;\n }\n\n const firstArg = call.arguments[0];\n if (!firstArg?.expression || firstArg.expression.type !== \"ArrowFunctionExpression\") {\n return null;\n }\n\n return member.property.value;\n};\n\n/**\n * Extract templates from a gql callback's arrow function body.\n * Handles both expression bodies and block bodies with return statements.\n */\nexport const extractTemplatesFromCallback = (\n arrow: ArrowFunctionExpression,\n schemaName: string,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[] => {\n const templates: ExtractedTemplate[] = [];\n\n const processExpression = (expr: Node): void => {\n // Direct tagged template: query(\"Name\")`...`\n if (expr.type === \"TaggedTemplateExpression\") {\n const tagged = expr as unknown as TaggedTemplateExpression;\n extractFromTaggedTemplate(tagged, schemaName, templates, positionCtx);\n return;\n }\n\n // Metadata chaining: query(\"Name\")`...`({ metadata: {} })\n if (expr.type === \"CallExpression\") {\n const call = expr as unknown as CallExpression;\n if (call.callee.type === \"TaggedTemplateExpression\") {\n extractFromTaggedTemplate(call.callee as TaggedTemplateExpression, schemaName, templates, positionCtx);\n }\n }\n };\n\n // Expression body: ({ query }) => query(\"Name\")`...`\n if (arrow.body.type !== \"BlockStatement\") {\n processExpression(arrow.body);\n return templates;\n }\n\n // Block body: ({ query }) => { return query(\"Name\")`...`; }\n for (const stmt of arrow.body.stmts) {\n if (stmt.type === \"ReturnStatement\" && stmt.argument) {\n processExpression(stmt.argument);\n }\n }\n\n return templates;\n};\n\n/**\n * Extract a single template from a tagged template expression.\n * Supports both bare-tag (Identifier) and curried (CallExpression) tag forms.\n */\nexport const extractFromTaggedTemplate = (\n tagged: TaggedTemplateExpression,\n schemaName: string,\n templates: ExtractedTemplate[],\n positionCtx?: PositionTrackingContext,\n): void => {\n // Tag can be:\n // - CallExpression: query(\"name\")`...` or fragment(\"name\", \"type\")`...` (curried syntax)\n // - Identifier: legacy bare-tag form (skipped if it contains interpolations)\n let kind: string;\n let elementName: string | undefined;\n let typeName: string | undefined;\n\n if (tagged.tag.type === \"Identifier\") {\n kind = tagged.tag.value;\n } else if (tagged.tag.type === \"CallExpression\") {\n const tagCall = tagged.tag as CallExpression;\n if (tagCall.callee.type === \"Identifier\") {\n kind = tagCall.callee.value;\n } else {\n return;\n }\n // Extract elementName and typeName from call arguments\n const firstArg = tagCall.arguments[0]?.expression;\n if (firstArg?.type === \"StringLiteral\") {\n elementName = (firstArg as { value: string }).value;\n }\n const secondArg = tagCall.arguments[1]?.expression;\n if (secondArg?.type === \"StringLiteral\") {\n typeName = (secondArg as { value: string }).value;\n }\n } else {\n return;\n }\n\n if (!isOperationKind(kind)) {\n return;\n }\n\n const { quasis, expressions } = tagged.template;\n\n // For legacy Identifier tag, skip templates with interpolations\n if (tagged.tag.type === \"Identifier\" && expressions.length > 0) {\n return;\n }\n\n if (quasis.length === 0) {\n return;\n }\n\n // Build content and optionally track position\n let contentStart = -1;\n let contentEnd = -1;\n const expressionRanges: { start: number; end: number }[] = [];\n\n const parts: string[] = [];\n for (let i = 0; i < quasis.length; i++) {\n const quasi = quasis[i];\n if (!quasi) continue;\n\n if (positionCtx) {\n const quasiStart = positionCtx.converter.byteOffsetToCharIndex(quasi.span.start - positionCtx.spanOffset);\n const quasiEnd = positionCtx.converter.byteOffsetToCharIndex(quasi.span.end - positionCtx.spanOffset);\n if (contentStart === -1) contentStart = quasiStart;\n contentEnd = quasiEnd;\n }\n\n parts.push(quasi.cooked ?? quasi.raw);\n if (i < expressions.length) {\n parts.push(`__FRAG_SPREAD_${i}__`);\n if (positionCtx) {\n // All SWC AST nodes have span; cast needed because Expression union type doesn't expose it uniformly\n const expr = expressions[i] as unknown as { span: { start: number; end: number } };\n const exprStart = positionCtx.converter.byteOffsetToCharIndex(expr.span.start - positionCtx.spanOffset);\n const exprEnd = positionCtx.converter.byteOffsetToCharIndex(expr.span.end - positionCtx.spanOffset);\n expressionRanges.push({ start: exprStart, end: exprEnd });\n }\n }\n }\n const content = parts.join(\"\");\n\n const template: ExtractedTemplate = {\n schemaName,\n kind,\n content,\n ...(elementName !== undefined ? { elementName } : {}),\n ...(typeName !== undefined ? { typeName } : {}),\n ...(positionCtx && contentStart !== -1 && contentEnd !== -1\n ? { contentRange: { start: contentStart, end: contentEnd } }\n : {}),\n ...(expressionRanges.length > 0 ? { expressionRanges } : {}),\n };\n\n templates.push(template);\n};\n\n/**\n * Find the innermost gql call, unwrapping method chains like .attach().\n */\nexport const findGqlCall = (identifiers: ReadonlySet<string>, node: Node): CallExpression | null => {\n if (!node || node.type !== \"CallExpression\") {\n return null;\n }\n\n const call = node as unknown as CallExpression;\n if (getGqlCallSchemaName(identifiers, call) !== null) {\n return call;\n }\n\n const callee = call.callee;\n if (callee.type !== \"MemberExpression\") {\n return null;\n }\n\n return findGqlCall(identifiers, callee.object as unknown as Node);\n};\n\n/**\n * Walk AST to find gql calls and extract templates.\n */\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx: PositionTrackingContext,\n): ExtractedTemplateWithPosition[];\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[];\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[] {\n const templates: ExtractedTemplate[] = [];\n\n const visit = (n: Node | ReadonlyArray<Node> | Record<string, unknown>): void => {\n if (!n || typeof n !== \"object\") {\n return;\n }\n\n if (\"type\" in n && n.type === \"CallExpression\") {\n const gqlCall = findGqlCall(identifiers, n as Node);\n if (gqlCall) {\n const schemaName = getGqlCallSchemaName(identifiers, gqlCall);\n if (schemaName) {\n const arrow = gqlCall.arguments[0]?.expression as ArrowFunctionExpression;\n templates.push(...extractTemplatesFromCallback(arrow, schemaName, positionCtx));\n }\n return; // Don't recurse into gql calls\n }\n }\n\n // Recurse into all array and object properties\n if (Array.isArray(n)) {\n for (const item of n) {\n visit(item as Node);\n }\n return;\n }\n\n for (const key of Object.keys(n)) {\n if (key === \"span\" || key === \"type\") {\n continue;\n }\n const value = (n as Record<string, unknown>)[key];\n if (value && typeof value === \"object\") {\n visit(value as Node);\n }\n }\n };\n\n visit(node);\n return templates;\n}\n","/**\n * GraphQL template formatting utilities.\n *\n * Pure string operations — no dependency on `graphql` package.\n * Consumers provide their own format function (e.g., graphql-js parse/print).\n *\n * @module\n */\n\nimport type { ExtractedTemplate, TemplateFormatEdit } from \"./types\";\n\n/** A function that formats GraphQL source text. */\nexport type FormatGraphqlFn = (source: string) => string;\n\n/**\n * Detect the base indentation for a template by looking at the line\n * containing the opening backtick.\n */\nexport const detectBaseIndent = (tsSource: string, contentStartOffset: number): string => {\n // Find the start of the line containing contentStartOffset\n let lineStart = contentStartOffset;\n while (lineStart > 0 && tsSource.charCodeAt(lineStart - 1) !== 10) {\n lineStart--;\n }\n\n // Extract leading whitespace from that line\n let i = lineStart;\n while (i < tsSource.length && (tsSource.charCodeAt(i) === 32 || tsSource.charCodeAt(i) === 9)) {\n i++;\n }\n\n return tsSource.slice(lineStart, i);\n};\n\n/**\n * Convert graphql-js 2-space indentation to the target indent unit.\n * Strips leading 2-space groups and replaces with equivalent indent units.\n */\nconst convertGraphqlIndent = (line: string, indentUnit: string): string => {\n if (indentUnit === \" \") return line; // No conversion needed\n let level = 0;\n let pos = 0;\n while (pos + 1 < line.length && line[pos] === \" \" && line[pos + 1] === \" \") {\n level++;\n pos += 2;\n }\n if (level === 0) return line;\n return indentUnit.repeat(level) + line.slice(pos);\n};\n\n/**\n * Re-indent formatted GraphQL to match the embedding context.\n *\n * **Inline templates** (content does NOT start with `\\n`):\n * - Line 0 (`{`): no prefix — appears right after backtick\n * - Lines 1..N: graphql-js indentation only (converted to target unit)\n *\n * **Block templates** (content starts with `\\n`):\n * - All lines: `baseIndent + indentUnit` prefix + converted graphql indent\n * - Trailing: `\\n` + `baseIndent`\n */\nexport const reindent = (formatted: string, baseIndent: string, originalContent: string, indentUnit: string = \" \"): string => {\n const trimmedFormatted = formatted.trim();\n\n // If original was single-line and formatted is also single-line, keep it\n if (!originalContent.includes(\"\\n\") && !trimmedFormatted.includes(\"\\n\")) {\n return trimmedFormatted;\n }\n\n const lines = trimmedFormatted.split(\"\\n\");\n\n // Match original leading/trailing newline pattern\n const startsWithNewline = originalContent.startsWith(\"\\n\");\n const endsWithNewline = /\\n\\s*$/.test(originalContent);\n\n const indentedLines = lines.map((line, i) => {\n if (line.trim() === \"\") return \"\";\n const converted = convertGraphqlIndent(line, indentUnit);\n\n if (!startsWithNewline) {\n // Inline template: first line has no prefix (appears after backtick);\n // body lines get baseIndent + converted graphql indent to align with TS context.\n if (i === 0) return converted;\n return baseIndent + converted;\n }\n\n // Block template: every line gets baseIndent + indentUnit prefix\n return `${baseIndent}${indentUnit}${converted}`;\n });\n\n let result = indentedLines.join(\"\\n\");\n if (startsWithNewline) {\n result = `\\n${result}`;\n }\n if (endsWithNewline) {\n result = `${result}\\n${baseIndent}`;\n }\n\n return result;\n};\n\n/**\n * Detect the indentation unit used in a TypeScript source file.\n *\n * Uses the smallest indentation width found as the indent unit.\n * In files with embedded templates (e.g., graphql in backticks),\n * template content lines are at `baseIndent + graphqlIndent`,\n * so their absolute indent is always >= the file's indent unit.\n */\nexport const detectIndentUnit = (tsSource: string): string => {\n let minSpaceIndent = Infinity;\n for (const line of tsSource.split(\"\\n\")) {\n if (line.length === 0 || line.trimStart().length === 0) continue;\n if (line[0] === \"\\t\") return \"\\t\";\n const match = line.match(/^( +)\\S/);\n if (match) minSpaceIndent = Math.min(minSpaceIndent, match[1]!.length);\n }\n if (minSpaceIndent === Infinity || minSpaceIndent <= 1) return \" \";\n return \" \".repeat(minSpaceIndent);\n};\n\nconst GRAPHQL_KEYWORDS = new Set([\"query\", \"mutation\", \"subscription\", \"fragment\"]);\n\n/**\n * Reconstruct a full GraphQL document from template content + metadata.\n *\n * For curried syntax, the template `content` is only the body (e.g., `{ user { id } }`).\n * GraphQL parsers need a full document (e.g., `query GetUser { user { id } }`).\n *\n * For bare-tag syntax, the content already starts with a keyword and is a full document.\n *\n * @returns The wrapped source and a regex pattern to strip the synthetic prefix after formatting\n */\nexport const buildGraphqlWrapper = (template: ExtractedTemplate): { wrapped: string; prefixPattern: RegExp | null } => {\n const content = template.content.trimStart();\n const firstWord = content.split(/[\\s({]/)[0] ?? \"\";\n\n // If content already starts with a GraphQL keyword, it's a full document (bare-tag)\n if (GRAPHQL_KEYWORDS.has(firstWord)) {\n return { wrapped: template.content, prefixPattern: null };\n }\n\n // Curried syntax — reconstruct the header\n if (template.elementName) {\n if (template.kind === \"fragment\" && template.typeName) {\n const prefix = `fragment ${template.elementName} on ${template.typeName} `;\n const prefixPattern = new RegExp(`^fragment\\\\s+${template.elementName}\\\\s+on\\\\s+${template.typeName}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n }\n const prefix = `${template.kind} ${template.elementName} `;\n const prefixPattern = new RegExp(`^${template.kind}\\\\s+${template.elementName}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n }\n\n // No elementName — try wrapping with just the kind\n const prefix = `${template.kind} `;\n const prefixPattern = new RegExp(`^${template.kind}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n};\n\n/**\n * Strip the reconstructed prefix from formatted output to get back the template body.\n * Uses regex pattern matching to handle whitespace normalization by the formatter\n * (e.g., `query Foo ($id: ID!)` → `query Foo($id: ID!)`).\n */\nexport const unwrapFormattedContent = (formatted: string, prefixPattern: RegExp | null): string => {\n if (!prefixPattern) return formatted;\n const match = formatted.match(prefixPattern);\n if (!match) return formatted;\n return formatted.slice(match[0].length);\n};\n\n/**\n * Format GraphQL templates within TypeScript source.\n *\n * Applies the given format function to each template's content, handles\n * wrapper reconstruction for curried syntax, and re-indents the result\n * to match the TypeScript embedding context.\n *\n * @returns Array of edits to apply to the source (sorted by position, not yet applied)\n */\nexport const formatTemplatesInSource = (\n templates: readonly ExtractedTemplate[],\n tsSource: string,\n formatGraphql: FormatGraphqlFn,\n): readonly TemplateFormatEdit[] => {\n const indentUnit = detectIndentUnit(tsSource);\n const edits: TemplateFormatEdit[] = [];\n\n for (const template of templates) {\n if (!template.contentRange) continue;\n\n // Wrap the content for formatting\n const { wrapped, prefixPattern } = buildGraphqlWrapper(template);\n\n let formatted: string;\n try {\n formatted = formatGraphql(wrapped);\n } catch {\n continue;\n }\n\n // Unwrap the prefix\n let unwrapped = unwrapFormattedContent(formatted, prefixPattern);\n\n // Restore interpolation expressions: replace __FRAG_SPREAD_N__ with original ${...} syntax\n if (template.expressionRanges && template.expressionRanges.length > 0) {\n for (let i = 0; i < template.expressionRanges.length; i++) {\n const range = template.expressionRanges[i]!;\n const exprText = tsSource.slice(range.start, range.end);\n unwrapped = unwrapped.replace(`__FRAG_SPREAD_${i}__`, `\\${${exprText}}`);\n }\n }\n\n // Fast path: skip if formatter produces identical output\n if (unwrapped === template.content) {\n continue;\n }\n\n // Detect base indentation from the TS source\n const baseIndent = detectBaseIndent(tsSource, template.contentRange.start);\n\n // Re-indent the formatted output\n const reindented = reindent(unwrapped, baseIndent, template.content, indentUnit);\n\n // Skip if no changes after re-indentation\n if (reindented === template.content) {\n continue;\n }\n\n edits.push({\n start: template.contentRange.start,\n end: template.contentRange.end,\n newText: reindented,\n });\n }\n\n return edits;\n};\n"],"mappings":"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"}
|
|
@@ -91,11 +91,24 @@ declare const detectBaseIndent: (tsSource: string, contentStartOffset: number) =
|
|
|
91
91
|
/**
|
|
92
92
|
* Re-indent formatted GraphQL to match the embedding context.
|
|
93
93
|
*
|
|
94
|
-
*
|
|
95
|
-
* -
|
|
96
|
-
*
|
|
94
|
+
* **Inline templates** (content does NOT start with `\n`):
|
|
95
|
+
* - Line 0 (`{`): no prefix — appears right after backtick
|
|
96
|
+
* - Lines 1..N: graphql-js indentation only (converted to target unit)
|
|
97
|
+
*
|
|
98
|
+
* **Block templates** (content starts with `\n`):
|
|
99
|
+
* - All lines: `baseIndent + indentUnit` prefix + converted graphql indent
|
|
100
|
+
* - Trailing: `\n` + `baseIndent`
|
|
101
|
+
*/
|
|
102
|
+
declare const reindent: (formatted: string, baseIndent: string, originalContent: string, indentUnit?: string) => string;
|
|
103
|
+
/**
|
|
104
|
+
* Detect the indentation unit used in a TypeScript source file.
|
|
105
|
+
*
|
|
106
|
+
* Uses the smallest indentation width found as the indent unit.
|
|
107
|
+
* In files with embedded templates (e.g., graphql in backticks),
|
|
108
|
+
* template content lines are at `baseIndent + graphqlIndent`,
|
|
109
|
+
* so their absolute indent is always >= the file's indent unit.
|
|
97
110
|
*/
|
|
98
|
-
declare const
|
|
111
|
+
declare const detectIndentUnit: (tsSource: string) => string;
|
|
99
112
|
/**
|
|
100
113
|
* Reconstruct a full GraphQL document from template content + metadata.
|
|
101
114
|
*
|
|
@@ -127,5 +140,5 @@ declare const unwrapFormattedContent: (formatted: string, prefixPattern: RegExp
|
|
|
127
140
|
*/
|
|
128
141
|
declare const formatTemplatesInSource: (templates: readonly ExtractedTemplate[], tsSource: string, formatGraphql: FormatGraphqlFn) => readonly TemplateFormatEdit[];
|
|
129
142
|
//#endregion
|
|
130
|
-
export { ExtractedTemplate, ExtractedTemplateWithPosition, FormatGraphqlFn, OPERATION_KINDS, OperationKind, PositionTrackingContext, TemplateFormatEdit, buildGraphqlWrapper, detectBaseIndent, extractFromTaggedTemplate, extractTemplatesFromCallback, findGqlCall, formatTemplatesInSource, getGqlCallSchemaName, isOperationKind, reindent, unwrapFormattedContent, walkAndExtract };
|
|
143
|
+
export { ExtractedTemplate, ExtractedTemplateWithPosition, FormatGraphqlFn, OPERATION_KINDS, OperationKind, PositionTrackingContext, TemplateFormatEdit, buildGraphqlWrapper, detectBaseIndent, detectIndentUnit, extractFromTaggedTemplate, extractTemplatesFromCallback, findGqlCall, formatTemplatesInSource, getGqlCallSchemaName, isOperationKind, reindent, unwrapFormattedContent, walkAndExtract };
|
|
131
144
|
//# sourceMappingURL=template-extraction.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-extraction.d.cts","names":[],"sources":["../src/template-extraction/types.ts","../src/template-extraction/extract.ts","../src/template-extraction/format.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAMA;AAGY,KAHA,aAAA,GAGiB,OAAA,GAIZ,UAAA,GAAa,cAAA,GAAA,UAAA;AAc9B;AAKY,KAvBA,iBAAA,GAuBkB;;;;ECfjB,SAAA,IAAA,EDJI,aCIgF;EAEpF;EAGD,SAAA,OAAA,EAAA,MAAA;EASC;EA2BA,SAAA,WAAA,CAAA,EAAA,MAAA;EACJ;EAEO,SAAA,QAAA,CAAA,EAAA,MAAA;EACb;EAAiB,SAAA,YAAA,CAAA,EAAA;IAwCP,SAAA,KAAA,EAAA,MAAA;IACH,SAAA,GAAA,EAAA,MAAA;EAEG,CAAA;EACG;EAAuB,SAAA,gBAAA,CAAA,EAAA,SAAA;IA+F1B,SAAA,KAgBZ,EAAA,MAAA;IAhBwC,SAAA,GAAA,EAAA,MAAA;EAA2B,CAAA,EAAA;CAAO;;AAqB3D,KDnMJ,6BAAA,GAAgC,iBCmMd,GAAA;EACtB,SAAA,YAAA,EAAA;IACO,SAAA,KAAA,EAAA,MAAA;IACA,SAAA,GAAA,EAAA,MAAA;EACZ,CAAA;CAA6B;AAChC;AACQ,KDpMI,kBAAA,GCoMJ;EACO,SAAA,KAAA,EAAA,MAAA;EACC,SAAA,GAAA,EAAA,MAAA;EACb,SAAA,OAAA,EAAA,MAAA;CAAiB;;;;AAxMP,cAdA,eAmCZ,EAnC2B,GAcsB,CAAA,MAAA,CAAA;AA2BrC,cAvCA,eA6EZ,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,KAAA,IA7EwD,aA6ExD;;AAnCe,KAvCJ,uBAAA,GAuCI;EACb,SAAA,UAAA,EAAA,MAAA;EAAiB,SAAA,SAAA,EAtCE,gBAsCF;AAwCpB,CAAA;;;;;AAmGa,cA1KA,oBA0LZ,EAAA,CAAA,WAAA,EA1LiD,WA0LjD,CAAA,MAAA,CAAA,EAAA,IAAA,EA1L4E,cA0L5E,EAAA,GAAA,MAAA,GAAA,IAAA;;;;;AAKe,cApKH,4BAoKiB,EAAA,CAAA,KAAA,EAnKrB,uBAmKqB,EAAA,UAAA,EAAA,MAAA,EAAA,WAAA,CAAA,EAjKd,uBAiKc,EAAA,GAhK3B,iBAgK2B,EAAA;;;;;AAIE,cA5HnB,yBA4HmB,EAAA,CAAA,MAAA,EA3HtB,wBA2HsB,EAAA,UAAA,EAAA,MAAA,EAAA,SAAA,EAzHnB,iBAyHmB,EAAA,EAAA,WAAA,CAAA,EAxHhB,uBAwHgB,EAAA,GAAA,IAAA;AAChC;;;AAGgB,cA7BH,WA6BG,EAAA,CAAA,WAAA,EA7ByB,WA6BzB,CAAA,MAAA,CAAA,EAAA,IAAA,EA7BoD,IA6BpD,EAAA,GA7B2D,cA6B3D,GAAA,IAAA;;;;iBARA,cAAA,OACR,mBACO,kCACA,0BACZ;iBACa,cAAA,OACR,mBACO,mCACC,0BACb;;;;KC3NS,eAAA;ADKZ;AAEA;AAGA;AASA;AA2Ba,cCxCA,gBD8EZ,EAAA,CAAA,QAAA,EAAA,MAAA,EAAA,kBAAA,EAAA,MAAA,EAAA,GAAA,MAAA;;;;;AAMD
|
|
1
|
+
{"version":3,"file":"template-extraction.d.cts","names":[],"sources":["../src/template-extraction/types.ts","../src/template-extraction/extract.ts","../src/template-extraction/format.ts"],"sourcesContent":[],"mappings":"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"}
|
|
@@ -91,11 +91,24 @@ declare const detectBaseIndent: (tsSource: string, contentStartOffset: number) =
|
|
|
91
91
|
/**
|
|
92
92
|
* Re-indent formatted GraphQL to match the embedding context.
|
|
93
93
|
*
|
|
94
|
-
*
|
|
95
|
-
* -
|
|
96
|
-
*
|
|
94
|
+
* **Inline templates** (content does NOT start with `\n`):
|
|
95
|
+
* - Line 0 (`{`): no prefix — appears right after backtick
|
|
96
|
+
* - Lines 1..N: graphql-js indentation only (converted to target unit)
|
|
97
|
+
*
|
|
98
|
+
* **Block templates** (content starts with `\n`):
|
|
99
|
+
* - All lines: `baseIndent + indentUnit` prefix + converted graphql indent
|
|
100
|
+
* - Trailing: `\n` + `baseIndent`
|
|
101
|
+
*/
|
|
102
|
+
declare const reindent: (formatted: string, baseIndent: string, originalContent: string, indentUnit?: string) => string;
|
|
103
|
+
/**
|
|
104
|
+
* Detect the indentation unit used in a TypeScript source file.
|
|
105
|
+
*
|
|
106
|
+
* Uses the smallest indentation width found as the indent unit.
|
|
107
|
+
* In files with embedded templates (e.g., graphql in backticks),
|
|
108
|
+
* template content lines are at `baseIndent + graphqlIndent`,
|
|
109
|
+
* so their absolute indent is always >= the file's indent unit.
|
|
97
110
|
*/
|
|
98
|
-
declare const
|
|
111
|
+
declare const detectIndentUnit: (tsSource: string) => string;
|
|
99
112
|
/**
|
|
100
113
|
* Reconstruct a full GraphQL document from template content + metadata.
|
|
101
114
|
*
|
|
@@ -127,5 +140,5 @@ declare const unwrapFormattedContent: (formatted: string, prefixPattern: RegExp
|
|
|
127
140
|
*/
|
|
128
141
|
declare const formatTemplatesInSource: (templates: readonly ExtractedTemplate[], tsSource: string, formatGraphql: FormatGraphqlFn) => readonly TemplateFormatEdit[];
|
|
129
142
|
//#endregion
|
|
130
|
-
export { ExtractedTemplate, ExtractedTemplateWithPosition, FormatGraphqlFn, OPERATION_KINDS, OperationKind, PositionTrackingContext, TemplateFormatEdit, buildGraphqlWrapper, detectBaseIndent, extractFromTaggedTemplate, extractTemplatesFromCallback, findGqlCall, formatTemplatesInSource, getGqlCallSchemaName, isOperationKind, reindent, unwrapFormattedContent, walkAndExtract };
|
|
143
|
+
export { ExtractedTemplate, ExtractedTemplateWithPosition, FormatGraphqlFn, OPERATION_KINDS, OperationKind, PositionTrackingContext, TemplateFormatEdit, buildGraphqlWrapper, detectBaseIndent, detectIndentUnit, extractFromTaggedTemplate, extractTemplatesFromCallback, findGqlCall, formatTemplatesInSource, getGqlCallSchemaName, isOperationKind, reindent, unwrapFormattedContent, walkAndExtract };
|
|
131
144
|
//# sourceMappingURL=template-extraction.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-extraction.d.mts","names":[],"sources":["../src/template-extraction/types.ts","../src/template-extraction/extract.ts","../src/template-extraction/format.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAMA;AAGY,KAHA,aAAA,GAGiB,OAIZ,GAAA,UAAA,GAAa,cAAA,GAAA,UAAA;AAc9B;AAKY,KAvBA,iBAAA,GAuBkB;;;;ECfjB,SAAA,IAAA,EDJI,aCIgF;EAEpF;EAGD,SAAA,OAAA,EAAA,MAAA;EASC;EA2BA,SAAA,WAAA,CAAA,EAAA,MAAA;EACJ;EAEO,SAAA,QAAA,CAAA,EAAA,MAAA;EACb;EAAiB,SAAA,YAAA,CAAA,EAAA;IAwCP,SAAA,KAAA,EAAA,MAAA;IACH,SAAA,GAAA,EAAA,MAAA;EAEG,CAAA;EACG;EAAuB,SAAA,gBAAA,CAAA,EAAA,SAAA;IA+F1B,SAAA,KAgBZ,EAAA,MAAA;IAhBwC,SAAA,GAAA,EAAA,MAAA;EAA2B,CAAA,EAAA;CAAO;;AAqB3D,KDnMJ,6BAAA,GAAgC,iBCmMd,GAAA;EACtB,SAAA,YAAA,EAAA;IACO,SAAA,KAAA,EAAA,MAAA;IACA,SAAA,GAAA,EAAA,MAAA;EACZ,CAAA;CAA6B;AAChC;AACQ,KDpMI,kBAAA,GCoMJ;EACO,SAAA,KAAA,EAAA,MAAA;EACC,SAAA,GAAA,EAAA,MAAA;EACb,SAAA,OAAA,EAAA,MAAA;CAAiB;;;;AAxMP,cAdA,eAmCZ,EAnC2B,GAcsB,CAAA,MAAA,CAAA;AA2BrC,cAvCA,eA6EZ,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,KAAA,IA7EwD,aA6ExD;;AAnCe,KAvCJ,uBAAA,GAuCI;EACb,SAAA,UAAA,EAAA,MAAA;EAAiB,SAAA,SAAA,EAtCE,gBAsCF;AAwCpB,CAAA;;;;;AAmGa,cA1KA,oBA0LZ,EAAA,CAAA,WAAA,EA1LiD,WA0LjD,CAAA,MAAA,CAAA,EAAA,IAAA,EA1L4E,cA0L5E,EAAA,GAAA,MAAA,GAAA,IAAA;;;;;AAKe,cApKH,4BAoKiB,EAAA,CAAA,KAAA,EAnKrB,uBAmKqB,EAAA,UAAA,EAAA,MAAA,EAAA,WAAA,CAAA,EAjKd,uBAiKc,EAAA,GAhK3B,iBAgK2B,EAAA;;;;;AAIE,cA5HnB,yBA4HmB,EAAA,CAAA,MAAA,EA3HtB,wBA2HsB,EAAA,UAAA,EAAA,MAAA,EAAA,SAAA,EAzHnB,iBAyHmB,EAAA,EAAA,WAAA,CAAA,EAxHhB,uBAwHgB,EAAA,GAAA,IAAA;AAChC;;;AAGgB,cA7BH,WA6BG,EAAA,CAAA,WAAA,EA7ByB,WA6BzB,CAAA,MAAA,CAAA,EAAA,IAAA,EA7BoD,IA6BpD,EAAA,GA7B2D,cA6B3D,GAAA,IAAA;;;;iBARA,cAAA,OACR,mBACO,kCACA,0BACZ;iBACa,cAAA,OACR,mBACO,mCACC,0BACb;;;;KC3NS,eAAA;ADKZ;AAEA;AAGA;AASA;AA2Ba,cCxCA,gBD8EZ,EAAA,CAAA,QAAA,EAAA,MAAA,EAAA,kBAAA,EAAA,MAAA,EAAA,GAAA,MAAA;;;;;AAMD
|
|
1
|
+
{"version":3,"file":"template-extraction.d.mts","names":[],"sources":["../src/template-extraction/types.ts","../src/template-extraction/extract.ts","../src/template-extraction/format.ts"],"sourcesContent":[],"mappings":"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"}
|
|
@@ -210,22 +210,48 @@ const detectBaseIndent = (tsSource, contentStartOffset) => {
|
|
|
210
210
|
return tsSource.slice(lineStart, i);
|
|
211
211
|
};
|
|
212
212
|
/**
|
|
213
|
+
* Convert graphql-js 2-space indentation to the target indent unit.
|
|
214
|
+
* Strips leading 2-space groups and replaces with equivalent indent units.
|
|
215
|
+
*/
|
|
216
|
+
const convertGraphqlIndent = (line, indentUnit) => {
|
|
217
|
+
if (indentUnit === " ") return line;
|
|
218
|
+
let level = 0;
|
|
219
|
+
let pos = 0;
|
|
220
|
+
while (pos + 1 < line.length && line[pos] === " " && line[pos + 1] === " ") {
|
|
221
|
+
level++;
|
|
222
|
+
pos += 2;
|
|
223
|
+
}
|
|
224
|
+
if (level === 0) return line;
|
|
225
|
+
return indentUnit.repeat(level) + line.slice(pos);
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
213
228
|
* Re-indent formatted GraphQL to match the embedding context.
|
|
214
229
|
*
|
|
215
|
-
*
|
|
216
|
-
* -
|
|
217
|
-
*
|
|
230
|
+
* **Inline templates** (content does NOT start with `\n`):
|
|
231
|
+
* - Line 0 (`{`): no prefix — appears right after backtick
|
|
232
|
+
* - Lines 1..N: graphql-js indentation only (converted to target unit)
|
|
233
|
+
*
|
|
234
|
+
* **Block templates** (content starts with `\n`):
|
|
235
|
+
* - All lines: `baseIndent + indentUnit` prefix + converted graphql indent
|
|
236
|
+
* - Trailing: `\n` + `baseIndent`
|
|
218
237
|
*/
|
|
219
|
-
const reindent = (formatted, baseIndent, originalContent) => {
|
|
238
|
+
const reindent = (formatted, baseIndent, originalContent, indentUnit = " ") => {
|
|
220
239
|
const trimmedFormatted = formatted.trim();
|
|
221
240
|
if (!originalContent.includes("\n") && !trimmedFormatted.includes("\n")) {
|
|
222
241
|
return trimmedFormatted;
|
|
223
242
|
}
|
|
224
|
-
const indent = `${baseIndent} `;
|
|
225
243
|
const lines = trimmedFormatted.split("\n");
|
|
226
|
-
const indentedLines = lines.map((line) => line.trim() === "" ? "" : indent + line);
|
|
227
244
|
const startsWithNewline = originalContent.startsWith("\n");
|
|
228
|
-
const endsWithNewline = originalContent
|
|
245
|
+
const endsWithNewline = /\n\s*$/.test(originalContent);
|
|
246
|
+
const indentedLines = lines.map((line, i) => {
|
|
247
|
+
if (line.trim() === "") return "";
|
|
248
|
+
const converted = convertGraphqlIndent(line, indentUnit);
|
|
249
|
+
if (!startsWithNewline) {
|
|
250
|
+
if (i === 0) return converted;
|
|
251
|
+
return baseIndent + converted;
|
|
252
|
+
}
|
|
253
|
+
return `${baseIndent}${indentUnit}${converted}`;
|
|
254
|
+
});
|
|
229
255
|
let result = indentedLines.join("\n");
|
|
230
256
|
if (startsWithNewline) {
|
|
231
257
|
result = `\n${result}`;
|
|
@@ -235,6 +261,25 @@ const reindent = (formatted, baseIndent, originalContent) => {
|
|
|
235
261
|
}
|
|
236
262
|
return result;
|
|
237
263
|
};
|
|
264
|
+
/**
|
|
265
|
+
* Detect the indentation unit used in a TypeScript source file.
|
|
266
|
+
*
|
|
267
|
+
* Uses the smallest indentation width found as the indent unit.
|
|
268
|
+
* In files with embedded templates (e.g., graphql in backticks),
|
|
269
|
+
* template content lines are at `baseIndent + graphqlIndent`,
|
|
270
|
+
* so their absolute indent is always >= the file's indent unit.
|
|
271
|
+
*/
|
|
272
|
+
const detectIndentUnit = (tsSource) => {
|
|
273
|
+
let minSpaceIndent = Infinity;
|
|
274
|
+
for (const line of tsSource.split("\n")) {
|
|
275
|
+
if (line.length === 0 || line.trimStart().length === 0) continue;
|
|
276
|
+
if (line[0] === " ") return " ";
|
|
277
|
+
const match = line.match(/^( +)\S/);
|
|
278
|
+
if (match) minSpaceIndent = Math.min(minSpaceIndent, match[1].length);
|
|
279
|
+
}
|
|
280
|
+
if (minSpaceIndent === Infinity || minSpaceIndent <= 1) return " ";
|
|
281
|
+
return " ".repeat(minSpaceIndent);
|
|
282
|
+
};
|
|
238
283
|
const GRAPHQL_KEYWORDS = new Set([
|
|
239
284
|
"query",
|
|
240
285
|
"mutation",
|
|
@@ -304,6 +349,7 @@ const unwrapFormattedContent = (formatted, prefixPattern) => {
|
|
|
304
349
|
* @returns Array of edits to apply to the source (sorted by position, not yet applied)
|
|
305
350
|
*/
|
|
306
351
|
const formatTemplatesInSource = (templates, tsSource, formatGraphql) => {
|
|
352
|
+
const indentUnit = detectIndentUnit(tsSource);
|
|
307
353
|
const edits = [];
|
|
308
354
|
for (const template of templates) {
|
|
309
355
|
if (!template.contentRange) continue;
|
|
@@ -326,7 +372,7 @@ const formatTemplatesInSource = (templates, tsSource, formatGraphql) => {
|
|
|
326
372
|
continue;
|
|
327
373
|
}
|
|
328
374
|
const baseIndent = detectBaseIndent(tsSource, template.contentRange.start);
|
|
329
|
-
const reindented = reindent(unwrapped, baseIndent, template.content);
|
|
375
|
+
const reindented = reindent(unwrapped, baseIndent, template.content, indentUnit);
|
|
330
376
|
if (reindented === template.content) {
|
|
331
377
|
continue;
|
|
332
378
|
}
|
|
@@ -340,5 +386,5 @@ const formatTemplatesInSource = (templates, tsSource, formatGraphql) => {
|
|
|
340
386
|
};
|
|
341
387
|
|
|
342
388
|
//#endregion
|
|
343
|
-
export { OPERATION_KINDS, buildGraphqlWrapper, detectBaseIndent, extractFromTaggedTemplate, extractTemplatesFromCallback, findGqlCall, formatTemplatesInSource, getGqlCallSchemaName, isOperationKind, reindent, unwrapFormattedContent, walkAndExtract };
|
|
389
|
+
export { OPERATION_KINDS, buildGraphqlWrapper, detectBaseIndent, detectIndentUnit, extractFromTaggedTemplate, extractTemplatesFromCallback, findGqlCall, formatTemplatesInSource, getGqlCallSchemaName, isOperationKind, reindent, unwrapFormattedContent, walkAndExtract };
|
|
344
390
|
//# sourceMappingURL=template-extraction.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-extraction.mjs","names":["templates: ExtractedTemplate[]","kind: string","elementName: string | undefined","typeName: string | undefined","expressionRanges: { start: number; end: number }[]","parts: string[]","template: ExtractedTemplate","prefix","prefixPattern","edits: TemplateFormatEdit[]","formatted: string"],"sources":["../src/template-extraction/extract.ts","../src/template-extraction/format.ts"],"sourcesContent":["/**\n * Template extraction from TypeScript source using SWC AST.\n *\n * Based on the typegen extractor (superset): supports both bare-tag\n * (`query\\`...\\``) and curried (`query(\"Name\")\\`...\\``) syntax.\n *\n * Position tracking is optional — pass spanOffset + converter to\n * populate contentRange on extracted templates.\n *\n * @module\n */\n\n// Re-export for convenience — SWC types are type-only imports\nimport type { ArrowFunctionExpression, CallExpression, MemberExpression, Node, TaggedTemplateExpression } from \"@swc/types\";\nimport type { SwcSpanConverter } from \"../utils/swc-span\";\nimport type { ExtractedTemplate, ExtractedTemplateWithPosition, OperationKind } from \"./types\";\n\nexport const OPERATION_KINDS = new Set<string>([\"query\", \"mutation\", \"subscription\", \"fragment\"]);\n\nexport const isOperationKind = (value: string): value is OperationKind => OPERATION_KINDS.has(value);\n\n/** Optional position tracking context for extraction. */\nexport type PositionTrackingContext = {\n readonly spanOffset: number;\n readonly converter: SwcSpanConverter;\n};\n\n/**\n * Check if a call expression is a gql.{schemaName}(...) call.\n * Returns the schema name if it is, null otherwise.\n */\nexport const getGqlCallSchemaName = (identifiers: ReadonlySet<string>, call: CallExpression): string | null => {\n const callee = call.callee;\n if (callee.type !== \"MemberExpression\") {\n return null;\n }\n\n const member = callee as MemberExpression;\n if (member.object.type !== \"Identifier\" || !identifiers.has(member.object.value)) {\n return null;\n }\n\n if (member.property.type !== \"Identifier\") {\n return null;\n }\n\n const firstArg = call.arguments[0];\n if (!firstArg?.expression || firstArg.expression.type !== \"ArrowFunctionExpression\") {\n return null;\n }\n\n return member.property.value;\n};\n\n/**\n * Extract templates from a gql callback's arrow function body.\n * Handles both expression bodies and block bodies with return statements.\n */\nexport const extractTemplatesFromCallback = (\n arrow: ArrowFunctionExpression,\n schemaName: string,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[] => {\n const templates: ExtractedTemplate[] = [];\n\n const processExpression = (expr: Node): void => {\n // Direct tagged template: query(\"Name\")`...`\n if (expr.type === \"TaggedTemplateExpression\") {\n const tagged = expr as unknown as TaggedTemplateExpression;\n extractFromTaggedTemplate(tagged, schemaName, templates, positionCtx);\n return;\n }\n\n // Metadata chaining: query(\"Name\")`...`({ metadata: {} })\n if (expr.type === \"CallExpression\") {\n const call = expr as unknown as CallExpression;\n if (call.callee.type === \"TaggedTemplateExpression\") {\n extractFromTaggedTemplate(call.callee as TaggedTemplateExpression, schemaName, templates, positionCtx);\n }\n }\n };\n\n // Expression body: ({ query }) => query(\"Name\")`...`\n if (arrow.body.type !== \"BlockStatement\") {\n processExpression(arrow.body);\n return templates;\n }\n\n // Block body: ({ query }) => { return query(\"Name\")`...`; }\n for (const stmt of arrow.body.stmts) {\n if (stmt.type === \"ReturnStatement\" && stmt.argument) {\n processExpression(stmt.argument);\n }\n }\n\n return templates;\n};\n\n/**\n * Extract a single template from a tagged template expression.\n * Supports both bare-tag (Identifier) and curried (CallExpression) tag forms.\n */\nexport const extractFromTaggedTemplate = (\n tagged: TaggedTemplateExpression,\n schemaName: string,\n templates: ExtractedTemplate[],\n positionCtx?: PositionTrackingContext,\n): void => {\n // Tag can be:\n // - CallExpression: query(\"name\")`...` or fragment(\"name\", \"type\")`...` (curried syntax)\n // - Identifier: legacy bare-tag form (skipped if it contains interpolations)\n let kind: string;\n let elementName: string | undefined;\n let typeName: string | undefined;\n\n if (tagged.tag.type === \"Identifier\") {\n kind = tagged.tag.value;\n } else if (tagged.tag.type === \"CallExpression\") {\n const tagCall = tagged.tag as CallExpression;\n if (tagCall.callee.type === \"Identifier\") {\n kind = tagCall.callee.value;\n } else {\n return;\n }\n // Extract elementName and typeName from call arguments\n const firstArg = tagCall.arguments[0]?.expression;\n if (firstArg?.type === \"StringLiteral\") {\n elementName = (firstArg as { value: string }).value;\n }\n const secondArg = tagCall.arguments[1]?.expression;\n if (secondArg?.type === \"StringLiteral\") {\n typeName = (secondArg as { value: string }).value;\n }\n } else {\n return;\n }\n\n if (!isOperationKind(kind)) {\n return;\n }\n\n const { quasis, expressions } = tagged.template;\n\n // For legacy Identifier tag, skip templates with interpolations\n if (tagged.tag.type === \"Identifier\" && expressions.length > 0) {\n return;\n }\n\n if (quasis.length === 0) {\n return;\n }\n\n // Build content and optionally track position\n let contentStart = -1;\n let contentEnd = -1;\n const expressionRanges: { start: number; end: number }[] = [];\n\n const parts: string[] = [];\n for (let i = 0; i < quasis.length; i++) {\n const quasi = quasis[i];\n if (!quasi) continue;\n\n if (positionCtx) {\n const quasiStart = positionCtx.converter.byteOffsetToCharIndex(quasi.span.start - positionCtx.spanOffset);\n const quasiEnd = positionCtx.converter.byteOffsetToCharIndex(quasi.span.end - positionCtx.spanOffset);\n if (contentStart === -1) contentStart = quasiStart;\n contentEnd = quasiEnd;\n }\n\n parts.push(quasi.cooked ?? quasi.raw);\n if (i < expressions.length) {\n parts.push(`__FRAG_SPREAD_${i}__`);\n if (positionCtx) {\n // All SWC AST nodes have span; cast needed because Expression union type doesn't expose it uniformly\n const expr = expressions[i] as unknown as { span: { start: number; end: number } };\n const exprStart = positionCtx.converter.byteOffsetToCharIndex(expr.span.start - positionCtx.spanOffset);\n const exprEnd = positionCtx.converter.byteOffsetToCharIndex(expr.span.end - positionCtx.spanOffset);\n expressionRanges.push({ start: exprStart, end: exprEnd });\n }\n }\n }\n const content = parts.join(\"\");\n\n const template: ExtractedTemplate = {\n schemaName,\n kind,\n content,\n ...(elementName !== undefined ? { elementName } : {}),\n ...(typeName !== undefined ? { typeName } : {}),\n ...(positionCtx && contentStart !== -1 && contentEnd !== -1\n ? { contentRange: { start: contentStart, end: contentEnd } }\n : {}),\n ...(expressionRanges.length > 0 ? { expressionRanges } : {}),\n };\n\n templates.push(template);\n};\n\n/**\n * Find the innermost gql call, unwrapping method chains like .attach().\n */\nexport const findGqlCall = (identifiers: ReadonlySet<string>, node: Node): CallExpression | null => {\n if (!node || node.type !== \"CallExpression\") {\n return null;\n }\n\n const call = node as unknown as CallExpression;\n if (getGqlCallSchemaName(identifiers, call) !== null) {\n return call;\n }\n\n const callee = call.callee;\n if (callee.type !== \"MemberExpression\") {\n return null;\n }\n\n return findGqlCall(identifiers, callee.object as unknown as Node);\n};\n\n/**\n * Walk AST to find gql calls and extract templates.\n */\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx: PositionTrackingContext,\n): ExtractedTemplateWithPosition[];\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[];\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[] {\n const templates: ExtractedTemplate[] = [];\n\n const visit = (n: Node | ReadonlyArray<Node> | Record<string, unknown>): void => {\n if (!n || typeof n !== \"object\") {\n return;\n }\n\n if (\"type\" in n && n.type === \"CallExpression\") {\n const gqlCall = findGqlCall(identifiers, n as Node);\n if (gqlCall) {\n const schemaName = getGqlCallSchemaName(identifiers, gqlCall);\n if (schemaName) {\n const arrow = gqlCall.arguments[0]?.expression as ArrowFunctionExpression;\n templates.push(...extractTemplatesFromCallback(arrow, schemaName, positionCtx));\n }\n return; // Don't recurse into gql calls\n }\n }\n\n // Recurse into all array and object properties\n if (Array.isArray(n)) {\n for (const item of n) {\n visit(item as Node);\n }\n return;\n }\n\n for (const key of Object.keys(n)) {\n if (key === \"span\" || key === \"type\") {\n continue;\n }\n const value = (n as Record<string, unknown>)[key];\n if (value && typeof value === \"object\") {\n visit(value as Node);\n }\n }\n };\n\n visit(node);\n return templates;\n}\n","/**\n * GraphQL template formatting utilities.\n *\n * Pure string operations — no dependency on `graphql` package.\n * Consumers provide their own format function (e.g., graphql-js parse/print).\n *\n * @module\n */\n\nimport type { ExtractedTemplate, TemplateFormatEdit } from \"./types\";\n\n/** A function that formats GraphQL source text. */\nexport type FormatGraphqlFn = (source: string) => string;\n\n/**\n * Detect the base indentation for a template by looking at the line\n * containing the opening backtick.\n */\nexport const detectBaseIndent = (tsSource: string, contentStartOffset: number): string => {\n // Find the start of the line containing contentStartOffset\n let lineStart = contentStartOffset;\n while (lineStart > 0 && tsSource.charCodeAt(lineStart - 1) !== 10) {\n lineStart--;\n }\n\n // Extract leading whitespace from that line\n let i = lineStart;\n while (i < tsSource.length && (tsSource.charCodeAt(i) === 32 || tsSource.charCodeAt(i) === 9)) {\n i++;\n }\n\n return tsSource.slice(lineStart, i);\n};\n\n/**\n * Re-indent formatted GraphQL to match the embedding context.\n *\n * - If original was single-line and formatted is single-line, keep as-is\n * - Otherwise, apply base indent + one level to each line, preserving\n * original leading/trailing newline pattern\n */\nexport const reindent = (formatted: string, baseIndent: string, originalContent: string): string => {\n const trimmedFormatted = formatted.trim();\n\n // If original was single-line and formatted is also single-line, keep it\n if (!originalContent.includes(\"\\n\") && !trimmedFormatted.includes(\"\\n\")) {\n return trimmedFormatted;\n }\n\n // For multi-line: use the indentation pattern from the original content\n const indent = `${baseIndent} `; // add one level of indentation\n const lines = trimmedFormatted.split(\"\\n\");\n const indentedLines = lines.map((line) => (line.trim() === \"\" ? \"\" : indent + line));\n\n // Match original leading/trailing newline pattern\n const startsWithNewline = originalContent.startsWith(\"\\n\");\n const endsWithNewline = originalContent.endsWith(\"\\n\");\n\n let result = indentedLines.join(\"\\n\");\n if (startsWithNewline) {\n result = `\\n${result}`;\n }\n if (endsWithNewline) {\n result = `${result}\\n${baseIndent}`;\n }\n\n return result;\n};\n\nconst GRAPHQL_KEYWORDS = new Set([\"query\", \"mutation\", \"subscription\", \"fragment\"]);\n\n/**\n * Reconstruct a full GraphQL document from template content + metadata.\n *\n * For curried syntax, the template `content` is only the body (e.g., `{ user { id } }`).\n * GraphQL parsers need a full document (e.g., `query GetUser { user { id } }`).\n *\n * For bare-tag syntax, the content already starts with a keyword and is a full document.\n *\n * @returns The wrapped source and a regex pattern to strip the synthetic prefix after formatting\n */\nexport const buildGraphqlWrapper = (template: ExtractedTemplate): { wrapped: string; prefixPattern: RegExp | null } => {\n const content = template.content.trimStart();\n const firstWord = content.split(/[\\s({]/)[0] ?? \"\";\n\n // If content already starts with a GraphQL keyword, it's a full document (bare-tag)\n if (GRAPHQL_KEYWORDS.has(firstWord)) {\n return { wrapped: template.content, prefixPattern: null };\n }\n\n // Curried syntax — reconstruct the header\n if (template.elementName) {\n if (template.kind === \"fragment\" && template.typeName) {\n const prefix = `fragment ${template.elementName} on ${template.typeName} `;\n const prefixPattern = new RegExp(`^fragment\\\\s+${template.elementName}\\\\s+on\\\\s+${template.typeName}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n }\n const prefix = `${template.kind} ${template.elementName} `;\n const prefixPattern = new RegExp(`^${template.kind}\\\\s+${template.elementName}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n }\n\n // No elementName — try wrapping with just the kind\n const prefix = `${template.kind} `;\n const prefixPattern = new RegExp(`^${template.kind}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n};\n\n/**\n * Strip the reconstructed prefix from formatted output to get back the template body.\n * Uses regex pattern matching to handle whitespace normalization by the formatter\n * (e.g., `query Foo ($id: ID!)` → `query Foo($id: ID!)`).\n */\nexport const unwrapFormattedContent = (formatted: string, prefixPattern: RegExp | null): string => {\n if (!prefixPattern) return formatted;\n const match = formatted.match(prefixPattern);\n if (!match) return formatted;\n return formatted.slice(match[0].length);\n};\n\n/**\n * Format GraphQL templates within TypeScript source.\n *\n * Applies the given format function to each template's content, handles\n * wrapper reconstruction for curried syntax, and re-indents the result\n * to match the TypeScript embedding context.\n *\n * @returns Array of edits to apply to the source (sorted by position, not yet applied)\n */\nexport const formatTemplatesInSource = (\n templates: readonly ExtractedTemplate[],\n tsSource: string,\n formatGraphql: FormatGraphqlFn,\n): readonly TemplateFormatEdit[] => {\n const edits: TemplateFormatEdit[] = [];\n\n for (const template of templates) {\n if (!template.contentRange) continue;\n\n // Wrap the content for formatting\n const { wrapped, prefixPattern } = buildGraphqlWrapper(template);\n\n let formatted: string;\n try {\n formatted = formatGraphql(wrapped);\n } catch {\n continue;\n }\n\n // Unwrap the prefix\n let unwrapped = unwrapFormattedContent(formatted, prefixPattern);\n\n // Restore interpolation expressions: replace __FRAG_SPREAD_N__ with original ${...} syntax\n if (template.expressionRanges && template.expressionRanges.length > 0) {\n for (let i = 0; i < template.expressionRanges.length; i++) {\n const range = template.expressionRanges[i]!;\n const exprText = tsSource.slice(range.start, range.end);\n unwrapped = unwrapped.replace(`__FRAG_SPREAD_${i}__`, `\\${${exprText}}`);\n }\n }\n\n // Fast path: skip if formatter produces identical output\n if (unwrapped === template.content) {\n continue;\n }\n\n // Detect base indentation from the TS source\n const baseIndent = detectBaseIndent(tsSource, template.contentRange.start);\n\n // Re-indent the formatted output\n const reindented = reindent(unwrapped, baseIndent, template.content);\n\n // Skip if no changes after re-indentation\n if (reindented === template.content) {\n continue;\n }\n\n edits.push({\n start: template.contentRange.start,\n end: template.contentRange.end,\n newText: reindented,\n });\n }\n\n return edits;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"template-extraction.mjs","names":["templates: ExtractedTemplate[]","kind: string","elementName: string | undefined","typeName: string | undefined","expressionRanges: { start: number; end: number }[]","parts: string[]","template: ExtractedTemplate","prefix","prefixPattern","edits: TemplateFormatEdit[]","formatted: string"],"sources":["../src/template-extraction/extract.ts","../src/template-extraction/format.ts"],"sourcesContent":["/**\n * Template extraction from TypeScript source using SWC AST.\n *\n * Based on the typegen extractor (superset): supports both bare-tag\n * (`query\\`...\\``) and curried (`query(\"Name\")\\`...\\``) syntax.\n *\n * Position tracking is optional — pass spanOffset + converter to\n * populate contentRange on extracted templates.\n *\n * @module\n */\n\n// Re-export for convenience — SWC types are type-only imports\nimport type { ArrowFunctionExpression, CallExpression, MemberExpression, Node, TaggedTemplateExpression } from \"@swc/types\";\nimport type { SwcSpanConverter } from \"../utils/swc-span\";\nimport type { ExtractedTemplate, ExtractedTemplateWithPosition, OperationKind } from \"./types\";\n\nexport const OPERATION_KINDS = new Set<string>([\"query\", \"mutation\", \"subscription\", \"fragment\"]);\n\nexport const isOperationKind = (value: string): value is OperationKind => OPERATION_KINDS.has(value);\n\n/** Optional position tracking context for extraction. */\nexport type PositionTrackingContext = {\n readonly spanOffset: number;\n readonly converter: SwcSpanConverter;\n};\n\n/**\n * Check if a call expression is a gql.{schemaName}(...) call.\n * Returns the schema name if it is, null otherwise.\n */\nexport const getGqlCallSchemaName = (identifiers: ReadonlySet<string>, call: CallExpression): string | null => {\n const callee = call.callee;\n if (callee.type !== \"MemberExpression\") {\n return null;\n }\n\n const member = callee as MemberExpression;\n if (member.object.type !== \"Identifier\" || !identifiers.has(member.object.value)) {\n return null;\n }\n\n if (member.property.type !== \"Identifier\") {\n return null;\n }\n\n const firstArg = call.arguments[0];\n if (!firstArg?.expression || firstArg.expression.type !== \"ArrowFunctionExpression\") {\n return null;\n }\n\n return member.property.value;\n};\n\n/**\n * Extract templates from a gql callback's arrow function body.\n * Handles both expression bodies and block bodies with return statements.\n */\nexport const extractTemplatesFromCallback = (\n arrow: ArrowFunctionExpression,\n schemaName: string,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[] => {\n const templates: ExtractedTemplate[] = [];\n\n const processExpression = (expr: Node): void => {\n // Direct tagged template: query(\"Name\")`...`\n if (expr.type === \"TaggedTemplateExpression\") {\n const tagged = expr as unknown as TaggedTemplateExpression;\n extractFromTaggedTemplate(tagged, schemaName, templates, positionCtx);\n return;\n }\n\n // Metadata chaining: query(\"Name\")`...`({ metadata: {} })\n if (expr.type === \"CallExpression\") {\n const call = expr as unknown as CallExpression;\n if (call.callee.type === \"TaggedTemplateExpression\") {\n extractFromTaggedTemplate(call.callee as TaggedTemplateExpression, schemaName, templates, positionCtx);\n }\n }\n };\n\n // Expression body: ({ query }) => query(\"Name\")`...`\n if (arrow.body.type !== \"BlockStatement\") {\n processExpression(arrow.body);\n return templates;\n }\n\n // Block body: ({ query }) => { return query(\"Name\")`...`; }\n for (const stmt of arrow.body.stmts) {\n if (stmt.type === \"ReturnStatement\" && stmt.argument) {\n processExpression(stmt.argument);\n }\n }\n\n return templates;\n};\n\n/**\n * Extract a single template from a tagged template expression.\n * Supports both bare-tag (Identifier) and curried (CallExpression) tag forms.\n */\nexport const extractFromTaggedTemplate = (\n tagged: TaggedTemplateExpression,\n schemaName: string,\n templates: ExtractedTemplate[],\n positionCtx?: PositionTrackingContext,\n): void => {\n // Tag can be:\n // - CallExpression: query(\"name\")`...` or fragment(\"name\", \"type\")`...` (curried syntax)\n // - Identifier: legacy bare-tag form (skipped if it contains interpolations)\n let kind: string;\n let elementName: string | undefined;\n let typeName: string | undefined;\n\n if (tagged.tag.type === \"Identifier\") {\n kind = tagged.tag.value;\n } else if (tagged.tag.type === \"CallExpression\") {\n const tagCall = tagged.tag as CallExpression;\n if (tagCall.callee.type === \"Identifier\") {\n kind = tagCall.callee.value;\n } else {\n return;\n }\n // Extract elementName and typeName from call arguments\n const firstArg = tagCall.arguments[0]?.expression;\n if (firstArg?.type === \"StringLiteral\") {\n elementName = (firstArg as { value: string }).value;\n }\n const secondArg = tagCall.arguments[1]?.expression;\n if (secondArg?.type === \"StringLiteral\") {\n typeName = (secondArg as { value: string }).value;\n }\n } else {\n return;\n }\n\n if (!isOperationKind(kind)) {\n return;\n }\n\n const { quasis, expressions } = tagged.template;\n\n // For legacy Identifier tag, skip templates with interpolations\n if (tagged.tag.type === \"Identifier\" && expressions.length > 0) {\n return;\n }\n\n if (quasis.length === 0) {\n return;\n }\n\n // Build content and optionally track position\n let contentStart = -1;\n let contentEnd = -1;\n const expressionRanges: { start: number; end: number }[] = [];\n\n const parts: string[] = [];\n for (let i = 0; i < quasis.length; i++) {\n const quasi = quasis[i];\n if (!quasi) continue;\n\n if (positionCtx) {\n const quasiStart = positionCtx.converter.byteOffsetToCharIndex(quasi.span.start - positionCtx.spanOffset);\n const quasiEnd = positionCtx.converter.byteOffsetToCharIndex(quasi.span.end - positionCtx.spanOffset);\n if (contentStart === -1) contentStart = quasiStart;\n contentEnd = quasiEnd;\n }\n\n parts.push(quasi.cooked ?? quasi.raw);\n if (i < expressions.length) {\n parts.push(`__FRAG_SPREAD_${i}__`);\n if (positionCtx) {\n // All SWC AST nodes have span; cast needed because Expression union type doesn't expose it uniformly\n const expr = expressions[i] as unknown as { span: { start: number; end: number } };\n const exprStart = positionCtx.converter.byteOffsetToCharIndex(expr.span.start - positionCtx.spanOffset);\n const exprEnd = positionCtx.converter.byteOffsetToCharIndex(expr.span.end - positionCtx.spanOffset);\n expressionRanges.push({ start: exprStart, end: exprEnd });\n }\n }\n }\n const content = parts.join(\"\");\n\n const template: ExtractedTemplate = {\n schemaName,\n kind,\n content,\n ...(elementName !== undefined ? { elementName } : {}),\n ...(typeName !== undefined ? { typeName } : {}),\n ...(positionCtx && contentStart !== -1 && contentEnd !== -1\n ? { contentRange: { start: contentStart, end: contentEnd } }\n : {}),\n ...(expressionRanges.length > 0 ? { expressionRanges } : {}),\n };\n\n templates.push(template);\n};\n\n/**\n * Find the innermost gql call, unwrapping method chains like .attach().\n */\nexport const findGqlCall = (identifiers: ReadonlySet<string>, node: Node): CallExpression | null => {\n if (!node || node.type !== \"CallExpression\") {\n return null;\n }\n\n const call = node as unknown as CallExpression;\n if (getGqlCallSchemaName(identifiers, call) !== null) {\n return call;\n }\n\n const callee = call.callee;\n if (callee.type !== \"MemberExpression\") {\n return null;\n }\n\n return findGqlCall(identifiers, callee.object as unknown as Node);\n};\n\n/**\n * Walk AST to find gql calls and extract templates.\n */\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx: PositionTrackingContext,\n): ExtractedTemplateWithPosition[];\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[];\nexport function walkAndExtract(\n node: Node,\n identifiers: ReadonlySet<string>,\n positionCtx?: PositionTrackingContext,\n): ExtractedTemplate[] {\n const templates: ExtractedTemplate[] = [];\n\n const visit = (n: Node | ReadonlyArray<Node> | Record<string, unknown>): void => {\n if (!n || typeof n !== \"object\") {\n return;\n }\n\n if (\"type\" in n && n.type === \"CallExpression\") {\n const gqlCall = findGqlCall(identifiers, n as Node);\n if (gqlCall) {\n const schemaName = getGqlCallSchemaName(identifiers, gqlCall);\n if (schemaName) {\n const arrow = gqlCall.arguments[0]?.expression as ArrowFunctionExpression;\n templates.push(...extractTemplatesFromCallback(arrow, schemaName, positionCtx));\n }\n return; // Don't recurse into gql calls\n }\n }\n\n // Recurse into all array and object properties\n if (Array.isArray(n)) {\n for (const item of n) {\n visit(item as Node);\n }\n return;\n }\n\n for (const key of Object.keys(n)) {\n if (key === \"span\" || key === \"type\") {\n continue;\n }\n const value = (n as Record<string, unknown>)[key];\n if (value && typeof value === \"object\") {\n visit(value as Node);\n }\n }\n };\n\n visit(node);\n return templates;\n}\n","/**\n * GraphQL template formatting utilities.\n *\n * Pure string operations — no dependency on `graphql` package.\n * Consumers provide their own format function (e.g., graphql-js parse/print).\n *\n * @module\n */\n\nimport type { ExtractedTemplate, TemplateFormatEdit } from \"./types\";\n\n/** A function that formats GraphQL source text. */\nexport type FormatGraphqlFn = (source: string) => string;\n\n/**\n * Detect the base indentation for a template by looking at the line\n * containing the opening backtick.\n */\nexport const detectBaseIndent = (tsSource: string, contentStartOffset: number): string => {\n // Find the start of the line containing contentStartOffset\n let lineStart = contentStartOffset;\n while (lineStart > 0 && tsSource.charCodeAt(lineStart - 1) !== 10) {\n lineStart--;\n }\n\n // Extract leading whitespace from that line\n let i = lineStart;\n while (i < tsSource.length && (tsSource.charCodeAt(i) === 32 || tsSource.charCodeAt(i) === 9)) {\n i++;\n }\n\n return tsSource.slice(lineStart, i);\n};\n\n/**\n * Convert graphql-js 2-space indentation to the target indent unit.\n * Strips leading 2-space groups and replaces with equivalent indent units.\n */\nconst convertGraphqlIndent = (line: string, indentUnit: string): string => {\n if (indentUnit === \" \") return line; // No conversion needed\n let level = 0;\n let pos = 0;\n while (pos + 1 < line.length && line[pos] === \" \" && line[pos + 1] === \" \") {\n level++;\n pos += 2;\n }\n if (level === 0) return line;\n return indentUnit.repeat(level) + line.slice(pos);\n};\n\n/**\n * Re-indent formatted GraphQL to match the embedding context.\n *\n * **Inline templates** (content does NOT start with `\\n`):\n * - Line 0 (`{`): no prefix — appears right after backtick\n * - Lines 1..N: graphql-js indentation only (converted to target unit)\n *\n * **Block templates** (content starts with `\\n`):\n * - All lines: `baseIndent + indentUnit` prefix + converted graphql indent\n * - Trailing: `\\n` + `baseIndent`\n */\nexport const reindent = (formatted: string, baseIndent: string, originalContent: string, indentUnit: string = \" \"): string => {\n const trimmedFormatted = formatted.trim();\n\n // If original was single-line and formatted is also single-line, keep it\n if (!originalContent.includes(\"\\n\") && !trimmedFormatted.includes(\"\\n\")) {\n return trimmedFormatted;\n }\n\n const lines = trimmedFormatted.split(\"\\n\");\n\n // Match original leading/trailing newline pattern\n const startsWithNewline = originalContent.startsWith(\"\\n\");\n const endsWithNewline = /\\n\\s*$/.test(originalContent);\n\n const indentedLines = lines.map((line, i) => {\n if (line.trim() === \"\") return \"\";\n const converted = convertGraphqlIndent(line, indentUnit);\n\n if (!startsWithNewline) {\n // Inline template: first line has no prefix (appears after backtick);\n // body lines get baseIndent + converted graphql indent to align with TS context.\n if (i === 0) return converted;\n return baseIndent + converted;\n }\n\n // Block template: every line gets baseIndent + indentUnit prefix\n return `${baseIndent}${indentUnit}${converted}`;\n });\n\n let result = indentedLines.join(\"\\n\");\n if (startsWithNewline) {\n result = `\\n${result}`;\n }\n if (endsWithNewline) {\n result = `${result}\\n${baseIndent}`;\n }\n\n return result;\n};\n\n/**\n * Detect the indentation unit used in a TypeScript source file.\n *\n * Uses the smallest indentation width found as the indent unit.\n * In files with embedded templates (e.g., graphql in backticks),\n * template content lines are at `baseIndent + graphqlIndent`,\n * so their absolute indent is always >= the file's indent unit.\n */\nexport const detectIndentUnit = (tsSource: string): string => {\n let minSpaceIndent = Infinity;\n for (const line of tsSource.split(\"\\n\")) {\n if (line.length === 0 || line.trimStart().length === 0) continue;\n if (line[0] === \"\\t\") return \"\\t\";\n const match = line.match(/^( +)\\S/);\n if (match) minSpaceIndent = Math.min(minSpaceIndent, match[1]!.length);\n }\n if (minSpaceIndent === Infinity || minSpaceIndent <= 1) return \" \";\n return \" \".repeat(minSpaceIndent);\n};\n\nconst GRAPHQL_KEYWORDS = new Set([\"query\", \"mutation\", \"subscription\", \"fragment\"]);\n\n/**\n * Reconstruct a full GraphQL document from template content + metadata.\n *\n * For curried syntax, the template `content` is only the body (e.g., `{ user { id } }`).\n * GraphQL parsers need a full document (e.g., `query GetUser { user { id } }`).\n *\n * For bare-tag syntax, the content already starts with a keyword and is a full document.\n *\n * @returns The wrapped source and a regex pattern to strip the synthetic prefix after formatting\n */\nexport const buildGraphqlWrapper = (template: ExtractedTemplate): { wrapped: string; prefixPattern: RegExp | null } => {\n const content = template.content.trimStart();\n const firstWord = content.split(/[\\s({]/)[0] ?? \"\";\n\n // If content already starts with a GraphQL keyword, it's a full document (bare-tag)\n if (GRAPHQL_KEYWORDS.has(firstWord)) {\n return { wrapped: template.content, prefixPattern: null };\n }\n\n // Curried syntax — reconstruct the header\n if (template.elementName) {\n if (template.kind === \"fragment\" && template.typeName) {\n const prefix = `fragment ${template.elementName} on ${template.typeName} `;\n const prefixPattern = new RegExp(`^fragment\\\\s+${template.elementName}\\\\s+on\\\\s+${template.typeName}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n }\n const prefix = `${template.kind} ${template.elementName} `;\n const prefixPattern = new RegExp(`^${template.kind}\\\\s+${template.elementName}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n }\n\n // No elementName — try wrapping with just the kind\n const prefix = `${template.kind} `;\n const prefixPattern = new RegExp(`^${template.kind}\\\\s*`);\n return { wrapped: prefix + template.content, prefixPattern };\n};\n\n/**\n * Strip the reconstructed prefix from formatted output to get back the template body.\n * Uses regex pattern matching to handle whitespace normalization by the formatter\n * (e.g., `query Foo ($id: ID!)` → `query Foo($id: ID!)`).\n */\nexport const unwrapFormattedContent = (formatted: string, prefixPattern: RegExp | null): string => {\n if (!prefixPattern) return formatted;\n const match = formatted.match(prefixPattern);\n if (!match) return formatted;\n return formatted.slice(match[0].length);\n};\n\n/**\n * Format GraphQL templates within TypeScript source.\n *\n * Applies the given format function to each template's content, handles\n * wrapper reconstruction for curried syntax, and re-indents the result\n * to match the TypeScript embedding context.\n *\n * @returns Array of edits to apply to the source (sorted by position, not yet applied)\n */\nexport const formatTemplatesInSource = (\n templates: readonly ExtractedTemplate[],\n tsSource: string,\n formatGraphql: FormatGraphqlFn,\n): readonly TemplateFormatEdit[] => {\n const indentUnit = detectIndentUnit(tsSource);\n const edits: TemplateFormatEdit[] = [];\n\n for (const template of templates) {\n if (!template.contentRange) continue;\n\n // Wrap the content for formatting\n const { wrapped, prefixPattern } = buildGraphqlWrapper(template);\n\n let formatted: string;\n try {\n formatted = formatGraphql(wrapped);\n } catch {\n continue;\n }\n\n // Unwrap the prefix\n let unwrapped = unwrapFormattedContent(formatted, prefixPattern);\n\n // Restore interpolation expressions: replace __FRAG_SPREAD_N__ with original ${...} syntax\n if (template.expressionRanges && template.expressionRanges.length > 0) {\n for (let i = 0; i < template.expressionRanges.length; i++) {\n const range = template.expressionRanges[i]!;\n const exprText = tsSource.slice(range.start, range.end);\n unwrapped = unwrapped.replace(`__FRAG_SPREAD_${i}__`, `\\${${exprText}}`);\n }\n }\n\n // Fast path: skip if formatter produces identical output\n if (unwrapped === template.content) {\n continue;\n }\n\n // Detect base indentation from the TS source\n const baseIndent = detectBaseIndent(tsSource, template.contentRange.start);\n\n // Re-indent the formatted output\n const reindented = reindent(unwrapped, baseIndent, template.content, indentUnit);\n\n // Skip if no changes after re-indentation\n if (reindented === template.content) {\n continue;\n }\n\n edits.push({\n start: template.contentRange.start,\n end: template.contentRange.end,\n newText: reindented,\n });\n }\n\n return edits;\n};\n"],"mappings":"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"}
|