weapp-vite
Version:
weapp-vite 一个现代化的小程序打包工具
333 lines (332 loc) • 15.4 kB
JavaScript
import { createRequire } from "node:module";
import path from "pathe";
import { MINI_PROGRAM_PLATFORM_DESCRIPTORS as MINI_PROGRAM_PLATFORM_ADAPTERS } from "@weapp-core/shared";
import { fs } from "@weapp-core/shared/fs";
import process from "node:process";
import { recursive } from "merge";
import { parse } from "vue/compiler-sfc";
//#region src/utils/file/autoRoutes.ts
const nodeRequire = createRequire(import.meta.url);
const AUTO_ROUTES_ID = "weapp-vite/auto-routes";
const AUTO_ROUTES_VIRTUAL_ID = "virtual:weapp-vite-auto-routes";
const AUTO_ROUTES_SPECIFIER_RE = /(['"])(?:weapp-vite\/auto-routes|virtual:weapp-vite-auto-routes)\1/g;
const AUTO_ROUTES_DYNAMIC_IMPORT_RE = /import\(\s*['"](?:weapp-vite\/auto-routes|virtual:weapp-vite-auto-routes)['"]\s*\)/g;
const AUTO_ROUTES_NAMED_IMPORT_ALIAS_RE = /\bas\b/g;
const AUTO_ROUTES_DEFAULT_AND_NAMED_IMPORT_RE = /^([A-Z_$][\w$]*)\s*,\s*(\{[^}]+\})$/i;
function hasAutoRoutesMacroImport(source) {
return source.includes(AUTO_ROUTES_ID) || source.includes(AUTO_ROUTES_VIRTUAL_ID);
}
function toObjectDestructureClause(namedImportClause) {
return namedImportClause.replace(AUTO_ROUTES_NAMED_IMPORT_ALIAS_RE, ":");
}
function resolveInlineAutoRoutesImport(line, inlineRoutes, replacementIndex) {
const trimmedLine = line.trim();
if (!trimmedLine.startsWith("import ") || !trimmedLine.includes(" from ") || !trimmedLine.includes(`'${AUTO_ROUTES_ID}'`) && !trimmedLine.includes(`"${AUTO_ROUTES_ID}"`) && !trimmedLine.includes(`'${AUTO_ROUTES_VIRTUAL_ID}'`) && !trimmedLine.includes(`"${AUTO_ROUTES_VIRTUAL_ID}"`)) return;
const clause = trimmedLine.slice(7, trimmedLine.lastIndexOf(" from ")).trim();
const inlineLiteral = JSON.stringify(inlineRoutes);
if (clause.startsWith("{")) return `const ${toObjectDestructureClause(clause)} = ${inlineLiteral};`;
if (clause.startsWith("* as ")) return `const ${clause.slice(5).trim()} = ${inlineLiteral};`;
const defaultAndNamedMatch = clause.match(AUTO_ROUTES_DEFAULT_AND_NAMED_IMPORT_RE);
if (defaultAndNamedMatch) {
const [, defaultName, namedClause] = defaultAndNamedMatch;
const localRef = `__weappViteAutoRoutesInline${replacementIndex}`;
return `const ${localRef} = ${inlineLiteral};\nconst ${defaultName} = ${localRef};\nconst ${toObjectDestructureClause(namedClause)} = ${localRef};`;
}
return `const ${clause} = ${inlineLiteral};`;
}
function getAutoRoutesMacroImportCandidates(baseDir = import.meta.dirname) {
return [
path.resolve(baseDir, "./auto-routes.mjs"),
path.resolve(baseDir, "../auto-routes.mjs"),
path.resolve(baseDir, "../../dist/auto-routes.mjs"),
path.resolve(baseDir, "../../src/auto-routes.ts"),
path.resolve(baseDir, "../../auto-routes.ts")
];
}
function resolveAutoRoutesMacroImportPath() {
const fallbackCandidates = getAutoRoutesMacroImportCandidates();
try {
const resolved = nodeRequire.resolve("weapp-vite/auto-routes");
if (fs.existsSync(resolved)) return resolved;
} catch {}
for (const candidate of fallbackCandidates) if (fs.existsSync(candidate)) return candidate;
throw new Error("无法解析 auto-routes 模块路径。");
}
async function resolveAutoRoutesInlineSnapshot() {
try {
const { getCompilerContext } = await import("./getInstance-D82nepOm.mjs");
const compilerContext = getCompilerContext();
const service = compilerContext.autoRoutesService;
const reference = service?.getReference?.();
if (!compilerContext.runtimeState.autoRoutes.loadingAppConfig) await service?.ensureFresh?.();
const nextReference = service?.getReference?.() ?? reference;
return {
pages: nextReference?.pages ?? [],
entries: nextReference?.entries ?? [],
subPackages: nextReference?.subPackages ?? []
};
} catch {
return {
pages: [],
entries: [],
subPackages: []
};
}
}
function inlineAutoRoutesImports(source, inlineRoutes) {
if (!hasAutoRoutesMacroImport(source)) return source;
let importReplacementIndex = 0;
return source.split("\n").map((line) => {
const replaced = resolveInlineAutoRoutesImport(line, inlineRoutes, importReplacementIndex);
if (replaced) {
importReplacementIndex += 1;
return replaced;
}
return line;
}).join("\n").replace(AUTO_ROUTES_DYNAMIC_IMPORT_RE, `Promise.resolve(${JSON.stringify(inlineRoutes)})`).replace(AUTO_ROUTES_SPECIFIER_RE, JSON.stringify(resolveAutoRoutesMacroImportPath()));
}
//#endregion
//#region src/platforms/sourceAssets.ts
const PORTABLE_TEMPLATE_EXTENSIONS = ["wxml", "html"];
const PORTABLE_STYLE_EXTENSIONS = [
"wxss",
"css",
"scss",
"less",
"sass",
"styl"
];
function uniqueExtensions(extensions) {
return Array.from(new Set(extensions.filter((extension) => Boolean(extension))));
}
const ALL_NATIVE_TEMPLATE_EXTENSIONS = uniqueExtensions(MINI_PROGRAM_PLATFORM_ADAPTERS.map((adapter) => adapter.outputExtensions.wxml));
const ALL_NATIVE_STYLE_EXTENSIONS = uniqueExtensions(MINI_PROGRAM_PLATFORM_ADAPTERS.map((adapter) => adapter.outputExtensions.wxss));
const ALL_NATIVE_STYLE_RESOLVER_EXTENSIONS = ALL_NATIVE_STYLE_EXTENSIONS.filter((extension) => extension !== "css");
const ALL_SOURCE_TEMPLATE_EXTENSIONS = uniqueExtensions([...PORTABLE_TEMPLATE_EXTENSIONS, ...ALL_NATIVE_TEMPLATE_EXTENSIONS]);
const ALL_SOURCE_STYLE_EXTENSIONS = uniqueExtensions([...PORTABLE_STYLE_EXTENSIONS, ...ALL_NATIVE_STYLE_EXTENSIONS]);
function getAdapter(platform) {
return platform ? MINI_PROGRAM_PLATFORM_ADAPTERS.find((adapter) => adapter.id === platform) : void 0;
}
/**
* 返回当前平台的模板源码选择顺序。
* 原生平台后缀优先,便携源码后缀作为兼容回退。
*/
function getSourceTemplateExtensions(platform) {
const nativeExtension = getAdapter(platform)?.outputExtensions.wxml;
return platform ? uniqueExtensions([nativeExtension, ...PORTABLE_TEMPLATE_EXTENSIONS]) : ALL_SOURCE_TEMPLATE_EXTENSIONS;
}
/**
* 返回当前平台的样式源码选择顺序。
* 原生平台后缀优先,预处理器和便携样式后缀继续可用。
*/
function getSourceStyleExtensions(platform) {
const nativeExtension = getAdapter(platform)?.outputExtensions.wxss;
return platform ? uniqueExtensions([nativeExtension, ...PORTABLE_STYLE_EXTENSIONS]) : ALL_SOURCE_STYLE_EXTENSIONS;
}
function isSourceTemplateExtension(extension) {
const normalized = extension.startsWith(".") ? extension.slice(1) : extension;
return getSourceTemplateExtensions().includes(normalized);
}
function isSourceStyleExtension(extension) {
const normalized = extension.startsWith(".") ? extension.slice(1) : extension;
return getSourceStyleExtensions().includes(normalized);
}
function isNativeTemplateSource(filePath, platform) {
const nativeExtension = getAdapter(platform)?.outputExtensions.wxml;
return Boolean(nativeExtension && filePath.endsWith(`.${nativeExtension}`));
}
//#endregion
//#region src/constants.ts
const VERSION = "__VERSION__";
/**
* 源代码支持的 js 文件格式
*/
const jsExtensions = ["ts", "js"];
const scriptExtensions = [
"ts",
"js",
"tsx",
"jsx"
];
/**
* 源代码支持的 vue 文件格式
*/
const vueExtensions = ["vue"];
/**
* 源代码支持的 json 文件格式
*/
const configExtensions = [
"jsonc",
"json",
...jsExtensions.map((x) => `json.${x}`)
];
/**
* 源代码支持的 css 文件格式
*/
const supportedCssLangs = ALL_SOURCE_STYLE_EXTENSIONS;
/**
* 源代码支持的 wxml 文件格式
*/
const templateExtensions = ALL_SOURCE_TEMPLATE_EXTENSIONS;
//#endregion
//#region src/utils/file/entry.ts
const pathExistsInFlight = /* @__PURE__ */ new Map();
const JS_OR_TS_RE = /\.[jt]s$/;
function pathExistsCached(filePath) {
const pending = pathExistsInFlight.get(filePath);
if (pending) return pending;
const next = fs.pathExists(filePath).finally(() => {
pathExistsInFlight.delete(filePath);
});
pathExistsInFlight.set(filePath, next);
return next;
}
function isJsOrTs(name) {
if (typeof name === "string") return JS_OR_TS_RE.test(name);
return false;
}
function normalizeFileExtension(extension) {
return extension ? extension.startsWith(".") ? extension : `.${extension}` : "";
}
const knownEntryExtensions = new Set([
...configExtensions,
...scriptExtensions,
...supportedCssLangs,
...templateExtensions,
...vueExtensions
].map(normalizeFileExtension));
function changeFileExtension(filePath, extension) {
if (typeof filePath !== "string") throw new TypeError(`Expected \`filePath\` to be a string, got \`${typeof filePath}\`.`);
if (typeof extension !== "string") throw new TypeError(`Expected \`extension\` to be a string, got \`${typeof extension}\`.`);
if (filePath === "") return "";
extension = normalizeFileExtension(extension);
const basename = path.basename(filePath, path.extname(filePath));
return path.join(path.dirname(filePath), basename + extension);
}
async function findEntryByExtensions(filepath, extensions) {
const normalizedExtensions = extensions.map(normalizeFileExtension);
const currentExtension = path.extname(filepath);
const shouldReplaceExtension = currentExtension ? knownEntryExtensions.has(currentExtension) : false;
const predictions = normalizedExtensions.map((ext) => {
return shouldReplaceExtension ? changeFileExtension(filepath, ext) : `${filepath}${ext}`;
});
const matchedIndex = (await Promise.all(predictions.map((targetPath) => pathExistsCached(targetPath)))).findIndex(Boolean);
return {
predictions,
path: matchedIndex >= 0 ? predictions[matchedIndex] : void 0
};
}
async function findVueEntry(filepath) {
return (await findEntryByExtensions(filepath, vueExtensions)).path;
}
async function findJsEntry(filepath) {
return findEntryByExtensions(filepath, scriptExtensions);
}
async function findJsonEntry(filepath) {
return findEntryByExtensions(filepath, configExtensions);
}
async function findCssEntry(filepath, platform) {
return findEntryByExtensions(filepath, getSourceStyleExtensions(platform));
}
async function findTemplateEntry(filepath, platform) {
return findEntryByExtensions(filepath, getSourceTemplateExtensions(platform));
}
function isTemplate(filepath) {
return isSourceTemplateExtension(path.extname(filepath));
}
async function touch(filename) {
const time = /* @__PURE__ */ new Date();
try {
await fs.utimes(filename, time, time);
} catch {
await fs.close(await fs.open(filename, "w"));
}
}
//#endregion
//#region src/utils/file/vueConfig.ts
const vueConfigCache = /* @__PURE__ */ new Map();
const configMtimeInFlight = /* @__PURE__ */ new Map();
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
const JSON_MACRO_HINT_RE = /\bdefine(?:App|Page|Component|Sitemap|Theme)Json\s*\(/;
function getMtimeCached(filePath) {
const pending = configMtimeInFlight.get(filePath);
if (pending) return pending;
const next = fs.stat(filePath).then((stat) => stat.mtimeMs).catch(() => void 0).finally(() => {
configMtimeInFlight.delete(filePath);
});
configMtimeInFlight.set(filePath, next);
return next;
}
async function isVueConfigCacheValid(vueFilePath, cache) {
const nextMtime = await getMtimeCached(vueFilePath);
if (nextMtime === void 0 || cache.fileMtimeMs === void 0) return false;
if (nextMtime !== cache.fileMtimeMs) return false;
if (cache.dependencies.length === 0) return true;
for (const dep of cache.dependencies) {
const nextDepMtime = await getMtimeCached(dep);
const cachedDepMtime = cache.dependencyMtimeMs.get(dep);
if (nextDepMtime === void 0 || cachedDepMtime === void 0 || nextDepMtime !== cachedDepMtime) return false;
}
return true;
}
/**
* 从 .vue 文件中提取 <json> 块的内容
* @param vueFilePath .vue 文件的路径
* @returns 提取的配置对象,如果不存在或解析失败则返回 undefined
*/
async function extractConfigFromVue(vueFilePath, options) {
try {
const cached = options?.force ? void 0 : vueConfigCache.get(vueFilePath);
if (cached && await isVueConfigCacheValid(vueFilePath, cached)) return cached.config;
const content = options?.source ?? (options?.readSource ? await options.readSource() : await fs.readFile(vueFilePath, "utf-8"));
if (content === void 0) return;
const { descriptor, errors } = parse(content, { filename: vueFilePath });
if (errors.length > 0) return;
const mergedConfig = {};
const macroDependencies = [];
const { parse: parseJson } = await import("comment-json");
const jsonBlocks = descriptor.customBlocks.filter((block) => block.type === "json");
for (const block of jsonBlocks) try {
const lang = (block.lang || "json").toLowerCase();
if (lang === "json" || lang === "jsonc" || lang === "json5" || lang === "txt") {
const config = parseJson(block.content, void 0, true);
if (config && typeof config === "object" && !Array.isArray(config)) Object.assign(mergedConfig, config);
continue;
}
} catch {}
const setupContent = descriptor.scriptSetup?.content;
if (typeof setupContent === "string" && JSON_MACRO_HINT_RE.test(setupContent)) {
const { extractJsonMacroFromScriptSetup } = await import("wevu/compiler");
try {
const preambleContent = descriptor.script?.content;
const autoRoutesInline = hasAutoRoutesMacroImport(setupContent) || preambleContent !== void 0 && hasAutoRoutesMacroImport(preambleContent) ? await resolveAutoRoutesInlineSnapshot() : void 0;
const macroEvalPreamble = preambleContent && autoRoutesInline ? inlineAutoRoutesImports(preambleContent, autoRoutesInline) : preambleContent;
const extracted = await extractJsonMacroFromScriptSetup(autoRoutesInline ? inlineAutoRoutesImports(setupContent, autoRoutesInline) : setupContent, vueFilePath, descriptor.scriptSetup?.lang, { preambleContent: macroEvalPreamble });
if (extracted.dependencies?.length) macroDependencies.push(...extracted.dependencies);
if (extracted.config && typeof extracted.config === "object" && !Array.isArray(extracted.config)) recursive(mergedConfig, extracted.config);
} catch (error) {
if (jsonBlocks.length === 0) throw error;
}
}
const normalizedDependencies = [...new Set(macroDependencies.filter((dep) => dep && !NODE_MODULES_RE.test(dep)).map((dep) => path.normalize(dep)))];
const dependencyMtimeMs = /* @__PURE__ */ new Map();
await Promise.all(normalizedDependencies.map(async (dep) => {
const mtime = await getMtimeCached(dep);
if (mtime !== void 0) dependencyMtimeMs.set(dep, mtime);
}));
const fileMtimeMs = await getMtimeCached(vueFilePath);
const hasConfig = Object.keys(mergedConfig).length > 0;
vueConfigCache.set(vueFilePath, {
config: hasConfig ? mergedConfig : void 0,
fileMtimeMs,
dependencies: normalizedDependencies,
dependencyMtimeMs
});
return hasConfig ? mergedConfig : void 0;
} catch (error) {
if (process.env.__WEAPP_VITE_DEBUG_VUE_CONFIG__) console.error("[extractConfigFromVue] failed:", vueFilePath, error);
return;
}
}
//#endregion
export { hasAutoRoutesMacroImport as C, MINI_PROGRAM_PLATFORM_ADAPTERS as E, getAutoRoutesMacroImportCandidates as S, resolveAutoRoutesInlineSnapshot as T, templateExtensions as _, findJsonEntry as a, isNativeTemplateSource as b, isJsOrTs as c, touch as d, VERSION as f, supportedCssLangs as g, scriptExtensions as h, findJsEntry as i, isTemplate as l, jsExtensions as m, changeFileExtension as n, findTemplateEntry as o, configExtensions as p, findCssEntry as r, findVueEntry as s, extractConfigFromVue as t, normalizeFileExtension as u, vueExtensions as v, inlineAutoRoutesImports as w, isSourceStyleExtension as x, ALL_NATIVE_STYLE_RESOLVER_EXTENSIONS as y };