UNPKG

weapp-vite

Version:

weapp-vite 一个现代化的小程序打包工具

278 lines (277 loc) 12.2 kB
import { createRequire } from "node:module"; import path from "pathe"; import { fs } from "@weapp-core/shared/fs"; import { parse } from "vue/compiler-sfc"; import process from "node:process"; import { recursive } from "merge"; //#region src/constants.ts const VERSION = "__VERSION__"; /** * 源代码支持的 js 文件格式 */ const jsExtensions = ["ts", "js"]; /** * 源代码支持的 vue 文件格式 */ const vueExtensions = ["vue"]; /** * 源代码支持的 json 文件格式 */ const configExtensions = [ "jsonc", "json", ...jsExtensions.map((x) => `json.${x}`) ]; /** * 源代码支持的 css 文件格式 */ const supportedCssLangs = [ "wxss", "css", "scss", "less", "sass", "styl" ]; /** * 源代码支持的 wxml 文件格式 */ const templateExtensions = ["wxml", "html"]; //#endregion //#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 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-BPQWdhpZ.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) { 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/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, ...jsExtensions, ...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, jsExtensions); } async function findJsonEntry(filepath) { return findEntryByExtensions(filepath, configExtensions); } async function findCssEntry(filepath) { return findEntryByExtensions(filepath, supportedCssLangs); } async function findTemplateEntry(filepath) { return findEntryByExtensions(filepath, templateExtensions); } function isTemplate(filepath) { return templateExtensions.some((ext) => filepath.endsWith(`.${ext}`)); } 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 autoRoutesInline = await resolveAutoRoutesInlineSnapshot(); const macroEvalPreamble = descriptor.script?.content ? inlineAutoRoutesImports(descriptor.script.content, autoRoutesInline) : void 0; const extracted = await extractJsonMacroFromScriptSetup(inlineAutoRoutesImports(setupContent, autoRoutesInline), 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 { jsExtensions as _, findJsonEntry as a, vueExtensions as b, isJsOrTs as c, touch as d, getAutoRoutesMacroImportCandidates as f, configExtensions as g, VERSION as h, findJsEntry as i, isTemplate as l, resolveAutoRoutesInlineSnapshot as m, changeFileExtension as n, findTemplateEntry as o, inlineAutoRoutesImports as p, findCssEntry as r, findVueEntry as s, extractConfigFromVue as t, normalizeFileExtension as u, supportedCssLangs as v, templateExtensions as y };