Optimize Windows packaged size pruning

This commit is contained in:
PerishCode
2026-05-06 11:25:23 +08:00
parent be166572fe
commit 19956ee4e6
3 changed files with 205 additions and 9 deletions
@@ -317,6 +317,50 @@ async function pruneBrokenSymlinks(root, current = root, removedPaths = [], reas
return removedPaths;
}
function isSourceBuildResidue(relativePath) {
const normalized = relativePath.split(path.sep).join("/");
return normalized.endsWith(".map") || normalized.endsWith(".tsbuildinfo");
}
async function pruneMatchingFilesSummary(root, includeRelativePath, reason) {
const summary = {
bytes: 0,
count: 0,
reason,
root,
};
async function visit(current) {
let metadata;
try {
metadata = await lstat(current);
} catch {
return;
}
if (metadata.isDirectory()) {
const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
for (const entry of entries) {
await visit(path.join(current, entry.name));
}
return;
}
const relativePath = path.relative(root, current);
if (relativePath.length === 0 || !includeRelativePath(relativePath)) return;
summary.bytes += metadata.size;
summary.count += 1;
await rm(current, { force: true });
}
await visit(root);
return summary.count > 0 ? [summary] : [];
}
async function pruneSourceBuildResidue(root, reason) {
return await pruneMatchingFilesSummary(root, isSourceBuildResidue, reason);
}
function isForbiddenCopiedEntry(relativePath, platformName) {
const normalized = relativePath.split(path.sep).join("/");
const withRootSlash = `/${normalized}`;
@@ -378,6 +422,7 @@ async function auditCopiedStandalone(config, installResult, platformName) {
const staticRoot = path.join(installResult.destinationWebRoot, ".next", "static");
const publicRoot = path.join(installResult.destinationWebRoot, "public");
const nodeModulesRoot = path.join(installResult.destinationRoot, "node_modules");
const webNodeModulesRoot = path.join(installResult.destinationWebRoot, "node_modules");
const requiredPaths = [serverPath, staticRoot, nodeModulesRoot];
if (await pathExists(config.webPublicSourceRoot)) requiredPaths.push(publicRoot);
@@ -413,6 +458,8 @@ async function auditCopiedStandalone(config, installResult, platformName) {
resolvedModules,
serverPath,
symlinks: closureStats.symlinks,
webNextBytes: await sizePathBytes(path.join(webNodeModulesRoot, "next")),
webNodeModulesBytes: await sizePathBytes(webNodeModulesRoot),
};
}
@@ -458,6 +505,39 @@ async function pruneRootSharp(appNodeModulesRoot) {
return removedPaths;
}
async function pruneRootWebPackage(appNodeModulesRoot, platformName) {
if (platformName !== "win32") return [];
const webPackageRoot = path.join(appNodeModulesRoot, "@open-design", "web");
const removedPaths = [];
for (const entry of [".next", "app", "next.config.ts", "public", "src"]) {
await removePathAndRecord(
path.join(webPackageRoot, entry),
"root @open-design/web standalone-safe package residue",
removedPaths,
);
}
return removedPaths;
}
async function auditRootWebPackage(appNodeModulesRoot) {
const webPackageRoot = path.join(appNodeModulesRoot, "@open-design", "web");
const packageJsonPath = path.join(webPackageRoot, "package.json");
const sidecarEntryPath = path.join(webPackageRoot, "dist", "sidecar", "index.js");
for (const requiredPath of [packageJsonPath, sidecarEntryPath]) {
if (!(await pathExists(requiredPath))) {
throw new Error(`[tools-pack web-standalone] root @open-design/web audit missing: ${requiredPath}`);
}
}
return {
bytes: await sizePathBytes(webPackageRoot),
packageJsonPath,
retainedDistBytes: await sizePathBytes(path.join(webPackageRoot, "dist")),
sidecarEntryPath,
webPackageRoot,
};
}
async function auditNoBrokenSymlinks(root, label) {
const stats = await collectClosureStats(root);
if (stats.brokenSymlinks.length > 0) {
@@ -485,6 +565,9 @@ async function runWebStandaloneAfterPack(context) {
const appNodeModulesRoot = resolveRootAppNodeModulesRoot(resourcesRoot);
const installResult = await installStandaloneResource(config, resourcesRoot, context.electronPlatformName);
const copiedPrune = config.pruneCopiedSharp ? await pruneCopiedSharp(installResult.destinationRoot) : [];
const copiedBuildResiduePrune = context.electronPlatformName === "win32"
? await pruneSourceBuildResidue(installResult.destinationRoot, "copied standalone source/build residue")
: [];
const brokenSymlinkPrune = await pruneBrokenSymlinks(
installResult.destinationRoot,
installResult.destinationRoot,
@@ -494,6 +577,13 @@ async function runWebStandaloneAfterPack(context) {
const copiedAudit = await auditCopiedStandalone(config, installResult, context.electronPlatformName);
const rootPrune = config.pruneRootNext ? await pruneRootNext(appNodeModulesRoot, context.electronPlatformName) : [];
const rootSharpPrune = config.pruneRootSharp ? await pruneRootSharp(appNodeModulesRoot) : [];
const rootWebPackagePrune = await pruneRootWebPackage(appNodeModulesRoot, context.electronPlatformName);
const rootBuildResiduePrune = context.electronPlatformName === "win32"
? await pruneSourceBuildResidue(appNodeModulesRoot, "root app source/build residue")
: [];
const rootWebPackageAudit = context.electronPlatformName === "win32"
? await auditRootWebPackage(appNodeModulesRoot)
: null;
const rootBrokenSymlinkPrune = await pruneBrokenSymlinks(
appNodeModulesRoot,
appNodeModulesRoot,
@@ -508,14 +598,18 @@ async function runWebStandaloneAfterPack(context) {
appPath,
brokenSymlinkPrune,
copiedAudit,
copiedBuildResiduePrune,
copiedPrune,
generatedAt: new Date().toISOString(),
platformName: context.electronPlatformName,
resourcesRoot,
rootBuildResiduePrune,
rootBrokenSymlinkPrune,
rootPrune,
rootSharpPrune,
rootSymlinkAudit,
rootWebPackageAudit,
rootWebPackagePrune,
sourceWebRoot: installResult.sourceWebRoot,
version: 1,
};
+107 -7
View File
@@ -37,6 +37,20 @@ const PRODUCT_NAME = "Open Design";
const DESKTOP_LOG_ECHO_ENV = "OD_DESKTOP_LOG_ECHO";
const WEB_STANDALONE_HOOK_CONFIG_ENV = "OD_TOOLS_PACK_WEB_STANDALONE_HOOK_CONFIG";
const WEB_STANDALONE_RESOURCE_NAME = "open-design-web-standalone";
const ELECTRON_BUILDER_ASAR = false;
const ELECTRON_BUILDER_FILE_PATTERNS = [
"**/*",
"!**/node_modules/.bin",
"!**/node_modules/electron{,/**/*}",
"!**/*.map",
"!**/*.tsbuildinfo",
"!**/.next/cache",
"!**/.next/cache/**",
"!**/node_modules/better-sqlite3/build/Release/obj",
"!**/node_modules/better-sqlite3/build/Release/obj/**",
"!**/node_modules/better-sqlite3/deps",
"!**/node_modules/better-sqlite3/deps/**",
] as const;
const NSIS_INSTALLER_LANGUAGE_BY_WEB_LOCALE = {
en: "en_US",
fa: "fa_IR",
@@ -110,6 +124,7 @@ export type WinPackResult = {
export type WinSizeReport = {
builder: {
asar: boolean;
filePatterns: readonly string[];
targets: Array<"dir" | "nsis">;
webOutputMode: ToolPackConfig["webOutputMode"];
};
@@ -118,14 +133,39 @@ export type WinSizeReport = {
outputRootBytes: number;
resourceRootBytes: number;
runtimeNamespaceRoot: string;
topLevel: {
appResourcesBytes: number;
copiedStandaloneBytes: number;
electronLocalesBytes: number;
resourcesBytes: number;
};
tracked: {
appNodeModulesBytes: number;
betterSqlite3Bytes: number;
betterSqlite3SourceResidueBytes: number;
bundledNodeBytes: number;
copiedStandaloneNextBytes: number;
copiedStandaloneNextSwcBytes: number;
copiedStandaloneNodeModulesBytes: number;
copiedStandaloneSharpLibvipsBytes: number;
copiedStandaloneSourcemapBytes: number;
copiedStandaloneTsbuildInfoBytes: number;
copiedStandaloneWebNextBytes: number;
copiedStandaloneWebNodeModulesBytes: number;
electronLocalesBytes: number;
markdownBytes: number;
nextBytes: number;
nextSwcBytes: number;
sharpLibvipsBytes: number;
sourcemapBytes: number;
tsbuildInfoBytes: number;
webCopiedStandaloneBytes: number;
webNextCacheBytes: number;
webPackageAppBytes: number;
webPackageBytes: number;
webPackageDistBytes: number;
webPackagePublicBytes: number;
webPackageSrcBytes: number;
webPackageStandaloneBytes: number;
};
unpackedBytes: number | null;
@@ -449,6 +489,23 @@ async function sizeExistingFileBytes(path: string): Promise<number | null> {
return metadata == null ? null : metadata.size;
}
async function sumChildDirectorySizes(path: string, includeChild: (name: string) => boolean): Promise<number> {
const entries = await readdir(path, { withFileTypes: true }).catch(() => []);
let total = 0;
for (const entry of entries) {
if (!entry.isDirectory() || !includeChild(entry.name)) continue;
total += await sizePathBytes(join(path, entry.name));
}
return total;
}
function isBetterSqlite3SourceResidue(path: string): boolean {
return (
path.includes("/node_modules/better-sqlite3/deps/") ||
path.includes("/node_modules/better-sqlite3/build/Release/obj/")
);
}
async function listChildDirectories(root: string): Promise<string[]> {
try {
const entries = await readdir(root, { withFileTypes: true });
@@ -835,7 +892,7 @@ async function runElectronBuilder(config: ToolPackConfig, paths: WinPaths): Prom
const builderConfig = {
appId: "io.open-design.desktop",
afterPack: webStandaloneHookConfigPath == null ? undefined : winResources.webStandaloneAfterPackHook,
asar: false,
asar: ELECTRON_BUILDER_ASAR,
buildDependenciesFromSource: false,
compression: "maximum",
directories: { output: paths.appBuilderOutputRoot },
@@ -852,7 +909,7 @@ async function runElectronBuilder(config: ToolPackConfig, paths: WinPaths): Prom
{ from: paths.resourceRoot, to: "open-design" },
{ from: paths.packagedConfigPath, to: "open-design-config.json" },
],
files: ["**/*", "!**/node_modules/.bin", "!**/node_modules/electron{,/**/*}"],
files: [...ELECTRON_BUILDER_FILE_PATTERNS],
forceCodeSigning: false,
icon: paths.winIconPath,
nodeGypRebuild: false,
@@ -946,9 +1003,15 @@ async function writeLocalLatestYml(config: ToolPackConfig, paths: WinPaths): Pro
async function collectWinSizeReport(config: ToolPackConfig, paths: WinPaths): Promise<WinSizeReport> {
const appResourcesRoot = join(paths.unpackedRoot, "resources");
const appNodeModulesRoot = join(appResourcesRoot, "app", "node_modules");
const copiedStandaloneRoot = join(appResourcesRoot, WEB_STANDALONE_RESOURCE_NAME);
const copiedStandaloneNodeModulesRoot = join(copiedStandaloneRoot, "node_modules");
const copiedStandaloneWebNodeModulesRoot = join(copiedStandaloneRoot, "apps", "web", "node_modules");
const electronLocalesRoot = join(paths.unpackedRoot, "locales");
const rootWebPackageRoot = join(appNodeModulesRoot, "@open-design", "web");
return {
builder: {
asar: false,
asar: ELECTRON_BUILDER_ASAR,
filePatterns: ELECTRON_BUILDER_FILE_PATTERNS,
targets: resolveWinTargets(config.to),
webOutputMode: config.webOutputMode,
},
@@ -957,15 +1020,52 @@ async function collectWinSizeReport(config: ToolPackConfig, paths: WinPaths): Pr
outputRootBytes: await sizePathBytes(config.roots.output.namespaceRoot),
resourceRootBytes: await sizePathBytes(paths.resourceRoot),
runtimeNamespaceRoot: config.roots.runtime.namespaceRoot,
topLevel: {
appResourcesBytes: await sizePathBytes(join(appResourcesRoot, "app")),
copiedStandaloneBytes: await sizePathBytes(copiedStandaloneRoot),
electronLocalesBytes: await sizePathBytes(electronLocalesRoot),
resourcesBytes: await sizePathBytes(appResourcesRoot),
},
tracked: {
appNodeModulesBytes: await sizePathBytes(appNodeModulesRoot),
betterSqlite3Bytes: await sizePathBytes(join(appNodeModulesRoot, "better-sqlite3")),
betterSqlite3SourceResidueBytes: await sizePathBytes(paths.unpackedRoot, {
includeFile: isBetterSqlite3SourceResidue,
}),
bundledNodeBytes: await sizePathBytes(join(paths.resourceRoot, "bin", "node.exe")),
copiedStandaloneNextBytes:
await sizePathBytes(join(copiedStandaloneNodeModulesRoot, "next")) +
await sizePathBytes(join(copiedStandaloneWebNodeModulesRoot, "next")),
copiedStandaloneNextSwcBytes:
await sumChildDirectorySizes(join(copiedStandaloneNodeModulesRoot, "@next"), (name) => name.startsWith("swc-win32-")) +
await sumChildDirectorySizes(join(copiedStandaloneWebNodeModulesRoot, "@next"), (name) => name.startsWith("swc-win32-")),
copiedStandaloneNodeModulesBytes: await sizePathBytes(copiedStandaloneNodeModulesRoot),
copiedStandaloneSharpLibvipsBytes: await sizePathBytes(
join(copiedStandaloneNodeModulesRoot, "@img", "sharp-libvips-win32-x64"),
),
copiedStandaloneSourcemapBytes: await sizePathBytes(copiedStandaloneRoot, {
includeFile: (path) => path.endsWith(".map"),
}),
copiedStandaloneTsbuildInfoBytes: await sizePathBytes(copiedStandaloneRoot, {
includeFile: (path) => path.endsWith(".tsbuildinfo"),
}),
copiedStandaloneWebNextBytes: await sizePathBytes(join(copiedStandaloneWebNodeModulesRoot, "next")),
copiedStandaloneWebNodeModulesBytes: await sizePathBytes(copiedStandaloneWebNodeModulesRoot),
electronLocalesBytes: await sizePathBytes(electronLocalesRoot),
markdownBytes: await sizePathBytes(paths.unpackedRoot, { includeFile: (path) => path.endsWith(".md") }),
nextBytes: await sizePathBytes(join(appNodeModulesRoot, "next")),
nextSwcBytes: await sumChildDirectorySizes(join(appNodeModulesRoot, "@next"), (name) => name.startsWith("swc-win32-")),
sharpLibvipsBytes: await sizePathBytes(join(appNodeModulesRoot, "@img", "sharp-libvips-win32-x64")),
sourcemapBytes: await sizePathBytes(paths.unpackedRoot, { includeFile: (path) => path.endsWith(".map") }),
tsbuildInfoBytes: await sizePathBytes(paths.unpackedRoot, { includeFile: (path) => path.endsWith(".tsbuildinfo") }),
webCopiedStandaloneBytes: await sizePathBytes(join(appResourcesRoot, WEB_STANDALONE_RESOURCE_NAME)),
webNextCacheBytes: await sizePathBytes(join(appNodeModulesRoot, "@open-design", "web", ".next", "cache")),
webPackageBytes: await sizePathBytes(join(appNodeModulesRoot, "@open-design", "web")),
webPackageStandaloneBytes: await sizePathBytes(join(appNodeModulesRoot, "@open-design", "web", ".next", "standalone")),
webCopiedStandaloneBytes: await sizePathBytes(copiedStandaloneRoot),
webNextCacheBytes: await sizePathBytes(join(rootWebPackageRoot, ".next", "cache")),
webPackageAppBytes: await sizePathBytes(join(rootWebPackageRoot, "app")),
webPackageBytes: await sizePathBytes(rootWebPackageRoot),
webPackageDistBytes: await sizePathBytes(join(rootWebPackageRoot, "dist")),
webPackagePublicBytes: await sizePathBytes(join(rootWebPackageRoot, "public")),
webPackageSrcBytes: await sizePathBytes(join(rootWebPackageRoot, "src")),
webPackageStandaloneBytes: await sizePathBytes(join(rootWebPackageRoot, ".next", "standalone")),
},
unpackedBytes: (await pathExists(paths.unpackedRoot)) ? await sizePathBytes(paths.unpackedRoot) : null,
};