import fs from "node:fs"; import path from "node:path"; import { createRequire } from "node:module"; import { parse as parseJavaScript } from "@babel/parser"; import { parse as parseVue, compileScript, compileTemplate, } from "@vue/compiler-sfc"; const root = path.resolve(import.meta.dirname, ".."); const require = createRequire(import.meta.url); const failures = []; const files = []; function walk(directory) { for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { const file = path.join(directory, entry.name); if (entry.isDirectory()) walk(file); else files.push(file); } } walk(path.join(root, "app")); const manifest = JSON.parse( fs.readFileSync(path.join(root, "package.json"), "utf8"), ); const dependencies = new Set( Object.keys({ ...manifest.dependencies, ...manifest.devDependencies }), ); function existsModule(file) { return ["", ".js", ".ts", ".json", ".vue", "/index.js", "/index.ts"].some( (extension) => fs.existsSync(file + extension), ); } function checkImport(source, owner) { if (source.startsWith("#")) return; // Nuxt-generated public/virtual modules. if (source.startsWith("~/")) { if (!existsModule(path.join(root, "app", source.slice(2)))) failures.push(`${owner}: missing ${source}`); } else if (source.startsWith(".")) { if (!existsModule(path.resolve(path.dirname(owner), source))) failures.push(`${owner}: missing ${source}`); } else { const name = source.startsWith("@") ? source.split("/").slice(0, 2).join("/") : source.split("/")[0]; if (!dependencies.has(name)) failures.push(`${owner}: undeclared dependency ${name}`); try { require.resolve(source); } catch (error) { failures.push(`${owner}: cannot resolve ${source}: ${error.message}`); } } } function inspect(node, owner) { if (!node || typeof node !== "object") return; if ( [ "ImportDeclaration", "ExportNamedDeclaration", "ExportAllDeclaration", ].includes(node.type) && node.source ) checkImport(node.source.value, owner); if (node.type === "CallExpression" && node.callee.type === "Import") { const value = node.arguments[0]; if (value.type === "StringLiteral") checkImport(value.value, owner); } const literal = node.type === "StringLiteral" ? node.value : node.type === "TemplateElement" ? (node.value.cooked ?? node.value.raw) : null; if ( literal !== null && /(?:\.output\/|\/releases\/|\/Users\/|\/www\/wwwroot\/)/.test(literal) ) failures.push( `${owner}:${node.loc?.start.line ?? 1}: runtime depends on deployment/machine path`, ); for (const [key, value] of Object.entries(node)) { if (["loc", "start", "end", "extra", "comments", "tokens"].includes(key)) continue; if (Array.isArray(value)) for (const item of value) inspect(item, owner); else if (value && typeof value === "object") inspect(value, owner); } } let components = 0; let modules = 0; for (const file of files) { if (!/\.(vue|js|ts)$/.test(file)) continue; const source = fs.readFileSync(file, "utf8"); try { if (file.endsWith(".vue")) { components++; const { descriptor, errors } = parseVue(source, { filename: file }); if (errors.length) throw Error(errors.join("\n")); const script = descriptor.scriptSetup ? compileScript(descriptor, { id: "source-check" }).content : descriptor.script?.content || ""; inspect( parseJavaScript(script, { sourceType: "module", plugins: ["typescript"], }), file, ); if (descriptor.template) { const result = compileTemplate({ source: descriptor.template.content, filename: file, id: "source-check", }); if (result.errors.length) throw Error(result.errors.join("\n")); } } else { modules++; inspect( parseJavaScript(source, { sourceType: "module", plugins: ["typescript"], }), file, ); } } catch (error) { failures.push(`${file}: ${error.message}`); } } if (failures.length) { console.error( failures.map((failure) => failure.replaceAll(root + "/", "")).join("\n"), ); process.exitCode = 1; } else console.log( `Source gate passed: ${components} Vue files, ${modules} modules; imports resolve without any historic release.`, );