// Pruning shared by the runtime staging scripts (Synara and Open Slide). // // Both runtimes are npm production closures that Tauri copies into the app // whole. Packages publish a great deal no runtime ever loads: source maps, // typings, tests, docs, binaries for other platforms, npm's .bin links. This // module removes only the categories named below — never "everything a keep // list misses" — and it protects every file a package.json entry point names, // then checks those files survived. LICENSE, NOTICE, and similar files are // never candidates: the license obligations travel with the code. import { chmodSync, closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { basename, dirname, join, relative } from "node:path"; import { readJson, removePath, walkFiles } from "./util.mjs"; // Package-root files that only describe the package to people. const PACKAGE_DOC_FILE = /^(?:readme|changelog|changes|history|upgrading|migrating|contributing|code_of_conduct|security)(?:\.(?:md|markdown|txt|rst))?$/i; // Package-root directories that hold docs, examples, or tests. A package that // names one of them as an entry point keeps it (see protectedEntryFiles). const PACKAGE_DOC_DIRECTORIES = new Set([ ".github", "__tests__", "benchmark", "benchmarks", "doc", "docs", "example", "examples", "test", "tests", ]); const SOURCE_MAP = /\.map$/; const DECLARATION = /(?:\.d\.(?:ts|mts|cts)|\.flow)$/; const TYPESCRIPT_SOURCE = /\.(?:ts|mts|cts|tsx)$/; const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/; const BUILD_METADATA = /\.(?:pdb|tsbuildinfo)$/; // Only these can be native code worth inspecting; reading the magic of every // JavaScript file would cost the walk for nothing. const NATIVE_CANDIDATE = /(?:^[^.]+$|\.(?:node|so|dylib|dll|exe)$|\.so\.\d+(?:\.\d+)*$)/; const MACH_O_CPU_ARM64 = 0x0100000c; const MACH_O_CPU_X86_64 = 0x01000007; function readHeader(path) { const descriptor = openSync(path, "r"); try { const header = Buffer.alloc(8); const length = readSync(descriptor, header, 0, header.length, 0); return header.subarray(0, length); } finally { closeSync(descriptor); } } /** * True for a binary macOS on `platform` can never load: an ELF or PE file, or * a thin Mach-O Node addon for the other CPU (an arm64 Node cannot dlopen an * x86_64 addon). A thin x86_64 executable could still run under Rosetta, and * universal binaries cover both CPUs, so neither is foreign. */ export function isForeignBinary(path, platform) { if (!NATIVE_CANDIDATE.test(basename(path))) return false; const header = readHeader(path); if (header.length < 4) return false; if (header.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))) return !platform.startsWith("linux-"); if (header[0] === 0x4d && header[1] === 0x5a && /\.(?:node|dll|exe)$/i.test(path)) return !platform.startsWith("win32-"); if (header.length === 8 && header.readUInt32LE(0) === 0xfeedfacf && path.endsWith(".node")) { const cpu = header.readUInt32LE(4); if (platform === "darwin-arm64") return cpu !== MACH_O_CPU_ARM64; if (platform === "darwin-x64") return cpu !== MACH_O_CPU_X86_64; return true; } return false; } /** Every installed package directory below `nodeModules`, nested installs included. */ function packageDirectories(nodeModules, found = []) { if (!existsSync(nodeModules)) return found; for (const entry of readdirSync(nodeModules, { withFileTypes: true })) { if (!entry.isDirectory() || entry.name.startsWith(".")) continue; const path = join(nodeModules, entry.name); const candidates = entry.name.startsWith("@") ? readdirSync(path, { withFileTypes: true }).filter((child) => child.isDirectory()).map((child) => join(path, child.name)) : [path]; for (const directory of candidates) { if (!existsSync(join(directory, "package.json"))) continue; found.push(directory); packageDirectories(join(directory, "node_modules"), found); } } return found; } // Every string target in an `exports`/`imports` map except type-only // conditions, which only an editor or a typechecker resolves. function conditionalTargets(value, targets) { if (typeof value === "string") targets.push(value); else if (Array.isArray(value)) for (const item of value) conditionalTargets(item, targets); else if (value && typeof value === "object") { for (const [key, child] of Object.entries(value)) { if (key === "types" || key === "typings" || key.startsWith("types@")) continue; conditionalTargets(child, targets); } } return targets; } /** * The files a package's manifest names as code to load: main, module, * browser, bin, and the non-type targets of exports and imports. Each target * resolves the way Node's legacy resolver would (extension, then index file); * targets with a `*` pattern protect nothing by themselves, so the categories * above must stay ones a pattern export cannot reasonably name. */ function protectedEntryFiles(packageDirectory) { const manifest = readJson(join(packageDirectory, "package.json")); const targets = [manifest.main ?? "index.js", manifest.module]; if (typeof manifest.browser === "string") targets.push(manifest.browser); else if (manifest.browser && typeof manifest.browser === "object") { targets.push(...Object.values(manifest.browser).filter((value) => typeof value === "string")); } if (typeof manifest.bin === "string") targets.push(manifest.bin); else if (manifest.bin && typeof manifest.bin === "object") targets.push(...Object.values(manifest.bin)); conditionalTargets(manifest.exports, targets); conditionalTargets(manifest.imports, targets); const files = new Set([join(packageDirectory, "package.json")]); for (const target of targets) { if (typeof target !== "string" || target.includes("*") || !target.startsWith(".") && target.includes(":")) continue; const base = join(packageDirectory, target); const candidates = [base, ...[".js", ".mjs", ".cjs", ".json", ".node"].map((extension) => base + extension)]; candidates.push(...["index.js", "index.mjs", "index.cjs", "index.json"].map((index) => join(base, index))); const resolved = candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile()); if (resolved) files.add(resolved); } return files; } // esbuild's postinstall replaces its JavaScript `bin/esbuild` shim with a // copy of the platform binary so the CLI skips Node's startup. Its JavaScript // API never runs that copy — it resolves @esbuild/ itself — so the // copy only doubles a 10 MB binary. A launcher keeps the CLI working. function restoreEsbuildLaunchers(packages, platform) { if (platform.startsWith("win32-")) return 0; let removedBytes = 0; for (const directory of packages) { if (basename(directory) !== "esbuild" || basename(dirname(directory)) !== "node_modules") continue; const copy = join(directory, "bin/esbuild"); const binary = join(dirname(directory), "@esbuild", platform, "bin/esbuild"); if (!existsSync(copy) || !existsSync(binary) || !readFileSync(copy).equals(readFileSync(binary))) continue; const launcher = `#!/bin/sh\nexec "$(dirname -- "$0")/../../@esbuild/${platform}/bin/esbuild" "$@"\n`; removedBytes += removePath(copy) - Buffer.byteLength(launcher); writeFileSync(copy, launcher); chmodSync(copy, 0o755); } return removedBytes; } /** * Remove what no runtime loads from the production install at `nodeModules` * and return the bytes removed. * * - `platform`: the npm platform the runtime ships for, e.g. "darwin-arm64". * - `typeScriptSources`: whether TypeScript sources go too. Node refuses to * strip types under node_modules, so a Node-only runtime can never load * them; a runtime that serves packages through Vite can. * * @param {string} nodeModules * @param {{ platform: string, typeScriptSources: "keep" | "remove" }} options */ export function pruneNodeModules(nodeModules, { platform, typeScriptSources }) { if (typeScriptSources !== "keep" && typeScriptSources !== "remove") { throw new Error(`typeScriptSources must be "keep" or "remove", not ${typeScriptSources}`); } const packages = packageDirectories(nodeModules); const entries = new Set(); for (const directory of packages) for (const file of protectedEntryFiles(directory)) entries.add(file); const entryDirectories = new Set(); for (const file of entries) { for (let directory = dirname(file); directory.startsWith(nodeModules); directory = dirname(directory)) { entryDirectories.add(directory); } } const protects = (path) => entries.has(path) || entryDirectories.has(path); let removedBytes = 0; // Tauri's resource walker copies a link's target, so every npm .bin link // ships as a second full copy of the executable it names. Nothing in a // bundled runtime runs package scripts through .bin. removedBytes += removePath(join(nodeModules, ".bin")); for (const directory of packages) removedBytes += removePath(join(directory, "node_modules/.bin")); for (const directory of packages) { for (const entry of readdirSync(directory, { withFileTypes: true })) { const path = join(directory, entry.name); const removable = entry.isDirectory() ? PACKAGE_DOC_DIRECTORIES.has(entry.name) : PACKAGE_DOC_FILE.test(entry.name); if (removable && !protects(path)) removedBytes += removePath(path); } } for (const path of walkFiles(nodeModules)) { if (entries.has(path)) continue; const name = basename(path); if ( SOURCE_MAP.test(name) || DECLARATION.test(name) || BUILD_METADATA.test(name) || TEST_FILE.test(name) || (typeScriptSources === "remove" && TYPESCRIPT_SOURCE.test(name)) || isForeignBinary(path, platform) ) { removedBytes += removePath(path); } } removedBytes += restoreEsbuildLaunchers(packages, platform); const missing = [...entries].filter((file) => !existsSync(file)); if (missing.length > 0) { throw new Error(`Pruning removed package entry points:\n${missing.map((file) => ` ${relative(nodeModules, file)}`).join("\n")}`); } return removedBytes; }