perf(plugins): parallelize builds, skip when dist/ exists, fix double-build

- Use --ignore-scripts during npm install to prevent duplicate builds
- Skip build entirely when dist/ directory already exists (pre-built plugins)
- Add buildPluginAsync() and runParallel() for concurrent plugin builds
- Convert all 5 build loops to parallel execution bounded by CPU count
- Prune devDependencies after build to avoid singleton duplication
This commit is contained in:
saberzero1 2026-03-14 22:02:49 +01:00
parent 85680868b7
commit 341b8b3779
No known key found for this signature in database

View File

@ -1,7 +1,8 @@
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import { execSync } from "child_process" import os from "os"
import { styleText } from "util" import { execSync, exec as execCb } from "child_process"
import { styleText, promisify } from "util"
import { import {
readPluginsJson, readPluginsJson,
writePluginsJson, writePluginsJson,
@ -18,12 +19,17 @@ import {
const INTERNAL_EXPORTS = new Set(["manifest", "default"]) const INTERNAL_EXPORTS = new Set(["manifest", "default"])
const execAsync = promisify(execCb)
function buildPlugin(pluginDir, name) { function buildPlugin(pluginDir, name) {
try { try {
const skipBuild = !needsBuild(pluginDir)
console.log(styleText("cyan", `${name}: installing dependencies...`)) console.log(styleText("cyan", `${name}: installing dependencies...`))
execSync("npm install", { cwd: pluginDir, stdio: "ignore" }) execSync("npm install --ignore-scripts", { cwd: pluginDir, stdio: "ignore" })
if (!skipBuild) {
console.log(styleText("cyan", `${name}: building...`)) console.log(styleText("cyan", `${name}: building...`))
execSync("npm run build", { cwd: pluginDir, stdio: "ignore" }) execSync("npm run build", { cwd: pluginDir, stdio: "ignore" })
}
// Remove devDependencies after build — they are no longer needed and their // Remove devDependencies after build — they are no longer needed and their
// presence can cause duplicate-singleton issues when a plugin ships its own // presence can cause duplicate-singleton issues when a plugin ships its own
// copy of a shared dependency (e.g. bases-page's ViewRegistry). // copy of a shared dependency (e.g. bases-page's ViewRegistry).
@ -39,6 +45,47 @@ function buildPlugin(pluginDir, name) {
} }
} }
async function buildPluginAsync(pluginDir, name) {
try {
const skipBuild = !needsBuild(pluginDir)
console.log(styleText("cyan", `${name}: installing dependencies...`))
await execAsync("npm install --ignore-scripts", { cwd: pluginDir })
if (!skipBuild) {
console.log(styleText("cyan", `${name}: building...`))
await execAsync("npm run build", { cwd: pluginDir })
}
await execAsync("npm prune --omit=dev", { cwd: pluginDir })
linkPeerPlugins(pluginDir)
return true
} catch (error) {
console.log(styleText("red", `${name}: build failed`))
return false
}
}
/**
* Run async tasks with bounded concurrency.
* @param {Array} items - Items to process
* @param {number} concurrency - Max parallel tasks
* @param {Function} fn - Async function to run per item
* @returns {Promise<Array>} Results in order
*/
async function runParallel(items, concurrency, fn) {
const results = new Array(items.length)
let nextIndex = 0
async function worker() {
while (nextIndex < items.length) {
const i = nextIndex++
results[i] = await fn(items[i], i)
}
}
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker())
await Promise.all(workers)
return results
}
function needsBuild(pluginDir) { function needsBuild(pluginDir) {
const distDir = path.join(pluginDir, "dist") const distDir = path.join(pluginDir, "dist")
return !fs.existsSync(distDir) return !fs.existsSync(distDir)
@ -289,13 +336,14 @@ export async function handlePluginInstall() {
if (pluginsToBuild.length > 0) { if (pluginsToBuild.length > 0) {
console.log() console.log()
console.log(styleText("cyan", "→ Building plugins...")) console.log(styleText("cyan", "→ Building plugins..."))
for (const { name, pluginDir } of pluginsToBuild) { const concurrency = Math.max(1, os.cpus().length)
if (!buildPlugin(pluginDir, name)) { const results = await runParallel(pluginsToBuild, concurrency, async ({ name, pluginDir }) => {
failed++ const ok = await buildPluginAsync(pluginDir, name)
installed-- if (ok) console.log(styleText("green", `${name} built`))
} else { return ok
console.log(styleText("green", `${name} built`)) })
} for (const ok of results) {
if (!ok) { failed++; installed-- }
} }
} }
@ -378,11 +426,12 @@ export async function handlePluginAdd(sources) {
if (addedPlugins.length > 0) { if (addedPlugins.length > 0) {
console.log() console.log()
console.log(styleText("cyan", "→ Building plugins...")) console.log(styleText("cyan", "→ Building plugins..."))
for (const { name, pluginDir } of addedPlugins) { const concurrency = Math.max(1, os.cpus().length)
if (buildPlugin(pluginDir, name)) { await runParallel(addedPlugins, concurrency, async ({ name, pluginDir }) => {
console.log(styleText("green", `${name} built`)) const ok = await buildPluginAsync(pluginDir, name)
} if (ok) console.log(styleText("green", `${name} built`))
} return ok
})
await regeneratePluginIndex() await regeneratePluginIndex()
} }
@ -693,11 +742,12 @@ export async function handlePluginUpdate(names) {
if (updatedPlugins.length > 0) { if (updatedPlugins.length > 0) {
console.log() console.log()
console.log(styleText("cyan", "→ Rebuilding updated plugins...")) console.log(styleText("cyan", "→ Rebuilding updated plugins..."))
for (const { name, pluginDir } of updatedPlugins) { const concurrency = Math.max(1, os.cpus().length)
if (buildPlugin(pluginDir, name)) { await runParallel(updatedPlugins, concurrency, async ({ name, pluginDir }) => {
console.log(styleText("green", `${name} rebuilt`)) const ok = await buildPluginAsync(pluginDir, name)
} if (ok) console.log(styleText("green", `${name} rebuilt`))
} return ok
})
await regeneratePluginIndex() await regeneratePluginIndex()
} }
@ -824,13 +874,14 @@ export async function handlePluginRestore() {
if (restoredPlugins.length > 0) { if (restoredPlugins.length > 0) {
console.log() console.log()
console.log(styleText("cyan", "→ Building restored plugins...")) console.log(styleText("cyan", "→ Building restored plugins..."))
for (const { name, pluginDir } of restoredPlugins) { const concurrency = Math.max(1, os.cpus().length)
if (!buildPlugin(pluginDir, name)) { const results = await runParallel(restoredPlugins, concurrency, async ({ name, pluginDir }) => {
failed++ const ok = await buildPluginAsync(pluginDir, name)
installed-- if (ok) console.log(styleText("green", `${name} built`))
} else { return ok
console.log(styleText("green", `${name} built`)) })
} for (const ok of results) {
if (!ok) { failed++; installed-- }
} }
await regeneratePluginIndex() await regeneratePluginIndex()
} }
@ -1029,12 +1080,14 @@ export async function handlePluginResolve({ dryRun = false } = {}) {
if (installed.length > 0) { if (installed.length > 0) {
console.log() console.log()
console.log(styleText("cyan", "→ Building plugins...")) console.log(styleText("cyan", "→ Building plugins..."))
for (const { name, pluginDir } of installed) { const concurrency = Math.max(1, os.cpus().length)
if (!buildPlugin(pluginDir, name)) { const results = await runParallel(installed, concurrency, async ({ name, pluginDir }) => {
failed++ const ok = await buildPluginAsync(pluginDir, name)
} else { if (ok) console.log(styleText("green", `${name} built`))
console.log(styleText("green", `${name} built`)) return ok
} })
for (const ok of results) {
if (!ok) failed++
} }
await regeneratePluginIndex() await regeneratePluginIndex()
} }