55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
import { parse } from "@babel/parser";
|
|
type Node = { type: string; start: number; end: number; [key: string]: unknown };
|
|
const node = (value: unknown): value is Node =>
|
|
!!value && typeof value === "object" && typeof (value as Node).type === "string";
|
|
|
|
/** One requested insertion into ordinary authored TypeScript, not regeneration.
|
|
* Ambiguous/custom wiring is left alone with an actionable error. */
|
|
export function addImplementation(
|
|
text: string,
|
|
factory: "createRuntime" | "serveMigration",
|
|
key: string,
|
|
importPath: string,
|
|
migrationOnly = false,
|
|
): string {
|
|
const ast = parse(text, { sourceType: "module", plugins: ["typescript"] });
|
|
const objects: Node[] = [];
|
|
const names = new Set<string>();
|
|
const visit = (value: unknown) => {
|
|
if (Array.isArray(value)) {
|
|
value.forEach(visit);
|
|
return;
|
|
}
|
|
if (!node(value)) return;
|
|
if (value.type === "Identifier") names.add(String(value.name));
|
|
if (
|
|
value.type === "CallExpression" &&
|
|
node(value.callee) &&
|
|
value.callee.type === "Identifier" &&
|
|
value.callee.name === factory &&
|
|
Array.isArray(value.arguments) &&
|
|
value.arguments.length === 1 &&
|
|
node(value.arguments[0]) &&
|
|
value.arguments[0].type === "ObjectExpression"
|
|
)
|
|
objects.push(value.arguments[0]);
|
|
Object.values(value).forEach(visit);
|
|
};
|
|
visit(ast);
|
|
if (objects.length !== 1)
|
|
throw new Error(
|
|
`Cannot safely add implementation: expected one ${factory}({...}) literal. Wire the handler in your authored server code instead.`,
|
|
);
|
|
const object = objects[0];
|
|
if (
|
|
(object.properties as Node[]).some((p) => node(p.key) && !p.computed && (p.key.name === key || p.key.value === key))
|
|
)
|
|
throw new Error(`Implementation already exists for ${key}`);
|
|
let alias = "qxImplementation";
|
|
for (let index = 1; names.has(alias); index++) alias = `qxImplementation${index}`;
|
|
const value = migrationOnly ? 'async () => { throw new Error("Migration-only export"); }' : alias;
|
|
const insertion = object.start + 1;
|
|
const edited = text.slice(0, insertion) + `\n ${JSON.stringify(key)}: ${value},\n` + text.slice(insertion);
|
|
return (migrationOnly ? "" : `import {handler as ${alias}} from ${JSON.stringify(importPath)};\n`) + edited;
|
|
}
|