@uni-ku/bundle-optimizer
Version:
uni-app 分包优化插件化实现
1,186 lines (1,160 loc) • 46.1 kB
JavaScript
;
const fs = require('node:fs');
const chalk = require('chalk');
const path = require('node:path');
const process$1 = require('node:process');
const uniCliShared = require('@dcloudio/uni-cli-shared');
const MagicString = require('magic-string');
const types = require('node:util/types');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
const chalk__default = /*#__PURE__*/_interopDefaultCompat(chalk);
const path__default = /*#__PURE__*/_interopDefaultCompat(path);
const process__default = /*#__PURE__*/_interopDefaultCompat(process$1);
const MagicString__default = /*#__PURE__*/_interopDefaultCompat(MagicString);
const name = "@uni-ku/bundle-optimizer";
var __defProp$3 = Object.defineProperty;
var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$3 = (obj, key, value) => {
__defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class Logger {
constructor(level = "INFO" /* INFO */, context = "Plugin", isImplicit = false) {
__publicField$3(this, "level");
__publicField$3(this, "context");
/** TODO: 可以使用其他的 debug 日志库 */
__publicField$3(this, "Debugger", null);
/** 全局兜底:是否是隐式log */
__publicField$3(this, "isImplicit");
__publicField$3(this, "onLog");
this.level = level;
this.context = context;
this.isImplicit = isImplicit;
}
log(level, message, isImplicit) {
if (this.shouldLog(level)) {
const coloredMessage = this.getColoredMessage(level, message);
this.onLog?.(this.context, level, message, Date.now());
if (isImplicit ?? this.isImplicit) ; else {
const c = 69;
const colorCode = `\x1B[3${`8;5;${c}`};1m`;
console.log(` ${chalk__default(`${colorCode}${this.context}`)} ${coloredMessage}`);
}
}
}
shouldLog(level) {
const levels = ["DEBUG" /* DEBUG */, "INFO" /* INFO */, "WARN" /* WARN */, "ERROR" /* ERROR */];
return levels.indexOf(level) >= levels.indexOf(this.level);
}
getColoredMessage(level, message) {
switch (level) {
case "DEBUG" /* DEBUG */:
return chalk__default.blue(`[${level}] ${message}`);
case "INFO" /* INFO */:
return chalk__default.green(`[${level}] ${message}`);
case "WARN" /* WARN */:
return chalk__default.yellow(`[${level}] ${message}`);
case "ERROR" /* ERROR */:
return chalk__default.red(`[${level}] ${message}`);
default:
return message;
}
}
debug(message, isImplicit) {
this.log("DEBUG" /* DEBUG */, message, isImplicit);
}
info(message, isImplicit) {
this.log("INFO" /* INFO */, message, isImplicit);
}
warn(message, isImplicit) {
this.log("WARN" /* WARN */, message, isImplicit);
}
error(message, isImplicit) {
this.log("ERROR" /* ERROR */, message, isImplicit);
}
}
const logger = new Logger("INFO" /* INFO */, "uni-ku:bundle-optimizer");
const EXTNAME_JS_RE = /\.(js|jsx|ts|uts|tsx|mjs|json)$/;
const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/;
const knownJsSrcRE = /\.(?:[jt]sx?|m[jt]s|vue|marko|svelte|astro|imba|mdx)(?:$|\?)/;
const ASSETS_DIR_RE = /^(\.?\/)?assets\//;
const SRC_DIR_RE = /^(\.?\/)?src\//;
const EXT_RE = /\.\w+$/;
const ROOT_DIR = process__default.env.VITE_ROOT_DIR;
if (!ROOT_DIR) {
throw new Error("`ROOT_DIR` is not defined");
}
function splitOnFirst(str, separator) {
if (!(typeof str === "string" && (typeof separator === "string" || separator instanceof RegExp))) {
throw new TypeError("Expected the arguments to be of type `string` and `RegExp`");
}
if (str === "" || separator === "") {
return [];
}
let separatorIndex = -1;
let matchLength = 1;
if (typeof separator === "string") {
separatorIndex = str.indexOf(separator);
matchLength = separator.length;
} else if (separator instanceof RegExp) {
const nonGlobalRegex = new RegExp(separator.source, separator.flags.replace("g", ""));
const match = nonGlobalRegex.exec(str);
if (match) {
separatorIndex = match.index;
matchLength = match[0].length;
}
}
if (separatorIndex === -1) {
return [];
}
return [
str.slice(0, separatorIndex),
str.slice(separatorIndex + matchLength)
];
}
const token = "%[a-f0-9]{2}";
const singleMatcher = new RegExp(`(${token})|([^%]+)`, "gi");
const multiMatcher = new RegExp(`(${token})+`, "gi");
function decodeComponents(components, split) {
try {
return [decodeURIComponent(components.join(""))];
} catch {
}
if (components.length === 1) {
return components;
}
split = split || 1;
const left = components.slice(0, split);
const right = components.slice(split);
return Array.prototype.concat.call([], decodeComponents(left), decodeComponents(right));
}
function decode$1(input) {
try {
return decodeURIComponent(input);
} catch {
let tokens = input.match(singleMatcher) || [];
for (let i = 1; i < tokens.length; i++) {
input = decodeComponents(tokens, i).join("");
tokens = input.match(singleMatcher) || [];
}
return input;
}
}
function customDecodeURIComponent(input) {
const replaceMap = {
"%FE%FF": "\uFFFD\uFFFD",
"%FF%FE": "\uFFFD\uFFFD"
};
let match = multiMatcher.exec(input);
while (match) {
try {
replaceMap[match[0]] = decodeURIComponent(match[0]);
} catch {
const result = decode$1(match[0]);
if (result !== match[0]) {
replaceMap[match[0]] = result;
}
}
match = multiMatcher.exec(input);
}
replaceMap["%C2"] = "\uFFFD";
const entries = Object.keys(replaceMap);
for (const key of entries) {
input = input.replace(new RegExp(key, "g"), replaceMap[key]);
}
return input;
}
function decodeUriComponent(encodedURI) {
if (typeof encodedURI !== "string") {
throw new TypeError(`Expected \`encodedURI\` to be of type \`string\`, got \`${typeof encodedURI}\``);
}
try {
return decodeURIComponent(encodedURI);
} catch {
return customDecodeURIComponent(encodedURI);
}
}
function isNil(value) {
return value === null || value === void 0;
}
function decode(value, options = {}) {
if (options.decode) {
return decodeUriComponent(value);
}
return value;
}
function parse(query, options = {}) {
options = {
decode: true,
arrayFormat: "none",
arrayFormatSeparator: ",",
types: /* @__PURE__ */ Object.create(null),
...options
};
if (!validateArrayFormatSeparator(options.arrayFormatSeparator)) {
throw new TypeError("arrayFormatSeparator must be single character string");
}
const formatter = parserForArrayFormat(options);
const returnValue = /* @__PURE__ */ Object.create(null);
if (typeof query !== "string") {
return returnValue;
}
query = query.trim().replace(/^[?#&]/, "");
if (!query) {
return returnValue;
}
for (const pair of query.split("&")) {
if (pair === "") {
continue;
}
const parameter = options.decode ? pair.replaceAll("+", " ") : pair;
let [key, value] = splitOnFirst(parameter, "=");
key ?? (key = parameter);
value = isNil(value) ? null : decode(value, options);
formatter(decode(key, options), value, returnValue);
}
return returnValue;
}
function validateArrayFormatSeparator(value) {
return typeof value === "string" && value.length === 1;
}
function parserForArrayFormat(options) {
let result = null;
function canBeArray(value) {
return typeof value === "string" && value.includes(options.arrayFormatSeparator);
}
function canBeEncodedArray(value) {
return typeof value === "string" && !canBeArray(value) && decode(value, options).includes(options.arrayFormatSeparator);
}
function tryBeArray(value) {
return canBeArray(value) || canBeEncodedArray(value);
}
switch (options.arrayFormat) {
case "index": {
return (key, value, accumulator) => {
result = /\[(\d*)\]$/.exec(key);
key = key.replace(/\[\d*\]$/, "");
if (!result) {
accumulator[key] = value;
return;
}
if (accumulator[key] === void 0) {
accumulator[key] = {};
}
accumulator[key][result[1]] = value;
};
}
case "bracket": {
return (key, value, accumulator) => {
result = /(\[\])$/.exec(key);
key = key.replace(/\[\]$/, "");
if (!result) {
accumulator[key] = value;
return;
}
if (accumulator[key] === void 0) {
accumulator[key] = [value];
return;
}
accumulator[key] = [...accumulator[key], value];
};
}
case "colon-list-separator": {
return (key, value, accumulator) => {
result = /(:list)$/.exec(key);
key = key.replace(/:list$/, "");
if (!result) {
accumulator[key] = value;
return;
}
if (accumulator[key] === void 0) {
accumulator[key] = [value];
return;
}
accumulator[key] = [...accumulator[key], value];
};
}
case "comma":
case "separator": {
return (key, value, accumulator) => {
value = isNil(value) ? value : canBeEncodedArray(value) ? decode(value, options) : value;
const newValue = isNil(value) ? value : tryBeArray(value) ? value.split(options.arrayFormatSeparator).map((item) => decode(item, options)) : decode(value, options);
accumulator[key] = newValue;
};
}
case "bracket-separator": {
return (key, value, accumulator) => {
const isArray = /\[\]$/.test(key);
key = key.replace(/\[\]$/, "");
if (!isArray) {
accumulator[key] = value ? decode(value, options) : value;
return;
}
const arrayValue = isNil(value) ? [] : decode(value, options).split(options.arrayFormatSeparator);
if (accumulator[key] === void 0) {
accumulator[key] = arrayValue;
return;
}
accumulator[key] = [...accumulator[key], ...arrayValue];
};
}
default: {
return (key, value, accumulator) => {
if (accumulator[key] === void 0) {
accumulator[key] = value;
return;
}
accumulator[key] = [...[accumulator[key]].flat(), value];
};
}
}
}
function createVitePathResolver(config) {
const tempAlias = config.resolve?.alias ?? [];
let alias = [];
if (!Array.isArray(tempAlias)) {
alias = Object.entries(tempAlias).map(([find, replacement]) => ({ find, replacement }));
} else {
alias = tempAlias;
}
return (source, relative = false) => {
for (let { find, replacement, customResolver: _customResolver } of alias) {
if (!find || !replacement)
continue;
source = normalizePath(source);
if (typeof replacement === "string" && !types.isRegExp(replacement) && !replacement.includes("*")) {
replacement = normalizePath(replacement);
}
if (!types.isRegExp(replacement) && typeof replacement === "string" && !replacement.includes("*") && !types.isRegExp(find) && typeof find === "string" && !find.includes("*")) {
if (source === find) {
return relative ? replacement : path__default.resolve(replacement);
} else if (source.startsWith(find)) {
const realPath = source.replace(find, replacement);
return relative ? realPath : path__default.resolve(realPath);
}
} else if (source.match(find) && (types.isRegExp(find) || !find.includes("*"))) {
const realPath = source.replace(find, replacement);
return relative ? realPath : path__default.resolve(realPath);
}
}
return source;
};
}
let vitePathResolver = null;
function getVitePathResolver() {
if (!vitePathResolver) {
throw new Error("Vite path resolver has not been initialized. Please call createVitePathResolver first.");
}
return vitePathResolver;
}
function initializeVitePathResolver(config) {
vitePathResolver = createVitePathResolver(config);
}
function lexFunctionCalls(code, functionName) {
const functionPattern = new RegExp(
`\\b${functionName}\\s*\\(\\s*([^\\)]*)\\s*\\)`,
// 函数名 + 参数部分
"g"
);
const matches = [];
let match;
while ((match = functionPattern.exec(code)) !== null) {
const argsString = match[1];
const fullMatchLocation = {
start: match.index,
// 函数调用的起始位置
end: match.index + match[0].length,
// 函数调用的结束位置
fullMatch: match[0]
// 完整匹配的函数调用
};
const functionCallPrefixEnd = match.index + match[0].indexOf("(") + 1;
const padStartCount = match[0].indexOf(argsString) - (match[0].indexOf("(") + 1);
const args = parseArguments(argsString.padStart(argsString.length + padStartCount), functionCallPrefixEnd);
matches.push({
full: fullMatchLocation,
// 完整匹配的函数调用
args
// 参数解析结果
});
}
return matches;
}
function parseArguments(argsString, functionPrefixEnd, code) {
const args = [];
const argPattern = /'(?:\\'|[^'])*'|"(?:\\"|[^"])*"|\d+(?:\.\d+)?|\w+/g;
let match;
while ((match = argPattern.exec(argsString)) !== null) {
const argValue = match[0];
const argStart = functionPrefixEnd + argsString.slice(0, match.index).length;
const argEnd = argStart + argValue.length;
let value = argValue;
if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
} else if (!Number.isNaN(Number(value))) {
value = Number(value);
}
args.push({
value,
// 只保留实际的参数值
start: argStart,
// 修正后的起始位置
end: argEnd
// 修正后的结束位置
});
}
return args;
}
const IMPORT_DEFAULT_WITH_QUERY_RE = /import\s+(\w+)\s+from\s+(['"])([^'"]+)\?(\w+(?:&\w+)*)\2(?:\s*;)?/g;
function parseValue(value) {
if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
return value.slice(1, -1);
}
return value;
}
function lexDefaultImportWithQuery(code) {
const matches = [];
let match;
while ((match = IMPORT_DEFAULT_WITH_QUERY_RE.exec(code)) !== null) {
const fullMatchLocation = {
start: match.index,
// 函数调用的起始位置
end: match.index + match[0].length,
// 函数调用的结束位置
fullMatch: match[0]
// 完整匹配的函数调用
};
const defaultVariable = {
value: parseValue(match[1]),
start: match.index + match[0].indexOf(match[1]),
end: match.index + match[0].indexOf(match[1]) + match[1].length
};
const modulePath = {
value: parseValue(match[3]),
start: match.index + match[0].indexOf(match[3]),
end: match.index + match[0].indexOf(match[3]) + match[3].length
};
let lastLength = 0;
const query = match[4].split("&").map((queryParam, _index, list) => {
lastLength += (list[_index - 1]?.length || 0) + 1;
const prevLength = modulePath.end + lastLength;
const start = prevLength + match[0].slice(prevLength - fullMatchLocation.start).indexOf(queryParam);
const end = start + queryParam.length;
return {
value: parseValue(queryParam),
start,
end
};
});
const fullPath = {
value: parseValue(`${match[3]}?${match[4]}`),
start: modulePath.start,
end: query[query.length - 1].end
};
matches.push({
full: fullMatchLocation,
// 完整匹配的函数调用
defaultVariable,
// 参数解析结果
modulePath,
query,
fullPath
// 完整路径信息
});
}
return matches;
}
function parseAsyncImports(source) {
return lexFunctionCalls(source, "AsyncImport");
}
function slash(p) {
return p.replace(/\\/g, "/");
}
function normalizePath(id) {
return process__default.platform === "win32" ? slash(id) : id;
}
function ensureDirectoryExists(filePath) {
const dir = path__default.dirname(filePath);
if (!fs__default.existsSync(dir)) {
fs__default.mkdirSync(dir, { recursive: true });
}
}
function moduleIdProcessor(id, rootDir = ROOT_DIR, removeQuery = true) {
rootDir = normalizePath(rootDir);
if (!rootDir.endsWith("/"))
rootDir += "/";
const normalized = normalizePath(id);
const name = removeQuery ? normalized.split("?")[0] : normalized;
const updatedName = name.replace(rootDir, "");
if (updatedName.startsWith("\0"))
return updatedName.slice(1);
return updatedName;
}
function calculateRelativePath(importer, imported) {
if (imported.match(/^(\.\/|\.\.\/)+/)) {
imported = path__default.resolve(path__default.dirname(importer), imported);
}
const relativePath = path__default.relative(path__default.dirname(importer), imported);
return relativePath.replace(/\\/g, "/");
}
function resolveAssetsPath(id) {
return id.replace(ASSETS_DIR_RE, "./");
}
function kebabCase(key) {
if (!key)
return key;
const result = key.replace(/([A-Z])/g, " $1").trim();
return result.split(" ").join("-").toLowerCase();
}
function findFirstNonConsecutiveBefore(arr) {
if (arr.length < 2)
return null;
const result = arr.find((value, index) => index > 0 && value !== arr[index - 1] + 1);
return result !== void 0 && result !== null ? arr[arr.indexOf(result) - 1] : null;
}
function parseQuerystring(url) {
if (!url || typeof url !== "string") {
return null;
}
const rmExtUrl = url.replace(EXT_RE, "");
const queryStr = rmExtUrl.split("?")[1] || "";
if (!queryStr) {
return null;
}
try {
return Object.entries(parse(queryStr)).reduce((acc, [key, value]) => {
acc[key] = value === null || value === "true" ? true : value === "false" ? false : value;
return acc;
}, {});
} catch (error) {
console.error("Error parsing query string:", error);
return null;
}
}
var __defProp$2 = Object.defineProperty;
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$2 = (obj, key, value) => {
__defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class ParseOptions {
constructor(options) {
__publicField$2(this, "options");
this.options = options;
}
get enable() {
const { enable: origin = true } = this.options;
return typeof origin === "boolean" ? {
"optimization": origin,
"async-component": origin,
"async-import": origin
} : {
"optimization": origin.optimization ?? true,
"async-component": origin["async-component"] ?? true,
"async-import": origin["async-import"] ?? true
};
}
get dts() {
const { dts: origin = true } = this.options;
if (typeof origin === "boolean") {
return {
"async-component": this.generateDtsOptions(origin, "async-component.d.ts"),
"async-import": this.generateDtsOptions(origin, "async-import.d.ts")
};
}
return {
"async-component": (origin.enable ?? true) !== false && this.generateDtsOptions(origin["async-component"], "async-component.d.ts", origin.base),
"async-import": (origin.enable ?? true) !== false && this.generateDtsOptions(origin["async-import"], "async-import.d.ts", origin.base)
};
}
generateDtsOptions(params = true, name, base = "./") {
if (params === false)
return false;
const path = typeof params === "boolean" ? `${normalizePath(base).replace(/\/$/, "")}/${name}` : params.enable !== false && normalizePath(params.path || `${normalizePath(params.base ?? base).replace(/\/$/, "")}/${params.name ?? name}`);
return path !== false && { enable: true, path };
}
get logger() {
const { logger: origin = false } = this.options;
const _ = ["optimization", "async-component", "async-import"];
const temp = typeof origin === "boolean" ? origin ? _ : false : origin;
return Object.fromEntries(_.map((item) => [item, (temp || []).includes(item)]));
}
}
var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => {
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class AsyncComponents {
constructor() {
__publicField$1(this, "scriptDescriptors", /* @__PURE__ */ new Map());
__publicField$1(this, "jsonAsyncComponentsCache", /* @__PURE__ */ new Map());
/** 当前状态下热更新时会导致把原有的json内容清除,操作过的page-json需要记录之前的内容 */
__publicField$1(this, "pageJsonCache", /* @__PURE__ */ new Map());
__publicField$1(this, "rename", (name) => name.startsWith("wx-") ? name.replace("wx-", "weixin-") : name);
}
addScriptDescriptor(filename, binding) {
binding && filename && this.scriptDescriptors.set(filename, {
bindingAsyncComponents: binding
});
}
addAsyncComponents(filename, json) {
this.jsonAsyncComponentsCache.set(filename, json);
}
generateBinding(tag, path) {
return { tag, value: path, type: "asyncComponent" };
}
getComponentPlaceholder(filename) {
const cache = this.jsonAsyncComponentsCache.get(filename);
if (!cache)
return null;
const componentPlaceholder = Object.entries(cache).reduce((p, [key, value]) => {
p[this.rename(key)] = "view";
return p;
}, {});
return componentPlaceholder;
}
generateComponentPlaceholderJson(filename, originJson = {}) {
const componentPlaceholder = this.getComponentPlaceholder(filename);
return Object.assign(originJson || {}, componentPlaceholder || {});
}
}
function AsyncComponentProcessor(options, enableLogger) {
const inputDir = process__default.env.UNI_INPUT_DIR;
const platform = process__default.env.UNI_PLATFORM;
const AsyncComponentsInstance = new AsyncComponents();
const isMP = platform?.startsWith("mp-");
function generateTypeFile(parseResult) {
if (options === false || options.enable === false)
return;
const typesFilePath = path__default.resolve(ROOT_DIR, normalizePath(options.path));
ensureDirectoryExists(typesFilePath);
let cache = [];
if (fs__default.existsSync(typesFilePath)) {
const list = lexFunctionCalls(fs__default.readFileSync(typesFilePath, "utf-8"), "import").flatMap(({ args }) => args.map(({ value }) => value.toString()));
list && list.length && (cache = Array.from(new Set(list)));
}
const typeDefinition = generateModuleDeclaration$1(parseResult, cache);
fs__default.writeFileSync(typesFilePath, typeDefinition);
logger.info(`[async-component] ${parseResult === void 0 ? "\u521D\u59CB\u5316" : "\u751F\u6210"}\u7C7B\u578B\u5B9A\u4E49\u6587\u4EF6 ${typesFilePath.replace(`${ROOT_DIR}\\`, "")}`, !enableLogger);
}
generateTypeFile();
logger.info("[async-component] \u5F02\u6B65\u7EC4\u4EF6\u5904\u7406\u5668\u5DF2\u542F\u7528", !enableLogger);
return {
name: "async-component-processor",
async transform(source, importer) {
const parseResult = lexDefaultImportWithQuery(source).filter(({ modulePath }) => modulePath.value.toString().split("?")[0].endsWith(".vue"));
if (!importer.split("?")[0].endsWith(".vue") || parseResult.length === 0 || !parseResult.some(({ query }) => query.some(({ value }) => value.toString().trim() === "async"))) {
return;
}
generateTypeFile(parseResult);
const filename = uniCliShared.removeExt(uniCliShared.normalizeMiniProgramFilename(importer, inputDir));
const tempBindings = {};
const magicString = new MagicString__default(source);
parseResult.forEach(({ full, fullPath, defaultVariable, modulePath, query }) => {
const cache = {};
query.forEach(({ start, end, value }, index, list) => {
const prevChar = source[start - 1];
if (["async", ""].includes(value.toString().trim()) && start !== end) {
magicString.overwrite(start, end, "");
if (prevChar === "&") {
magicString.overwrite(start - 1, start, "");
}
cache[index] = { start, end, value };
if (isMP) {
const url = modulePath.value.toString();
let normalizedPath = getVitePathResolver()(url, true);
normalizedPath = calculateRelativePath(importer, normalizedPath);
normalizedPath = normalizedPath.replace(/\.vue$/, "");
const tag = kebabCase(defaultVariable.value.toString());
tempBindings[tag] = AsyncComponentsInstance.generateBinding(tag, normalizedPath);
}
}
});
if (cache[0]) {
const flag = findFirstNonConsecutiveBefore(Object.keys(cache).map(Number));
const { start, end } = flag !== null ? query[flag + 1] : cache[0];
const char = flag !== null ? "&" : "?";
const prevChar = source[start - 1];
if (prevChar === char) {
magicString.overwrite(start - 1, start, "");
}
}
});
if (isMP) {
AsyncComponentsInstance.addScriptDescriptor(filename, tempBindings);
AsyncComponentsInstance.addAsyncComponents(filename, tempBindings);
}
return {
code: magicString.toString(),
map: magicString.generateMap({ hires: true })
};
},
generateBundle(_, bundle) {
if (!isMP)
return;
AsyncComponentsInstance.jsonAsyncComponentsCache.forEach((value, key) => {
const chunk = bundle[`${key}.json`];
const asyncComponents = Object.entries(value).reduce((p, [key2, value2]) => (p[AsyncComponentsInstance.rename(key2)] = value2.value, p), {});
if (chunk && chunk.type === "asset" && AsyncComponentsInstance.jsonAsyncComponentsCache.get(key)) {
const jsonCode = JSON.parse(chunk.source.toString());
AsyncComponentsInstance.pageJsonCache.set(key, jsonCode);
jsonCode.componentPlaceholder = AsyncComponentsInstance.generateComponentPlaceholderJson(key, jsonCode.componentPlaceholder);
jsonCode.usingComponents = Object.assign(jsonCode.usingComponents || {}, asyncComponents);
chunk.source = JSON.stringify(jsonCode, null, 2);
} else {
let componentPlaceholder = AsyncComponentsInstance.generateComponentPlaceholderJson(key);
let usingComponents = asyncComponents;
const cache = AsyncComponentsInstance.pageJsonCache.get(key);
if (cache) {
usingComponents = Object.assign(cache.usingComponents || {}, usingComponents);
componentPlaceholder = Object.assign(cache.componentPlaceholder || {}, componentPlaceholder);
}
bundle[`${key}.json`] = {
type: "asset",
name: key,
fileName: `${key}.json`,
source: JSON.stringify(Object.assign({}, cache, { usingComponents, componentPlaceholder }), null, 2)
};
}
});
},
buildStart() {
AsyncComponentsInstance.jsonAsyncComponentsCache.clear();
AsyncComponentsInstance.scriptDescriptors.clear();
}
};
}
function generateModuleDeclaration$1(parsedResults, cache) {
let typeDefs = "";
const prefixList = [
"/* eslint-disable */",
"/* prettier-ignore */",
"// @ts-nocheck",
"// Generated by @uni-ku/bundle-optimizer",
"declare module '*?async' {",
" const component: any",
" export = component",
"}"
];
prefixList.forEach((prefix) => {
typeDefs += `${prefix}
`;
});
function generateDeclareModule(modulePath, fullPath) {
typeDefs += `declare module '${fullPath}' {
`;
typeDefs += ` const component: typeof import('${modulePath}')
`;
typeDefs += ` export = component
`;
typeDefs += `}
`;
}
cache?.forEach((item) => {
const modulePath = item;
const fullPath = `${modulePath}?async`;
generateDeclareModule(modulePath, fullPath);
});
parsedResults?.filter((item) => !cache?.includes(item.modulePath.value.toString())).forEach((result) => {
const modulePath = result.modulePath.value;
const fullPath = result.fullPath.value;
generateDeclareModule(modulePath, fullPath);
});
return typeDefs;
}
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class AsyncImports {
constructor() {
__publicField(this, "cache", /* @__PURE__ */ new Map());
__publicField(this, "valueMap", /* @__PURE__ */ new Map());
}
addCache(id, value, realPath) {
if (this.cache.has(id) && !this.cache.get(id)?.includes(value)) {
this.cache.get(id)?.push(value);
} else {
this.cache.set(id, [value]);
}
if (!realPath) {
return;
}
if (this.valueMap.has(value) && !this.valueMap.get(value)?.includes(realPath)) {
this.valueMap.get(value)?.push(realPath);
} else {
this.valueMap.set(value, [realPath]);
}
}
getCache(id) {
return this.cache.get(id);
}
getRealPath(value) {
return this.valueMap.get(value);
}
clearCache() {
this.cache.clear();
this.valueMap.clear();
}
}
function AsyncImportProcessor(options, enableLogger) {
const platform = process__default.env.UNI_PLATFORM;
const isMP = platform?.startsWith("mp");
const isH5 = platform === "h5";
const isApp = platform === "app";
const AsyncImportsInstance = new AsyncImports();
function generateTypeFile(paths) {
if (options === false || options.enable === false)
return;
const typesFilePath = path__default.resolve(ROOT_DIR, normalizePath(options.path));
ensureDirectoryExists(typesFilePath);
let cache = [];
if (fs__default.existsSync(typesFilePath)) {
const list = lexFunctionCalls(fs__default.readFileSync(typesFilePath, "utf-8"), "import").flatMap(({ args }) => args.map(({ value }) => value.toString()));
list && list.length && (cache = Array.from(new Set(list)));
}
const typeDefinition = generateModuleDeclaration(paths, cache);
fs__default.writeFileSync(typesFilePath, typeDefinition);
logger.info(`[async-import] ${paths === void 0 ? "\u521D\u59CB\u5316" : "\u751F\u6210"}\u7C7B\u578B\u5B9A\u4E49\u6587\u4EF6 ${typesFilePath.replace(`${ROOT_DIR}\\`, "")}`, !enableLogger);
}
generateTypeFile();
logger.info("[async-import] \u5F02\u6B65\u5BFC\u5165\u5904\u7406\u5668\u5DF2\u542F\u7528", !enableLogger);
return {
name: "async-import-processor",
enforce: "post",
// 插件执行时机,在其他处理后执行
async transform(code, id) {
const asyncImports = parseAsyncImports(code);
if (asyncImports.length > 0 && !isApp) {
const magicString = new MagicString__default(code);
const paths = asyncImports.map((item) => item.args[0].value.toString());
generateTypeFile(paths);
for (const { full, args } of asyncImports) {
for (const { start, end, value } of args) {
const target = value.toString();
let resolveId = (await this.resolve(target, id))?.id;
if (resolveId) {
resolveId = moduleIdProcessor(resolveId);
}
AsyncImportsInstance.addCache(moduleIdProcessor(id), target, resolveId);
magicString.overwrite(full.start, full.start + "AsyncImport".length, "import", { contentOnly: true });
}
}
return {
code: magicString.toString(),
map: magicString.generateMap({ hires: true })
};
}
},
renderDynamicImport(options2) {
const cache = AsyncImportsInstance.getCache(moduleIdProcessor(options2.moduleId));
if (cache && options2.targetModuleId && !isApp && !isH5) {
const targetModuleId = moduleIdProcessor(options2.targetModuleId).replace(JS_TYPES_RE, "");
const temp = cache.map((item) => ({
value: moduleIdProcessor(item.match(/^(\.\/|\.\.\/)+/) ? path__default.resolve(path__default.dirname(options2.moduleId), item) : getVitePathResolver()(item).replace(SRC_DIR_RE, "src/")),
realPath: AsyncImportsInstance.getRealPath(item)?.[0]
}));
if (temp.some((item) => moduleIdProcessor(item.realPath ?? item.value).replace(JS_TYPES_RE, "") === targetModuleId)) {
return {
left: "AsyncImport(",
right: ")"
};
}
}
},
generateBundle({ format }, bundle) {
if (!["es", "cjs", "iife"].includes(format) || isApp)
return;
const pageComponents = [];
const hashFileMap = Object.entries(bundle).reduce((acc, [file, chunk]) => {
if (chunk.type === "chunk") {
let moduleId = chunk.facadeModuleId ?? void 0;
if (moduleId?.startsWith("uniPage://") || moduleId?.startsWith("uniComponent://")) {
const moduleIds = chunk.moduleIds.filter((id) => id !== moduleId).map((id) => moduleIdProcessor(id));
if (moduleIds.length >= 1 && moduleIds.length < chunk.moduleIds.length) {
moduleId = moduleIds.at(-1);
} else if (!moduleIds.length && chunk.fileName) {
pageComponents.push(chunk);
return acc;
}
}
if (moduleId) {
acc[moduleIdProcessor(moduleId)] = chunk.fileName;
} else {
const temp = chunk.moduleIds.filter((id) => !id.startsWith("\0"));
temp.forEach((id) => {
acc[moduleIdProcessor(id)] = chunk.fileName;
});
}
}
return acc;
}, {});
if (pageComponents.length) {
const chunks = Object.values(bundle);
for (let index = 0; index < chunks.length; index++) {
const chunk = chunks[index];
if (chunk.type === "chunk") {
const targetKey = Object.keys(hashFileMap).find((key) => {
const value = hashFileMap[key];
return typeof value === "string" ? chunk.imports.includes(value) : value.some((item) => chunk.imports.includes(item));
});
if (targetKey) {
const old = typeof hashFileMap[targetKey] === "string" ? [hashFileMap[targetKey]] : hashFileMap[targetKey] || [];
hashFileMap[targetKey] = [...old, chunk.fileName];
}
}
}
}
for (const file in bundle) {
const chunk = bundle[file];
if (chunk.type === "chunk" && chunk.code.includes("AsyncImport")) {
const code = chunk.code;
const asyncImports = parseAsyncImports(code);
if (asyncImports.length > 0) {
const magicString = new MagicString__default(code);
asyncImports.forEach(({ full, args }) => {
args.forEach(({ start, end, value }) => {
const url = value.toString();
if (isMP ? Object.values(hashFileMap).flat().includes(normalizePath(path__default.posix.join(path__default.dirname(chunk.fileName), url))) : Object.values(hashFileMap).flat().map(resolveAssetsPath).includes(url)) {
magicString.overwrite(full.start, full.start + "AsyncImport".length, isMP ? "require.async" : "import", { contentOnly: true });
}
});
});
chunk.code = magicString.toString();
}
}
}
}
};
}
function generateModuleDeclaration(paths, cache) {
const moduleMapEntries = Array.from(/* @__PURE__ */ new Set([...cache || [], ...paths || []]))?.map((p) => {
return ` '${p}': typeof import('${p}')`;
}).join("\n");
return `/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by @uni-ku/bundle-optimizer
export {}
interface ModuleMap {
${moduleMapEntries ? `${moduleMapEntries}
[path: string]: any` : " [path: string]: any"}
}
declare global {
function AsyncImport<T extends keyof ModuleMap>(arg: T): Promise<ModuleMap[T]>
}
`;
}
function SubPackagesOptimization(enableLogger) {
const platform = process.env.UNI_PLATFORM;
const inputDir = process.env.UNI_INPUT_DIR;
if (!platform || !inputDir) {
throw new Error("`UNI_INPUT_DIR` or `UNI_PLATFORM` is not defined");
}
const manifestJson = uniCliShared.parseManifestJsonOnce(inputDir);
const platformOptions = manifestJson[platform] || {};
const optimization = platformOptions.optimization || {};
process.env.UNI_OPT_TRACE = `${!!optimization.subPackages}`;
const pagesJsonPath = path__default.resolve(inputDir, "pages.json");
const jsonStr = fs__default.readFileSync(pagesJsonPath, "utf8");
const { appJson } = uniCliShared.parseMiniProgramPagesJson(jsonStr, platform, { subpackages: true });
const pagesFlat = {
pages: appJson.pages || [],
subPackages: (appJson.subPackages || []).flatMap((pkg) => {
return pkg.pages.map((page) => `${pkg.root}/${page}`.replace(/\/{2,}/g, "/"));
}),
get all() {
return [...this.pages, ...this.subPackages];
}
};
logger.info(`pagesFlat: ${JSON.stringify(pagesFlat, null, 2)}`, true);
process.UNI_SUBPACKAGES = appJson.subPackages || {};
const UNI_SUBPACKAGES = process.UNI_SUBPACKAGES || {};
const subPkgsInfo = Object.values(UNI_SUBPACKAGES);
const normalFilter = ({ independent }) => !independent;
const independentFilter = ({ independent }) => independent;
const map2Root = ({ root }) => `${root.replace(/\/$/, "")}/`;
const subPackageRoots = subPkgsInfo.map(map2Root);
const normalSubPackageRoots = subPkgsInfo.filter(normalFilter).map(map2Root);
subPkgsInfo.filter(independentFilter).map(map2Root);
function moduleIdProcessor$1(id, removeQuery = true) {
return moduleIdProcessor(id, process.env.UNI_INPUT_DIR, removeQuery);
}
const findSubPackages = function(importers) {
return importers.reduce((pkgs, item) => {
const pkgRoot = normalSubPackageRoots.find((root) => moduleIdProcessor$1(item).indexOf(root) === 0);
pkgRoot && pkgs.add(pkgRoot);
return pkgs;
}, /* @__PURE__ */ new Set());
};
const findMainPackage = function(importers) {
const list = importers.filter((item) => {
const id = moduleIdProcessor$1(item);
return !subPackageRoots.some((root) => id.indexOf(root) === 0) && !id.includes("node_modules");
});
return list;
};
const findNodeModules = function(importers) {
const mainPackageList = findMainPackage(importers);
return importers.filter((item) => {
const id = moduleIdProcessor$1(item);
return !mainPackageList.includes(item) && !subPackageRoots.some((root) => id.indexOf(root) === 0) && id.includes("node_modules");
});
};
const findNodeModulesComponent = function(importers) {
const list = findNodeModules(importers);
const nodeModulesComponent = new Set(list.map((item) => moduleIdProcessor$1(item)).filter((name) => name.endsWith(".vue") || name.endsWith(".nvue")));
return nodeModulesComponent;
};
const findMainPackageComponent = function(importers) {
const list = findMainPackage(importers);
const mainPackageComponent = new Set(list.map((item) => moduleIdProcessor$1(item)).filter((name) => name.endsWith(".vue") || name.endsWith(".nvue")));
return mainPackageComponent;
};
const hasEntryFile = function(importers, meta) {
const list = findMainPackage(importers);
return list.some((item) => meta.getModuleInfo(item)?.isEntry);
};
const isVueScript = (moduleInfo) => {
if (!moduleInfo.id || !moduleInfo.importers?.length) {
return false;
}
const importer = moduleIdProcessor$1(moduleInfo.importers[0]);
const id = moduleInfo.id;
const clearId = moduleIdProcessor$1(id, false);
const parsedUrl = parseQuerystring(clearId);
return parsedUrl && parsedUrl.type === "script" && parsedUrl.vue && importer === moduleIdProcessor$1(id);
};
logger.info("[optimization] \u5206\u5305\u4F18\u5316\u63D2\u4EF6\u5DF2\u542F\u7528", !enableLogger);
return {
name: "uniapp-subpackages-optimization",
enforce: "post",
// 控制执行顺序,post 保证在其他插件之后执行
config(config, { command }) {
if (!platform.startsWith("mp")) {
logger.warn("[optimization] \u5206\u5305\u4F18\u5316\u63D2\u4EF6\u4EC5\u9700\u5728\u5C0F\u7A0B\u5E8F\u5E73\u53F0\u542F\u7528\uFF0C\u8DF3\u8FC7", !enableLogger);
return;
}
const UNI_OPT_TRACE = process.env.UNI_OPT_TRACE === "true";
logger.info(`[optimization] \u5206\u5305\u4F18\u5316\u5F00\u542F\u72B6\u6001: ${UNI_OPT_TRACE}`, false);
if (!UNI_OPT_TRACE)
return;
const originalOutput = config?.build?.rollupOptions?.output;
const existingManualChunks = Array.isArray(originalOutput) ? originalOutput[0]?.manualChunks : originalOutput?.manualChunks;
const mergedManualChunks = (id, meta) => {
function getDependencyGraph(startId, getRelated = (info) => info.importers) {
const visited = /* @__PURE__ */ new Set();
const result = [];
function traverse(currentId, getRelated2) {
if (visited.has(currentId))
return;
visited.add(currentId);
result.push(currentId);
const moduleInfo = meta.getModuleInfo(currentId);
if (!moduleInfo)
return;
getRelated2(moduleInfo).forEach((relatedId) => {
traverse(relatedId, getRelated2);
});
}
traverse(startId, getRelated);
return result;
}
const normalizedId = normalizePath(id);
const filename = normalizedId.split("?")[0];
let mainFlag = false;
if (EXTNAME_JS_RE.test(filename) && (filename.startsWith(normalizePath(inputDir)) || filename.includes("node_modules"))) {
const moduleInfo = meta.getModuleInfo(id);
if (!moduleInfo) {
throw new Error(`moduleInfo is not found: ${id}`);
}
const importersGraph = getDependencyGraph(id);
const newMatchSubPackages = findSubPackages(importersGraph);
const newMainPackageComponent = findMainPackageComponent(importersGraph);
const nodeModulesComponent = findNodeModulesComponent(importersGraph);
const isEntry = hasEntryFile(importersGraph, meta);
if (!isEntry && newMatchSubPackages.size === 1 && newMainPackageComponent.size === 0 && nodeModulesComponent.size === 0) {
logger.info(`[optimization] \u5B50\u5305: ${[...newMatchSubPackages].join(", ")} <- ${filename}`, !enableLogger);
return `${newMatchSubPackages.values().next().value}common/vendor`;
}
mainFlag = id;
}
if (existingManualChunks && typeof existingManualChunks === "function") {
const result = existingManualChunks(id, meta);
if (result === void 0) {
const moduleInfo = meta.getModuleInfo(id);
if (moduleInfo) {
const clearId = moduleIdProcessor$1(moduleInfo.id);
if (mainFlag === id && !moduleInfo.isEntry && !findNodeModules([moduleInfo.id]).length) {
logger.info(`[optimization] \u4E3B\u5305\u5185\u5BB9\u5F3A\u5236\u843D\u76D8: ${clearId}`, !enableLogger);
return clearId;
}
if (isVueScript(moduleInfo) && !path__default.isAbsolute(clearId)) {
const target = clearId.replace(knownJsSrcRE, "");
if (!pagesFlat.all.includes(target)) {
logger.info(`[optimization] \u89C4\u6574 vue-script \u6A21\u5757: ${target} -> ${target}-vendor`, !enableLogger);
return `${target}-vendor`;
}
}
}
}
logger.warn(`[optimization] default: ${result} <- ${id}`, !enableLogger);
return result;
}
};
return {
build: {
rollupOptions: {
output: {
manualChunks: mergedManualChunks
}
}
}
};
}
};
}
const index = (options = {}) => {
const parse = new ParseOptions(options);
let logToFile = options.logToFile;
if (logToFile) {
logToFile = typeof logToFile === "string" ? logToFile : `node_modules/.cache/${name}/logs.log`;
if (typeof logToFile !== "string") {
logger.warn("logToFile should be a string, using default path: node_modules/.cache/bundle-optimizer/logs.log");
logToFile = `node_modules/.cache/${name}/logs.log`;
}
ensureDirectoryExists(logToFile);
try {
fs__default.unlinkSync(logToFile);
} catch (error) {
logger.error(`Failed to delete old log file: ${error}`);
}
logger.onLog = (context, level, message, timestamp) => {
const line = `${context} ${level} [${timestamp}]: ${message}`;
fs__default.writeFileSync(logToFile, `${line}
`, { flag: "a" });
};
}
return [
{
name: "optimization:initialized",
config(config) {
initializeVitePathResolver(config);
}
},
// 分包优化
parse.enable.optimization && SubPackagesOptimization(parse.logger.optimization),
// js/ts插件的异步调用
// 处理 `AsyncImport` 函数调用的路径传参
parse.enable["async-import"] && AsyncImportProcessor(parse.dts["async-import"], parse.logger["async-import"]),
// vue组件的异步调用
// 处理 `.vue?async` 查询参数的静态导入
parse.enable["async-component"] && AsyncComponentProcessor(parse.dts["async-component"], parse.logger["async-component"])
];
};
module.exports = index;