From 612390db536417e2f68c501329bfccf5c6795e45 Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Wed, 18 Mar 2026 14:44:07 +0100 Subject: [PATCH] feat(refactor): move plugins to typescript (#557) * feat(refactor): move plugins to typescript * chore: add timeout to plugin build function --- plugins/starter | 2 +- server/utils/plugin-loader.js | 57 ++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/plugins/starter b/plugins/starter index bfa6332..4895cd3 160000 --- a/plugins/starter +++ b/plugins/starter @@ -1 +1 @@ -Subproject commit bfa63328103ca330a012bc083e4f934adbc2086e +Subproject commit 4895cd3fd33362471e739b786493aba048487bcc diff --git a/server/utils/plugin-loader.js b/server/utils/plugin-loader.js index e48b768..9d91068 100644 --- a/server/utils/plugin-loader.js +++ b/server/utils/plugin-loader.js @@ -93,6 +93,55 @@ export function validateManifest(manifest) { return { valid: true }; } +const BUILD_TIMEOUT_MS = 60_000; + +/** Run `npm run build` if the plugin's package.json declares a build script. */ +function runBuildIfNeeded(dir, packageJsonPath, onSuccess, onError) { + try { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + if (!pkg.scripts?.build) { + return onSuccess(); + } + } catch { + return onSuccess(); // Unreadable package.json — skip build + } + + const buildProcess = spawn('npm', ['run', 'build'], { + cwd: dir, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stderr = ''; + let settled = false; + + const timer = setTimeout(() => { + if (settled) return; + settled = true; + buildProcess.removeAllListeners(); + buildProcess.kill(); + onError(new Error('npm run build timed out')); + }, BUILD_TIMEOUT_MS); + + buildProcess.stderr.on('data', (data) => { stderr += data.toString(); }); + + buildProcess.on('close', (code) => { + if (settled) return; + settled = true; + clearTimeout(timer); + if (code !== 0) { + return onError(new Error(`npm run build failed (exit code ${code}): ${stderr.trim()}`)); + } + onSuccess(); + }); + + buildProcess.on('error', (err) => { + if (settled) return; + settled = true; + clearTimeout(timer); + onError(new Error(`Failed to spawn build: ${err.message}`)); + }); +} + export function scanPlugins() { const pluginsDir = getPluginsDir(); const config = getPluginsConfig(); @@ -289,7 +338,7 @@ export function installPluginFromGit(url) { // --ignore-scripts prevents postinstall hooks from executing arbitrary code. const packageJsonPath = path.join(tempDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { - const npmProcess = spawn('npm', ['install', '--production', '--ignore-scripts'], { + const npmProcess = spawn('npm', ['install', '--ignore-scripts'], { cwd: tempDir, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -299,7 +348,7 @@ export function installPluginFromGit(url) { cleanupTemp(); return reject(new Error(`npm install for ${repoName} failed (exit code ${npmCode})`)); } - finalize(manifest); + runBuildIfNeeded(tempDir, packageJsonPath, () => finalize(manifest), (err) => { cleanupTemp(); reject(err); }); }); npmProcess.on('error', (err) => { @@ -356,7 +405,7 @@ export function updatePluginFromGit(name) { // Re-run npm install if package.json exists const packageJsonPath = path.join(pluginDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { - const npmProcess = spawn('npm', ['install', '--production', '--ignore-scripts'], { + const npmProcess = spawn('npm', ['install', '--ignore-scripts'], { cwd: pluginDir, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -364,7 +413,7 @@ export function updatePluginFromGit(name) { if (npmCode !== 0) { return reject(new Error(`npm install for ${name} failed (exit code ${npmCode})`)); } - resolve(manifest); + runBuildIfNeeded(pluginDir, packageJsonPath, () => resolve(manifest), (err) => reject(err)); }); npmProcess.on('error', (err) => reject(err)); } else {