Skip to content

Commit 981cdc0

Browse files
authored
Merge pull request Soomgo-Mobile#34 from Soomgo-Mobile/feature/issue-32
Add babel plugin for integrating configs
2 parents 691c6d9 + ef9cb10 commit 981cdc0

17 files changed

+10828
-10
lines changed

babel-plugin-code-push/index.js

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
const path = require("path");
2+
const fs = require("fs");
3+
const { parseExpression, parse } = require("@babel/parser");
4+
5+
const OPTIONS_TO_BUNDLE = [
6+
"bundleHost",
7+
"runtimeVersion",
8+
"versioning",
9+
];
10+
11+
module.exports = function (babel, options) {
12+
const { types: t } = babel;
13+
const configPath =
14+
options.configPath != null
15+
? path.resolve(options.configPath)
16+
: path.resolve(process.cwd(), "codepush.config.js");
17+
18+
// Load config and imports from `codepush.config.js`
19+
const { config, configImports, importedIdentifiers } = loadConfig(
20+
babel,
21+
configPath
22+
);
23+
24+
return {
25+
visitor: {
26+
Program(path) {
27+
// Track imports in the input file to avoid duplicates
28+
const existingImports = new Set();
29+
path.traverse({
30+
ImportDeclaration(importPath) {
31+
existingImports.add(importPath.node.source.value);
32+
},
33+
});
34+
35+
// Add missing imports from codepush.config.js to the input file
36+
configImports.forEach((importNode) => {
37+
if (!existingImports.has(importNode.source.value)) {
38+
// Clone the import node from codepush.config.js and add it to the input file
39+
path.node.body.unshift(t.cloneNode(importNode));
40+
}
41+
});
42+
},
43+
ImportDeclaration(path, state) {
44+
if (
45+
path.node.source.value.includes("@bravemobile/react-native-code-push")
46+
) {
47+
const defaultImport = path.node.specifiers.find((specifier) =>
48+
t.isImportDefaultSpecifier(specifier)
49+
);
50+
51+
// Save the imported name (e.g., "codePush") for later use
52+
if (defaultImport) {
53+
state.file.metadata.codePushImportName = defaultImport.local.name;
54+
}
55+
}
56+
},
57+
CallExpression(path, state) {
58+
const codePushImportName = state.file.metadata.codePushImportName;
59+
if (!codePushImportName) return;
60+
61+
// Check if the current CallExpression is a call to the codePush function
62+
if (t.isIdentifier(path.node.callee, { name: codePushImportName })) {
63+
// Create an AST object representation of the configuration options to bundle
64+
const configObjectExpression = t.objectExpression(
65+
OPTIONS_TO_BUNDLE.map((key) =>
66+
t.objectProperty(
67+
t.identifier(key),
68+
serializeConfigToNode(babel, importedIdentifiers, config[key])
69+
)
70+
)
71+
);
72+
73+
// Replace the arguments of codePush with the generated config object
74+
path.node.arguments = [configObjectExpression];
75+
}
76+
},
77+
},
78+
};
79+
};
80+
81+
/** loads config file from configPath */
82+
function loadConfig(babel, configPath) {
83+
if (!fs.existsSync(configPath)) {
84+
throw new Error(
85+
"codepush.config.js not found. Please ensure it exists in the root directory."
86+
);
87+
}
88+
89+
const { types: t } = babel;
90+
const configModule = require(configPath);
91+
92+
const configCode = fs.readFileSync(configPath, "utf8");
93+
const ast = parse(configCode, {
94+
sourceType: "module",
95+
});
96+
97+
// Extract import declarations and track imported identifiers
98+
const imports = [];
99+
const importedIdentifiers = new Set();
100+
101+
const convertRequireIntoImportStatement = (declaration) => {
102+
const moduleName = declaration.init.arguments[0].value;
103+
if (t.isIdentifier(declaration.id)) {
104+
// Case for `const fs = require("fs")`
105+
return t.importDeclaration(
106+
[t.importDefaultSpecifier(declaration.id)],
107+
t.stringLiteral(moduleName)
108+
);
109+
} else if (t.isObjectPattern(declaration.id)) {
110+
// Case for `const { parse } = require("module")`
111+
const importSpecifiers = declaration.id.properties.map((property) =>
112+
t.importSpecifier(property.value, property.key)
113+
);
114+
return t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName));
115+
}
116+
};
117+
118+
ast.program.body.forEach((node) => {
119+
if (t.isImportDeclaration(node)) {
120+
// Handle import statements
121+
imports.push(node);
122+
node.specifiers.forEach((specifier) => {
123+
importedIdentifiers.add(specifier.local.name);
124+
});
125+
} else if (t.isVariableDeclaration(node)) {
126+
// Handle require function
127+
node.declarations.forEach((declaration) => {
128+
if (
129+
t.isCallExpression(declaration.init) &&
130+
t.isIdentifier(declaration.init.callee, { name: "require" }) &&
131+
declaration.init.arguments.length === 1 &&
132+
t.isStringLiteral(declaration.init.arguments[0])
133+
) {
134+
const importDeclaration =
135+
convertRequireIntoImportStatement(declaration);
136+
imports.push(importDeclaration);
137+
declaration.id.properties.forEach((dec) => {
138+
importedIdentifiers.add(dec.value.name); // Track the imported identifier
139+
});
140+
}
141+
});
142+
}
143+
});
144+
145+
return {
146+
config: configModule.default || configModule,
147+
configImports: imports,
148+
importedIdentifiers,
149+
};
150+
}
151+
152+
/** Helper to serialize config values to AST nodes */
153+
function serializeConfigToNode(babel, importedIdentifiers, value) {
154+
const { types: t } = babel;
155+
if (["string", "number", "boolean"].includes(typeof value) || value == null) {
156+
return t.valueToNode(value); // Handle primitive values
157+
}
158+
159+
if (Array.isArray(value)) {
160+
return t.arrayExpression(
161+
// Recursively handle arrays
162+
value.map((v) => serializeConfigToNode(babel, importedIdentifiers, v))
163+
);
164+
}
165+
166+
if (typeof value === "object") {
167+
return t.objectExpression(
168+
Object.entries(value).map(([key, val]) =>
169+
t.objectProperty(
170+
t.identifier(key),
171+
serializeConfigToNode(babel, importedIdentifiers, val)
172+
)
173+
)
174+
);
175+
}
176+
177+
// Use identifier for imported symbols instead of inlining
178+
if (importedIdentifiers.has(value.name)) {
179+
return t.identifier(value.name);
180+
}
181+
182+
// For inline functions, parse and serialize them as expressions
183+
if (typeof value === "function") {
184+
const valueString = value.toString();
185+
try {
186+
return parseExpression(valueString, { sourceType: "module" });
187+
} catch (error) {
188+
throw new Error(
189+
`Failed to parse function ${value.name || "anonymous"}: ${
190+
error.message
191+
}`
192+
);
193+
}
194+
}
195+
196+
throw new Error(`Unsupported config value type: ${typeof value}`);
197+
}

0 commit comments

Comments
 (0)