import { generate } from "jsr:@davidbonnet/astring";
import { assertEquals, assertThrows } from "jsr:@std/assert";
import { parse, type Program } from "npm:acorn";
const hash = (content: string) =>
crypto.subtle
.digest("SHA-256", new TextEncoder().encode(content))
.then((b) => new Uint8Array(b))
.then(Array.from)
.then((a) => a.map((b: any) => b.toString(16).padStart(2, "0")).join(""));
const js = async (strings: TemplateStringsArray, ...values: any[]) => {
const code = String.raw({ raw: strings }, ...values);
const ast = parse(code, { ecmaVersion: 2024 });
const normalizedASTs = ast.body
.map((chunk) => structuredClone(chunk))
.map((chunk) => {
walk(chunk, normalize);
return chunk;
});
const contentMap = Object.fromEntries(
await Promise.all(
normalizedASTs.map(async (ast) => [await hash(JSON.stringify(ast)), ast]),
),
);
return {
ast,
contentMap,
};
};
const walk = (
node: object,
callback: (node: Record<string, unknown>) => void,
): void => {
if (typeof node !== "object" || node === null) {
return;
}
callback(node as Record<string, unknown>);
for (const value of Object.values(node)) {
switch (true) {
case typeof value === "object" && value !== null:
walk(value, callback);
break;
case Array.isArray(value):
value.forEach((v) => walk(v, callback));
break;
default:
break;
}
}
};
const normalize = (node: Record<string, unknown>) => {
delete node.start;
delete node.end;
delete node.raw;
};
const modules = {};
const variantA = await js`console.log('hello world!')`;
const variantB = await js`console.log( "hello world!" );`;
assertEquals(variantA.ast, variantA.ast);
assertThrows(() => {
assertEquals(variantA.ast, variantB.ast);
});
assertEquals(variantA.contentMap, variantB.contentMap);
const chunk1 = await js`
// comments should be ignored
function helloWorld() {}
const goodByeCruelWorld = () => {}
`;