From 360082d8bd3f9f2051696401116a62479c59d18b Mon Sep 17 00:00:00 2001 From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:31:19 -0400 Subject: [PATCH] devex: rollup via module instead of subprocess --- .../modules/selfhosted/DevWatcherService.js | 143 ++++++++++++++---- .../modules/selfhosted/SelfHostedModule.js | 43 ++---- 2 files changed, 133 insertions(+), 53 deletions(-) diff --git a/src/backend/src/modules/selfhosted/DevWatcherService.js b/src/backend/src/modules/selfhosted/DevWatcherService.js index d48d57186..f3623200a 100644 --- a/src/backend/src/modules/selfhosted/DevWatcherService.js +++ b/src/backend/src/modules/selfhosted/DevWatcherService.js @@ -21,6 +21,7 @@ const BaseService = require("../../services/BaseService"); const path_ = require('node:path'); const fs = require('node:fs'); +const rollupModule = require("rollup"); class ProxyLogger { constructor (log) { @@ -69,7 +70,10 @@ class DevWatcherService extends BaseService { async ['__on_ready.webserver'] () { const svc_process = this.services.get('process'); - const { root, commands, webpack } = this.args; + let { root, commands, webpack, rollup } = this.args; + if ( ! webpack ) webpack = []; + if ( ! rollup ) rollup = []; + let promises = []; for ( const entry of commands ) { const { directory } = entry; @@ -82,49 +86,67 @@ class DevWatcherService extends BaseService { const p = this.start_a_webpack_watcher_(entry); promises.push(p); } + for ( const entry of rollup ) { + const p = this.start_a_rollup_watcher_(entry); + promises.push(p); + } await Promise.all(promises); // It's difficult to tell when webpack is "done" its first // run so we just wait a bit before we say we're ready. await new Promise((resolve) => setTimeout(resolve, 5000)); } - - async start_a_webpack_watcher_ (entry) { - let webpackConfigPath, moduleType; { - - const possibleWebpackConfigNames = [ - ['webpack.config.js', 'package.json'], - ['webpack.config.cjs', 'commonjs'], - ['webpack.config.mjs', 'module'], - ]; - - for ( const [configName, supposedModuleType] of possibleWebpackConfigNames ) { - // There isn't really an async fs.exists() funciton. I assume this - // is because 'exists' is already a very fast operation. - const supposedPath = path_.join(this.args.root, entry.directory, configName); - if ( fs.existsSync(supposedPath) ) { - webpackConfigPath = supposedPath; - moduleType = supposedModuleType; - break; - } + + async get_configjs ({ directory, configIsFor, possibleConfigNames }) { + let configjsPath, moduleType; + + for ( const [configName, supposedModuleType] of possibleConfigNames ) { + // There isn't really an async fs.exists() funciton. I assume this + // is because 'exists' is already a very fast operation. + const supposedPath = path_.join(this.args.root, directory, configName); + if ( fs.existsSync(supposedPath) ) { + configjsPath = supposedPath; + moduleType = supposedModuleType; + break; } - } - - if ( ! webpackConfigPath ) { - throw new Error(`could not find webpack config for: ${entry.directory}`); + + if ( ! configjsPath ) { + throw new Error(`could not find ${configIsFor} config for: ${directory}`); } - + // If the webpack config ends with .js it could be an ES6 module or a // CJS module, so the absolute safest thing to do so as not to completely // break in specific patch version of supported versions of node.js is // to read the package.json and see what it says is the import mechanism. if ( moduleType === 'package.json' ) { - const packageJSONPath = path_.join(this.args.root, entry.directory, 'package.json'); + const packageJSONPath = path_.join(this.args.root, directory, 'package.json'); const packageJSONObject = JSON.parse(fs.readFileSync(packageJSONPath)); moduleType = packageJSONObject?.type ?? 'module'; } + return { + configjsPath, + moduleType, + }; + } + + async start_a_webpack_watcher_ (entry) { + const possibleConfigNames = [ + ['webpack.config.js', 'package.json'], + ['webpack.config.cjs', 'commonjs'], + ['webpack.config.mjs', 'module'], + ]; + + const { + configjsPath: webpackConfigPath, + moduleType, + } = await this.get_configjs({ + directory: entry.directory, + configIsFor: 'webpack', // for error message + possibleConfigNames, + }); + let oldEnv; if ( entry.env ) { @@ -169,6 +191,75 @@ class DevWatcherService extends BaseService { } }); } + + async start_a_rollup_watcher_ (entry) { + const possibleConfigNames = [ + ['rollup.config.js', 'package.json'], + ['rollup.config.cjs', 'commonjs'], + ['rollup.config.mjs', 'module'], + ]; + + const { + configjsPath: rollupConfigPath, + moduleType, + } = await this.get_configjs({ + directory: entry.directory, + configIsFor: 'rollup', // for error message + possibleConfigNames, + }); + + const updateRollupPaths = (config, newBase) => { + const onoutput = o => ({ ...o, file: o.file ? path_.join(newBase, o.file) : o.file }); + return { + ...config, + input: path_.join(newBase, config.input), + output: Array.isArray(config.output) + ? config.output.map(onoutput) + : onoutput(config.output), + }; + }; + + let oldEnv; + + if ( entry.env ) { + oldEnv = process.env; + const newEnv = Object.create(process.env); + for ( const k in entry.env ) { + newEnv[k] = entry.env[k]; + } + process.env = newEnv; // Yep, it totally lets us do this + } + + let rollupConfig = moduleType === 'module' + ? (await import(rollupConfigPath)).default + : require(rollupConfigPath); + + if ( oldEnv ) process.env = oldEnv; + + rollupConfig = updateRollupPaths( + rollupConfig, + path_.join(this.args.root, entry.directory), + ); + // rollupConfig.watch = true; // I mean why can't it just... + + const watcher = rollupModule.watch(rollupConfig); + let errorAfterLastEnd = false; + watcher.on('event', (event) => { + if ( event.code === 'END' ) { + if ( errorAfterLastEnd ) { + errorAfterLastEnd = false; + return; + } + this.log.info(`✅ updated ${entry.directory} using Rollup`); + } else if ( event.code === 'ERROR' ) { + this.log.error(`error information: ${entry.directory} using Rollup`, { + event, + }); + this.log.error(`❌ failed to update ${entry.directory} using Rollup`); + errorAfterLastEnd = true; + } + }); + } }; module.exports = DevWatcherService; diff --git a/src/backend/src/modules/selfhosted/SelfHostedModule.js b/src/backend/src/modules/selfhosted/SelfHostedModule.js index 0387610f8..9fc7f5271 100644 --- a/src/backend/src/modules/selfhosted/SelfHostedModule.js +++ b/src/backend/src/modules/selfhosted/SelfHostedModule.js @@ -51,6 +51,22 @@ class SelfHostedModule extends AdvancedBase { { services.registerService('__dev-watcher', DevWatcherService, { root: path_.resolve(__dirname, RELATIVE_PATH), + rollup: [ + { + name: 'phoenix', + directory: 'src/phoenix', + env: { + PUTER_JS_URL: ({ global_config: config }) => config.origin + '/sdk/puter.dev.js', + }, + }, + { + name: 'terminal', + directory: 'src/terminal', + env: { + PUTER_JS_URL: ({ global_config: config }) => config.origin + '/sdk/puter.dev.js', + }, + }, + ], webpack: [ { name: 'puter.js', @@ -74,33 +90,6 @@ class SelfHostedModule extends AdvancedBase { }, ], commands: [ - { - name: 'terminal:rollup-watch', - directory: 'src/terminal', - command: 'npx', - args: ['rollup', '-c', 'rollup.config.js', '--watch'], - env: { - PUTER_JS_URL: ({ global_config: config }) => config.origin + '/sdk/puter.dev.js', - }, - }, - { - name: 'phoenix:rollup-watch', - directory: 'src/phoenix', - command: 'npx', - args: ['rollup', '-c', 'rollup.config.js', '--watch'], - env: { - PUTER_JS_URL: ({ global_config: config }) => config.origin + '/sdk/puter.dev.js', - }, - }, - { - name: 'git:rollup-watch', - directory: 'src/git', - command: 'npx', - args: ['rollup', '-c', 'rollup.config.js', '--watch'], - env: { - PUTER_JS_URL: ({ global_config: config }) => config.origin + '/sdk/puter.dev.js', - }, - }, ], }); }