53 lines
2.5 KiB
TypeScript
53 lines
2.5 KiB
TypeScript
import { parse } from "@babel/parser";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
/** Enforce the authored bundled-code contract, not a security sandbox. */
|
|
export function bundlePolicyErrors(source: string, filename: string): string[] {
|
|
const ast = parse(source, { sourceType: "module", plugins: ["typescript", "jsx"] });
|
|
const errors: string[] = [];
|
|
const visit = (value: unknown) => {
|
|
if (Array.isArray(value)) {
|
|
value.forEach(visit);
|
|
return;
|
|
}
|
|
if (!value || typeof value !== "object") return;
|
|
const n = value as Record<string, any>;
|
|
if (typeof n.type !== "string") return;
|
|
const fail = (message: string) => errors.push(`${filename}:${n.loc?.start.line ?? 1}: ${message}`);
|
|
if (n.type === "MetaProperty" && n.meta.name === "import")
|
|
fail("Bundled package code cannot use import.meta; import packaged assets statically");
|
|
if (n.type === "Identifier" && ["__dirname", "__filename"].includes(n.name))
|
|
fail("Bundled package code cannot depend on module filesystem locations");
|
|
if (["CallExpression", "NewExpression"].includes(n.type)) {
|
|
if (n.callee?.type === "Identifier" && ["eval", "Function"].includes(n.callee.name))
|
|
fail("Dynamic code generation is unsupported in bundled package code");
|
|
if (
|
|
(n.callee?.type === "Import" || (n.callee?.type === "Identifier" && n.callee.name === "require")) &&
|
|
(n.arguments.length !== 1 || n.arguments[0].type !== "StringLiteral")
|
|
)
|
|
fail("Module imports must have a static string specifier");
|
|
}
|
|
if (n.type === "ImportExpression" && n.source.type !== "StringLiteral")
|
|
fail("Module imports must have a static string specifier");
|
|
Object.values(n).forEach(visit);
|
|
};
|
|
visit(ast);
|
|
return errors;
|
|
}
|
|
export async function checkBundleSources(directory: string): Promise<void> {
|
|
const errors: string[] = [];
|
|
async function walk(current: string) {
|
|
for (const entry of await fs.readdir(current, { withFileTypes: true })) {
|
|
if (["gen", "node_modules"].includes(entry.name)) continue;
|
|
const file = path.join(current, entry.name);
|
|
if (entry.isSymbolicLink()) throw new Error(`Authored source symlinks are unsupported: ${file}`);
|
|
if (entry.isDirectory()) await walk(file);
|
|
else if (/\.[cm]?[jt]sx?$/.test(entry.name) && !entry.name.endsWith(".d.ts"))
|
|
errors.push(...bundlePolicyErrors(await fs.readFile(file, "utf8"), file));
|
|
}
|
|
}
|
|
await walk(directory);
|
|
if (errors.length) throw new Error(errors.join("\n"));
|
|
}
|