674 lines
23 KiB
JavaScript
674 lines
23 KiB
JavaScript
/** 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";
|
|
import { spawnSync } from "node:child_process";
|
|
import { parse } from "@babel/parser";
|
|
import generator from "@babel/generator";
|
|
import traverser from "@babel/traverse";
|
|
import * as t from "@babel/types";
|
|
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");
|
|
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"))
|
|
)
|
|
).default;
|
|
const chunkDir = path.join(input, "public/_nuxt");
|
|
const asts = new Map();
|
|
// 先解析元数据声明的脚本,再按导入图选择业务模块,不直接复制旧框架 bundle。
|
|
const files = [
|
|
...new Set(
|
|
Object.values(pc.modules)
|
|
.filter((v) => v.resourceType === "script")
|
|
.map((v) => v.file),
|
|
),
|
|
];
|
|
for (const file of files)
|
|
asts.set(
|
|
file,
|
|
parse(fs.readFileSync(path.join(chunkDir, file), "utf8"), {
|
|
sourceType: "module",
|
|
}),
|
|
);
|
|
const key = (file, name) => `${file}:${name}`;
|
|
const textValue = (n) =>
|
|
t.isTemplateLiteral(n) && n.expressions.length === 0
|
|
? 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));
|
|
const ast = asts.get(file);
|
|
if (!ast) throw Error(`Missing ${file}`);
|
|
const imports = new Map();
|
|
for (const n of ast.program.body)
|
|
if (t.isImportDeclaration(n))
|
|
for (const s of n.specifiers)
|
|
imports.set(s.local.name, {
|
|
file: path.basename(n.source.value),
|
|
name: t.isImportDefaultSpecifier(s)
|
|
? "default"
|
|
: s.imported?.name || s.imported?.value,
|
|
});
|
|
for (const n of ast.program.body) {
|
|
if (t.isExportDefaultDeclaration(n) && exported === "default")
|
|
return {
|
|
file,
|
|
name: exported,
|
|
local: t.isIdentifier(n.declaration) ? n.declaration.name : null,
|
|
};
|
|
if (t.isExportNamedDeclaration(n))
|
|
for (const s of n.specifiers)
|
|
if ((s.exported.name || s.exported.value) === exported) {
|
|
const imp = n.source
|
|
? { file: path.basename(n.source.value), name: s.local.name }
|
|
: imports.get(s.local.name);
|
|
if (imp?.file.endsWith(".js"))
|
|
return resolve(imp.file, imp.name, seen);
|
|
return { file, name: exported, local: s.local.name };
|
|
}
|
|
}
|
|
throw Error(`No export ${file}:${exported}`);
|
|
}
|
|
const replacements = new Map();
|
|
const replace = (file, alias, source, imported) =>
|
|
replacements.set(key(file, alias), { source, imported });
|
|
// 以下短名映射仅适用于本次可信发布版本,不是跨版本通用的编译器符号表。
|
|
const vue = {
|
|
$: "watchSyncEffect",
|
|
A: "nextTick",
|
|
B: "provide",
|
|
Bt: "toDisplayString",
|
|
C: "guardReactiveProps",
|
|
D: "isVNode",
|
|
E: "inject",
|
|
F: "onErrorCaptured",
|
|
G: "resolveDirective",
|
|
H: "renderList",
|
|
I: "onMounted",
|
|
J: "useId",
|
|
K: "resolveDynamicComponent",
|
|
L: "onUnmounted",
|
|
Lt: "normalizeClass",
|
|
M: "onBeforeMount",
|
|
N: "onBeforeUnmount",
|
|
O: "mergeModels",
|
|
P: "onDeactivated",
|
|
Q: "watchEffect",
|
|
R: "onUpdated",
|
|
Rt: "normalizeProps",
|
|
S: "getCurrentInstance",
|
|
T: "hasInjectionContext",
|
|
U: "renderSlot",
|
|
V: "queuePostFlushCb",
|
|
W: "resolveComponent",
|
|
X: "warn",
|
|
Y: "useModel",
|
|
Z: "watch",
|
|
_: "createStaticVNode",
|
|
_t: "toValue",
|
|
a: "Suspense",
|
|
at: "isReadonly",
|
|
b: "defineAsyncComponent",
|
|
c: "cloneVNode",
|
|
ct: "onScopeDispose",
|
|
d: "createBlock",
|
|
dt: "ref",
|
|
et: "withCtx",
|
|
f: "createCommentVNode",
|
|
ft: "shallowReactive",
|
|
g: "createSlots",
|
|
gt: "toRefs",
|
|
h: "createRenderer",
|
|
ht: "toRef",
|
|
i: "KeepAlive",
|
|
it: "isReactive",
|
|
j: "onActivated",
|
|
k: "mergeProps",
|
|
l: "computed",
|
|
lt: "reactive",
|
|
m: "createHydrationRenderer",
|
|
mt: "toRaw",
|
|
n: "BaseTransitionPropsValidators",
|
|
nt: "effectScope",
|
|
o: "Teleport",
|
|
ot: "isRef",
|
|
p: "createElementBlock",
|
|
pt: "shallowRef",
|
|
q: "toHandlers",
|
|
r: "Fragment",
|
|
rt: "getCurrentScope",
|
|
s: "callWithAsyncErrorHandling",
|
|
st: "markRaw",
|
|
t: "BaseTransition",
|
|
tt: "withDirectives",
|
|
u: "createElementVNode",
|
|
ut: "readonly",
|
|
v: "createTextVNode",
|
|
vt: "unref",
|
|
w: "h",
|
|
x: "defineComponent",
|
|
y: "createVNode",
|
|
z: "openBlock",
|
|
zt: "normalizeStyle",
|
|
};
|
|
const shared = {
|
|
At: "isPlainObject",
|
|
Ct: "includeBooleanAttr",
|
|
Dt: "isModelListener",
|
|
Et: "isFunction",
|
|
Ft: "looseEqual",
|
|
It: "looseToNumber",
|
|
Mt: "isSpecialBooleanAttr",
|
|
Nt: "isString",
|
|
Ot: "isObject",
|
|
Pt: "isSymbol",
|
|
St: "hyphenate",
|
|
Tt: "isArray",
|
|
Vt: "toNumber",
|
|
bt: "capitalize",
|
|
jt: "isPromise",
|
|
kt: "isOn",
|
|
wt: "invokeArrayFns",
|
|
xt: "extend",
|
|
yt: "camelize",
|
|
};
|
|
for (const [a, n] of Object.entries(vue)) replace("CaDPEW5H.js", a, "vue", n);
|
|
for (const [a, n] of Object.entries(shared))
|
|
replace("CaDPEW5H.js", a, "@vue/shared", n);
|
|
for (const [a, n] of Object.entries({
|
|
a: "vModelText",
|
|
c: "withModifiers",
|
|
i: "vModelRadio",
|
|
n: "createApp",
|
|
o: "vShow",
|
|
r: "createSSRApp",
|
|
s: "withKeys",
|
|
t: "Transition",
|
|
}))
|
|
replace("CHOHVZjb.js", a, "vue", n);
|
|
for (const [file, map] of Object.entries({
|
|
"BXrXAUMD.js": { a: "useNuxtApp", o: "useRuntimeConfig" },
|
|
"DNcWRfqH.js": { f: "useRoute", p: "useRouter", u: "navigateTo" },
|
|
"BJ9CQb77.js": { n: "useHead" },
|
|
"ClFYPW8t.js": { t: "useState" },
|
|
}))
|
|
for (const [a, n] of Object.entries(map)) replace(file, a, "#app", n);
|
|
replace("Cq-ZALh2.js", "D", "vue-router", "onBeforeRouteLeave");
|
|
replace("Gudm9e0k.js", "t", "#components", "NuxtLink");
|
|
replace("BnCkphz4.js", "t", "#components", "Icon");
|
|
replace("uBIymjUX.js", "t", "~/utils/load-module.js", "loadModule");
|
|
replace("BLvPXbVl.js", "_", "pinia", "createPinia");
|
|
replace("BLvPXbVl.js", "v", "pinia", "setActivePinia");
|
|
replace("BLvPXbVl.js", "y", "pinia", "shouldHydrate");
|
|
replace("BLvPXbVl.js", "s", "~/utils/crypto.js", "getCrypto");
|
|
replace("BSkUL2SB.js", "n", "primevue/usetoast", "useToast");
|
|
replace("iGbquyGC.js", "t", "primevue/tooltip", "default");
|
|
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),
|
|
);
|
|
for (const [source, mod] of own) {
|
|
const r = resolve(mod.file, "default");
|
|
if (owners.has(r.file) && owners.get(r.file).source !== source)
|
|
throw Error(`Multiple owners ${r.file}`);
|
|
owners.set(r.file, { source, exported: r.name, local: r.local });
|
|
replacements.set(key(r.file, r.name), {
|
|
source: `~/${source}`,
|
|
imported: "default",
|
|
label: path.basename(source, ".vue"),
|
|
});
|
|
}
|
|
// Official libraries replace bundled copies. Resolve tiny re-export wrappers first.
|
|
for (const [source, mod] of Object.entries(pc.modules)) {
|
|
if (!source.includes("/node_modules/") || mod.resourceType !== "script")
|
|
continue;
|
|
let npm = source.slice(source.lastIndexOf("/node_modules/") + 14);
|
|
if (npm.startsWith("nuxt/") || npm.startsWith("@nuxt/icon")) continue;
|
|
npm = npm.replace(/\/index\.mjs$/, "");
|
|
if (npm === "@novnc/novnc/core/rfb.js") npm = "@novnc/novnc";
|
|
const r = resolve(mod.file, "default");
|
|
if (!owners.has(r.file))
|
|
replacements.set(key(r.file, r.name), {
|
|
source: npm,
|
|
imported: "default",
|
|
label: npm.split("/").at(-1),
|
|
});
|
|
}
|
|
// The existing chart component is recovered separately; ECharts stays a library dependency.
|
|
const chartOwner = [...owners.entries()].find(
|
|
([, v]) => v.source === "components/ChartBox.vue",
|
|
);
|
|
if (chartOwner)
|
|
replacements.set(key(chartOwner[0], chartOwner[1].exported), {
|
|
source: "~/components/ChartBox.vue",
|
|
imported: "default",
|
|
label: "ChartBox",
|
|
});
|
|
const names = new Map();
|
|
for (const d of Object.values(pc.dependencies))
|
|
for (const v of Object.values(d.preload || {}))
|
|
if (v.name && v.file?.endsWith(".js") && !names.has(v.file))
|
|
names.set(v.file, v.name);
|
|
const paths = new Map();
|
|
const taken = new Set();
|
|
for (const file of files) {
|
|
let output = owners.has(file)
|
|
? `app/${owners.get(file).source}`
|
|
: `app/lib/${(names.get(file) || "module").replace(/[^\w-]/g, "-")}.js`;
|
|
let i = 2;
|
|
const base = output;
|
|
while (taken.has(output)) output = base.replace(/\.js$/, `-${i++}.js`);
|
|
paths.set(file, output);
|
|
taken.add(output);
|
|
}
|
|
const authExports = {
|
|
// 为已核实的认证公共接口恢复业务名;未知模块导出仍保留原有绑定。
|
|
a: "loginErrorMessage",
|
|
c: "ApiError",
|
|
d: "apiCacheKey",
|
|
f: "apiCacheVersion",
|
|
g: "sanitizeMessage",
|
|
h: "operationMessage",
|
|
i: "securityMethods",
|
|
l: "useApi",
|
|
m: "writeApiCache",
|
|
n: "assertSecurityComplete",
|
|
o: "isValidJwt",
|
|
p: "readApiCache",
|
|
r: "securityChallenge",
|
|
t: "useAuth",
|
|
u: "clearApiCache",
|
|
};
|
|
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);
|
|
return (
|
|
replacements.get(key(r.file, r.name)) || {
|
|
source: "~/" + paths.get(r.file).replace(/^app\//, ""),
|
|
imported: exportNames.get(key(r.file, r.name)) || r.name,
|
|
file: r.file,
|
|
label: names.get(r.file),
|
|
}
|
|
);
|
|
}
|
|
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 ||
|
|
(file.endsWith(".vue")
|
|
? "vue"
|
|
: file.endsWith(".json")
|
|
? "json"
|
|
: "babel"),
|
|
printWidth: 110,
|
|
});
|
|
patchFiles.push({ file, code: formatted });
|
|
}
|
|
// Save route metadata without compiled preload machinery; Nuxt owns the new module graph.
|
|
const entry = asts.get("CNMaXYQo.js");
|
|
let routes;
|
|
traverse(entry, {
|
|
VariableDeclarator(p) {
|
|
if (p.node.id.name === "Qi") routes = t.cloneNode(p.node.init, true);
|
|
},
|
|
});
|
|
for (const route of routes.elements) {
|
|
let file;
|
|
for (const prop of route.properties)
|
|
if (prop.key.name === "component")
|
|
traverse(
|
|
t.file(
|
|
t.program([t.expressionStatement(t.cloneNode(prop.value, true))]),
|
|
),
|
|
{
|
|
CallExpression(p) {
|
|
if (p.node.callee.type === "Import")
|
|
file = path.basename(textValue(p.node.arguments[0]));
|
|
},
|
|
},
|
|
);
|
|
const owner = owners.get(resolve(file, "default").file);
|
|
const metaProps =
|
|
route.properties.find((p) => p.key.name === "meta")?.value.properties || [];
|
|
const alias = route.properties.find((p) => p.key.name === "alias");
|
|
if (alias)
|
|
metaProps.push(t.objectProperty(t.identifier("alias"), alias.value));
|
|
const redirect = route.properties.find((p) => p.key.name === "redirect");
|
|
if (redirect)
|
|
metaProps.push(t.objectProperty(t.identifier("redirect"), redirect.value));
|
|
route._source = owner.source;
|
|
route._meta = metaProps;
|
|
}
|
|
while (pending.length) {
|
|
// 从业务入口遍历依赖闭包;emitted 去重,以免循环依赖反复生成同一文件。
|
|
const file = pending.shift();
|
|
if (emitted.has(file)) continue;
|
|
emitted.add(file);
|
|
if (!asts.has(file)) throw Error(`No AST ${file}`);
|
|
let ast = t.cloneNode(asts.get(file), true);
|
|
if (file === "BDbvURLy.js") {
|
|
ast.program.body = ast.program.body.filter(
|
|
(n) =>
|
|
!(t.isFunctionDeclaration(n) && n.id.name === "y") &&
|
|
!(t.isImportDeclaration(n) && n.source.value === "./j97ZVn8z.js"),
|
|
);
|
|
ast.program.body.unshift(
|
|
t.importDeclaration(
|
|
[t.importSpecifier(t.identifier("y"), t.identifier("reloadNuxtApp"))],
|
|
t.stringLiteral("#app"),
|
|
),
|
|
);
|
|
}
|
|
if (file === "BLvPXbVl.js") {
|
|
// 针对此发布包剥离内嵌的 Pinia/密码库实现,保留业务逻辑并接回正式 npm 依赖。
|
|
// Business logic only; Pinia, file downloads and cryptography are npm dependencies.
|
|
let keep = false;
|
|
const body = [];
|
|
for (const n of ast.program.body) {
|
|
if (t.isImportDeclaration(n)) {
|
|
body.push(n);
|
|
continue;
|
|
}
|
|
if (t.isVariableDeclaration(n)) {
|
|
for (const d of n.declarations) {
|
|
if (d.id.name === "ce") keep = true;
|
|
if (d.id.name === "Oe") keep = false;
|
|
if (d.id.name === "ot") keep = true;
|
|
if (keep) body.push(t.variableDeclaration(n.kind, [d]));
|
|
}
|
|
} else if (t.isExportNamedDeclaration(n)) {
|
|
n.specifiers = n.specifiers.filter(
|
|
(s) => !["_", "v", "y", "s"].includes(s.exported.name),
|
|
);
|
|
body.push(n);
|
|
} else if (keep) body.push(n);
|
|
}
|
|
body.unshift(
|
|
t.importDeclaration(
|
|
[t.importDefaultSpecifier(t.identifier("CryptoJS"))],
|
|
t.stringLiteral("crypto-js"),
|
|
),
|
|
t.importDeclaration(
|
|
[t.importSpecifier(t.identifier("se"), t.identifier("defineStore"))],
|
|
t.stringLiteral("pinia"),
|
|
),
|
|
);
|
|
ast.program.body = body;
|
|
traverse(ast, {
|
|
MemberExpression(p) {
|
|
if (
|
|
t.isIdentifier(p.node.object, { name: "at" }) &&
|
|
t.isIdentifier(p.node.property, { name: "default" })
|
|
)
|
|
p.replaceWith(t.identifier("CryptoJS"));
|
|
},
|
|
});
|
|
}
|
|
// Discard old dependency indexes: Vite reconstructs preload URLs from new imports.
|
|
traverse(ast, {
|
|
CallExpression(p) {
|
|
if (t.isIdentifier(p.node.callee, { name: "__vite__mapDeps" }))
|
|
p.replaceWith(t.arrayExpression([]));
|
|
},
|
|
VariableDeclarator(p) {
|
|
if (p.node.id.name === "__vite__mapDeps") p.remove();
|
|
},
|
|
});
|
|
const owner = owners.get(file);
|
|
const dependencies = [];
|
|
const bindingNames = new Set();
|
|
traverse(ast, {
|
|
BindingIdentifier(p) {
|
|
bindingNames.add(p.node.name);
|
|
},
|
|
});
|
|
traverse(ast, {
|
|
Program(p) {
|
|
// 只重写被引用的导入绑定,同步改名所有引用以避免与当前作用域已有变量冲突。
|
|
p.scope.crawl();
|
|
for (const stmt of [...p.get("body")]) {
|
|
if (!stmt.isImportDeclaration()) continue;
|
|
const original = stmt.node.source.value;
|
|
if (!original.startsWith(".")) continue;
|
|
const dep = path.basename(original);
|
|
const imports = [];
|
|
for (const spec of [...stmt.node.specifiers]) {
|
|
if (!p.scope.getBinding(spec.local.name)?.referenced) continue;
|
|
const alias = t.isImportDefaultSpecifier(spec)
|
|
? "default"
|
|
: spec.imported?.name || spec.imported?.value;
|
|
const target = lookup(dep, alias);
|
|
if (target.file) dependencies.push(target.file);
|
|
let name =
|
|
target.imported === "default"
|
|
? target.label || path.basename(target.source) || "Component"
|
|
: target.imported;
|
|
if (name.length < 3 && target.label) name = target.label + "_" + name;
|
|
name = name.replace(/[^\w$]/g, "_");
|
|
if (!/^[A-Za-z_$]/.test(name)) name = "module_" + name;
|
|
if (name !== spec.local.name) {
|
|
while (bindingNames.has(name) || p.scope.hasGlobal(name))
|
|
name += "Imported";
|
|
p.scope.rename(spec.local.name, name);
|
|
bindingNames.add(name);
|
|
}
|
|
const local = t.identifier(spec.local.name);
|
|
imports.push(
|
|
t.importDeclaration(
|
|
[
|
|
target.imported === "default"
|
|
? t.importDefaultSpecifier(local)
|
|
: t.importSpecifier(local, t.identifier(target.imported)),
|
|
],
|
|
t.stringLiteral(target.source),
|
|
),
|
|
);
|
|
}
|
|
if (!stmt.node.specifiers.length) {
|
|
if (file === "BY4y5R3Q.js" && dep === "DNcWRfqH.js") {
|
|
stmt.remove();
|
|
continue;
|
|
}
|
|
throw Error(`Side effect import ${file} ${original}`);
|
|
}
|
|
stmt.replaceWithMultiple(imports);
|
|
}
|
|
// Semantic exported API names are stable throughout the reconstructed project.
|
|
for (const s of p.node.body)
|
|
if (t.isExportNamedDeclaration(s))
|
|
for (const spec of s.specifiers) {
|
|
const n = exportNames.get(key(file, spec.exported.name));
|
|
if (n) spec.exported = t.identifier(n);
|
|
}
|
|
},
|
|
CallExpression(p) {
|
|
if (p.node.callee.type !== "Import") return;
|
|
const value = textValue(p.node.arguments[0]);
|
|
if (!value?.startsWith("./")) return;
|
|
const dep = path.basename(value);
|
|
const target = lookup(dep, "default");
|
|
if (target.file) dependencies.push(target.file);
|
|
p.node.arguments[0] = t.stringLiteral(target.source);
|
|
},
|
|
});
|
|
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),
|
|
);
|
|
if (!defaultExport) {
|
|
const exportNode = ast.program.body.find(
|
|
(n) =>
|
|
t.isExportNamedDeclaration(n) &&
|
|
n.specifiers.some((s) => s.exported.name === owner.exported),
|
|
);
|
|
const spec = exportNode?.specifiers.find(
|
|
(s) => s.exported.name === owner.exported,
|
|
);
|
|
if (!spec) throw Error(`Missing own export ${owner.source}`);
|
|
exportNode.specifiers = exportNode.specifiers.filter((s) => s !== spec);
|
|
ast.program.body.push(
|
|
t.exportDefaultDeclaration(t.identifier(spec.local.name)),
|
|
);
|
|
}
|
|
const route = routes.elements.find((r) => r._source === owner.source);
|
|
if (route?._meta.length)
|
|
ast.program.body.unshift(
|
|
t.expressionStatement(
|
|
t.callExpression(t.identifier("definePageMeta"), [
|
|
t.objectExpression(route._meta),
|
|
]),
|
|
),
|
|
);
|
|
const styles = Object.values(
|
|
pc.dependencies[owner.source]?.styles || {},
|
|
).map((v) => v.file);
|
|
for (const css of styles)
|
|
ast.program.body.unshift(
|
|
t.importDeclaration([], t.stringLiteral("~/assets/styles/" + css)),
|
|
);
|
|
await add(
|
|
paths.get(file),
|
|
`<script>\n${generate(ast, { comments: true }).code}\n</script>\n`,
|
|
);
|
|
} else await add(paths.get(file), generate(ast, { comments: true }).code);
|
|
}
|
|
await add(
|
|
"app/utils/load-module.js",
|
|
"export function loadModule(load) { return load(); }",
|
|
);
|
|
await add(
|
|
"app/utils/crypto.js",
|
|
"import CryptoJS from 'crypto-js'; export function getCrypto() { return CryptoJS.SHA256; }",
|
|
);
|
|
await add(
|
|
"app/utils/brand.js",
|
|
"export const logoUrl = '/images/brand/logo.png';",
|
|
);
|
|
await add(
|
|
"docs/module-inventory.json",
|
|
// 此清单是本地重建记录;docs 已被 Git 忽略,不应作为产品运行依赖或强制加入版本库。
|
|
JSON.stringify(
|
|
{
|
|
pages: own.filter(([p]) => p.startsWith("pages/")).map(([p]) => p),
|
|
components: own
|
|
.filter(([p]) => p.startsWith("components/"))
|
|
.map(([p]) => p),
|
|
layouts: own.filter(([p]) => p.startsWith("layouts/")).map(([p]) => p),
|
|
shared: [...emitted]
|
|
.filter((f) => !owners.has(f))
|
|
.map((f) => paths.get(f)),
|
|
routes: routes.elements.map((r) => ({
|
|
source: r._source,
|
|
path: textValue(r.properties.find((p) => p.key.name === "path").value),
|
|
})),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
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
|
|
.split("\n")
|
|
.map((l) => "+" + l)
|
|
.join("\n")}\n*** End Patch\n`;
|
|
const result = spawnSync("apply_patch", [], {
|
|
input: patch,
|
|
encoding: "utf8",
|
|
});
|
|
if (result.status !== 0) throw Error(result.stdout + result.stderr);
|
|
}
|
|
// 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")
|
|
.map((v) => v.file),
|
|
),
|
|
];
|
|
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/")})`,
|
|
);
|
|
const out = path.join(root, "app/assets/styles", file);
|
|
const patch = `*** Begin Patch\n*** Add File: ${out}\n+${css.replace(/\n/g, "\n+")}\n*** End Patch\n`;
|
|
const result = spawnSync("apply_patch", [], {
|
|
input: patch,
|
|
encoding: "utf8",
|
|
});
|
|
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("._") &&
|
|
!["builds"].includes(name)
|
|
) {
|
|
const src = path.join(input, "public", name);
|
|
if (fs.lstatSync(src).isDirectory() || !name.endsWith(".js"))
|
|
fs.cpSync(src, path.join(root, "public", name), {
|
|
recursive: true,
|
|
filter: (p) => !path.basename(p).startsWith("._"),
|
|
});
|
|
}
|
|
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(
|
|
path.join(chunkDir, name),
|
|
path.join(root, "public/icons", name),
|
|
);
|
|
console.log(
|
|
JSON.stringify({
|
|
sourceFiles: patchFiles.length,
|
|
businessModules: emitted.size,
|
|
css: cssFiles.length,
|
|
}),
|
|
);
|