Skip to content

Commit 38fe65d

Browse files
committed
Options AST
1 parent e0fdee9 commit 38fe65d

File tree

3 files changed

+709
-0
lines changed

3 files changed

+709
-0
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import recast from "recast";
2+
import ts from "recast/parsers/typescript.js";
3+
import babel from "recast/parsers/babel.js";
4+
5+
const {namedTypes: n, builders: b} = recast.types;
6+
7+
export function optionsWithSetup(sourceCode, fileName = "", keys = []) {
8+
// Choose the appropriate parser.
9+
const parser =
10+
fileName.endsWith(".ts") || fileName.endsWith(".tsx") ? ts : babel;
11+
12+
// Parse the source code.
13+
const ast = recast.parse(sourceCode, {
14+
parser,
15+
sourceFileName: fileName
16+
});
17+
18+
// ------------------------------
19+
// Step 1: Remove any existing useFusion imports.
20+
recast.types.visit(ast, {
21+
visitImportDeclaration(path) {
22+
const specifiers = path.node.specifiers;
23+
if (
24+
specifiers &&
25+
specifiers.some(
26+
spec => n.ImportSpecifier.check(spec) && spec.imported.name === "useFusion"
27+
)
28+
) {
29+
path.prune();
30+
return false;
31+
}
32+
this.traverse(path);
33+
}
34+
});
35+
36+
// ------------------------------
37+
// Step 2: Find the default export (an object) and rewrite it as __default__.
38+
let defaultExportObject = null;
39+
recast.types.visit(ast, {
40+
visitExportDefaultDeclaration(path) {
41+
if (n.ObjectExpression.check(path.node.declaration)) {
42+
defaultExportObject = path.node.declaration;
43+
const defaultVarDecl = b.variableDeclaration("const", [
44+
b.variableDeclarator(b.identifier("__default__"), defaultExportObject)
45+
]);
46+
path.replace(defaultVarDecl);
47+
return false;
48+
}
49+
this.traverse(path);
50+
}
51+
});
52+
if (!defaultExportObject) {
53+
throw new Error("Default export is not an object expression.");
54+
}
55+
56+
// ------------------------------
57+
// Step 3: Locate the 'setup' property in __default__.
58+
let setupProperty = null;
59+
defaultExportObject.properties.forEach(prop => {
60+
const keyName =
61+
n.Identifier.check(prop.key)
62+
? prop.key.name
63+
: n.Literal.check(prop.key)
64+
? prop.key.value
65+
: null;
66+
if (keyName === "setup") {
67+
setupProperty = prop;
68+
}
69+
});
70+
if (!setupProperty) {
71+
throw new Error("No setup function found in default export.");
72+
}
73+
74+
// If the setup property is an ObjectMethod, convert it into a normal property with a FunctionExpression.
75+
let setupFunctionNode = null;
76+
if (n.ObjectMethod && n.ObjectMethod.check(setupProperty)) {
77+
setupFunctionNode = b.functionExpression(
78+
null,
79+
setupProperty.params,
80+
setupProperty.body,
81+
setupProperty.generator,
82+
setupProperty.async
83+
);
84+
const newProp = b.property("init", setupProperty.key, setupFunctionNode);
85+
newProp.shorthand = false;
86+
const index = defaultExportObject.properties.indexOf(setupProperty);
87+
defaultExportObject.properties[index] = newProp;
88+
setupProperty = newProp;
89+
} else {
90+
setupFunctionNode = setupProperty.value;
91+
}
92+
93+
// ------------------------------
94+
// Step 4: Traverse the setup() function body.
95+
// Update any useFusion calls and collect the keys used.
96+
const fusionUsedKeys = new Set();
97+
let foundUseFusionCall = false;
98+
recast.types.visit(setupFunctionNode, {
99+
visitCallExpression(path) {
100+
if (
101+
n.Identifier.check(path.node.callee) &&
102+
path.node.callee.name === "useFusion"
103+
) {
104+
foundUseFusionCall = true;
105+
if (path.node.arguments.length === 0) {
106+
// No arguments passed: inject provided keys.
107+
const newArray = b.arrayExpression(keys.map(key => b.literal(key)));
108+
// Mark all keys as used.
109+
keys.forEach(key => fusionUsedKeys.add(key));
110+
path.node.arguments.push(newArray);
111+
} else {
112+
// There is at least one argument.
113+
const origArg = path.node.arguments[0];
114+
if (n.ArrayExpression.check(origArg)) {
115+
if (origArg.elements.length > 0) {
116+
// Non-empty array: collect the keys.
117+
origArg.elements.forEach(elem => {
118+
if (n.Literal.check(elem) && typeof elem.value === "string") {
119+
fusionUsedKeys.add(elem.value);
120+
}
121+
});
122+
}
123+
// Else: explicitly passed empty array—leave it as-is.
124+
}
125+
}
126+
// In all cases, update the call to add a second parameter.
127+
path.node.arguments = [
128+
path.node.arguments[0],
129+
b.logicalExpression(
130+
"||",
131+
b.memberExpression(b.identifier("__fusionProvidedProps"), b.identifier("fusion")),
132+
b.objectExpression([])
133+
)
134+
];
135+
}
136+
this.traverse(path);
137+
}
138+
});
139+
const usedKeysArray = foundUseFusionCall ? Array.from(fusionUsedKeys) : [];
140+
const missingKeys = keys.filter(key => !usedKeysArray.includes(key));
141+
142+
// ------------------------------
143+
// Step 5: Insert a top-level declaration for __fusionProvidedProps.
144+
const fusionPropsDecl = b.variableDeclaration("let", [
145+
b.variableDeclarator(b.identifier("__fusionProvidedProps"), null)
146+
]);
147+
ast.program.body.unshift(fusionPropsDecl);
148+
149+
// ------------------------------
150+
// Step 6: Insert a new import for useFusion.
151+
let lastImportIndex = -1;
152+
ast.program.body.forEach((node, idx) => {
153+
if (n.ImportDeclaration.check(node)) {
154+
lastImportIndex = idx;
155+
}
156+
});
157+
const fusionImport = b.importDeclaration(
158+
[b.importSpecifier(b.identifier("useFusion"))],
159+
b.literal("__aliasedFusionPath__")
160+
);
161+
ast.program.body.splice(lastImportIndex + 1, 0, fusionImport);
162+
163+
// ------------------------------
164+
// Step 7: Append a wrapper to override the setup function.
165+
// If a useFusion call exists, then missingKeys (the keys not handled inside setup) are used;
166+
// otherwise, all provided keys are used.
167+
const missingKeysForWrapper = foundUseFusionCall ? missingKeys : keys;
168+
let fusionDataDecl;
169+
if (missingKeysForWrapper.length === 0) {
170+
fusionDataDecl = b.variableDeclaration("const", [
171+
b.variableDeclarator(b.identifier("fusionData"), b.objectExpression([]))
172+
]);
173+
} else {
174+
const missingArrayExpr = b.arrayExpression(
175+
missingKeysForWrapper.map(key => b.literal(key))
176+
);
177+
const fusionDataCall = b.callExpression(b.identifier("useFusion"), [
178+
missingArrayExpr,
179+
b.logicalExpression(
180+
"||",
181+
b.memberExpression(b.identifier("props"), b.identifier("fusion")),
182+
b.objectExpression([])
183+
)
184+
]);
185+
fusionDataDecl = b.variableDeclaration("const", [
186+
b.variableDeclarator(b.identifier("fusionData"), fusionDataCall)
187+
]);
188+
}
189+
const userReturnsDecl = b.variableDeclaration("let", [
190+
b.variableDeclarator(
191+
b.identifier("userReturns"),
192+
b.conditionalExpression(
193+
b.binaryExpression(
194+
"===",
195+
b.unaryExpression("typeof", b.identifier("userSetup"), true),
196+
b.literal("function")
197+
),
198+
b.callExpression(b.identifier("userSetup"), [
199+
b.identifier("props"),
200+
b.identifier("ctx")
201+
]),
202+
b.objectExpression([])
203+
)
204+
)
205+
]);
206+
const newSetupBody = b.blockStatement([
207+
b.expressionStatement(
208+
b.assignmentExpression("=", b.identifier("__fusionProvidedProps"), b.identifier("props"))
209+
),
210+
fusionDataDecl,
211+
userReturnsDecl,
212+
b.returnStatement(
213+
b.objectExpression([
214+
b.spreadElement(b.identifier("fusionData")),
215+
b.spreadElement(b.identifier("userReturns"))
216+
])
217+
)
218+
]);
219+
const newSetupFunc = b.functionExpression(null, [b.identifier("props"), b.identifier("ctx")], newSetupBody);
220+
const userSetupDecl = b.variableDeclaration("const", [
221+
b.variableDeclarator(
222+
b.identifier("userSetup"),
223+
b.memberExpression(b.identifier("__default__"), b.identifier("setup"))
224+
)
225+
]);
226+
const setupOverride = b.expressionStatement(
227+
b.assignmentExpression(
228+
"=",
229+
b.memberExpression(b.identifier("__default__"), b.identifier("setup")),
230+
newSetupFunc
231+
)
232+
);
233+
const exportDefaultDecl = b.exportDefaultDeclaration(b.identifier("__default__"));
234+
ast.program.body.push(userSetupDecl, setupOverride, exportDefaultDecl);
235+
236+
// ------------------------------
237+
// Generate output.
238+
const output = recast.print(ast, {
239+
quote: "double",
240+
sourceMapName: fileName || "transformed.js"
241+
});
242+
return {code: output.code, map: output.map, remaining: missingKeys};
243+
}
244+
245+
export default optionsWithSetup;

0 commit comments

Comments
 (0)