@soda-gql/tools 0.13.2 → 0.14.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/bin.cjs +2958 -3777
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.d.cts +7 -223
- package/dist/bin.d.cts.map +1 -1
- package/dist/codegen.cjs +2896 -3741
- package/dist/codegen.cjs.map +1 -1
- package/dist/codegen.d.cts +8 -224
- package/dist/codegen.d.cts.map +1 -1
- package/dist/codegen.d.mts +10 -226
- package/dist/codegen.d.mts.map +1 -1
- package/dist/codegen.mjs +2836 -3730
- package/dist/codegen.mjs.map +1 -1
- package/dist/{formatter-D5rL2R7k.cjs → formatter-DnyPtv8R.cjs} +17 -174
- package/dist/formatter-DnyPtv8R.cjs.map +1 -0
- package/dist/formatter.cjs +16 -173
- package/dist/formatter.cjs.map +1 -1
- package/dist/formatter.d.cts +0 -2
- package/dist/formatter.d.cts.map +1 -1
- package/dist/formatter.d.mts +0 -2
- package/dist/formatter.d.mts.map +1 -1
- package/dist/formatter.mjs +16 -173
- package/dist/formatter.mjs.map +1 -1
- package/dist/typegen.cjs +92 -4
- package/dist/typegen.cjs.map +1 -1
- package/dist/typegen.d.cts +1 -1
- package/dist/typegen.d.cts.map +1 -1
- package/dist/typegen.d.mts +1 -1
- package/dist/typegen.d.mts.map +1 -1
- package/dist/typegen.mjs +94 -6
- package/dist/typegen.mjs.map +1 -1
- package/package.json +5 -5
- package/dist/formatter-D5rL2R7k.cjs.map +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const require_bin = require('./bin.cjs');
|
|
3
3
|
let neverthrow = require("neverthrow");
|
|
4
|
-
let node_crypto = require("node:crypto");
|
|
5
4
|
let node_module = require("node:module");
|
|
6
5
|
let __soda_gql_common_template_extraction = require("@soda-gql/common/template-extraction");
|
|
7
6
|
let __soda_gql_common_utils = require("@soda-gql/common/utils");
|
|
@@ -86,128 +85,6 @@ const collectGqlIdentifiers = (module$1) => {
|
|
|
86
85
|
}
|
|
87
86
|
return gqlIdentifiers;
|
|
88
87
|
};
|
|
89
|
-
/**
|
|
90
|
-
* Check if a call expression is a fragment definition call.
|
|
91
|
-
* Pattern: fragment.TypeName({ ... }) where `fragment` comes from gql factory destructuring.
|
|
92
|
-
*/
|
|
93
|
-
const isFragmentDefinitionCall = (node, fragmentIdentifiers) => {
|
|
94
|
-
if (node.callee.type !== "MemberExpression") return false;
|
|
95
|
-
const { object } = node.callee;
|
|
96
|
-
if (object.type !== "Identifier") return false;
|
|
97
|
-
if (!fragmentIdentifiers.has(object.value)) return false;
|
|
98
|
-
const firstArg = node.arguments[0];
|
|
99
|
-
if (!firstArg || firstArg.expression.type !== "ObjectExpression") return false;
|
|
100
|
-
return true;
|
|
101
|
-
};
|
|
102
|
-
/**
|
|
103
|
-
* Check if an object expression already has a `key` property.
|
|
104
|
-
*/
|
|
105
|
-
const hasKeyProperty = (obj) => {
|
|
106
|
-
return obj.properties.some((prop) => {
|
|
107
|
-
if (prop.type === "KeyValueProperty" && prop.key.type === "Identifier") {
|
|
108
|
-
return prop.key.value === "key";
|
|
109
|
-
}
|
|
110
|
-
return false;
|
|
111
|
-
});
|
|
112
|
-
};
|
|
113
|
-
/**
|
|
114
|
-
* Collect fragment identifiers from gql factory arrow function parameter.
|
|
115
|
-
* Looks for patterns like: ({ fragment }) => ... or ({ fragment: f }) => ...
|
|
116
|
-
*/
|
|
117
|
-
const collectFragmentIdentifiers = (arrowFunction) => {
|
|
118
|
-
const fragmentIdentifiers = new Set();
|
|
119
|
-
const param = arrowFunction.params[0];
|
|
120
|
-
if (param?.type !== "ObjectPattern") return fragmentIdentifiers;
|
|
121
|
-
for (const p of param.properties) {
|
|
122
|
-
if (p.type === "KeyValuePatternProperty" && p.key.type === "Identifier" && p.key.value === "fragment") {
|
|
123
|
-
const localName = extractIdentifierName(p.value);
|
|
124
|
-
if (localName) fragmentIdentifiers.add(localName);
|
|
125
|
-
}
|
|
126
|
-
if (p.type === "AssignmentPatternProperty" && p.key.type === "Identifier" && p.key.value === "fragment") {
|
|
127
|
-
fragmentIdentifiers.add(p.key.value);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return fragmentIdentifiers;
|
|
131
|
-
};
|
|
132
|
-
/**
|
|
133
|
-
* Extract identifier name from a pattern node.
|
|
134
|
-
*/
|
|
135
|
-
const extractIdentifierName = (pattern) => {
|
|
136
|
-
if (pattern.type === "Identifier") return pattern.value;
|
|
137
|
-
if (pattern.type === "BindingIdentifier") return pattern.value;
|
|
138
|
-
return null;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
//#endregion
|
|
142
|
-
//#region packages/tools/src/formatter/insertion.ts
|
|
143
|
-
/**
|
|
144
|
-
* The newline string to insert after object opening brace
|
|
145
|
-
*/
|
|
146
|
-
const NEWLINE_INSERTION = "\n";
|
|
147
|
-
/**
|
|
148
|
-
* Generate a random 8-character hex key for fragment identification.
|
|
149
|
-
* Uses Node.js crypto for cryptographically secure random bytes.
|
|
150
|
-
*/
|
|
151
|
-
const generateFragmentKey = () => {
|
|
152
|
-
return (0, node_crypto.randomBytes)(4).toString("hex");
|
|
153
|
-
};
|
|
154
|
-
/**
|
|
155
|
-
* Create the key property insertion string.
|
|
156
|
-
*
|
|
157
|
-
* For single-line objects: returns ` key: "xxxxxxxx",\n`
|
|
158
|
-
* For multi-line objects (with indentation): returns `{indentation}key: "xxxxxxxx",\n`
|
|
159
|
-
* (the existing indentation before the next property is preserved)
|
|
160
|
-
*
|
|
161
|
-
* @param key - The hex key string
|
|
162
|
-
* @param indentation - Optional indentation string for multi-line objects
|
|
163
|
-
*/
|
|
164
|
-
const createKeyInsertion = (key, indentation) => {
|
|
165
|
-
if (indentation !== undefined) {
|
|
166
|
-
return `${indentation}key: "${key}",\n`;
|
|
167
|
-
}
|
|
168
|
-
return ` key: "${key}",\n`;
|
|
169
|
-
};
|
|
170
|
-
/**
|
|
171
|
-
* Check if there's already a newline after the opening brace.
|
|
172
|
-
* Uses string inspection rather than AST.
|
|
173
|
-
*
|
|
174
|
-
* @param source - The source code string
|
|
175
|
-
* @param objectStartPos - The position of the `{` character in the source
|
|
176
|
-
*/
|
|
177
|
-
const hasExistingNewline = (source, objectStartPos) => {
|
|
178
|
-
const pos = objectStartPos + 1;
|
|
179
|
-
const nextChar = source[pos];
|
|
180
|
-
return nextChar === "\n" || nextChar === "\r";
|
|
181
|
-
};
|
|
182
|
-
/**
|
|
183
|
-
* Detect the indentation used after an opening brace.
|
|
184
|
-
* Returns the whitespace string after the newline, or null if no newline.
|
|
185
|
-
*
|
|
186
|
-
* @param source - The source code string
|
|
187
|
-
* @param objectStartPos - The position of the `{` character
|
|
188
|
-
*/
|
|
189
|
-
const detectIndentationAfterBrace = (source, objectStartPos) => {
|
|
190
|
-
const afterBrace = objectStartPos + 1;
|
|
191
|
-
const char = source[afterBrace];
|
|
192
|
-
if (char !== "\n" && char !== "\r") {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
let pos = afterBrace + 1;
|
|
196
|
-
if (char === "\r" && source[pos] === "\n") {
|
|
197
|
-
pos++;
|
|
198
|
-
}
|
|
199
|
-
let indentation = "";
|
|
200
|
-
while (pos < source.length) {
|
|
201
|
-
const c = source[pos];
|
|
202
|
-
if (c === " " || c === " ") {
|
|
203
|
-
indentation += c;
|
|
204
|
-
pos++;
|
|
205
|
-
} else {
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return indentation;
|
|
210
|
-
};
|
|
211
88
|
|
|
212
89
|
//#endregion
|
|
213
90
|
//#region packages/tools/src/formatter/format.ts
|
|
@@ -237,37 +114,19 @@ const getGraphqlModule = () => {
|
|
|
237
114
|
}
|
|
238
115
|
return (0, neverthrow.ok)(_graphqlModule);
|
|
239
116
|
};
|
|
117
|
+
const NEWLINE_INSERTION = "\n";
|
|
240
118
|
/**
|
|
241
119
|
* Simple recursive AST traversal
|
|
242
120
|
*/
|
|
243
121
|
const traverseNode = (node, context, gqlIdentifiers, onObjectExpression) => {
|
|
244
122
|
if (node.type === "CallExpression" && isGqlDefinitionCall(node, gqlIdentifiers)) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const fragmentIds = collectFragmentIdentifiers(arrow);
|
|
250
|
-
context = {
|
|
251
|
-
...context,
|
|
252
|
-
insideGqlDefinition: true,
|
|
253
|
-
fragmentIdentifiers: new Set([...context.fragmentIdentifiers, ...fragmentIds])
|
|
254
|
-
};
|
|
255
|
-
} else {
|
|
256
|
-
context = {
|
|
257
|
-
...context,
|
|
258
|
-
insideGqlDefinition: true
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (node.type === "CallExpression" && context.insideGqlDefinition && isFragmentDefinitionCall(node, context.fragmentIdentifiers)) {
|
|
263
|
-
const call = node;
|
|
264
|
-
const firstArg = call.arguments[0];
|
|
265
|
-
if (firstArg?.expression.type === "ObjectExpression" && context.currentArrowFunction) {
|
|
266
|
-
onObjectExpression(firstArg.expression, context.currentArrowFunction, { isFragmentConfig: true });
|
|
267
|
-
}
|
|
123
|
+
context = {
|
|
124
|
+
...context,
|
|
125
|
+
insideGqlDefinition: true
|
|
126
|
+
};
|
|
268
127
|
}
|
|
269
128
|
if (node.type === "ObjectExpression" && context.insideGqlDefinition && context.currentArrowFunction && isFieldSelectionObject(node, context.currentArrowFunction)) {
|
|
270
|
-
onObjectExpression(node, context.currentArrowFunction
|
|
129
|
+
onObjectExpression(node, context.currentArrowFunction);
|
|
271
130
|
}
|
|
272
131
|
if (node.type === "CallExpression") {
|
|
273
132
|
const call = node;
|
|
@@ -318,8 +177,7 @@ const traverseNode = (node, context, gqlIdentifiers, onObjectExpression) => {
|
|
|
318
177
|
const traverse = (module$1, gqlIdentifiers, onObjectExpression) => {
|
|
319
178
|
const initialContext = {
|
|
320
179
|
insideGqlDefinition: false,
|
|
321
|
-
currentArrowFunction: null
|
|
322
|
-
fragmentIdentifiers: new Set()
|
|
180
|
+
currentArrowFunction: null
|
|
323
181
|
};
|
|
324
182
|
for (const statement of module$1.body) {
|
|
325
183
|
traverseNode(statement, initialContext, gqlIdentifiers, onObjectExpression);
|
|
@@ -327,10 +185,9 @@ const traverse = (module$1, gqlIdentifiers, onObjectExpression) => {
|
|
|
327
185
|
};
|
|
328
186
|
/**
|
|
329
187
|
* Format soda-gql field selection objects by inserting newlines.
|
|
330
|
-
* Optionally injects fragment keys for anonymous fragments.
|
|
331
188
|
*/
|
|
332
189
|
const format = (options) => {
|
|
333
|
-
const { sourceCode, filePath
|
|
190
|
+
const { sourceCode, filePath } = options;
|
|
334
191
|
let module$1;
|
|
335
192
|
try {
|
|
336
193
|
const program = (0, __swc_core.parseSync)(sourceCode, {
|
|
@@ -366,26 +223,11 @@ const format = (options) => {
|
|
|
366
223
|
});
|
|
367
224
|
}
|
|
368
225
|
const insertionPoints = [];
|
|
369
|
-
traverse(module$1, gqlIdentifiers, (object
|
|
226
|
+
traverse(module$1, gqlIdentifiers, (object) => {
|
|
370
227
|
const objectStart = converter.byteOffsetToCharIndex(object.span.start - spanOffset);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const indentation = detectIndentationAfterBrace(sourceCode, objectStart) ?? "";
|
|
375
|
-
let insertPos = objectStart + 2;
|
|
376
|
-
if (sourceCode[objectStart + 1] === "\r") insertPos++;
|
|
377
|
-
insertionPoints.push({
|
|
378
|
-
position: insertPos,
|
|
379
|
-
content: createKeyInsertion(key, indentation)
|
|
380
|
-
});
|
|
381
|
-
} else {
|
|
382
|
-
insertionPoints.push({
|
|
383
|
-
position: objectStart + 1,
|
|
384
|
-
content: createKeyInsertion(key)
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
if (!callbackContext.isFragmentConfig && !hasExistingNewline(sourceCode, objectStart)) {
|
|
228
|
+
const nextChar = sourceCode[objectStart + 1];
|
|
229
|
+
const hasNewline = nextChar === "\n" || nextChar === "\r";
|
|
230
|
+
if (!hasNewline) {
|
|
389
231
|
insertionPoints.push({
|
|
390
232
|
position: objectStart + 1,
|
|
391
233
|
content: NEWLINE_INSERTION
|
|
@@ -471,11 +313,12 @@ const needsFormat = (options) => {
|
|
|
471
313
|
return (0, neverthrow.ok)(false);
|
|
472
314
|
}
|
|
473
315
|
let needsFormatting = false;
|
|
474
|
-
traverse(module$1, gqlIdentifiers, (object
|
|
316
|
+
traverse(module$1, gqlIdentifiers, (object) => {
|
|
475
317
|
if (needsFormatting) return;
|
|
476
|
-
if (callbackContext.isFragmentConfig) return;
|
|
477
318
|
const objectStart = converter.byteOffsetToCharIndex(object.span.start - spanOffset);
|
|
478
|
-
|
|
319
|
+
const nextChar = sourceCode[objectStart + 1];
|
|
320
|
+
const hasNewline = nextChar === "\n" || nextChar === "\r";
|
|
321
|
+
if (!hasNewline) {
|
|
479
322
|
needsFormatting = true;
|
|
480
323
|
}
|
|
481
324
|
});
|
|
@@ -507,4 +350,4 @@ const needsFormat = (options) => {
|
|
|
507
350
|
//#endregion
|
|
508
351
|
exports.format = format;
|
|
509
352
|
exports.needsFormat = needsFormat;
|
|
510
|
-
//# sourceMappingURL=formatter-
|
|
353
|
+
//# sourceMappingURL=formatter-DnyPtv8R.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter-DnyPtv8R.cjs","names":["bodyObject: ObjectExpression | null","module","require","_graphqlModule: GraphqlModule | undefined","_graphqlModuleError: Error | undefined","initialContext: TraversalContext","module","module: Module","insertionPoints: InsertionPoint[]","positionCtx: PositionTrackingContext","defaultFormat: FormatGraphqlFn"],"sources":["../src/formatter/detection.ts","../src/formatter/format.ts"],"sourcesContent":["import type {\n ArrowFunctionExpression,\n CallExpression,\n Expression,\n Module,\n ObjectExpression,\n ObjectPatternProperty,\n} from \"@swc/types\";\n\n/**\n * Check if an expression is a reference to gql\n * Handles: gql, namespace.gql\n */\nexport const isGqlReference = (node: Expression, gqlIdentifiers: ReadonlySet<string>): boolean => {\n // Unwrap TsNonNullExpression: gql! -> gql\n const unwrapped = node.type === \"TsNonNullExpression\" ? (node as { type: string; expression: Expression }).expression : node;\n if (unwrapped.type === \"Identifier\") {\n return gqlIdentifiers.has(unwrapped.value);\n }\n if (unwrapped.type === \"MemberExpression\" && unwrapped.property.type === \"Identifier\" && unwrapped.property.value === \"gql\") {\n return true;\n }\n return false;\n};\n\n/**\n * Check if a call expression is a gql definition call\n * Handles: gql.default(...), gql.model(...), gql.schemaName(...) (multi-schema)\n */\nexport const isGqlDefinitionCall = (node: CallExpression, gqlIdentifiers: ReadonlySet<string>): boolean => {\n if (node.callee.type !== \"MemberExpression\") return false;\n const { object } = node.callee;\n\n // Check object is a gql reference first\n if (!isGqlReference(object, gqlIdentifiers)) return false;\n\n // Verify first argument is an arrow function (factory pattern)\n // SWC's arguments are ExprOrSpread[], so access via .expression\n const firstArg = node.arguments[0];\n if (!firstArg || (firstArg.expression.type !== \"ArrowFunctionExpression\" && firstArg.expression.type !== \"FunctionExpression\"))\n return false;\n\n return true;\n};\n\n/**\n * Check if an object expression is a field selection object\n * Field selection objects are returned from arrow functions with ({ f }) or ({ f, $ }) parameter\n */\nexport const isFieldSelectionObject = (object: ObjectExpression, parent: ArrowFunctionExpression): boolean => {\n // The object must be the body of the arrow function\n // Handle both direct ObjectExpression and parenthesized ObjectExpression: `({ ... })`\n let bodyObject: ObjectExpression | null = null;\n\n if (parent.body.type === \"ObjectExpression\") {\n bodyObject = parent.body;\n } else if (parent.body.type === \"ParenthesisExpression\") {\n // Handle `({ f }) => ({ ...f.id() })` pattern where body is parenthesized\n const inner = parent.body.expression;\n if (inner.type === \"ObjectExpression\") {\n bodyObject = inner;\n }\n }\n\n if (!bodyObject) return false;\n if (bodyObject.span.start !== object.span.start) return false;\n\n // Check if first parameter has 'f' destructured\n const param = parent.params[0];\n if (!param || param.type !== \"ObjectPattern\") return false;\n\n return param.properties.some((p: ObjectPatternProperty) => {\n if (p.type === \"KeyValuePatternProperty\" && p.key.type === \"Identifier\") {\n return p.key.value === \"f\";\n }\n if (p.type === \"AssignmentPatternProperty\" && p.key.type === \"Identifier\") {\n return p.key.value === \"f\";\n }\n return false;\n });\n};\n\n/**\n * Collect gql identifiers from import declarations\n */\nexport const collectGqlIdentifiers = (module: Module): Set<string> => {\n const gqlIdentifiers = new Set<string>();\n\n for (const item of module.body) {\n if (item.type !== \"ImportDeclaration\") continue;\n\n for (const specifier of item.specifiers) {\n if (specifier.type === \"ImportSpecifier\") {\n const imported = specifier.imported?.value ?? specifier.local.value;\n if (imported === \"gql\") {\n gqlIdentifiers.add(specifier.local.value);\n }\n }\n if (specifier.type === \"ImportDefaultSpecifier\") {\n // Check if default import might be gql\n if (specifier.local.value === \"gql\") {\n gqlIdentifiers.add(specifier.local.value);\n }\n }\n if (specifier.type === \"ImportNamespaceSpecifier\") {\n // namespace import: import * as ns from \"...\"\n // Would need ns.gql pattern - handled by isGqlReference\n }\n }\n }\n\n return gqlIdentifiers;\n};\n","import { createRequire } from \"node:module\";\nimport {\n type FormatGraphqlFn,\n formatTemplatesInSource,\n type PositionTrackingContext,\n walkAndExtract,\n} from \"@soda-gql/common/template-extraction\";\nimport { createSwcSpanConverter } from \"@soda-gql/common/utils\";\nimport { parseSync } from \"@swc/core\";\nimport type { ArrowFunctionExpression, CallExpression, Module, Node, ObjectExpression } from \"@swc/types\";\nimport { err, ok, type Result } from \"neverthrow\";\n\nconst require = createRequire(import.meta.url);\n\ntype GraphqlModule = {\n parse: (source: string, options?: { noLocation?: boolean }) => unknown;\n print: (ast: unknown) => string;\n};\n\nlet _graphqlModule: GraphqlModule | undefined;\nlet _graphqlModuleError: Error | undefined;\n\nconst getGraphqlModule = (): Result<GraphqlModule, FormatError> => {\n if (_graphqlModuleError) {\n return err({\n type: \"FormatError\",\n code: \"MISSING_DEPENDENCY\",\n message: 'The \"graphql\" package is required for soda-gql formatter. Install it with: bun add graphql',\n });\n }\n if (!_graphqlModule) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n _graphqlModule = require(\"graphql\") as GraphqlModule;\n } catch (cause) {\n _graphqlModuleError = cause instanceof Error ? cause : new Error(String(cause));\n return err({\n type: \"FormatError\",\n code: \"MISSING_DEPENDENCY\",\n message: 'The \"graphql\" package is required for soda-gql formatter. Install it with: bun add graphql',\n cause,\n });\n }\n }\n return ok(_graphqlModule);\n};\n\nimport { collectGqlIdentifiers, isFieldSelectionObject, isGqlDefinitionCall } from \"./detection\";\nimport type { FormatError, FormatOptions, FormatResult } from \"./types\";\n\nconst NEWLINE_INSERTION = \"\\n\";\n\ntype InsertionPoint = {\n readonly position: number;\n readonly content: string;\n readonly endPosition?: number;\n};\n\ntype TraversalContext = {\n insideGqlDefinition: boolean;\n currentArrowFunction: ArrowFunctionExpression | null;\n};\n\ntype TraversalCallback = (object: ObjectExpression, parent: ArrowFunctionExpression) => void;\n\n/**\n * Simple recursive AST traversal\n */\nconst traverseNode = (\n node: Node,\n context: TraversalContext,\n gqlIdentifiers: ReadonlySet<string>,\n onObjectExpression: TraversalCallback,\n): void => {\n // Check for gql definition call entry\n if (node.type === \"CallExpression\" && isGqlDefinitionCall(node as CallExpression, gqlIdentifiers)) {\n context = { ...context, insideGqlDefinition: true };\n }\n\n // Handle object expressions - check if it's the body of the current arrow function\n if (\n node.type === \"ObjectExpression\" &&\n context.insideGqlDefinition &&\n context.currentArrowFunction &&\n isFieldSelectionObject(node as ObjectExpression, context.currentArrowFunction)\n ) {\n onObjectExpression(node as ObjectExpression, context.currentArrowFunction);\n }\n\n // Recursively visit children\n if (node.type === \"CallExpression\") {\n const call = node as CallExpression;\n traverseNode(call.callee as Node, context, gqlIdentifiers, onObjectExpression);\n for (const arg of call.arguments) {\n traverseNode(arg.expression as Node, context, gqlIdentifiers, onObjectExpression);\n }\n } else if (node.type === \"ArrowFunctionExpression\") {\n const arrow = node as ArrowFunctionExpression;\n // Update context with the new arrow function\n const childContext = { ...context, currentArrowFunction: arrow };\n if (arrow.body.type !== \"BlockStatement\") {\n traverseNode(arrow.body as Node, childContext, gqlIdentifiers, onObjectExpression);\n }\n } else if (node.type === \"ParenthesisExpression\") {\n // Handle parenthesized expressions like `({ ...f.id() })`\n // biome-ignore lint/suspicious/noExplicitAny: SWC types\n const paren = node as any;\n if (paren.expression) {\n traverseNode(paren.expression as Node, context, gqlIdentifiers, onObjectExpression);\n }\n } else if (node.type === \"MemberExpression\") {\n // biome-ignore lint/suspicious/noExplicitAny: SWC types\n const member = node as any;\n traverseNode(member.object as Node, context, gqlIdentifiers, onObjectExpression);\n } else if (node.type === \"ObjectExpression\") {\n const obj = node as ObjectExpression;\n for (const prop of obj.properties) {\n if (prop.type === \"SpreadElement\") {\n traverseNode(prop.arguments as Node, context, gqlIdentifiers, onObjectExpression);\n } else if (prop.type === \"KeyValueProperty\") {\n traverseNode(prop.value as Node, context, gqlIdentifiers, onObjectExpression);\n }\n }\n } else {\n // Generic traversal for other node types\n for (const value of Object.values(node)) {\n if (Array.isArray(value)) {\n for (const child of value) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n traverseNode(child as Node, context, gqlIdentifiers, onObjectExpression);\n }\n }\n } else if (value && typeof value === \"object\" && \"type\" in value) {\n traverseNode(value as Node, context, gqlIdentifiers, onObjectExpression);\n }\n }\n }\n};\n\nconst traverse = (module: Module, gqlIdentifiers: ReadonlySet<string>, onObjectExpression: TraversalCallback): void => {\n const initialContext: TraversalContext = {\n insideGqlDefinition: false,\n currentArrowFunction: null,\n };\n for (const statement of module.body) {\n traverseNode(statement, initialContext, gqlIdentifiers, onObjectExpression);\n }\n};\n\n/**\n * Format soda-gql field selection objects by inserting newlines.\n */\nexport const format = (options: FormatOptions): Result<FormatResult, FormatError> => {\n const { sourceCode, filePath } = options;\n\n // Parse source code with SWC\n let module: Module;\n try {\n const program = parseSync(sourceCode, {\n syntax: \"typescript\",\n tsx: filePath?.endsWith(\".tsx\") ?? true,\n target: \"es2022\",\n decorators: false,\n dynamicImport: true,\n });\n\n if (program.type !== \"Module\") {\n return err({\n type: \"FormatError\",\n code: \"PARSE_ERROR\",\n message: `Not a module${filePath ? ` (${filePath})` : \"\"}`,\n });\n }\n module = program;\n } catch (cause) {\n return err({\n type: \"FormatError\",\n code: \"PARSE_ERROR\",\n message: `Failed to parse source code${filePath ? ` (${filePath})` : \"\"}`,\n cause,\n });\n }\n\n // Calculate span offset for position normalization\n // SWC's BytePos counter accumulates across parseSync calls within the same process\n // Using module.span.start ensures correct position calculation regardless of accumulation\n const spanOffset = module.span.start;\n\n // SWC returns UTF-8 byte offsets; JS strings use UTF-16 code units.\n // The converter handles this mapping (fast path for ASCII-only sources).\n const converter = createSwcSpanConverter(sourceCode);\n\n // Collect gql identifiers from imports\n const gqlIdentifiers = collectGqlIdentifiers(module);\n if (gqlIdentifiers.size === 0) {\n return ok({ modified: false, sourceCode });\n }\n\n // Collect insertion points\n const insertionPoints: InsertionPoint[] = [];\n\n traverse(module, gqlIdentifiers, (object) => {\n // Calculate actual position in source (byte offset → char index)\n const objectStart = converter.byteOffsetToCharIndex(object.span.start - spanOffset);\n\n // For field selection objects, insert newline if not present\n const nextChar = sourceCode[objectStart + 1];\n const hasNewline = nextChar === \"\\n\" || nextChar === \"\\r\";\n if (!hasNewline) {\n insertionPoints.push({\n position: objectStart + 1,\n content: NEWLINE_INSERTION,\n });\n }\n });\n\n // Tagged template formatting\n const graphqlResult = getGraphqlModule();\n if (graphqlResult.isErr()) {\n return err(graphqlResult.error);\n }\n const { parse: parseGraphql, print: printGraphql } = graphqlResult.value;\n\n const positionCtx: PositionTrackingContext = { spanOffset, converter };\n const templates = walkAndExtract(module as unknown as Node, gqlIdentifiers, positionCtx);\n\n if (templates.length > 0) {\n const defaultFormat: FormatGraphqlFn = (source) => {\n const ast = parseGraphql(source, { noLocation: false });\n return printGraphql(ast);\n };\n\n const templateEdits = formatTemplatesInSource(templates, sourceCode, defaultFormat);\n\n for (const edit of templateEdits) {\n insertionPoints.push({\n position: edit.start,\n content: edit.newText,\n endPosition: edit.end,\n });\n }\n }\n\n // Apply insertions\n if (insertionPoints.length === 0) {\n return ok({ modified: false, sourceCode });\n }\n\n // Sort in descending order to insert from end to beginning\n // This preserves earlier positions while modifying later parts\n const sortedPoints = [...insertionPoints].sort((a, b) => b.position - a.position);\n\n let result = sourceCode;\n for (const point of sortedPoints) {\n const end = point.endPosition ?? point.position;\n result = result.slice(0, point.position) + point.content + result.slice(end);\n }\n\n return ok({ modified: true, sourceCode: result });\n};\n\n/**\n * Check if a file needs formatting (has unformatted field selections).\n * Useful for pre-commit hooks or CI checks.\n */\nexport const needsFormat = (options: FormatOptions): Result<boolean, FormatError> => {\n const { sourceCode, filePath } = options;\n\n // Parse source code with SWC\n let module: Module;\n try {\n const program = parseSync(sourceCode, {\n syntax: \"typescript\",\n tsx: filePath?.endsWith(\".tsx\") ?? true,\n target: \"es2022\",\n decorators: false,\n dynamicImport: true,\n });\n\n if (program.type !== \"Module\") {\n return err({\n type: \"FormatError\",\n code: \"PARSE_ERROR\",\n message: `Not a module${filePath ? ` (${filePath})` : \"\"}`,\n });\n }\n module = program;\n } catch (cause) {\n return err({\n type: \"FormatError\",\n code: \"PARSE_ERROR\",\n message: `Failed to parse source code${filePath ? ` (${filePath})` : \"\"}`,\n cause,\n });\n }\n\n const spanOffset = module.span.start;\n const converter = createSwcSpanConverter(sourceCode);\n const gqlIdentifiers = collectGqlIdentifiers(module);\n\n if (gqlIdentifiers.size === 0) {\n return ok(false);\n }\n\n let needsFormatting = false;\n\n traverse(module, gqlIdentifiers, (object) => {\n if (needsFormatting) return; // Early exit\n\n const objectStart = converter.byteOffsetToCharIndex(object.span.start - spanOffset);\n const nextChar = sourceCode[objectStart + 1];\n const hasNewline = nextChar === \"\\n\" || nextChar === \"\\r\";\n if (!hasNewline) {\n needsFormatting = true;\n }\n });\n\n // Check tagged templates\n if (!needsFormatting) {\n const graphqlResult = getGraphqlModule();\n if (graphqlResult.isErr()) {\n return err(graphqlResult.error);\n }\n const { parse: parseGraphql, print: printGraphql } = graphqlResult.value;\n\n const positionCtx: PositionTrackingContext = { spanOffset, converter };\n const templates = walkAndExtract(module as unknown as Node, gqlIdentifiers, positionCtx);\n\n if (templates.length > 0) {\n const defaultFormat: FormatGraphqlFn = (source) => {\n const ast = parseGraphql(source, { noLocation: false });\n return printGraphql(ast);\n };\n\n const templateEdits = formatTemplatesInSource(templates, sourceCode, defaultFormat);\n if (templateEdits.length > 0) {\n needsFormatting = true;\n }\n }\n }\n\n return ok(needsFormatting);\n};\n"],"mappings":"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"}
|
package/dist/formatter.cjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const require_codegen = require('./codegen.cjs');
|
|
2
2
|
let neverthrow = require("neverthrow");
|
|
3
|
-
let node_crypto = require("node:crypto");
|
|
4
3
|
let node_module = require("node:module");
|
|
5
4
|
let __soda_gql_common_template_extraction = require("@soda-gql/common/template-extraction");
|
|
6
5
|
let __soda_gql_common_utils = require("@soda-gql/common/utils");
|
|
@@ -85,128 +84,6 @@ const collectGqlIdentifiers = (module$1) => {
|
|
|
85
84
|
}
|
|
86
85
|
return gqlIdentifiers;
|
|
87
86
|
};
|
|
88
|
-
/**
|
|
89
|
-
* Check if a call expression is a fragment definition call.
|
|
90
|
-
* Pattern: fragment.TypeName({ ... }) where `fragment` comes from gql factory destructuring.
|
|
91
|
-
*/
|
|
92
|
-
const isFragmentDefinitionCall = (node, fragmentIdentifiers) => {
|
|
93
|
-
if (node.callee.type !== "MemberExpression") return false;
|
|
94
|
-
const { object } = node.callee;
|
|
95
|
-
if (object.type !== "Identifier") return false;
|
|
96
|
-
if (!fragmentIdentifiers.has(object.value)) return false;
|
|
97
|
-
const firstArg = node.arguments[0];
|
|
98
|
-
if (!firstArg || firstArg.expression.type !== "ObjectExpression") return false;
|
|
99
|
-
return true;
|
|
100
|
-
};
|
|
101
|
-
/**
|
|
102
|
-
* Check if an object expression already has a `key` property.
|
|
103
|
-
*/
|
|
104
|
-
const hasKeyProperty = (obj) => {
|
|
105
|
-
return obj.properties.some((prop) => {
|
|
106
|
-
if (prop.type === "KeyValueProperty" && prop.key.type === "Identifier") {
|
|
107
|
-
return prop.key.value === "key";
|
|
108
|
-
}
|
|
109
|
-
return false;
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
/**
|
|
113
|
-
* Collect fragment identifiers from gql factory arrow function parameter.
|
|
114
|
-
* Looks for patterns like: ({ fragment }) => ... or ({ fragment: f }) => ...
|
|
115
|
-
*/
|
|
116
|
-
const collectFragmentIdentifiers = (arrowFunction) => {
|
|
117
|
-
const fragmentIdentifiers = new Set();
|
|
118
|
-
const param = arrowFunction.params[0];
|
|
119
|
-
if (param?.type !== "ObjectPattern") return fragmentIdentifiers;
|
|
120
|
-
for (const p of param.properties) {
|
|
121
|
-
if (p.type === "KeyValuePatternProperty" && p.key.type === "Identifier" && p.key.value === "fragment") {
|
|
122
|
-
const localName = extractIdentifierName(p.value);
|
|
123
|
-
if (localName) fragmentIdentifiers.add(localName);
|
|
124
|
-
}
|
|
125
|
-
if (p.type === "AssignmentPatternProperty" && p.key.type === "Identifier" && p.key.value === "fragment") {
|
|
126
|
-
fragmentIdentifiers.add(p.key.value);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return fragmentIdentifiers;
|
|
130
|
-
};
|
|
131
|
-
/**
|
|
132
|
-
* Extract identifier name from a pattern node.
|
|
133
|
-
*/
|
|
134
|
-
const extractIdentifierName = (pattern) => {
|
|
135
|
-
if (pattern.type === "Identifier") return pattern.value;
|
|
136
|
-
if (pattern.type === "BindingIdentifier") return pattern.value;
|
|
137
|
-
return null;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
//#endregion
|
|
141
|
-
//#region packages/tools/src/formatter/insertion.ts
|
|
142
|
-
/**
|
|
143
|
-
* The newline string to insert after object opening brace
|
|
144
|
-
*/
|
|
145
|
-
const NEWLINE_INSERTION = "\n";
|
|
146
|
-
/**
|
|
147
|
-
* Generate a random 8-character hex key for fragment identification.
|
|
148
|
-
* Uses Node.js crypto for cryptographically secure random bytes.
|
|
149
|
-
*/
|
|
150
|
-
const generateFragmentKey = () => {
|
|
151
|
-
return (0, node_crypto.randomBytes)(4).toString("hex");
|
|
152
|
-
};
|
|
153
|
-
/**
|
|
154
|
-
* Create the key property insertion string.
|
|
155
|
-
*
|
|
156
|
-
* For single-line objects: returns ` key: "xxxxxxxx",\n`
|
|
157
|
-
* For multi-line objects (with indentation): returns `{indentation}key: "xxxxxxxx",\n`
|
|
158
|
-
* (the existing indentation before the next property is preserved)
|
|
159
|
-
*
|
|
160
|
-
* @param key - The hex key string
|
|
161
|
-
* @param indentation - Optional indentation string for multi-line objects
|
|
162
|
-
*/
|
|
163
|
-
const createKeyInsertion = (key, indentation) => {
|
|
164
|
-
if (indentation !== undefined) {
|
|
165
|
-
return `${indentation}key: "${key}",\n`;
|
|
166
|
-
}
|
|
167
|
-
return ` key: "${key}",\n`;
|
|
168
|
-
};
|
|
169
|
-
/**
|
|
170
|
-
* Check if there's already a newline after the opening brace.
|
|
171
|
-
* Uses string inspection rather than AST.
|
|
172
|
-
*
|
|
173
|
-
* @param source - The source code string
|
|
174
|
-
* @param objectStartPos - The position of the `{` character in the source
|
|
175
|
-
*/
|
|
176
|
-
const hasExistingNewline = (source, objectStartPos) => {
|
|
177
|
-
const pos = objectStartPos + 1;
|
|
178
|
-
const nextChar = source[pos];
|
|
179
|
-
return nextChar === "\n" || nextChar === "\r";
|
|
180
|
-
};
|
|
181
|
-
/**
|
|
182
|
-
* Detect the indentation used after an opening brace.
|
|
183
|
-
* Returns the whitespace string after the newline, or null if no newline.
|
|
184
|
-
*
|
|
185
|
-
* @param source - The source code string
|
|
186
|
-
* @param objectStartPos - The position of the `{` character
|
|
187
|
-
*/
|
|
188
|
-
const detectIndentationAfterBrace = (source, objectStartPos) => {
|
|
189
|
-
const afterBrace = objectStartPos + 1;
|
|
190
|
-
const char = source[afterBrace];
|
|
191
|
-
if (char !== "\n" && char !== "\r") {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
let pos = afterBrace + 1;
|
|
195
|
-
if (char === "\r" && source[pos] === "\n") {
|
|
196
|
-
pos++;
|
|
197
|
-
}
|
|
198
|
-
let indentation = "";
|
|
199
|
-
while (pos < source.length) {
|
|
200
|
-
const c = source[pos];
|
|
201
|
-
if (c === " " || c === " ") {
|
|
202
|
-
indentation += c;
|
|
203
|
-
pos++;
|
|
204
|
-
} else {
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return indentation;
|
|
209
|
-
};
|
|
210
87
|
|
|
211
88
|
//#endregion
|
|
212
89
|
//#region packages/tools/src/formatter/format.ts
|
|
@@ -236,37 +113,19 @@ const getGraphqlModule = () => {
|
|
|
236
113
|
}
|
|
237
114
|
return (0, neverthrow.ok)(_graphqlModule);
|
|
238
115
|
};
|
|
116
|
+
const NEWLINE_INSERTION = "\n";
|
|
239
117
|
/**
|
|
240
118
|
* Simple recursive AST traversal
|
|
241
119
|
*/
|
|
242
120
|
const traverseNode = (node, context, gqlIdentifiers, onObjectExpression) => {
|
|
243
121
|
if (node.type === "CallExpression" && isGqlDefinitionCall(node, gqlIdentifiers)) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const fragmentIds = collectFragmentIdentifiers(arrow);
|
|
249
|
-
context = {
|
|
250
|
-
...context,
|
|
251
|
-
insideGqlDefinition: true,
|
|
252
|
-
fragmentIdentifiers: new Set([...context.fragmentIdentifiers, ...fragmentIds])
|
|
253
|
-
};
|
|
254
|
-
} else {
|
|
255
|
-
context = {
|
|
256
|
-
...context,
|
|
257
|
-
insideGqlDefinition: true
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (node.type === "CallExpression" && context.insideGqlDefinition && isFragmentDefinitionCall(node, context.fragmentIdentifiers)) {
|
|
262
|
-
const call = node;
|
|
263
|
-
const firstArg = call.arguments[0];
|
|
264
|
-
if (firstArg?.expression.type === "ObjectExpression" && context.currentArrowFunction) {
|
|
265
|
-
onObjectExpression(firstArg.expression, context.currentArrowFunction, { isFragmentConfig: true });
|
|
266
|
-
}
|
|
122
|
+
context = {
|
|
123
|
+
...context,
|
|
124
|
+
insideGqlDefinition: true
|
|
125
|
+
};
|
|
267
126
|
}
|
|
268
127
|
if (node.type === "ObjectExpression" && context.insideGqlDefinition && context.currentArrowFunction && isFieldSelectionObject(node, context.currentArrowFunction)) {
|
|
269
|
-
onObjectExpression(node, context.currentArrowFunction
|
|
128
|
+
onObjectExpression(node, context.currentArrowFunction);
|
|
270
129
|
}
|
|
271
130
|
if (node.type === "CallExpression") {
|
|
272
131
|
const call = node;
|
|
@@ -317,8 +176,7 @@ const traverseNode = (node, context, gqlIdentifiers, onObjectExpression) => {
|
|
|
317
176
|
const traverse = (module$1, gqlIdentifiers, onObjectExpression) => {
|
|
318
177
|
const initialContext = {
|
|
319
178
|
insideGqlDefinition: false,
|
|
320
|
-
currentArrowFunction: null
|
|
321
|
-
fragmentIdentifiers: new Set()
|
|
179
|
+
currentArrowFunction: null
|
|
322
180
|
};
|
|
323
181
|
for (const statement of module$1.body) {
|
|
324
182
|
traverseNode(statement, initialContext, gqlIdentifiers, onObjectExpression);
|
|
@@ -326,10 +184,9 @@ const traverse = (module$1, gqlIdentifiers, onObjectExpression) => {
|
|
|
326
184
|
};
|
|
327
185
|
/**
|
|
328
186
|
* Format soda-gql field selection objects by inserting newlines.
|
|
329
|
-
* Optionally injects fragment keys for anonymous fragments.
|
|
330
187
|
*/
|
|
331
188
|
const format = (options) => {
|
|
332
|
-
const { sourceCode, filePath
|
|
189
|
+
const { sourceCode, filePath } = options;
|
|
333
190
|
let module$1;
|
|
334
191
|
try {
|
|
335
192
|
const program = (0, __swc_core.parseSync)(sourceCode, {
|
|
@@ -365,26 +222,11 @@ const format = (options) => {
|
|
|
365
222
|
});
|
|
366
223
|
}
|
|
367
224
|
const insertionPoints = [];
|
|
368
|
-
traverse(module$1, gqlIdentifiers, (object
|
|
225
|
+
traverse(module$1, gqlIdentifiers, (object) => {
|
|
369
226
|
const objectStart = converter.byteOffsetToCharIndex(object.span.start - spanOffset);
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const indentation = detectIndentationAfterBrace(sourceCode, objectStart) ?? "";
|
|
374
|
-
let insertPos = objectStart + 2;
|
|
375
|
-
if (sourceCode[objectStart + 1] === "\r") insertPos++;
|
|
376
|
-
insertionPoints.push({
|
|
377
|
-
position: insertPos,
|
|
378
|
-
content: createKeyInsertion(key, indentation)
|
|
379
|
-
});
|
|
380
|
-
} else {
|
|
381
|
-
insertionPoints.push({
|
|
382
|
-
position: objectStart + 1,
|
|
383
|
-
content: createKeyInsertion(key)
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (!callbackContext.isFragmentConfig && !hasExistingNewline(sourceCode, objectStart)) {
|
|
227
|
+
const nextChar = sourceCode[objectStart + 1];
|
|
228
|
+
const hasNewline = nextChar === "\n" || nextChar === "\r";
|
|
229
|
+
if (!hasNewline) {
|
|
388
230
|
insertionPoints.push({
|
|
389
231
|
position: objectStart + 1,
|
|
390
232
|
content: NEWLINE_INSERTION
|
|
@@ -470,11 +312,12 @@ const needsFormat = (options) => {
|
|
|
470
312
|
return (0, neverthrow.ok)(false);
|
|
471
313
|
}
|
|
472
314
|
let needsFormatting = false;
|
|
473
|
-
traverse(module$1, gqlIdentifiers, (object
|
|
315
|
+
traverse(module$1, gqlIdentifiers, (object) => {
|
|
474
316
|
if (needsFormatting) return;
|
|
475
|
-
if (callbackContext.isFragmentConfig) return;
|
|
476
317
|
const objectStart = converter.byteOffsetToCharIndex(object.span.start - spanOffset);
|
|
477
|
-
|
|
318
|
+
const nextChar = sourceCode[objectStart + 1];
|
|
319
|
+
const hasNewline = nextChar === "\n" || nextChar === "\r";
|
|
320
|
+
if (!hasNewline) {
|
|
478
321
|
needsFormatting = true;
|
|
479
322
|
}
|
|
480
323
|
});
|