@soda-gql/formatter 0.2.0 → 0.4.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.cjs +221 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +32 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +32 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +220 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
let __swc_core = require("@swc/core");
|
|
2
|
+
let neverthrow = require("neverthrow");
|
|
3
|
+
|
|
4
|
+
//#region packages/formatter/src/detection.ts
|
|
5
|
+
/**
|
|
6
|
+
* Check if an expression is a reference to gql
|
|
7
|
+
* Handles: gql, namespace.gql
|
|
8
|
+
*/
|
|
9
|
+
const isGqlReference = (node, gqlIdentifiers) => {
|
|
10
|
+
if (node.type === "Identifier") return gqlIdentifiers.has(node.value);
|
|
11
|
+
if (node.type === "MemberExpression" && node.property.type === "Identifier" && node.property.value === "gql") return true;
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Check if a call expression is a gql definition call
|
|
16
|
+
* Handles: gql.default(...), gql.model(...), gql.schemaName(...) (multi-schema)
|
|
17
|
+
*/
|
|
18
|
+
const isGqlDefinitionCall = (node, gqlIdentifiers) => {
|
|
19
|
+
if (node.callee.type !== "MemberExpression") return false;
|
|
20
|
+
const { object } = node.callee;
|
|
21
|
+
if (!isGqlReference(object, gqlIdentifiers)) return false;
|
|
22
|
+
const firstArg = node.arguments[0];
|
|
23
|
+
if (!firstArg || firstArg.expression.type !== "ArrowFunctionExpression") return false;
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Check if an object expression is a field selection object
|
|
28
|
+
* Field selection objects are returned from arrow functions with ({ f }) or ({ f, $ }) parameter
|
|
29
|
+
*/
|
|
30
|
+
const isFieldSelectionObject = (object, parent) => {
|
|
31
|
+
let bodyObject = null;
|
|
32
|
+
if (parent.body.type === "ObjectExpression") bodyObject = parent.body;
|
|
33
|
+
else if (parent.body.type === "ParenthesisExpression") {
|
|
34
|
+
const inner = parent.body.expression;
|
|
35
|
+
if (inner.type === "ObjectExpression") bodyObject = inner;
|
|
36
|
+
}
|
|
37
|
+
if (!bodyObject) return false;
|
|
38
|
+
if (bodyObject.span.start !== object.span.start) return false;
|
|
39
|
+
const param = parent.params[0];
|
|
40
|
+
if (!param || param.type !== "ObjectPattern") return false;
|
|
41
|
+
return param.properties.some((p) => {
|
|
42
|
+
if (p.type === "KeyValuePatternProperty" && p.key.type === "Identifier") return p.key.value === "f";
|
|
43
|
+
if (p.type === "AssignmentPatternProperty" && p.key.type === "Identifier") return p.key.value === "f";
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Collect gql identifiers from import declarations
|
|
49
|
+
*/
|
|
50
|
+
const collectGqlIdentifiers = (module$1) => {
|
|
51
|
+
const gqlIdentifiers = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const item of module$1.body) {
|
|
53
|
+
if (item.type !== "ImportDeclaration") continue;
|
|
54
|
+
for (const specifier of item.specifiers) {
|
|
55
|
+
if (specifier.type === "ImportSpecifier") {
|
|
56
|
+
if ((specifier.imported?.value ?? specifier.local.value) === "gql") gqlIdentifiers.add(specifier.local.value);
|
|
57
|
+
}
|
|
58
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
59
|
+
if (specifier.local.value === "gql") gqlIdentifiers.add(specifier.local.value);
|
|
60
|
+
}
|
|
61
|
+
if (specifier.type === "ImportNamespaceSpecifier") {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return gqlIdentifiers;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region packages/formatter/src/insertion.ts
|
|
69
|
+
/**
|
|
70
|
+
* The newline string to insert after object opening brace
|
|
71
|
+
*/
|
|
72
|
+
const NEWLINE_INSERTION = "\n";
|
|
73
|
+
/**
|
|
74
|
+
* Check if there's already a newline after the opening brace.
|
|
75
|
+
* Uses string inspection rather than AST.
|
|
76
|
+
*
|
|
77
|
+
* @param source - The source code string
|
|
78
|
+
* @param objectStartPos - The position of the `{` character in the source
|
|
79
|
+
*/
|
|
80
|
+
const hasExistingNewline = (source, objectStartPos) => {
|
|
81
|
+
const nextChar = source[objectStartPos + 1];
|
|
82
|
+
return nextChar === "\n" || nextChar === "\r";
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region packages/formatter/src/format.ts
|
|
87
|
+
/**
|
|
88
|
+
* Simple recursive AST traversal
|
|
89
|
+
*/
|
|
90
|
+
const traverseNode = (node, context, gqlIdentifiers, onObjectExpression) => {
|
|
91
|
+
if (node.type === "CallExpression" && isGqlDefinitionCall(node, gqlIdentifiers)) context = {
|
|
92
|
+
...context,
|
|
93
|
+
insideGqlDefinition: true
|
|
94
|
+
};
|
|
95
|
+
if (node.type === "ObjectExpression" && context.insideGqlDefinition && context.currentArrowFunction && isFieldSelectionObject(node, context.currentArrowFunction)) onObjectExpression(node, context.currentArrowFunction);
|
|
96
|
+
if (node.type === "CallExpression") {
|
|
97
|
+
const call = node;
|
|
98
|
+
traverseNode(call.callee, context, gqlIdentifiers, onObjectExpression);
|
|
99
|
+
for (const arg of call.arguments) traverseNode(arg.expression, context, gqlIdentifiers, onObjectExpression);
|
|
100
|
+
} else if (node.type === "ArrowFunctionExpression") {
|
|
101
|
+
const arrow = node;
|
|
102
|
+
const childContext = {
|
|
103
|
+
...context,
|
|
104
|
+
currentArrowFunction: arrow
|
|
105
|
+
};
|
|
106
|
+
if (arrow.body.type !== "BlockStatement") traverseNode(arrow.body, childContext, gqlIdentifiers, onObjectExpression);
|
|
107
|
+
} else if (node.type === "ParenthesisExpression") {
|
|
108
|
+
const paren = node;
|
|
109
|
+
if (paren.expression) traverseNode(paren.expression, context, gqlIdentifiers, onObjectExpression);
|
|
110
|
+
} else if (node.type === "MemberExpression") traverseNode(node.object, context, gqlIdentifiers, onObjectExpression);
|
|
111
|
+
else if (node.type === "ObjectExpression") {
|
|
112
|
+
const obj = node;
|
|
113
|
+
for (const prop of obj.properties) if (prop.type === "SpreadElement") traverseNode(prop.arguments, context, gqlIdentifiers, onObjectExpression);
|
|
114
|
+
else if (prop.type === "KeyValueProperty") traverseNode(prop.value, context, gqlIdentifiers, onObjectExpression);
|
|
115
|
+
} else for (const value of Object.values(node)) if (Array.isArray(value)) {
|
|
116
|
+
for (const child of value) if (child && typeof child === "object" && "type" in child) traverseNode(child, context, gqlIdentifiers, onObjectExpression);
|
|
117
|
+
} else if (value && typeof value === "object" && "type" in value) traverseNode(value, context, gqlIdentifiers, onObjectExpression);
|
|
118
|
+
};
|
|
119
|
+
const traverse = (module$1, gqlIdentifiers, onObjectExpression) => {
|
|
120
|
+
for (const statement of module$1.body) traverseNode(statement, {
|
|
121
|
+
insideGqlDefinition: false,
|
|
122
|
+
currentArrowFunction: null
|
|
123
|
+
}, gqlIdentifiers, onObjectExpression);
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Format soda-gql field selection objects by inserting newlines.
|
|
127
|
+
* This preserves multi-line formatting when using Biome/Prettier.
|
|
128
|
+
*/
|
|
129
|
+
const format = (options) => {
|
|
130
|
+
const { sourceCode, filePath } = options;
|
|
131
|
+
let module$1;
|
|
132
|
+
try {
|
|
133
|
+
const program = (0, __swc_core.parseSync)(sourceCode, {
|
|
134
|
+
syntax: "typescript",
|
|
135
|
+
tsx: filePath?.endsWith(".tsx") ?? true,
|
|
136
|
+
target: "es2022",
|
|
137
|
+
decorators: false,
|
|
138
|
+
dynamicImport: true
|
|
139
|
+
});
|
|
140
|
+
if (program.type !== "Module") return (0, neverthrow.err)({
|
|
141
|
+
type: "FormatError",
|
|
142
|
+
code: "PARSE_ERROR",
|
|
143
|
+
message: `Not a module${filePath ? ` (${filePath})` : ""}`
|
|
144
|
+
});
|
|
145
|
+
module$1 = program;
|
|
146
|
+
} catch (cause) {
|
|
147
|
+
return (0, neverthrow.err)({
|
|
148
|
+
type: "FormatError",
|
|
149
|
+
code: "PARSE_ERROR",
|
|
150
|
+
message: `Failed to parse source code${filePath ? ` (${filePath})` : ""}`,
|
|
151
|
+
cause
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const spanOffset = module$1.span.end - sourceCode.length + 1;
|
|
155
|
+
const gqlIdentifiers = collectGqlIdentifiers(module$1);
|
|
156
|
+
if (gqlIdentifiers.size === 0) return (0, neverthrow.ok)({
|
|
157
|
+
modified: false,
|
|
158
|
+
sourceCode
|
|
159
|
+
});
|
|
160
|
+
const insertionPoints = [];
|
|
161
|
+
traverse(module$1, gqlIdentifiers, (object, _parent) => {
|
|
162
|
+
const objectStart = object.span.start - spanOffset;
|
|
163
|
+
if (hasExistingNewline(sourceCode, objectStart)) return;
|
|
164
|
+
insertionPoints.push(objectStart + 1);
|
|
165
|
+
});
|
|
166
|
+
if (insertionPoints.length === 0) return (0, neverthrow.ok)({
|
|
167
|
+
modified: false,
|
|
168
|
+
sourceCode
|
|
169
|
+
});
|
|
170
|
+
const sortedPoints = [...insertionPoints].sort((a, b) => b - a);
|
|
171
|
+
let result = sourceCode;
|
|
172
|
+
for (const pos of sortedPoints) result = result.slice(0, pos) + NEWLINE_INSERTION + result.slice(pos);
|
|
173
|
+
return (0, neverthrow.ok)({
|
|
174
|
+
modified: true,
|
|
175
|
+
sourceCode: result
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Check if a file needs formatting (has unformatted field selections).
|
|
180
|
+
* Useful for pre-commit hooks or CI checks.
|
|
181
|
+
*/
|
|
182
|
+
const needsFormat = (options) => {
|
|
183
|
+
const { sourceCode, filePath } = options;
|
|
184
|
+
let module$1;
|
|
185
|
+
try {
|
|
186
|
+
const program = (0, __swc_core.parseSync)(sourceCode, {
|
|
187
|
+
syntax: "typescript",
|
|
188
|
+
tsx: filePath?.endsWith(".tsx") ?? true,
|
|
189
|
+
target: "es2022",
|
|
190
|
+
decorators: false,
|
|
191
|
+
dynamicImport: true
|
|
192
|
+
});
|
|
193
|
+
if (program.type !== "Module") return (0, neverthrow.err)({
|
|
194
|
+
type: "FormatError",
|
|
195
|
+
code: "PARSE_ERROR",
|
|
196
|
+
message: `Not a module${filePath ? ` (${filePath})` : ""}`
|
|
197
|
+
});
|
|
198
|
+
module$1 = program;
|
|
199
|
+
} catch (cause) {
|
|
200
|
+
return (0, neverthrow.err)({
|
|
201
|
+
type: "FormatError",
|
|
202
|
+
code: "PARSE_ERROR",
|
|
203
|
+
message: `Failed to parse source code${filePath ? ` (${filePath})` : ""}`,
|
|
204
|
+
cause
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const spanOffset = module$1.span.end - sourceCode.length + 1;
|
|
208
|
+
const gqlIdentifiers = collectGqlIdentifiers(module$1);
|
|
209
|
+
if (gqlIdentifiers.size === 0) return (0, neverthrow.ok)(false);
|
|
210
|
+
let needsFormatting = false;
|
|
211
|
+
traverse(module$1, gqlIdentifiers, (object, _parent) => {
|
|
212
|
+
if (needsFormatting) return;
|
|
213
|
+
if (!hasExistingNewline(sourceCode, object.span.start - spanOffset)) needsFormatting = true;
|
|
214
|
+
});
|
|
215
|
+
return (0, neverthrow.ok)(needsFormatting);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
//#endregion
|
|
219
|
+
exports.format = format;
|
|
220
|
+
exports.needsFormat = needsFormat;
|
|
221
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["bodyObject: ObjectExpression | null","module","module","module: Module","insertionPoints: number[]"],"sources":["../src/detection.ts","../src/insertion.ts","../src/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 if (node.type === \"Identifier\") {\n return gqlIdentifiers.has(node.value);\n }\n if (node.type === \"MemberExpression\" && node.property.type === \"Identifier\" && node.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\") 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","/**\n * The newline string to insert after object opening brace\n */\nexport const NEWLINE_INSERTION = \"\\n\";\n\n/**\n * Check if there's already a newline after the opening brace.\n * Uses string inspection rather than AST.\n *\n * @param source - The source code string\n * @param objectStartPos - The position of the `{` character in the source\n */\nexport const hasExistingNewline = (source: string, objectStartPos: number): boolean => {\n // Skip the `{` character\n const pos = objectStartPos + 1;\n\n // Check if next character is a newline\n const nextChar = source[pos];\n return nextChar === \"\\n\" || nextChar === \"\\r\";\n};\n","import { parseSync } from \"@swc/core\";\nimport type { ArrowFunctionExpression, CallExpression, Module, Node, ObjectExpression } from \"@swc/types\";\nimport { err, ok, type Result } from \"neverthrow\";\nimport { collectGqlIdentifiers, isFieldSelectionObject, isGqlDefinitionCall } from \"./detection\";\nimport { hasExistingNewline, NEWLINE_INSERTION } from \"./insertion\";\nimport type { FormatError, FormatOptions, FormatResult } from \"./types\";\n\ntype TraversalContext = {\n insideGqlDefinition: boolean;\n currentArrowFunction: ArrowFunctionExpression | null;\n};\n\n/**\n * Simple recursive AST traversal\n */\nconst traverseNode = (\n node: Node,\n context: TraversalContext,\n gqlIdentifiers: ReadonlySet<string>,\n onObjectExpression: (object: ObjectExpression, parent: ArrowFunctionExpression) => void,\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 = (\n module: Module,\n gqlIdentifiers: ReadonlySet<string>,\n onObjectExpression: (object: ObjectExpression, parent: ArrowFunctionExpression) => void,\n): void => {\n for (const statement of module.body) {\n traverseNode(statement, { insideGqlDefinition: false, currentArrowFunction: null }, gqlIdentifiers, onObjectExpression);\n }\n};\n\n/**\n * Format soda-gql field selection objects by inserting newlines.\n * This preserves multi-line formatting when using Biome/Prettier.\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 const spanOffset = module.span.end - sourceCode.length + 1;\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: number[] = [];\n\n traverse(module, gqlIdentifiers, (object, _parent) => {\n // Calculate actual position in source\n const objectStart = object.span.start - spanOffset;\n\n // Check if already has newline\n if (hasExistingNewline(sourceCode, objectStart)) return;\n\n // Record insertion point (position after `{`)\n insertionPoints.push(objectStart + 1);\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 - a);\n\n let result = sourceCode;\n for (const pos of sortedPoints) {\n result = result.slice(0, pos) + NEWLINE_INSERTION + result.slice(pos);\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.end - sourceCode.length + 1;\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, _parent) => {\n if (needsFormatting) return; // Early exit\n\n const objectStart = object.span.start - spanOffset;\n if (!hasExistingNewline(sourceCode, objectStart)) {\n needsFormatting = true;\n }\n });\n\n return ok(needsFormatting);\n};\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Result } from "neverthrow";
|
|
2
|
+
|
|
3
|
+
//#region packages/formatter/src/types.d.ts
|
|
4
|
+
type FormatOptions = {
|
|
5
|
+
readonly sourceCode: string;
|
|
6
|
+
readonly filePath?: string;
|
|
7
|
+
};
|
|
8
|
+
type FormatResult = {
|
|
9
|
+
readonly modified: boolean;
|
|
10
|
+
readonly sourceCode: string;
|
|
11
|
+
};
|
|
12
|
+
type FormatError = {
|
|
13
|
+
readonly type: "FormatError";
|
|
14
|
+
readonly code: "PARSE_ERROR" | "TRANSFORM_ERROR";
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly cause?: unknown;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region packages/formatter/src/format.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Format soda-gql field selection objects by inserting newlines.
|
|
22
|
+
* This preserves multi-line formatting when using Biome/Prettier.
|
|
23
|
+
*/
|
|
24
|
+
declare const format: (options: FormatOptions) => Result<FormatResult, FormatError>;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a file needs formatting (has unformatted field selections).
|
|
27
|
+
* Useful for pre-commit hooks or CI checks.
|
|
28
|
+
*/
|
|
29
|
+
declare const needsFormat: (options: FormatOptions) => Result<boolean, FormatError>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { type FormatError, type FormatOptions, type FormatResult, format, needsFormat };
|
|
32
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/format.ts"],"sourcesContent":[],"mappings":";;;KAAY,aAAA;;;AAAZ,CAAA;AAKY,KAAA,YAAA,GAAY;EAKZ,SAAA,QAAW,EAAA,OAAA;;;KAAX,WAAA;EC0FC,SAsEZ,IAAA,EAAA,aAAA;EAtE+B,SAAA,IAAA,EAAA,aAAA,GAAA,iBAAA;EAAuB,SAAA,OAAA,EAAA,MAAA;EAAc,SAAA,KAAA,CAAA,EAAA,OAAA;CAArB;;;;ADpGhD;AAKA;AAKA;cC0Fa,kBAAmB,kBAAgB,OAAO,cAAc;;;AAArE;;AAAuD,cA4E1C,WA5E0C,EAAA,CAAA,OAAA,EA4ElB,aA5EkB,EAAA,GA4EF,MA5EE,CAAA,OAAA,EA4Ec,WA5Ed,CAAA"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Result } from "neverthrow";
|
|
2
|
+
|
|
3
|
+
//#region packages/formatter/src/types.d.ts
|
|
4
|
+
type FormatOptions = {
|
|
5
|
+
readonly sourceCode: string;
|
|
6
|
+
readonly filePath?: string;
|
|
7
|
+
};
|
|
8
|
+
type FormatResult = {
|
|
9
|
+
readonly modified: boolean;
|
|
10
|
+
readonly sourceCode: string;
|
|
11
|
+
};
|
|
12
|
+
type FormatError = {
|
|
13
|
+
readonly type: "FormatError";
|
|
14
|
+
readonly code: "PARSE_ERROR" | "TRANSFORM_ERROR";
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly cause?: unknown;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region packages/formatter/src/format.d.ts
|
|
20
|
+
/**
|
|
21
|
+
* Format soda-gql field selection objects by inserting newlines.
|
|
22
|
+
* This preserves multi-line formatting when using Biome/Prettier.
|
|
23
|
+
*/
|
|
24
|
+
declare const format: (options: FormatOptions) => Result<FormatResult, FormatError>;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a file needs formatting (has unformatted field selections).
|
|
27
|
+
* Useful for pre-commit hooks or CI checks.
|
|
28
|
+
*/
|
|
29
|
+
declare const needsFormat: (options: FormatOptions) => Result<boolean, FormatError>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { type FormatError, type FormatOptions, type FormatResult, format, needsFormat };
|
|
32
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/format.ts"],"sourcesContent":[],"mappings":";;;KAAY,aAAA;;;AAAZ,CAAA;AAKY,KAAA,YAAA,GAAY;EAKZ,SAAA,QAAW,EAAA,OAAA;;;KAAX,WAAA;EC0FC,SAsEZ,IAAA,EAAA,aAAA;EAtE+B,SAAA,IAAA,EAAA,aAAA,GAAA,iBAAA;EAAuB,SAAA,OAAA,EAAA,MAAA;EAAc,SAAA,KAAA,CAAA,EAAA,OAAA;CAArB;;;;ADpGhD;AAKA;AAKA;cC0Fa,kBAAmB,kBAAgB,OAAO,cAAc;;;AAArE;;AAAuD,cA4E1C,WA5E0C,EAAA,CAAA,OAAA,EA4ElB,aA5EkB,EAAA,GA4EF,MA5EE,CAAA,OAAA,EA4Ec,WA5Ed,CAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { parseSync } from "@swc/core";
|
|
2
|
+
import { err, ok } from "neverthrow";
|
|
3
|
+
|
|
4
|
+
//#region packages/formatter/src/detection.ts
|
|
5
|
+
/**
|
|
6
|
+
* Check if an expression is a reference to gql
|
|
7
|
+
* Handles: gql, namespace.gql
|
|
8
|
+
*/
|
|
9
|
+
const isGqlReference = (node, gqlIdentifiers) => {
|
|
10
|
+
if (node.type === "Identifier") return gqlIdentifiers.has(node.value);
|
|
11
|
+
if (node.type === "MemberExpression" && node.property.type === "Identifier" && node.property.value === "gql") return true;
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Check if a call expression is a gql definition call
|
|
16
|
+
* Handles: gql.default(...), gql.model(...), gql.schemaName(...) (multi-schema)
|
|
17
|
+
*/
|
|
18
|
+
const isGqlDefinitionCall = (node, gqlIdentifiers) => {
|
|
19
|
+
if (node.callee.type !== "MemberExpression") return false;
|
|
20
|
+
const { object } = node.callee;
|
|
21
|
+
if (!isGqlReference(object, gqlIdentifiers)) return false;
|
|
22
|
+
const firstArg = node.arguments[0];
|
|
23
|
+
if (!firstArg || firstArg.expression.type !== "ArrowFunctionExpression") return false;
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Check if an object expression is a field selection object
|
|
28
|
+
* Field selection objects are returned from arrow functions with ({ f }) or ({ f, $ }) parameter
|
|
29
|
+
*/
|
|
30
|
+
const isFieldSelectionObject = (object, parent) => {
|
|
31
|
+
let bodyObject = null;
|
|
32
|
+
if (parent.body.type === "ObjectExpression") bodyObject = parent.body;
|
|
33
|
+
else if (parent.body.type === "ParenthesisExpression") {
|
|
34
|
+
const inner = parent.body.expression;
|
|
35
|
+
if (inner.type === "ObjectExpression") bodyObject = inner;
|
|
36
|
+
}
|
|
37
|
+
if (!bodyObject) return false;
|
|
38
|
+
if (bodyObject.span.start !== object.span.start) return false;
|
|
39
|
+
const param = parent.params[0];
|
|
40
|
+
if (!param || param.type !== "ObjectPattern") return false;
|
|
41
|
+
return param.properties.some((p) => {
|
|
42
|
+
if (p.type === "KeyValuePatternProperty" && p.key.type === "Identifier") return p.key.value === "f";
|
|
43
|
+
if (p.type === "AssignmentPatternProperty" && p.key.type === "Identifier") return p.key.value === "f";
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Collect gql identifiers from import declarations
|
|
49
|
+
*/
|
|
50
|
+
const collectGqlIdentifiers = (module) => {
|
|
51
|
+
const gqlIdentifiers = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const item of module.body) {
|
|
53
|
+
if (item.type !== "ImportDeclaration") continue;
|
|
54
|
+
for (const specifier of item.specifiers) {
|
|
55
|
+
if (specifier.type === "ImportSpecifier") {
|
|
56
|
+
if ((specifier.imported?.value ?? specifier.local.value) === "gql") gqlIdentifiers.add(specifier.local.value);
|
|
57
|
+
}
|
|
58
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
59
|
+
if (specifier.local.value === "gql") gqlIdentifiers.add(specifier.local.value);
|
|
60
|
+
}
|
|
61
|
+
if (specifier.type === "ImportNamespaceSpecifier") {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return gqlIdentifiers;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region packages/formatter/src/insertion.ts
|
|
69
|
+
/**
|
|
70
|
+
* The newline string to insert after object opening brace
|
|
71
|
+
*/
|
|
72
|
+
const NEWLINE_INSERTION = "\n";
|
|
73
|
+
/**
|
|
74
|
+
* Check if there's already a newline after the opening brace.
|
|
75
|
+
* Uses string inspection rather than AST.
|
|
76
|
+
*
|
|
77
|
+
* @param source - The source code string
|
|
78
|
+
* @param objectStartPos - The position of the `{` character in the source
|
|
79
|
+
*/
|
|
80
|
+
const hasExistingNewline = (source, objectStartPos) => {
|
|
81
|
+
const nextChar = source[objectStartPos + 1];
|
|
82
|
+
return nextChar === "\n" || nextChar === "\r";
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region packages/formatter/src/format.ts
|
|
87
|
+
/**
|
|
88
|
+
* Simple recursive AST traversal
|
|
89
|
+
*/
|
|
90
|
+
const traverseNode = (node, context, gqlIdentifiers, onObjectExpression) => {
|
|
91
|
+
if (node.type === "CallExpression" && isGqlDefinitionCall(node, gqlIdentifiers)) context = {
|
|
92
|
+
...context,
|
|
93
|
+
insideGqlDefinition: true
|
|
94
|
+
};
|
|
95
|
+
if (node.type === "ObjectExpression" && context.insideGqlDefinition && context.currentArrowFunction && isFieldSelectionObject(node, context.currentArrowFunction)) onObjectExpression(node, context.currentArrowFunction);
|
|
96
|
+
if (node.type === "CallExpression") {
|
|
97
|
+
const call = node;
|
|
98
|
+
traverseNode(call.callee, context, gqlIdentifiers, onObjectExpression);
|
|
99
|
+
for (const arg of call.arguments) traverseNode(arg.expression, context, gqlIdentifiers, onObjectExpression);
|
|
100
|
+
} else if (node.type === "ArrowFunctionExpression") {
|
|
101
|
+
const arrow = node;
|
|
102
|
+
const childContext = {
|
|
103
|
+
...context,
|
|
104
|
+
currentArrowFunction: arrow
|
|
105
|
+
};
|
|
106
|
+
if (arrow.body.type !== "BlockStatement") traverseNode(arrow.body, childContext, gqlIdentifiers, onObjectExpression);
|
|
107
|
+
} else if (node.type === "ParenthesisExpression") {
|
|
108
|
+
const paren = node;
|
|
109
|
+
if (paren.expression) traverseNode(paren.expression, context, gqlIdentifiers, onObjectExpression);
|
|
110
|
+
} else if (node.type === "MemberExpression") traverseNode(node.object, context, gqlIdentifiers, onObjectExpression);
|
|
111
|
+
else if (node.type === "ObjectExpression") {
|
|
112
|
+
const obj = node;
|
|
113
|
+
for (const prop of obj.properties) if (prop.type === "SpreadElement") traverseNode(prop.arguments, context, gqlIdentifiers, onObjectExpression);
|
|
114
|
+
else if (prop.type === "KeyValueProperty") traverseNode(prop.value, context, gqlIdentifiers, onObjectExpression);
|
|
115
|
+
} else for (const value of Object.values(node)) if (Array.isArray(value)) {
|
|
116
|
+
for (const child of value) if (child && typeof child === "object" && "type" in child) traverseNode(child, context, gqlIdentifiers, onObjectExpression);
|
|
117
|
+
} else if (value && typeof value === "object" && "type" in value) traverseNode(value, context, gqlIdentifiers, onObjectExpression);
|
|
118
|
+
};
|
|
119
|
+
const traverse = (module, gqlIdentifiers, onObjectExpression) => {
|
|
120
|
+
for (const statement of module.body) traverseNode(statement, {
|
|
121
|
+
insideGqlDefinition: false,
|
|
122
|
+
currentArrowFunction: null
|
|
123
|
+
}, gqlIdentifiers, onObjectExpression);
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Format soda-gql field selection objects by inserting newlines.
|
|
127
|
+
* This preserves multi-line formatting when using Biome/Prettier.
|
|
128
|
+
*/
|
|
129
|
+
const format = (options) => {
|
|
130
|
+
const { sourceCode, filePath } = options;
|
|
131
|
+
let module;
|
|
132
|
+
try {
|
|
133
|
+
const program = parseSync(sourceCode, {
|
|
134
|
+
syntax: "typescript",
|
|
135
|
+
tsx: filePath?.endsWith(".tsx") ?? true,
|
|
136
|
+
target: "es2022",
|
|
137
|
+
decorators: false,
|
|
138
|
+
dynamicImport: true
|
|
139
|
+
});
|
|
140
|
+
if (program.type !== "Module") return err({
|
|
141
|
+
type: "FormatError",
|
|
142
|
+
code: "PARSE_ERROR",
|
|
143
|
+
message: `Not a module${filePath ? ` (${filePath})` : ""}`
|
|
144
|
+
});
|
|
145
|
+
module = program;
|
|
146
|
+
} catch (cause) {
|
|
147
|
+
return err({
|
|
148
|
+
type: "FormatError",
|
|
149
|
+
code: "PARSE_ERROR",
|
|
150
|
+
message: `Failed to parse source code${filePath ? ` (${filePath})` : ""}`,
|
|
151
|
+
cause
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const spanOffset = module.span.end - sourceCode.length + 1;
|
|
155
|
+
const gqlIdentifiers = collectGqlIdentifiers(module);
|
|
156
|
+
if (gqlIdentifiers.size === 0) return ok({
|
|
157
|
+
modified: false,
|
|
158
|
+
sourceCode
|
|
159
|
+
});
|
|
160
|
+
const insertionPoints = [];
|
|
161
|
+
traverse(module, gqlIdentifiers, (object, _parent) => {
|
|
162
|
+
const objectStart = object.span.start - spanOffset;
|
|
163
|
+
if (hasExistingNewline(sourceCode, objectStart)) return;
|
|
164
|
+
insertionPoints.push(objectStart + 1);
|
|
165
|
+
});
|
|
166
|
+
if (insertionPoints.length === 0) return ok({
|
|
167
|
+
modified: false,
|
|
168
|
+
sourceCode
|
|
169
|
+
});
|
|
170
|
+
const sortedPoints = [...insertionPoints].sort((a, b) => b - a);
|
|
171
|
+
let result = sourceCode;
|
|
172
|
+
for (const pos of sortedPoints) result = result.slice(0, pos) + NEWLINE_INSERTION + result.slice(pos);
|
|
173
|
+
return ok({
|
|
174
|
+
modified: true,
|
|
175
|
+
sourceCode: result
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Check if a file needs formatting (has unformatted field selections).
|
|
180
|
+
* Useful for pre-commit hooks or CI checks.
|
|
181
|
+
*/
|
|
182
|
+
const needsFormat = (options) => {
|
|
183
|
+
const { sourceCode, filePath } = options;
|
|
184
|
+
let module;
|
|
185
|
+
try {
|
|
186
|
+
const program = parseSync(sourceCode, {
|
|
187
|
+
syntax: "typescript",
|
|
188
|
+
tsx: filePath?.endsWith(".tsx") ?? true,
|
|
189
|
+
target: "es2022",
|
|
190
|
+
decorators: false,
|
|
191
|
+
dynamicImport: true
|
|
192
|
+
});
|
|
193
|
+
if (program.type !== "Module") return err({
|
|
194
|
+
type: "FormatError",
|
|
195
|
+
code: "PARSE_ERROR",
|
|
196
|
+
message: `Not a module${filePath ? ` (${filePath})` : ""}`
|
|
197
|
+
});
|
|
198
|
+
module = program;
|
|
199
|
+
} catch (cause) {
|
|
200
|
+
return err({
|
|
201
|
+
type: "FormatError",
|
|
202
|
+
code: "PARSE_ERROR",
|
|
203
|
+
message: `Failed to parse source code${filePath ? ` (${filePath})` : ""}`,
|
|
204
|
+
cause
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const spanOffset = module.span.end - sourceCode.length + 1;
|
|
208
|
+
const gqlIdentifiers = collectGqlIdentifiers(module);
|
|
209
|
+
if (gqlIdentifiers.size === 0) return ok(false);
|
|
210
|
+
let needsFormatting = false;
|
|
211
|
+
traverse(module, gqlIdentifiers, (object, _parent) => {
|
|
212
|
+
if (needsFormatting) return;
|
|
213
|
+
if (!hasExistingNewline(sourceCode, object.span.start - spanOffset)) needsFormatting = true;
|
|
214
|
+
});
|
|
215
|
+
return ok(needsFormatting);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
//#endregion
|
|
219
|
+
export { format, needsFormat };
|
|
220
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["bodyObject: ObjectExpression | null","module: Module","insertionPoints: number[]"],"sources":["../src/detection.ts","../src/insertion.ts","../src/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 if (node.type === \"Identifier\") {\n return gqlIdentifiers.has(node.value);\n }\n if (node.type === \"MemberExpression\" && node.property.type === \"Identifier\" && node.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\") 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","/**\n * The newline string to insert after object opening brace\n */\nexport const NEWLINE_INSERTION = \"\\n\";\n\n/**\n * Check if there's already a newline after the opening brace.\n * Uses string inspection rather than AST.\n *\n * @param source - The source code string\n * @param objectStartPos - The position of the `{` character in the source\n */\nexport const hasExistingNewline = (source: string, objectStartPos: number): boolean => {\n // Skip the `{` character\n const pos = objectStartPos + 1;\n\n // Check if next character is a newline\n const nextChar = source[pos];\n return nextChar === \"\\n\" || nextChar === \"\\r\";\n};\n","import { parseSync } from \"@swc/core\";\nimport type { ArrowFunctionExpression, CallExpression, Module, Node, ObjectExpression } from \"@swc/types\";\nimport { err, ok, type Result } from \"neverthrow\";\nimport { collectGqlIdentifiers, isFieldSelectionObject, isGqlDefinitionCall } from \"./detection\";\nimport { hasExistingNewline, NEWLINE_INSERTION } from \"./insertion\";\nimport type { FormatError, FormatOptions, FormatResult } from \"./types\";\n\ntype TraversalContext = {\n insideGqlDefinition: boolean;\n currentArrowFunction: ArrowFunctionExpression | null;\n};\n\n/**\n * Simple recursive AST traversal\n */\nconst traverseNode = (\n node: Node,\n context: TraversalContext,\n gqlIdentifiers: ReadonlySet<string>,\n onObjectExpression: (object: ObjectExpression, parent: ArrowFunctionExpression) => void,\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 = (\n module: Module,\n gqlIdentifiers: ReadonlySet<string>,\n onObjectExpression: (object: ObjectExpression, parent: ArrowFunctionExpression) => void,\n): void => {\n for (const statement of module.body) {\n traverseNode(statement, { insideGqlDefinition: false, currentArrowFunction: null }, gqlIdentifiers, onObjectExpression);\n }\n};\n\n/**\n * Format soda-gql field selection objects by inserting newlines.\n * This preserves multi-line formatting when using Biome/Prettier.\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 const spanOffset = module.span.end - sourceCode.length + 1;\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: number[] = [];\n\n traverse(module, gqlIdentifiers, (object, _parent) => {\n // Calculate actual position in source\n const objectStart = object.span.start - spanOffset;\n\n // Check if already has newline\n if (hasExistingNewline(sourceCode, objectStart)) return;\n\n // Record insertion point (position after `{`)\n insertionPoints.push(objectStart + 1);\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 - a);\n\n let result = sourceCode;\n for (const pos of sortedPoints) {\n result = result.slice(0, pos) + NEWLINE_INSERTION + result.slice(pos);\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.end - sourceCode.length + 1;\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, _parent) => {\n if (needsFormatting) return; // Early exit\n\n const objectStart = object.span.start - spanOffset;\n if (!hasExistingNewline(sourceCode, objectStart)) {\n needsFormatting = true;\n }\n });\n\n return ok(needsFormatting);\n};\n"],"mappings":";;;;;;;;AAaA,MAAa,kBAAkB,MAAkB,mBAAiD;AAChG,KAAI,KAAK,SAAS,aAChB,QAAO,eAAe,IAAI,KAAK,MAAM;AAEvC,KAAI,KAAK,SAAS,sBAAsB,KAAK,SAAS,SAAS,gBAAgB,KAAK,SAAS,UAAU,MACrG,QAAO;AAET,QAAO;;;;;;AAOT,MAAa,uBAAuB,MAAsB,mBAAiD;AACzG,KAAI,KAAK,OAAO,SAAS,mBAAoB,QAAO;CACpD,MAAM,EAAE,WAAW,KAAK;AAGxB,KAAI,CAAC,eAAe,QAAQ,eAAe,CAAE,QAAO;CAIpD,MAAM,WAAW,KAAK,UAAU;AAChC,KAAI,CAAC,YAAY,SAAS,WAAW,SAAS,0BAA2B,QAAO;AAEhF,QAAO;;;;;;AAOT,MAAa,0BAA0B,QAA0B,WAA6C;CAG5G,IAAIA,aAAsC;AAE1C,KAAI,OAAO,KAAK,SAAS,mBACvB,cAAa,OAAO;UACX,OAAO,KAAK,SAAS,yBAAyB;EAEvD,MAAM,QAAQ,OAAO,KAAK;AAC1B,MAAI,MAAM,SAAS,mBACjB,cAAa;;AAIjB,KAAI,CAAC,WAAY,QAAO;AACxB,KAAI,WAAW,KAAK,UAAU,OAAO,KAAK,MAAO,QAAO;CAGxD,MAAM,QAAQ,OAAO,OAAO;AAC5B,KAAI,CAAC,SAAS,MAAM,SAAS,gBAAiB,QAAO;AAErD,QAAO,MAAM,WAAW,MAAM,MAA6B;AACzD,MAAI,EAAE,SAAS,6BAA6B,EAAE,IAAI,SAAS,aACzD,QAAO,EAAE,IAAI,UAAU;AAEzB,MAAI,EAAE,SAAS,+BAA+B,EAAE,IAAI,SAAS,aAC3D,QAAO,EAAE,IAAI,UAAU;AAEzB,SAAO;GACP;;;;;AAMJ,MAAa,yBAAyB,WAAgC;CACpE,MAAM,iCAAiB,IAAI,KAAa;AAExC,MAAK,MAAM,QAAQ,OAAO,MAAM;AAC9B,MAAI,KAAK,SAAS,oBAAqB;AAEvC,OAAK,MAAM,aAAa,KAAK,YAAY;AACvC,OAAI,UAAU,SAAS,mBAErB;SADiB,UAAU,UAAU,SAAS,UAAU,MAAM,WAC7C,MACf,gBAAe,IAAI,UAAU,MAAM,MAAM;;AAG7C,OAAI,UAAU,SAAS,0BAErB;QAAI,UAAU,MAAM,UAAU,MAC5B,gBAAe,IAAI,UAAU,MAAM,MAAM;;AAG7C,OAAI,UAAU,SAAS,4BAA4B;;;AAOvD,QAAO;;;;;;;;ACzGT,MAAa,oBAAoB;;;;;;;;AASjC,MAAa,sBAAsB,QAAgB,mBAAoC;CAKrF,MAAM,WAAW,OAHL,iBAAiB;AAI7B,QAAO,aAAa,QAAQ,aAAa;;;;;;;;ACH3C,MAAM,gBACJ,MACA,SACA,gBACA,uBACS;AAET,KAAI,KAAK,SAAS,oBAAoB,oBAAoB,MAAwB,eAAe,CAC/F,WAAU;EAAE,GAAG;EAAS,qBAAqB;EAAM;AAIrD,KACE,KAAK,SAAS,sBACd,QAAQ,uBACR,QAAQ,wBACR,uBAAuB,MAA0B,QAAQ,qBAAqB,CAE9E,oBAAmB,MAA0B,QAAQ,qBAAqB;AAI5E,KAAI,KAAK,SAAS,kBAAkB;EAClC,MAAM,OAAO;AACb,eAAa,KAAK,QAAgB,SAAS,gBAAgB,mBAAmB;AAC9E,OAAK,MAAM,OAAO,KAAK,UACrB,cAAa,IAAI,YAAoB,SAAS,gBAAgB,mBAAmB;YAE1E,KAAK,SAAS,2BAA2B;EAClD,MAAM,QAAQ;EAEd,MAAM,eAAe;GAAE,GAAG;GAAS,sBAAsB;GAAO;AAChE,MAAI,MAAM,KAAK,SAAS,iBACtB,cAAa,MAAM,MAAc,cAAc,gBAAgB,mBAAmB;YAE3E,KAAK,SAAS,yBAAyB;EAGhD,MAAM,QAAQ;AACd,MAAI,MAAM,WACR,cAAa,MAAM,YAAoB,SAAS,gBAAgB,mBAAmB;YAE5E,KAAK,SAAS,mBAGvB,cADe,KACK,QAAgB,SAAS,gBAAgB,mBAAmB;UACvE,KAAK,SAAS,oBAAoB;EAC3C,MAAM,MAAM;AACZ,OAAK,MAAM,QAAQ,IAAI,WACrB,KAAI,KAAK,SAAS,gBAChB,cAAa,KAAK,WAAmB,SAAS,gBAAgB,mBAAmB;WACxE,KAAK,SAAS,mBACvB,cAAa,KAAK,OAAe,SAAS,gBAAgB,mBAAmB;OAKjF,MAAK,MAAM,SAAS,OAAO,OAAO,KAAK,CACrC,KAAI,MAAM,QAAQ,MAAM,EACtB;OAAK,MAAM,SAAS,MAClB,KAAI,SAAS,OAAO,UAAU,YAAY,UAAU,MAClD,cAAa,OAAe,SAAS,gBAAgB,mBAAmB;YAGnE,SAAS,OAAO,UAAU,YAAY,UAAU,MACzD,cAAa,OAAe,SAAS,gBAAgB,mBAAmB;;AAMhF,MAAM,YACJ,QACA,gBACA,uBACS;AACT,MAAK,MAAM,aAAa,OAAO,KAC7B,cAAa,WAAW;EAAE,qBAAqB;EAAO,sBAAsB;EAAM,EAAE,gBAAgB,mBAAmB;;;;;;AAQ3H,MAAa,UAAU,YAA8D;CACnF,MAAM,EAAE,YAAY,aAAa;CAGjC,IAAIC;AACJ,KAAI;EACF,MAAM,UAAU,UAAU,YAAY;GACpC,QAAQ;GACR,KAAK,UAAU,SAAS,OAAO,IAAI;GACnC,QAAQ;GACR,YAAY;GACZ,eAAe;GAChB,CAAC;AAEF,MAAI,QAAQ,SAAS,SACnB,QAAO,IAAI;GACT,MAAM;GACN,MAAM;GACN,SAAS,eAAe,WAAW,KAAK,SAAS,KAAK;GACvD,CAAC;AAEJ,WAAS;UACF,OAAO;AACd,SAAO,IAAI;GACT,MAAM;GACN,MAAM;GACN,SAAS,8BAA8B,WAAW,KAAK,SAAS,KAAK;GACrE;GACD,CAAC;;CAKJ,MAAM,aAAa,OAAO,KAAK,MAAM,WAAW,SAAS;CAGzD,MAAM,iBAAiB,sBAAsB,OAAO;AACpD,KAAI,eAAe,SAAS,EAC1B,QAAO,GAAG;EAAE,UAAU;EAAO;EAAY,CAAC;CAI5C,MAAMC,kBAA4B,EAAE;AAEpC,UAAS,QAAQ,iBAAiB,QAAQ,YAAY;EAEpD,MAAM,cAAc,OAAO,KAAK,QAAQ;AAGxC,MAAI,mBAAmB,YAAY,YAAY,CAAE;AAGjD,kBAAgB,KAAK,cAAc,EAAE;GACrC;AAGF,KAAI,gBAAgB,WAAW,EAC7B,QAAO,GAAG;EAAE,UAAU;EAAO;EAAY,CAAC;CAK5C,MAAM,eAAe,CAAC,GAAG,gBAAgB,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE;CAE/D,IAAI,SAAS;AACb,MAAK,MAAM,OAAO,aAChB,UAAS,OAAO,MAAM,GAAG,IAAI,GAAG,oBAAoB,OAAO,MAAM,IAAI;AAGvE,QAAO,GAAG;EAAE,UAAU;EAAM,YAAY;EAAQ,CAAC;;;;;;AAOnD,MAAa,eAAe,YAAyD;CACnF,MAAM,EAAE,YAAY,aAAa;CAGjC,IAAID;AACJ,KAAI;EACF,MAAM,UAAU,UAAU,YAAY;GACpC,QAAQ;GACR,KAAK,UAAU,SAAS,OAAO,IAAI;GACnC,QAAQ;GACR,YAAY;GACZ,eAAe;GAChB,CAAC;AAEF,MAAI,QAAQ,SAAS,SACnB,QAAO,IAAI;GACT,MAAM;GACN,MAAM;GACN,SAAS,eAAe,WAAW,KAAK,SAAS,KAAK;GACvD,CAAC;AAEJ,WAAS;UACF,OAAO;AACd,SAAO,IAAI;GACT,MAAM;GACN,MAAM;GACN,SAAS,8BAA8B,WAAW,KAAK,SAAS,KAAK;GACrE;GACD,CAAC;;CAGJ,MAAM,aAAa,OAAO,KAAK,MAAM,WAAW,SAAS;CACzD,MAAM,iBAAiB,sBAAsB,OAAO;AAEpD,KAAI,eAAe,SAAS,EAC1B,QAAO,GAAG,MAAM;CAGlB,IAAI,kBAAkB;AAEtB,UAAS,QAAQ,iBAAiB,QAAQ,YAAY;AACpD,MAAI,gBAAiB;AAGrB,MAAI,CAAC,mBAAmB,YADJ,OAAO,KAAK,QAAQ,WACQ,CAC9C,mBAAkB;GAEpB;AAEF,QAAO,GAAG,gBAAgB"}
|