Skip to content

Instantly share code, notes, and snippets.

@bvisness
Created September 13, 2024 20:33
Show Gist options
  • Save bvisness/15dcfb99f7f182063c1569320adbe373 to your computer and use it in GitHub Desktop.
Save bvisness/15dcfb99f7f182063c1569320adbe373 to your computer and use it in GitHub Desktop.
React "detranspiler"
#!/usr/bin/env node
// This script takes a script called `s01.js` and tries to de-transpile it somewhat,
// converting calls to `jsx` and `jsxs` into JSX syntax.
const acorn = require("acorn");
const walk = require("acorn-walk");
const { readFileSync, writeFileSync } = require("fs");
const prettier = require("prettier");
const s01 = readFileSync("s01.js", "utf-8");
function s(source, node) {
return source.substring(node.start, node.end);
}
function printNode(source, node) {
console.log();
}
// s02: Coalesce (0, foo) to (foo)
{
const dumbos = [];
walk.simple(acorn.parse(s01, { ecmaVersion: 2020 }), {
SequenceExpression(node, state) {
const first = node.expressions[0];
if (first.type === "Literal" && first.value === 0) {
dumbos.push(node);
}
},
});
function replaceNodes(source, nodes, f) {
let result = source;
for (let i = nodes.length - 1; i >= 0; i--) {
const n = nodes[i];
const before = result.substring(0, n.start);
const after = result.substring(n.end);
result = before + f(n) + after;
}
return result;
}
const s02 = replaceNodes(s01, dumbos, node => s(s01, node.expressions[1]));
writeFileSync("s02.js", s02);
}
// s03: Rewrite `s.jsxs` and `s.jsx` to JSX components
{
const s02 = readFileSync("s02.js", "utf-8");
function isJSX(node) {
return (
node.type === "CallExpression"
&& node.callee.type === "MemberExpression"
&& ["jsx", "jsxs"].includes(node.callee.property.name)
);
}
// Build a tree of just JSX nodes that we can recursively walk later to find and replace.
const currentJSXes = [];
walk.ancestor(acorn.parse(s02, { ecmaVersion: 2020 }), {
CallExpression(node, state, ancestors) {
if (!isJSX(node)) {
return;
}
const lastDepth = currentJSXes.length - 1;
const depth = ancestors.filter(n => n !== node && isJSX(n)).length;
if (depth < lastDepth) {
// We have jumped up to a parent node. Create that parent node and push all the
// current-depth children to it.
console.assert(lastDepth - depth === 1, lastDepth, depth); // invariant of what "depth" means + the walk order
const newParent = { n: node, children: currentJSXes[currentJSXes.length - 1] };
currentJSXes[depth].push(newParent);
currentJSXes.length--;
} else if (depth > lastDepth) {
// We have gone deeper. Push empty arrays for each new level, and the new node at the end.
const numNewLevels = depth - lastDepth;
for (let i = 0; i < numNewLevels - 1; i++) {
currentJSXes.push([]);
}
currentJSXes.push([{ n: node, children: [] }]);
console.assert(currentJSXes.length === depth + 1, "pushing new levels: expected length %o, got length %o", depth + 1, currentJSXes.length);
} else {
// Sibling at current depth with no JSX children. Push to current depth list.
currentJSXes[currentJSXes.length-1].push({ n: node, children: [] });
}
},
});
console.assert(currentJSXes.length === 1);
const jsxes = currentJSXes[0];
function replaceJSXes(source, sourceStart, jsxes) {
let result = source;
for (let i = jsxes.length - 1; i >= 0; i--) {
const jsx = jsxes[i];
const before = result.substring(0, jsx.n.start - sourceStart);
const after = result.substring(jsx.n.end - sourceStart);
const [component, props, key] = jsx.n.arguments; // TODO: Handle key?
let componentStr = component.type === "Literal" ? component.value : s(s02, component);
if (componentStr.endsWith("()")) {
componentStr = componentStr.replace("()", "_Call"); // hack to avoid syntax errors
}
const propsStrs = [];
for (const property of props.properties) { // because ObjectExpression
let name, value;
if (property.type === "SpreadElement") {
// for spreads, use no name...very hacky
name = null;
value = property;
} else {
console.assert(property.type === "Property", "expected Property, got %o", property.type);
name = property.key.name;
value = property.value;
}
if (name === "children") {
continue;
}
let valueStr = s(s02, value);
if (property.method) {
valueStr = "function" + valueStr;
}
let propStr;
if (!name) {
propStr = `{ ${valueStr} }`;
} else {
propStr = `${name}={ ${valueStr} }`;
}
propsStrs.push(propStr);
}
const propsStr = (propsStrs.length ? " " : "") + propsStrs.join(" ");
const childrenProp = props.properties.find(p => p.type === "Property" && p.key.name === "children");
let childNodes = [];
if (childrenProp) {
const allChildrenNode = childrenProp.value;
childNodes = allChildrenNode.type === "ArrayExpression" ? allChildrenNode.elements : [allChildrenNode];
}
const childStrs = [];
for (const cn of childNodes) {
// We have previously tracked all the JSX nodes that occur inside this node,
// but when rendering this node's children, we don't know which of those actually
// occur inside that particular child node. So, filter based on start and end.
const jsxChildrenOfChild = jsx.children.filter(childJSX =>
cn.start <= childJSX.n.start && childJSX.n.end <= cn.end
);
const rendered = replaceJSXes(s(s02, cn), cn.start, jsxChildrenOfChild);
if (isJSX(cn)) {
childStrs.push(rendered);
} else {
childStrs.push(`{ ${rendered} }`);
}
}
let childrenStr = ("\n" + childStrs.join("\n") + "\n");
if (childrenStr.trim() === "") {
childrenStr = "";
}
const transpiled = `<${componentStr}${propsStr}>${ childrenStr }</${componentStr}>`;
result = before + transpiled + after;
}
return result;
}
const s03 = replaceJSXes(s02, 0, jsxes);
writeFileSync("s03.js", s03);
}
// s04: autoformat
(async () => {
const s03 = readFileSync("s03.js", "utf-8");
const s04 = await prettier.format(s03, { parser: "acorn" });
writeFileSync("s04.js", s04);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment