mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
Reduce App package sizes (#453)
## Summary - Trim Electron app packaging to only include runtime shell assets and remove unused workspace runtime dependencies. - Copy server resources explicitly, filtering stale build artifacts and pruning known non-runtime dependency files. - Bundle only the Node executable instead of the full Node distribution. ## Validation - Built macOS Electron artifacts with `npm run build:mac --workspace @neuralnomads/codenomad-electron-app`. - Ran packaged server CLI smoke test: `dist/bin.js --version` returned `0.16.0`. - Ran packaged dependency import smoke test for pruned runtime dependencies. ## Size Impact - macOS x64 zip: 161M - macOS arm64 zip: 161M - macOS arm64 app bundle: 435M - Packaged server node_modules: 25M
This commit is contained in:
parent
f019fcc2b7
commit
3ccdd36ad8
10 changed files with 387 additions and 135 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -13409,8 +13409,6 @@
|
|||
"version": "0.16.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codenomad/ui": "file:../ui",
|
||||
"@neuralnomads/codenomad": "file:../server",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -80,9 +80,9 @@ export default defineConfig({
|
|||
port: 3000,
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
cssMinify: false,
|
||||
sourcemap: true,
|
||||
minify: true,
|
||||
cssMinify: true,
|
||||
sourcemap: false,
|
||||
outDir: resolve(__dirname, "dist/renderer"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
|
|
|
|||
|
|
@ -37,11 +37,10 @@
|
|||
"build:all": "node scripts/build.js all",
|
||||
"package:mac": "node scripts/build.js mac",
|
||||
"package:win": "node scripts/build.js win",
|
||||
"package:linux": "node scripts/build.js linux"
|
||||
"package:linux": "node scripts/build.js linux",
|
||||
"smoke:resources": "node ../../scripts/smoke-packaged-resources.cjs --resources electron/resources --loading dist/renderer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@neuralnomads/codenomad": "file:../server",
|
||||
"@codenomad/ui": "file:../ui",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -66,7 +65,10 @@
|
|||
"buildResources": "electron/resources"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"dist/main/**/*",
|
||||
"dist/preload/**/*",
|
||||
"dist/renderer/loading.html",
|
||||
"dist/renderer/assets/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extraResources": [
|
||||
|
|
|
|||
|
|
@ -141,6 +141,20 @@ async function build(platform) {
|
|||
env: { NODE_PATH: workspaceNodeModulesPath, CODENOMAD_NODE_TARGET: job.nodeTarget },
|
||||
})
|
||||
|
||||
console.log(`\n🔎 Validating resources for ${job.nodeTarget}...\n`)
|
||||
await run(process.execPath, [
|
||||
join(workspaceRoot, "scripts", "smoke-packaged-resources.cjs"),
|
||||
"--resources",
|
||||
join(appDir, "electron", "resources"),
|
||||
"--loading",
|
||||
join(appDir, "dist", "renderer"),
|
||||
"--target",
|
||||
job.nodeTarget,
|
||||
], {
|
||||
cwd: workspaceRoot,
|
||||
shell: false,
|
||||
})
|
||||
|
||||
console.log(`\n📦 Packaging ${job.nodeTarget}...\n`)
|
||||
await run(npxCmd, ["electron-builder", "--publish=never", ...job.args], {
|
||||
env: { CODENOMAD_NODE_TARGET: job.nodeTarget },
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ const serverDest = join(resourcesRoot, "server")
|
|||
const npmExecPath = process.env.npm_execpath
|
||||
const npmNodeExecPath = process.env.npm_node_execpath
|
||||
const { prepareBundledNodeRuntime } = require(join(workspaceRoot, "scripts", "prepare-node-runtime.cjs"))
|
||||
const { copyPackagedServerResources } = require(join(workspaceRoot, "scripts", "desktop-server-resources.cjs"))
|
||||
|
||||
const serverSources = ["dist", "public", "node_modules", "package.json"]
|
||||
const serverDepsMarker = join(serverRoot, "node_modules", "fastify", "package.json")
|
||||
|
||||
function log(message) {
|
||||
|
|
@ -68,65 +68,10 @@ function ensureServerDependencies() {
|
|||
}
|
||||
}
|
||||
|
||||
function copyServerArtifacts() {
|
||||
fs.rmSync(serverDest, { recursive: true, force: true })
|
||||
fs.mkdirSync(serverDest, { recursive: true })
|
||||
|
||||
for (const name of serverSources) {
|
||||
const from = join(serverRoot, name)
|
||||
const to = join(serverDest, name)
|
||||
if (!fs.existsSync(from)) {
|
||||
throw new Error(`Missing required server artifact: ${from}`)
|
||||
}
|
||||
fs.cpSync(from, to, { recursive: true, dereference: true })
|
||||
log(`copied ${name} to Electron resources`)
|
||||
}
|
||||
}
|
||||
|
||||
function stripNodeModuleBins() {
|
||||
const root = join(serverDest, "node_modules")
|
||||
if (!fs.existsSync(root)) {
|
||||
return
|
||||
}
|
||||
|
||||
const stack = [root]
|
||||
let removed = 0
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()
|
||||
if (!current) break
|
||||
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const full = join(current, entry.name)
|
||||
if (entry.name === ".bin") {
|
||||
fs.rmSync(full, { recursive: true, force: true })
|
||||
removed += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(full)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
log(`removed ${removed} node_modules/.bin directories`)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
ensureServerBuild()
|
||||
ensureServerDependencies()
|
||||
copyServerArtifacts()
|
||||
stripNodeModuleBins()
|
||||
copyPackagedServerResources({ serverRoot, serverDest, log })
|
||||
await prepareBundledNodeRuntime({ resourcesRoot })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
"sync:version": "node ./scripts/sync-tauri-version.js",
|
||||
"prebuild": "node ./scripts/prebuild.js",
|
||||
"bundle:server": "npm run prebuild",
|
||||
"build": "tauri build"
|
||||
"build": "tauri build",
|
||||
"smoke:resources": "node ../../scripts/smoke-packaged-resources.cjs --resources src-tauri/resources --loading src-tauri/resources/ui-loading"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.9.4"
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ const serverDest = path.resolve(root, "src-tauri", "resources", "server")
|
|||
const uiLoadingDest = path.resolve(root, "src-tauri", "resources", "ui-loading")
|
||||
const resourcesRoot = path.resolve(root, "src-tauri", "resources")
|
||||
const { prepareBundledNodeRuntime } = require(path.join(workspaceRoot, "scripts", "prepare-node-runtime.cjs"))
|
||||
|
||||
const sources = ["dist", "public", "node_modules", "package.json"]
|
||||
const { copyPackagedServerResources } = require(path.join(workspaceRoot, "scripts", "desktop-server-resources.cjs"))
|
||||
|
||||
const serverInstallCommand =
|
||||
"npm install --omit=dev --ignore-scripts --workspaces=false --package-lock=false --install-strategy=shallow --fund=false --audit=false"
|
||||
|
|
@ -248,60 +247,6 @@ function ensureEsbuildPlatformBinary() {
|
|||
})
|
||||
}
|
||||
|
||||
function copyServerArtifacts() {
|
||||
fs.rmSync(serverDest, { recursive: true, force: true })
|
||||
fs.mkdirSync(serverDest, { recursive: true })
|
||||
|
||||
for (const name of sources) {
|
||||
const from = path.join(serverRoot, name)
|
||||
const to = path.join(serverDest, name)
|
||||
if (!fs.existsSync(from)) {
|
||||
console.warn(`[prebuild] skipped missing ${from}`)
|
||||
continue
|
||||
}
|
||||
fs.cpSync(from, to, { recursive: true, dereference: true })
|
||||
console.log(`[prebuild] copied ${from} -> ${to}`)
|
||||
}
|
||||
}
|
||||
|
||||
function stripNodeModuleBins() {
|
||||
const root = path.join(serverDest, "node_modules")
|
||||
if (!fs.existsSync(root)) {
|
||||
return
|
||||
}
|
||||
|
||||
const stack = [root]
|
||||
let removed = 0
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()
|
||||
if (!current) break
|
||||
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const full = path.join(current, entry.name)
|
||||
if (entry.name === ".bin") {
|
||||
fs.rmSync(full, { recursive: true, force: true })
|
||||
removed += 1
|
||||
continue
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(full)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`[prebuild] removed ${removed} node_modules/.bin directories`)
|
||||
}
|
||||
}
|
||||
|
||||
function copyUiLoadingAssets() {
|
||||
const loadingSource = path.join(uiDist, "loading.html")
|
||||
const assetsSource = path.join(uiDist, "assets")
|
||||
|
|
@ -332,10 +277,20 @@ function copyUiLoadingAssets() {
|
|||
ensureServerDependencies()
|
||||
ensureUiBuild()
|
||||
syncServerUiBundle()
|
||||
copyServerArtifacts()
|
||||
stripNodeModuleBins()
|
||||
copyPackagedServerResources({
|
||||
serverRoot,
|
||||
serverDest,
|
||||
log: (message) => console.log(`[prebuild] ${message}`),
|
||||
})
|
||||
copyUiLoadingAssets()
|
||||
await prepareBundledNodeRuntime({ resourcesRoot })
|
||||
execSync(
|
||||
`${JSON.stringify(process.execPath)} ${JSON.stringify(path.join(workspaceRoot, "scripts", "smoke-packaged-resources.cjs"))} --resources ${JSON.stringify(resourcesRoot)} --loading ${JSON.stringify(uiLoadingDest)}`,
|
||||
{
|
||||
cwd: workspaceRoot,
|
||||
stdio: "inherit",
|
||||
},
|
||||
)
|
||||
})().catch((err) => {
|
||||
console.error("[prebuild] failed:", err)
|
||||
process.exit(1)
|
||||
|
|
|
|||
212
scripts/desktop-server-resources.cjs
Normal file
212
scripts/desktop-server-resources.cjs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
const excludedDistRoots = new Set(["codenomad-server", "opencode-config", "opencode-config-template", "opencode-config.js"])
|
||||
|
||||
function copyPackagedServerResources(options) {
|
||||
const { serverRoot, serverDest, log = () => {} } = options
|
||||
|
||||
fs.rmSync(serverDest, { recursive: true, force: true })
|
||||
fs.mkdirSync(serverDest, { recursive: true })
|
||||
|
||||
copyRequiredArtifact(serverRoot, serverDest, "package.json", log)
|
||||
copyRequiredArtifact(serverRoot, serverDest, "public", log)
|
||||
copyRequiredArtifact(serverRoot, serverDest, "node_modules", log)
|
||||
copyServerDist(serverRoot, serverDest, log)
|
||||
stripNodeModuleBins(path.join(serverDest, "node_modules"), log)
|
||||
pruneKnownServerDependencies(path.join(serverDest, "node_modules"), log)
|
||||
}
|
||||
|
||||
function copyRequiredArtifact(serverRoot, serverDest, name, log) {
|
||||
const from = path.join(serverRoot, name)
|
||||
const to = path.join(serverDest, name)
|
||||
if (!fs.existsSync(from)) {
|
||||
throw new Error(`Missing required server artifact: ${from}`)
|
||||
}
|
||||
fs.cpSync(from, to, { recursive: true, dereference: true })
|
||||
log(`copied ${name}`)
|
||||
}
|
||||
|
||||
function copyServerDist(serverRoot, serverDest, log) {
|
||||
const from = path.join(serverRoot, "dist")
|
||||
const to = path.join(serverDest, "dist")
|
||||
|
||||
if (!fs.existsSync(from)) {
|
||||
throw new Error(`Missing required server artifact: ${from}`)
|
||||
}
|
||||
|
||||
fs.cpSync(from, to, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter(source) {
|
||||
const relative = path.relative(from, source)
|
||||
if (!relative) return true
|
||||
const [root] = relative.split(path.sep)
|
||||
if (excludedDistRoots.has(root)) return false
|
||||
return !/\.test\.js$/.test(path.basename(relative))
|
||||
},
|
||||
})
|
||||
log("copied filtered dist")
|
||||
}
|
||||
|
||||
function stripNodeModuleBins(root, log) {
|
||||
if (!fs.existsSync(root)) return
|
||||
|
||||
const stack = [root]
|
||||
let removed = 0
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()
|
||||
if (!current) break
|
||||
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const full = path.join(current, entry.name)
|
||||
if (entry.name === ".bin") {
|
||||
fs.rmSync(full, { recursive: true, force: true })
|
||||
removed += 1
|
||||
continue
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(full)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
log(`removed ${removed} node_modules/.bin directories`)
|
||||
}
|
||||
}
|
||||
|
||||
function removeIfExists(target) {
|
||||
if (!fs.existsSync(target)) return 0
|
||||
fs.rmSync(target, { recursive: true, force: true })
|
||||
return 1
|
||||
}
|
||||
|
||||
function removeFilesMatching(root, patterns) {
|
||||
if (!fs.existsSync(root)) return 0
|
||||
|
||||
const stack = [root]
|
||||
let removed = 0
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()
|
||||
if (!current) break
|
||||
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const full = path.join(current, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(full)
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.isFile() && patterns.some((pattern) => pattern.test(entry.name))) {
|
||||
fs.rmSync(full, { force: true })
|
||||
removed += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
function prunePackage(root, options) {
|
||||
if (!fs.existsSync(root)) return 0
|
||||
|
||||
let removed = 0
|
||||
for (const relativePath of options.remove ?? []) {
|
||||
removed += removeIfExists(path.join(root, relativePath))
|
||||
}
|
||||
if (options.filePatterns?.length) {
|
||||
removed += removeFilesMatching(root, options.filePatterns)
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
function pruneKnownServerDependencies(root, log) {
|
||||
if (!fs.existsSync(root)) return
|
||||
|
||||
let removed = 0
|
||||
const declarationAndMaps = [/\.d\.[cm]?ts$/, /\.map$/]
|
||||
const packageDocs = [/\.md$/i, /\.markdown$/i]
|
||||
|
||||
removed += prunePackage(path.join(root, "openai"), {
|
||||
remove: ["CHANGELOG.md", "README.md", "bin", "src"],
|
||||
filePatterns: [...declarationAndMaps],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "fastify"), {
|
||||
remove: ["docs", "examples", "integration", "test", "types", "build", "fastify.d.ts"],
|
||||
filePatterns: [...packageDocs],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "@fastify", "cors"), {
|
||||
remove: ["bench.js", "benchmark", "test", "types"],
|
||||
filePatterns: [...packageDocs],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "@fastify", "reply-from"), {
|
||||
remove: ["examples", "test", "types"],
|
||||
filePatterns: [...packageDocs],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "@fastify", "static"), {
|
||||
remove: ["example", "test", "types", "tsconfig.eslint.json"],
|
||||
filePatterns: [...packageDocs],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "pino"), {
|
||||
remove: [
|
||||
"benchmarks",
|
||||
"browser.js",
|
||||
"build",
|
||||
"docs",
|
||||
"docsify",
|
||||
"examples",
|
||||
"favicon-16x16.png",
|
||||
"favicon-32x32.png",
|
||||
"favicon.ico",
|
||||
"index.html",
|
||||
"pino-banner.png",
|
||||
"pino-logo-hire.png",
|
||||
"pino-tree.png",
|
||||
"pino.d.ts",
|
||||
"pretty-demo.png",
|
||||
"test",
|
||||
"tsconfig.json",
|
||||
],
|
||||
filePatterns: [...packageDocs],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "undici"), {
|
||||
remove: ["docs", "index.d.ts", "scripts", "types"],
|
||||
filePatterns: [...packageDocs],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "zod"), {
|
||||
remove: ["README.md", "src"],
|
||||
filePatterns: [...declarationAndMaps],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "yaml"), {
|
||||
remove: ["README.md", "bin.mjs", "browser"],
|
||||
filePatterns: [...declarationAndMaps],
|
||||
})
|
||||
removed += prunePackage(path.join(root, "node-forge"), {
|
||||
remove: ["README.md", "flash"],
|
||||
})
|
||||
|
||||
if (removed > 0) {
|
||||
log(`removed ${removed} known non-runtime files/directories from server dependencies`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
copyPackagedServerResources,
|
||||
}
|
||||
|
|
@ -127,15 +127,13 @@ function extractArchive(archivePath, destination) {
|
|||
run("tar", ["-xzf", archivePath, "-C", destination])
|
||||
}
|
||||
|
||||
function pruneForRuntime(sourceRoot, destinationRoot) {
|
||||
fs.cpSync(sourceRoot, destinationRoot, { recursive: true, dereference: true })
|
||||
for (const name of ["CHANGELOG.md", "LICENSE", "README.md", "corepack", "npm", "npx"]) {
|
||||
fs.rmSync(path.join(destinationRoot, "bin", name), { recursive: true, force: true })
|
||||
}
|
||||
fs.rmSync(path.join(destinationRoot, "lib", "node_modules", "npm"), { recursive: true, force: true })
|
||||
fs.rmSync(path.join(destinationRoot, "lib", "node_modules", "corepack"), { recursive: true, force: true })
|
||||
fs.rmSync(path.join(destinationRoot, "node_modules", "npm"), { recursive: true, force: true })
|
||||
fs.rmSync(path.join(destinationRoot, "node_modules", "corepack"), { recursive: true, force: true })
|
||||
function pruneForRuntime(sourceRoot, destinationRoot, binaryRelativePath) {
|
||||
const sourceBinary = path.join(sourceRoot, binaryRelativePath)
|
||||
const destinationBinary = path.join(destinationRoot, binaryRelativePath)
|
||||
|
||||
fs.rmSync(destinationRoot, { recursive: true, force: true })
|
||||
fs.mkdirSync(path.dirname(destinationBinary), { recursive: true })
|
||||
fs.copyFileSync(sourceBinary, destinationBinary)
|
||||
}
|
||||
|
||||
async function prepareBundledNodeRuntime(options) {
|
||||
|
|
@ -175,7 +173,7 @@ async function prepareBundledNodeRuntime(options) {
|
|||
throw new Error(`Node binary missing after extraction: ${path.join(extractedRoot, spec.binary)}`)
|
||||
}
|
||||
|
||||
pruneForRuntime(extractedRoot, runtimeRoot)
|
||||
pruneForRuntime(extractedRoot, runtimeRoot, spec.binary)
|
||||
if (!target.startsWith("win32-")) {
|
||||
fs.chmodSync(runtimeBinary, 0o755)
|
||||
}
|
||||
|
|
|
|||
127
scripts/smoke-packaged-resources.cjs
Normal file
127
scripts/smoke-packaged-resources.cjs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env node
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { spawnSync } = require("child_process")
|
||||
|
||||
const requiredPackages = [
|
||||
"yaml",
|
||||
"fastify",
|
||||
"@fastify/static",
|
||||
"@fastify/cors",
|
||||
"@fastify/reply-from",
|
||||
"openai",
|
||||
"pino",
|
||||
"undici",
|
||||
"zod",
|
||||
"node-forge",
|
||||
]
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {}
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index]
|
||||
if (arg === "--resources") options.resources = argv[++index]
|
||||
else if (arg === "--loading") options.loading = argv[++index]
|
||||
else if (arg === "--target") options.target = argv[++index]
|
||||
else throw new Error(`Unknown argument: ${arg}`)
|
||||
}
|
||||
if (!options.resources) throw new Error("Missing --resources <path>")
|
||||
return options
|
||||
}
|
||||
|
||||
function platformDirName() {
|
||||
const platform = process.platform === "darwin" ? "darwin" : process.platform
|
||||
const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : process.arch
|
||||
return `${platform}-${arch}`
|
||||
}
|
||||
|
||||
function targetParts(target) {
|
||||
const [platform, ...archParts] = target.split("-")
|
||||
const arch = archParts.join("-")
|
||||
if (!platform || !arch) throw new Error(`Invalid target: ${target}`)
|
||||
return { platform, arch }
|
||||
}
|
||||
|
||||
function nodeBinary(resourcesRoot, target) {
|
||||
const { platform } = targetParts(target)
|
||||
const executable = platform === "win32" ? "node.exe" : path.join("bin", "node")
|
||||
return path.join(resourcesRoot, "node", target, executable)
|
||||
}
|
||||
|
||||
function canExecuteTarget(target) {
|
||||
return target === platformDirName()
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, { stdio: "inherit", ...options })
|
||||
if (result.error) throw result.error
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command} exited with code ${result.status ?? 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
function smokeServer(resourcesRoot, target) {
|
||||
const serverRoot = path.join(resourcesRoot, "server")
|
||||
const entrypoint = path.join(serverRoot, "dist", "bin.js")
|
||||
const node = nodeBinary(resourcesRoot, target)
|
||||
|
||||
for (const requiredPath of [node, entrypoint, path.join(serverRoot, "node_modules")]) {
|
||||
if (!fs.existsSync(requiredPath)) throw new Error(`Missing packaged runtime path: ${requiredPath}`)
|
||||
}
|
||||
|
||||
for (const packageName of requiredPackages) {
|
||||
const packageRoot = path.join(serverRoot, "node_modules", ...packageName.split("/"))
|
||||
if (!fs.existsSync(packageRoot)) throw new Error(`Missing packaged dependency: ${packageName}`)
|
||||
}
|
||||
|
||||
console.log(`packaged server static checks ok for ${target}`)
|
||||
|
||||
if (!canExecuteTarget(target)) {
|
||||
console.log(`skipping executable smoke for ${target} on host ${platformDirName()}`)
|
||||
return
|
||||
}
|
||||
|
||||
run(node, [entrypoint, "--version"])
|
||||
|
||||
const requireScript = [
|
||||
"import { createRequire } from 'module';",
|
||||
"import path from 'path';",
|
||||
`const root = ${JSON.stringify(serverRoot)};`,
|
||||
"const req = createRequire(path.join(root, 'dist/bin.js'));",
|
||||
`${JSON.stringify(requiredPackages)}.forEach((name) => req(name));`,
|
||||
"console.log('packaged dependency imports ok');",
|
||||
].join(" ")
|
||||
|
||||
run(node, ["--input-type=module", "-e", requireScript])
|
||||
}
|
||||
|
||||
function smokeLoadingAssets(loadingRoot) {
|
||||
if (!loadingRoot) return
|
||||
|
||||
const htmlPath = path.join(loadingRoot, "loading.html")
|
||||
if (!fs.existsSync(htmlPath)) throw new Error(`Missing loading HTML: ${htmlPath}`)
|
||||
|
||||
const html = fs.readFileSync(htmlPath, "utf8")
|
||||
const refs = [...html.matchAll(/(?:src|href)=["']([^"']+)["']/g)].map((match) => match[1])
|
||||
for (const ref of refs) {
|
||||
if (/^(?:https?:)?\/\//.test(ref)) continue
|
||||
if (!ref.startsWith("/assets/") && !ref.startsWith("assets/")) continue
|
||||
|
||||
const relative = ref.replace(/^\//, "")
|
||||
const target = path.join(loadingRoot, relative)
|
||||
if (!fs.existsSync(target)) throw new Error(`Missing loading asset referenced by ${htmlPath}: ${ref}`)
|
||||
}
|
||||
|
||||
console.log("loading asset references ok")
|
||||
}
|
||||
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2))
|
||||
const resourcesRoot = path.resolve(options.resources)
|
||||
const target = options.target ?? platformDirName()
|
||||
smokeServer(resourcesRoot, target)
|
||||
smokeLoadingAssets(options.loading ? path.resolve(options.loading) : null)
|
||||
} catch (error) {
|
||||
console.error("[smoke-packaged-resources] failed:", error)
|
||||
process.exit(1)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue