docs: introduce project and keep local reports out of source delivery
This commit is contained in:
1 parent
db5762abd4
commit
d37ce57d09
45 files changed
+232
-14820
No files matched your search
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* 静态源码门禁:解析应用的 JS/TS/Vue,检查显式导入和机器/旧部署路径依赖。
|
||||
* 这是构建前检查,不替代浏览器业务验收,也不会启动或修改生产服务。
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
@@ -13,6 +17,7 @@ const require = createRequire(import.meta.url);
|
||||
const failures = [];
|
||||
const files = [];
|
||||
function walk(directory) {
|
||||
// 仅递归 app/;不扫描 node_modules、构建产物或本地 docs 报告。
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const file = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) walk(file);
|
||||
@@ -27,11 +32,13 @@ const dependencies = new Set(
|
||||
Object.keys({ ...manifest.dependencies, ...manifest.devDependencies }),
|
||||
);
|
||||
function existsModule(file) {
|
||||
// 与当前源码使用的简写导入形式对齐,允许目录下的 index.js / index.ts。
|
||||
return ["", ".js", ".ts", ".json", ".vue", "/index.js", "/index.ts"].some(
|
||||
(extension) => fs.existsSync(file + extension),
|
||||
);
|
||||
}
|
||||
function checkImport(source, owner) {
|
||||
// # 开头的虚拟模块由 Nuxt 生成,本地文件与 npm 包则分别核对实际路径及依赖声明。
|
||||
if (source.startsWith("#")) return; // Nuxt-generated public/virtual modules.
|
||||
if (source.startsWith("~/")) {
|
||||
if (!existsModule(path.join(root, "app", source.slice(2))))
|
||||
@@ -53,6 +60,7 @@ function checkImport(source, owner) {
|
||||
}
|
||||
}
|
||||
function inspect(node, owner) {
|
||||
// 按 AST 检查,不把注释中的历史说明误判成运行时路径依赖。
|
||||
if (!node || typeof node !== "object") return;
|
||||
if (
|
||||
[
|
||||
@@ -64,6 +72,7 @@ function inspect(node, owner) {
|
||||
)
|
||||
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);
|
||||
}
|
||||
@@ -81,6 +90,7 @@ function inspect(node, owner) {
|
||||
`${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);
|
||||
@@ -94,6 +104,7 @@ for (const file of files) {
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
try {
|
||||
if (file.endsWith(".vue")) {
|
||||
// script setup 先展开编译宏,再解析脚本;有 template 时另外检查模板编译错误。
|
||||
components++;
|
||||
const { descriptor, errors } = parseVue(source, { filename: file });
|
||||
if (errors.length) throw Error(errors.join("\n"));
|
||||
@@ -129,6 +140,7 @@ for (const file of files) {
|
||||
failures.push(`${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
// 一次报告全部失败并以非零状态退出,方便开发者和 CI 定位多个问题。
|
||||
if (failures.length) {
|
||||
console.error(
|
||||
failures.map((failure) => failure.replaceAll(root + "/", "")).join("\n"),
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/** One-time source reconstruction. Input is explicit; generated app has no runtime link to it. */
|
||||
/**
|
||||
* 一次性重建留档,不参与日常开发、构建或发布。
|
||||
* 输入必须是可信的自有 Nuxt 发布目录:脚本会 import 其中的元数据,因此不是安全的第三方解包器。
|
||||
* 只在空工程执行;已有 app/pages 时直接拒绝,避免覆盖后续维护的源码和注释。
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -12,6 +17,7 @@ import { format } from "prettier";
|
||||
const generate = generator.default;
|
||||
const traverse = traverser.default;
|
||||
const input = path.resolve(process.argv[2] || "MISSING_INPUT");
|
||||
// 所有输出定位到本工程;不向输入发布目录回写,也不改变生产发布软链接。
|
||||
const root = path.resolve(import.meta.dirname, "..");
|
||||
if (!fs.existsSync(path.join(input, "server/chunks/virtual/precomputed.mjs")))
|
||||
throw Error("Pass the active .output directory");
|
||||
@@ -19,6 +25,7 @@ if (fs.existsSync(path.join(root, "app/pages")))
|
||||
throw Error(
|
||||
"Refusing to overwrite editable source; this importer is only for a new empty project",
|
||||
);
|
||||
// 这一步会执行输入模块,调用方必须先验证输入可信性。
|
||||
const pc = (
|
||||
await import(
|
||||
pathToFileURL(path.join(input, "server/chunks/virtual/precomputed.mjs"))
|
||||
@@ -26,6 +33,7 @@ const pc = (
|
||||
).default;
|
||||
const chunkDir = path.join(input, "public/_nuxt");
|
||||
const asts = new Map();
|
||||
// 先解析元数据声明的脚本,再按导入图选择业务模块,不直接复制旧框架 bundle。
|
||||
const files = [
|
||||
...new Set(
|
||||
Object.values(pc.modules)
|
||||
@@ -46,6 +54,7 @@ const textValue = (n) =>
|
||||
? n.quasis[0].value.cooked
|
||||
: n?.value;
|
||||
function resolve(file, exported, seen = new Set()) {
|
||||
// 顺着重导出定位真正的定义;seen 检测循环,缺失导出则终止而非猜测模块归属。
|
||||
if (seen.has(key(file, exported)))
|
||||
throw Error(`Cyclic export ${file}:${exported}`);
|
||||
seen.add(key(file, exported));
|
||||
@@ -84,6 +93,7 @@ function resolve(file, exported, seen = new Set()) {
|
||||
const replacements = new Map();
|
||||
const replace = (file, alias, source, imported) =>
|
||||
replacements.set(key(file, alias), { source, imported });
|
||||
// 以下短名映射仅适用于本次可信发布版本,不是跨版本通用的编译器符号表。
|
||||
const vue = {
|
||||
$: "watchSyncEffect",
|
||||
A: "nextTick",
|
||||
@@ -218,6 +228,7 @@ replace("DEP0rg1v.js", "t", "~/utils/brand.js", "logoUrl");
|
||||
replace("ecKN0m8a.js", "r", "#internal/nuxt/paths", "publicAssetsURL");
|
||||
|
||||
const owners = new Map();
|
||||
// 页面、布局和业务组件一一对应源码路径,重复归属会报错,避免静默覆盖。
|
||||
const own = Object.entries(pc.modules).filter(([p]) =>
|
||||
/^(components|layouts|pages)\//.test(p),
|
||||
);
|
||||
@@ -276,6 +287,7 @@ for (const file of files) {
|
||||
taken.add(output);
|
||||
}
|
||||
const authExports = {
|
||||
// 为已核实的认证公共接口恢复业务名;未知模块导出仍保留原有绑定。
|
||||
a: "loginErrorMessage",
|
||||
c: "ApiError",
|
||||
d: "apiCacheKey",
|
||||
@@ -296,6 +308,7 @@ const exportNames = new Map(
|
||||
Object.entries(authExports).map(([a, n]) => [key("BLvPXbVl.js", a), n]),
|
||||
);
|
||||
function lookup(file, exported) {
|
||||
// 优先使用官方包/手工适配映射,其余依赖落到当前 app/lib,而不是历史发布路径。
|
||||
const direct = replacements.get(key(file, exported));
|
||||
if (direct) return direct;
|
||||
const r = resolve(file, exported);
|
||||
@@ -312,6 +325,7 @@ const emitted = new Set();
|
||||
const pending = [...owners.keys()].filter((f) => f !== chartOwner?.[0]);
|
||||
const patchFiles = [];
|
||||
async function add(file, code, parser) {
|
||||
// 先收集格式化内容,所有代码文件最终通过 apply_patch 新增,失败即中止。
|
||||
const formatted = await format(code, {
|
||||
parser:
|
||||
parser ||
|
||||
@@ -360,6 +374,7 @@ for (const route of routes.elements) {
|
||||
route._meta = metaProps;
|
||||
}
|
||||
while (pending.length) {
|
||||
// 从业务入口遍历依赖闭包;emitted 去重,以免循环依赖反复生成同一文件。
|
||||
const file = pending.shift();
|
||||
if (emitted.has(file)) continue;
|
||||
emitted.add(file);
|
||||
@@ -379,6 +394,7 @@ while (pending.length) {
|
||||
);
|
||||
}
|
||||
if (file === "BLvPXbVl.js") {
|
||||
// 针对此发布包剥离内嵌的 Pinia/密码库实现,保留业务逻辑并接回正式 npm 依赖。
|
||||
// Business logic only; Pinia, file downloads and cryptography are npm dependencies.
|
||||
let keep = false;
|
||||
const body = [];
|
||||
@@ -442,6 +458,7 @@ while (pending.length) {
|
||||
});
|
||||
traverse(ast, {
|
||||
Program(p) {
|
||||
// 只重写被引用的导入绑定,同步改名所有引用以避免与当前作用域已有变量冲突。
|
||||
p.scope.crawl();
|
||||
for (const stmt of [...p.get("body")]) {
|
||||
if (!stmt.isImportDeclaration()) continue;
|
||||
@@ -510,6 +527,7 @@ while (pending.length) {
|
||||
});
|
||||
for (const dep of dependencies) if (!emitted.has(dep)) pending.push(dep);
|
||||
if (owner) {
|
||||
// 业务组件包装为可编辑 SFC,恢复路由元数据并显式导入原有作用域 CSS。
|
||||
const defaultExport = ast.program.body.find((n) =>
|
||||
t.isExportDefaultDeclaration(n),
|
||||
);
|
||||
@@ -564,6 +582,7 @@ await add(
|
||||
);
|
||||
await add(
|
||||
"docs/module-inventory.json",
|
||||
// 此清单是本地重建记录;docs 已被 Git 忽略,不应作为产品运行依赖或强制加入版本库。
|
||||
JSON.stringify(
|
||||
{
|
||||
pages: own.filter(([p]) => p.startsWith("pages/")).map(([p]) => p),
|
||||
@@ -584,6 +603,7 @@ await add(
|
||||
),
|
||||
);
|
||||
for (const { file, code } of patchFiles) {
|
||||
// apply_patch 只新增目标;不使用整目录替换,也不清理已有文件。
|
||||
const target = path.join(root, file);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
const patch = `*** Begin Patch\n*** Add File: ${target}\n${code
|
||||
@@ -598,6 +618,7 @@ for (const { file, code } of patchFiles) {
|
||||
}
|
||||
// Static binary assets retain their deployed paths; no JS bundle is copied.
|
||||
const cssFiles = [
|
||||
// 样式保留发布版 scope 选择器,避免在模板已丢失时重新分配作用域导致外观失配。
|
||||
...new Set(
|
||||
Object.values(pc.modules)
|
||||
.filter((v) => v.resourceType === "style")
|
||||
@@ -608,6 +629,7 @@ fs.mkdirSync(path.join(root, "app/assets/styles"), { recursive: true });
|
||||
for (const file of cssFiles) {
|
||||
let css = fs.readFileSync(path.join(chunkDir, file), "utf8");
|
||||
css = css.replace(
|
||||
// 图标字体改用 public/icons 的稳定地址,脱离旧 _nuxt 输出目录。
|
||||
/url\(([^)]*primeicons[^)]*)\)/g,
|
||||
(_, v) =>
|
||||
`url(${v.replace(/^["']?|["']?$/g, "").replace(/^\.\//, "/icons/")})`,
|
||||
@@ -621,6 +643,7 @@ for (const file of cssFiles) {
|
||||
if (result.status !== 0) throw Error(result.stdout + result.stderr);
|
||||
}
|
||||
for (const name of fs.readdirSync(path.join(input, "public")))
|
||||
// public 的图片和字体保留原访问路径;不复制 _nuxt bundle 或构建版本目录。
|
||||
if (
|
||||
name !== "_nuxt" &&
|
||||
!name.startsWith("._") &&
|
||||
@@ -634,6 +657,7 @@ for (const name of fs.readdirSync(path.join(input, "public")))
|
||||
});
|
||||
}
|
||||
fs.mkdirSync(path.join(root, "public/icons"), { recursive: true });
|
||||
// PrimeIcons 字体单独归档,配合前面的 CSS URL 重写使用。
|
||||
for (const name of fs.readdirSync(chunkDir))
|
||||
if (name.startsWith("primeicons.") && !name.endsWith(".js"))
|
||||
fs.copyFileSync(
|
||||
|
||||
Reference in new issue
Block a user