From 2b34462ad913aead6456039b964b72bc010e4745 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 17 Sep 2025 23:51:07 -0400 Subject: [PATCH 01/17] chore: enhance npm start command to run compass sync and sandbox COMPASS-9851 --- package.json | 2 +- packages/compass-web/package.json | 4 +- .../compass-web/scripts/sync-dist-to-mms.js | 102 ---------- .../compass-web/scripts/sync-dist-to-mms.mjs | 166 ++++++++++++++++ scripts/start.md | 21 +++ scripts/start.mts | 178 ++++++++++++++++++ 6 files changed, 368 insertions(+), 105 deletions(-) delete mode 100644 packages/compass-web/scripts/sync-dist-to-mms.js create mode 100644 packages/compass-web/scripts/sync-dist-to-mms.mjs create mode 100644 scripts/start.md create mode 100644 scripts/start.mts diff --git a/package.json b/package.json index 6c84e549a17..7b79e730573 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "package-compass": "npm run package-compass --workspace=mongodb-compass --", "package-compass-debug": "npm run package-compass-debug --workspace=mongodb-compass --", "package-compass-nocompile": "npm run package-compass-nocompile --workspace=mongodb-compass --", - "start": "npm run start --workspace=mongodb-compass", + "start": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/start.mts", "start-web": "npm run start --workspace=@mongodb-js/compass-web", "test": "lerna run test --concurrency 1 --stream", "test-changed": "lerna run test --stream --concurrency 1 --since origin/HEAD", diff --git a/packages/compass-web/package.json b/packages/compass-web/package.json index 20d24d44a46..3e88887c9b1 100644 --- a/packages/compass-web/package.json +++ b/packages/compass-web/package.json @@ -49,8 +49,8 @@ "start": "electron ./scripts/electron-proxy.js", "analyze": "npm run webpack -- --mode production --analyze", "watch": "npm run webpack -- --mode development --watch", - "sync": "node scripts/sync-dist-to-mms.js", - "typecheck": "tsc -p tsconfig.json --noEmit", + "sync": "node scripts/sync-dist-to-mms.mjs", + "typecheck": "tsc -p tsconfig-lint.json --noEmit", "eslint": "eslint-compass", "prettier": "prettier-compass", "lint": "npm run eslint . && npm run prettier -- --check .", diff --git a/packages/compass-web/scripts/sync-dist-to-mms.js b/packages/compass-web/scripts/sync-dist-to-mms.js deleted file mode 100644 index 87ca17a4137..00000000000 --- a/packages/compass-web/scripts/sync-dist-to-mms.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict'; -const fs = require('fs'); -const path = require('path'); -const child_process = require('child_process'); -const os = require('os'); -const util = require('util'); -const { debounce } = require('lodash'); - -if (!process.env.MMS_HOME) { - throw new Error( - 'Missing required environment variable $MMS_HOME. Make sure you finished the "Cloud Developer Setup" process' - ); -} - -const srcDir = path.resolve(__dirname, '..', 'dist'); - -const destDir = path.dirname( - child_process.execFileSync( - 'node', - ['-e', "console.log(require.resolve('@mongodb-js/compass-web'))"], - { cwd: process.env.MMS_HOME, encoding: 'utf-8' } - ) -); - -const tmpDir = path.join( - os.tmpdir(), - `mongodb-js--compass-web-${Date.now().toString(36)}` -); - -fs.mkdirSync(srcDir, { recursive: true }); - -// Create a copy of current dist that will be overriden by link, we'll restore -// it when we are done -fs.mkdirSync(tmpDir, { recursive: true }); -fs.cpSync(destDir, tmpDir, { recursive: true }); - -const copyDist = debounce( - function () { - fs.cpSync(srcDir, destDir, { recursive: true }); - }, - 1_000, - { - leading: true, - trailing: true, - } -); - -// The existing approach of using `npm / pnpm link` commands doesn't play well -// with webpack that will start to resolve other modules relative to the imports -// from compass-web inevitably causing some modules to resolve from the compass -// monorepo instead of mms one. To work around that we are just watching for any -// file changes in the dist folder and copying them as-is to whatever place -// compass-web was installed in mms node_modules -const distWatcher = fs.watch(srcDir, function () { - copyDist(); -}); - -const webpackWatchProcess = child_process.spawn('npm', ['run', 'watch'], { - stdio: 'inherit', -}); - -const failProofRunner = () => - new (class FailProofRunner extends Array { - append(...fns) { - this.push(...fns); - return this; - } - - run() { - const errors = this.map((f) => { - try { - f(); - } catch (e) { - return e; - } - }).filter((e) => e); - - if (errors.length) { - fs.writeSync( - process.stdout.fd, - util.inspect(errors, { depth: 20 }) + '\n' - ); - } - - return errors.length; - } - })(); - -function cleanup(signalName) { - const errorCount = failProofRunner() - .append(() => distWatcher.close()) - .append(() => webpackWatchProcess.kill(signalName)) - .append(() => fs.cpSync(tmpDir, destDir, { recursive: true })) - .append(() => fs.rmSync(tmpDir, { recursive: true, force: true })) - .run(); - fs.writeSync(process.stdout.fd, 'Exit compass-web sync...\n'); - process.exit(errorCount); -} - -for (const evt of ['SIGINT', 'SIGTERM']) { - process.on(evt, cleanup); -} diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mjs b/packages/compass-web/scripts/sync-dist-to-mms.mjs new file mode 100644 index 00000000000..822e4652e18 --- /dev/null +++ b/packages/compass-web/scripts/sync-dist-to-mms.mjs @@ -0,0 +1,166 @@ +import process from 'node:process'; +import fs from 'node:fs'; +import path from 'node:path'; +import child_process from 'node:child_process'; +import os from 'node:os'; +import util from 'node:util'; +import net from 'node:net'; +import timers from 'node:timers/promises'; + +if (!process.env.MMS_HOME) { + throw new Error( + 'Missing required environment variable $MMS_HOME. Make sure you finished the "Cloud Developer Setup" process' + ); +} + +function isDevServerRunning(port, host = '127.0.0.1') { + return new Promise((resolve) => { + const socket = new net.Socket(); + socket.setTimeout(1000); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('timeout', () => { + socket.destroy(); + resolve(false); + }); + socket.once('error', () => { + resolve(false); + }); + socket.connect(port, host); + }); +} + +let devServer; +if (!(await isDevServerRunning(8081))) { + console.log('mms dev server is not running... launching!'); + child_process.execFileSync('pnpm', ['install'], { + cwd: process.env.MMS_HOME, + stdio: 'inherit', + }); + child_process.execFileSync('pnpm', ['run', 'init'], { + cwd: process.env.MMS_HOME, + stdio: 'inherit', + }); + const halfRamMb = Math.min( + Math.floor(os.totalmem() / 2 / 1024 / 1024), + 16384 + ); + // Merge with existing NODE_OPTIONS if present + const existingNodeOptions = process.env.NODE_OPTIONS ?? ''; + const mergedNodeOptions = [ + `--max_old_space_size=${halfRamMb}`, + existingNodeOptions, + ] + .filter(Boolean) + .join(' '); + + devServer = child_process.spawn('pnpm', ['run', 'start'], { + cwd: process.env.MMS_HOME, + env: { + ...process.env, + NODE_OPTIONS: mergedNodeOptions, + }, + stdio: 'inherit', + }); + + // Wait for dev server to be ready before proceeding + console.log('Waiting for dev server to start...'); + let retries = 30; // 30 seconds max + while (retries > 0 && !(await isDevServerRunning(8081))) { + await timers.setTimeout(1000); + retries--; + } + + if (retries === 0) { + console.warn('Dev server may not be fully ready, proceeding anyway...'); + } else { + console.log('Dev server is ready!'); + } +} + +const srcDir = path.resolve(import.meta.dirname, '..', 'dist'); + +const destDir = path.dirname( + child_process.execFileSync( + 'node', + ['-e', "console.log(require.resolve('@mongodb-js/compass-web'))"], + { cwd: process.env.MMS_HOME, encoding: 'utf-8' } + ) +); + +const tmpDir = path.join( + os.tmpdir(), + `mongodb-js--compass-web-${Date.now().toString(36)}` +); + +fs.mkdirSync(srcDir, { recursive: true }); + +// Create a copy of current dist that will be overriden by link, we'll restore +// it when we are done +fs.mkdirSync(tmpDir, { recursive: true }); +fs.cpSync(destDir, tmpDir, { recursive: true }); + +let oneSec = null; +async function copyDist() { + // If a copy is already in progress, return early (debounce) + if (oneSec) return; + fs.cpSync(srcDir, destDir, { recursive: true }); + oneSec = timers.setTimeout(1000); + await oneSec; + oneSec = null; +} + +// The existing approach of using `npm / pnpm link` commands doesn't play well +// with webpack that will start to resolve other modules relative to the imports +// from compass-web inevitably causing some modules to resolve from the compass +// monorepo instead of mms one. To work around that we are just watching for any +// file changes in the dist folder and copying them as-is to whatever place +// compass-web was installed in mms node_modules +const distWatcher = fs.watch(srcDir, () => void copyDist()); + +const webpackWatchProcess = child_process.spawn('npm', ['run', 'watch'], { + stdio: 'inherit', +}); + +const failProofRunner = () => + new (class FailProofRunner extends Array { + append(...fns) { + this.push(...fns); + return this; + } + + run() { + const errors = this.map((f) => { + try { + f(); + } catch (e) { + return e; + } + }).filter((e) => e); + + if (errors.length) { + fs.writeSync( + process.stdout.fd, + util.inspect(errors, { depth: 20 }) + '\n' + ); + } + + return errors.length; + } + })(); + +function cleanup(signalName) { + const errorCount = failProofRunner() + .append(() => distWatcher.close()) + .append(() => webpackWatchProcess.kill(signalName)) + .append(() => devServer?.kill(signalName)) + .append(() => fs.cpSync(tmpDir, destDir, { recursive: true })) + .append(() => fs.rmSync(tmpDir, { recursive: true, force: true })) + .run(); + fs.writeSync(process.stdout.fd, 'Exit compass-web sync...\n'); + process.exit(errorCount); +} + +process.on('SIGINT', cleanup).on('SIGTERM', cleanup); diff --git a/scripts/start.md b/scripts/start.md new file mode 100644 index 00000000000..15dfd79b56d --- /dev/null +++ b/scripts/start.md @@ -0,0 +1,21 @@ +# Start Script Help + +Usage: start.mts [-h/--help] [targets... [targetOptions...]] + +## Options + +- `-h, --help` Show this help message + +## Targets (can be used in any combination) + +- `desktop` Start MongoDB Compass Desktop (default if no targets specified) +- `sandbox` Start MongoDB Compass Web Sandbox, useful for UI-only changes +- `sync` Start Cloud Sync, in combination with redirector/redwood can be used to test data explorer changes + +## Examples + +start.mts # Start desktop (default) +start.mts desktop --production # Start desktop explicitly +start.mts web # Start web sandbox only +start.mts desktop web # Start both desktop and web +start.mts sync --flagA -b web -c # Start mms-sync and web diff --git a/scripts/start.mts b/scripts/start.mts new file mode 100644 index 00000000000..f0d3d3f8e3c --- /dev/null +++ b/scripts/start.mts @@ -0,0 +1,178 @@ +import process from 'node:process'; +import child_process from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import timers from 'node:timers/promises'; +import { pipeline } from 'node:stream/promises'; +import { Transform } from 'node:stream'; + +// Get raw arguments after the script name, skipping node and script path +const args = process.argv.slice(2); + +// Check if help flag appears before any target (making it "our" help) +const validTargets = ['desktop', 'sandbox', 'sync']; +const firstTargetIndex = args.findIndex((arg) => validTargets.includes(arg)); +const helpIndex = args.findIndex((arg) => arg === '-h' || arg === '--help'); + +const isOurHelp = + helpIndex !== -1 && (firstTargetIndex === -1 || helpIndex < firstTargetIndex); + +if (isOurHelp) { + console.log( + await fs.readFile(path.join(import.meta.dirname, 'start.md'), 'utf8') + ); + process.exit(0); +} + +// Parse positional arguments and group them by target +const targets = { + desktop: { enabled: false, args: [] as string[] }, + sandbox: { enabled: false, args: [] as string[] }, + sync: { enabled: false, args: [] as string[] }, +}; + +let currentTarget: keyof typeof targets | null = null; + +for (const arg of args) { + if (validTargets.includes(arg as keyof typeof targets)) { + // Switch to new target + currentTarget = arg as keyof typeof targets; + targets[currentTarget].enabled = true; + } else if (currentTarget) { + // Add argument to current target's args (including help flags destined for targets) + targets[currentTarget].args.push(arg); + } + // If arg is not a valid target and no current target, ignore it +} + +// If no targets specified, default to desktop +if ( + !targets.desktop.enabled && + !targets.sandbox.enabled && + !targets.sync.enabled +) { + targets.desktop.enabled = true; +} + +const subProcesses: child_process.ChildProcess[] = []; +async function cleanup(signal: NodeJS.Signals) { + for (const p of subProcesses) p.kill(signal); + console.log('\nstart | requested termination.'); + await timers.setTimeout(5000); + const stillRunning = subProcesses.filter((p) => p.exitCode === null); + for (const p of stillRunning) p.kill('SIGKILL'); + console.log('\nstart | done.'); + process.exit(0); +} + +process.on('SIGINT', cleanup).on('SIGTERM', cleanup); + +// Helper function to create a transform stream that prefixes lines +function createPrefixTransform(prefix: string) { + let buffer = ''; + return new Transform({ + transform(chunk, _, callback) { + buffer += chunk.toString(); + const lines = buffer.split('\n'); + // Keep the last line in buffer (might be incomplete) + buffer = lines.pop() || ''; + // Process complete lines + for (const line of lines) { + this.push(`${prefix} | ${line}\n`); + } + callback(); + }, + + flush(callback) { + // Process any remaining data in buffer + if (buffer) { + this.push(`${prefix} | ${buffer}\n`); + } + callback(); + }, + }); +} + +// Helper function to spawn a target process +function spawnTarget( + command: string, + workspace: string, + args: string[], + targetName: string +) { + // Only set color-forcing env vars if user hasn't set color preferences + const colorEnv: Record = {}; + if ( + !process.env.NO_COLOR && + !process.env.FORCE_COLOR && + !process.env.CLICOLOR_FORCE + ) { + colorEnv.FORCE_COLOR = '1'; + colorEnv.CLICOLOR_FORCE = '1'; + } + + const spawnArgs: Parameters = [ + 'npm', + [ + 'run', + command, + `--workspace=${workspace}`, + ...(args.length ? ['--', ...args] : []), + ], + { + stdio: 'pipe', + env: { ...process.env, ...colorEnv }, + }, + ]; + + const paddedName = targetName.padEnd(8); + console.log(`start | ${spawnArgs[0]} ${spawnArgs[1].join(' ')}`); + const subProcess = child_process.spawn(...spawnArgs); + + // Set up stdout pipeline with error handling + if (subProcess.stdout) { + subProcess.stdout.setEncoding('utf-8'); + pipeline( + subProcess.stdout, + createPrefixTransform(paddedName), + process.stdout, + { end: false } // Don't end process.stdout when subprocess ends + ).catch((err) => console.error(`start | stdout pipeline error:`, err)); + } + + // Set up stderr pipeline with error handling + if (subProcess.stderr) { + subProcess.stderr.setEncoding('utf-8'); + pipeline( + subProcess.stderr, + createPrefixTransform(paddedName), + process.stderr, + { end: false } // Don't end process.stderr when subprocess ends + ).catch((err) => console.error(`start | stderr pipeline error:`, err)); + } + + return subProcess; +} + +if (targets.desktop.enabled) { + subProcesses.push( + spawnTarget('start', 'mongodb-compass', targets.desktop.args, 'desktop') + ); +} + +if (targets.sync.enabled) { + subProcesses.push( + spawnTarget('sync', '@mongodb-js/compass-web', targets.sync.args, 'sync') + ); +} + +if (targets.sandbox.enabled) { + subProcesses.push( + spawnTarget( + 'start', + '@mongodb-js/compass-web', + targets.sandbox.args, + 'sandbox' + ) + ); +} From aa26f8af22a529fc4004f5ce1da4f6e534fef2c4 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 18 Sep 2025 18:19:02 -0400 Subject: [PATCH 02/17] make desktop run normally --- scripts/start.mts | 62 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/scripts/start.mts b/scripts/start.mts index f0d3d3f8e3c..9a0e50dd723 100644 --- a/scripts/start.mts +++ b/scripts/start.mts @@ -54,6 +54,10 @@ if ( targets.desktop.enabled = true; } +// Check if we need prefixing (more than one target enabled) +const enabledTargets = Object.values(targets).filter((t) => t.enabled); +const needsPrefixing = enabledTargets.length > 1; + const subProcesses: child_process.ChildProcess[] = []; async function cleanup(signal: NodeJS.Signals) { for (const p of subProcesses) p.kill(signal); @@ -98,7 +102,8 @@ function spawnTarget( command: string, workspace: string, args: string[], - targetName: string + targetName: string, + usePrefixing: boolean ) { // Only set color-forcing env vars if user hasn't set color preferences const colorEnv: Record = {}; @@ -132,23 +137,35 @@ function spawnTarget( // Set up stdout pipeline with error handling if (subProcess.stdout) { subProcess.stdout.setEncoding('utf-8'); - pipeline( - subProcess.stdout, - createPrefixTransform(paddedName), - process.stdout, - { end: false } // Don't end process.stdout when subprocess ends - ).catch((err) => console.error(`start | stdout pipeline error:`, err)); + if (usePrefixing) { + pipeline( + subProcess.stdout, + createPrefixTransform(paddedName), + process.stdout, + { end: false } + ).catch((err) => console.error(`start | stdout pipeline error:`, err)); + } else { + pipeline(subProcess.stdout, process.stdout, { end: false }).catch((err) => + console.error(`start | stdout pipeline error:`, err) + ); + } } // Set up stderr pipeline with error handling if (subProcess.stderr) { subProcess.stderr.setEncoding('utf-8'); - pipeline( - subProcess.stderr, - createPrefixTransform(paddedName), - process.stderr, - { end: false } // Don't end process.stderr when subprocess ends - ).catch((err) => console.error(`start | stderr pipeline error:`, err)); + if (usePrefixing) { + pipeline( + subProcess.stderr, + createPrefixTransform(paddedName), + process.stderr, + { end: false } + ).catch((err) => console.error(`start | stderr pipeline error:`, err)); + } else { + pipeline(subProcess.stderr, process.stderr, { end: false }).catch((err) => + console.error(`start | stderr pipeline error:`, err) + ); + } } return subProcess; @@ -156,13 +173,25 @@ function spawnTarget( if (targets.desktop.enabled) { subProcesses.push( - spawnTarget('start', 'mongodb-compass', targets.desktop.args, 'desktop') + spawnTarget( + 'start', + 'mongodb-compass', + targets.desktop.args, + 'desktop', + needsPrefixing + ) ); } if (targets.sync.enabled) { subProcesses.push( - spawnTarget('sync', '@mongodb-js/compass-web', targets.sync.args, 'sync') + spawnTarget( + 'sync', + '@mongodb-js/compass-web', + targets.sync.args, + 'sync', + needsPrefixing + ) ); } @@ -172,7 +201,8 @@ if (targets.sandbox.enabled) { 'start', '@mongodb-js/compass-web', targets.sandbox.args, - 'sandbox' + 'sandbox', + needsPrefixing ) ); } From 7570add8ed0835bae226dd65a35c7e46f0ef3dc1 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 18 Sep 2025 18:19:09 -0400 Subject: [PATCH 03/17] remove init --- packages/compass-web/scripts/sync-dist-to-mms.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mjs b/packages/compass-web/scripts/sync-dist-to-mms.mjs index 822e4652e18..986798447d9 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mjs +++ b/packages/compass-web/scripts/sync-dist-to-mms.mjs @@ -39,10 +39,6 @@ if (!(await isDevServerRunning(8081))) { cwd: process.env.MMS_HOME, stdio: 'inherit', }); - child_process.execFileSync('pnpm', ['run', 'init'], { - cwd: process.env.MMS_HOME, - stdio: 'inherit', - }); const halfRamMb = Math.min( Math.floor(os.totalmem() / 2 / 1024 / 1024), 16384 From 1e8debf22770fbf369d85c9bc0f19805f399f5ff Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 19 Sep 2025 16:51:45 -0400 Subject: [PATCH 04/17] fix: to avoid port overlap prevent sandbox running with other commands --- packages/compass-web/.eslintrc.js | 13 +++++++ .../compass-web/scripts/sync-dist-to-mms.mjs | 34 +++++++++++-------- scripts/start.md | 25 ++++++++++---- scripts/start.mts | 27 +++++++++++++-- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/packages/compass-web/.eslintrc.js b/packages/compass-web/.eslintrc.js index f64a0ab086d..970d914f9ab 100644 --- a/packages/compass-web/.eslintrc.js +++ b/packages/compass-web/.eslintrc.js @@ -6,4 +6,17 @@ module.exports = { tsconfigRootDir: __dirname, project: ['./tsconfig.json'], }, + overrides: [ + { + files: ['**/*.mjs'], + parserOptions: { + ecmaVersion: 2023, + sourceType: 'module', + }, + env: { + es2023: true, + node: true, + }, + }, + ], }; diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mjs b/packages/compass-web/scripts/sync-dist-to-mms.mjs index 986798447d9..ab9fd2caa2e 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mjs +++ b/packages/compass-web/scripts/sync-dist-to-mms.mjs @@ -16,24 +16,28 @@ if (!process.env.MMS_HOME) { function isDevServerRunning(port, host = '127.0.0.1') { return new Promise((resolve) => { const socket = new net.Socket(); - socket.setTimeout(1000); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('timeout', () => { - socket.destroy(); - resolve(false); - }); - socket.once('error', () => { - resolve(false); - }); - socket.connect(port, host); + socket + .setTimeout(1000) + .on('connect', () => { + socket.destroy(); + resolve(true); + }) + .on('error', () => { + socket.destroy(); + resolve(false); + }) + .on('timeout', () => { + socket.destroy(); + resolve(false); + }) + .connect(port, host); }); } +const okToRunMMS = !process.argv.includes('--no-mms'); + let devServer; -if (!(await isDevServerRunning(8081))) { +if (okToRunMMS || !(await isDevServerRunning(8081))) { console.log('mms dev server is not running... launching!'); child_process.execFileSync('pnpm', ['install'], { cwd: process.env.MMS_HOME, @@ -74,6 +78,8 @@ if (!(await isDevServerRunning(8081))) { } else { console.log('Dev server is ready!'); } +} else { + console.log('Skipping running MMS dev server...'); } const srcDir = path.resolve(import.meta.dirname, '..', 'dist'); diff --git a/scripts/start.md b/scripts/start.md index 15dfd79b56d..cdaa42a171b 100644 --- a/scripts/start.md +++ b/scripts/start.md @@ -6,16 +6,27 @@ Usage: start.mts [-h/--help] [targets... [targetOptions...]] - `-h, --help` Show this help message -## Targets (can be used in any combination) +## Targets - `desktop` Start MongoDB Compass Desktop (default if no targets specified) - `sandbox` Start MongoDB Compass Web Sandbox, useful for UI-only changes - `sync` Start Cloud Sync, in combination with redirector/redwood can be used to test data explorer changes + - `--no-mms` can be passed to the sync subcommand to not run MMS's dev server. -## Examples +**Note:** `sandbox` must be run alone and cannot be combined with other targets. -start.mts # Start desktop (default) -start.mts desktop --production # Start desktop explicitly -start.mts web # Start web sandbox only -start.mts desktop web # Start both desktop and web -start.mts sync --flagA -b web -c # Start mms-sync and web +## Port Configuration + +The `desktop` and `sandbox` targets both use the same webpack dev server port (4242) by design. When running both targets simultaneously, the sandbox will automatically detect that the desktop target's webpack dev server is already running and will skip starting its own, sharing the same webpack dev server instance. + +Since `sync` must run alone, there are no port conflicts with other targets. + +### Well-Known Ports + +| Port | Service | Target | Description | +| ---- | ---------------------- | -------------------- | ----------------------------------------------- | +| 4242 | Webpack Dev Server | `desktop`, `sandbox` | Serves compiled frontend assets with hot reload | +| 7777 | HTTP Proxy Server | `sandbox` | Express proxy server for Atlas API requests | +| 1337 | WebSocket Proxy Server | `sandbox` | WebSocket proxy for MongoDB connections | +| 8080 | Atlas Local Backend | `sync` | Local MMS backend server (when MMS_HOME is set) | +| 8081 | MMS Dev Server | `sync` | Local MMS development server | diff --git a/scripts/start.mts b/scripts/start.mts index 9a0e50dd723..caac81f14e2 100644 --- a/scripts/start.mts +++ b/scripts/start.mts @@ -45,6 +45,18 @@ for (const arg of args) { // If arg is not a valid target and no current target, ignore it } +// Check for mutually exclusive targets +if ( + targets.sandbox.enabled && + (targets.desktop.enabled || targets.sync.enabled) +) { + console.error('Error: sandbox target must be run alone.'); + console.error( + 'Please run sandbox by itself, not combined with other targets.' + ); + process.exit(1); +} + // If no targets specified, default to desktop if ( !targets.desktop.enabled && @@ -62,14 +74,14 @@ const subProcesses: child_process.ChildProcess[] = []; async function cleanup(signal: NodeJS.Signals) { for (const p of subProcesses) p.kill(signal); console.log('\nstart | requested termination.'); - await timers.setTimeout(5000); + await timers.setTimeout(10_000); const stillRunning = subProcesses.filter((p) => p.exitCode === null); for (const p of stillRunning) p.kill('SIGKILL'); console.log('\nstart | done.'); process.exit(0); } -process.on('SIGINT', cleanup).on('SIGTERM', cleanup); +process.once('SIGINT', cleanup).once('SIGTERM', cleanup); // Helper function to create a transform stream that prefixes lines function createPrefixTransform(prefix: string) { @@ -125,7 +137,7 @@ function spawnTarget( ...(args.length ? ['--', ...args] : []), ], { - stdio: 'pipe', + stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr all piped env: { ...process.env, ...colorEnv }, }, ]; @@ -206,3 +218,12 @@ if (targets.sandbox.enabled) { ) ); } + +// Forward stdin to all subprocesses using pipeline +for (const subProcess of subProcesses) { + if (subProcess.stdin) + pipeline(process.stdin, subProcess.stdin, { end: false }).catch((err) => { + if (err.code !== 'EPIPE' && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') + console.error(`start | stdin pipeline error:`, err); + }); +} From 47bcf138785736299d1f01e769bd83a4b42f5a30 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 19 Sep 2025 16:52:16 -0400 Subject: [PATCH 05/17] fix: don't leave behind electron processes --- scripts/start.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/start.mts b/scripts/start.mts index caac81f14e2..ee0afcbd4ab 100644 --- a/scripts/start.mts +++ b/scripts/start.mts @@ -76,7 +76,7 @@ async function cleanup(signal: NodeJS.Signals) { console.log('\nstart | requested termination.'); await timers.setTimeout(10_000); const stillRunning = subProcesses.filter((p) => p.exitCode === null); - for (const p of stillRunning) p.kill('SIGKILL'); + for (const p of stillRunning) p.kill('SIGTERM'); console.log('\nstart | done.'); process.exit(0); } From 8068f7b7df6ead74205f1b317ccf450e9a677fde Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 22 Sep 2025 14:23:16 -0400 Subject: [PATCH 06/17] chore: rm lodash --- package-lock.json | 2 -- packages/compass-web/package.json | 1 - 2 files changed, 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a1da082bcea..12e1fffa162 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52072,7 +52072,6 @@ "express": "^4.21.1", "express-http-proxy": "^2.0.0", "is-ip": "^5.0.1", - "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.19.0", "mongodb-build-info": "^1.7.2", @@ -65007,7 +65006,6 @@ "express": "^4.21.1", "express-http-proxy": "^2.0.0", "is-ip": "^5.0.1", - "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.19.0", "mongodb-build-info": "^1.7.2", diff --git a/packages/compass-web/package.json b/packages/compass-web/package.json index 3e88887c9b1..31a1ee5de24 100644 --- a/packages/compass-web/package.json +++ b/packages/compass-web/package.json @@ -127,7 +127,6 @@ "express": "^4.21.1", "express-http-proxy": "^2.0.0", "is-ip": "^5.0.1", - "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.19.0", "mongodb-build-info": "^1.7.2", From f2425036e1dd9e482cf88ff195b5ec27fa189587 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 29 Sep 2025 12:16:43 +0200 Subject: [PATCH 07/17] fix pnpm version --- .../compass-web/scripts/sync-dist-to-mms.mjs | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mjs b/packages/compass-web/scripts/sync-dist-to-mms.mjs index ab9fd2caa2e..dee3a92b86c 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mjs +++ b/packages/compass-web/scripts/sync-dist-to-mms.mjs @@ -34,15 +34,15 @@ function isDevServerRunning(port, host = '127.0.0.1') { }); } -const okToRunMMS = !process.argv.includes('--no-mms'); - let devServer; -if (okToRunMMS || !(await isDevServerRunning(8081))) { +if (!(await isDevServerRunning(8081))) { console.log('mms dev server is not running... launching!'); - child_process.execFileSync('pnpm', ['install'], { - cwd: process.env.MMS_HOME, - stdio: 'inherit', - }); + + const { engines } = JSON.parse( + await fs.readFile(path.join(process.env.MMS_HOME, 'package.json'), 'utf8') + ); + const pnpmVersion = engines.pnpm ?? 'latest'; + const halfRamMb = Math.min( Math.floor(os.totalmem() / 2 / 1024 / 1024), 16384 @@ -56,14 +56,18 @@ if (okToRunMMS || !(await isDevServerRunning(8081))) { .filter(Boolean) .join(' '); - devServer = child_process.spawn('pnpm', ['run', 'start'], { - cwd: process.env.MMS_HOME, - env: { - ...process.env, - NODE_OPTIONS: mergedNodeOptions, - }, - stdio: 'inherit', - }); + devServer = child_process.spawn( + 'npx', + [`pnpm@${pnpmVersion}`, 'run', 'start'], + { + cwd: process.env.MMS_HOME, + env: { + ...process.env, + NODE_OPTIONS: mergedNodeOptions, + }, + stdio: 'inherit', + } + ); // Wait for dev server to be ready before proceeding console.log('Waiting for dev server to start...'); From 38271efa1f7fea11252d781688d41518e56b63ed Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 29 Sep 2025 12:19:58 +0200 Subject: [PATCH 08/17] fs fix --- packages/compass-web/scripts/sync-dist-to-mms.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mjs b/packages/compass-web/scripts/sync-dist-to-mms.mjs index dee3a92b86c..8fc7defcf56 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mjs +++ b/packages/compass-web/scripts/sync-dist-to-mms.mjs @@ -1,5 +1,5 @@ import process from 'node:process'; -import fs from 'node:fs'; +import fs, { promises as asyncFs } from 'node:fs'; import path from 'node:path'; import child_process from 'node:child_process'; import os from 'node:os'; @@ -39,7 +39,10 @@ if (!(await isDevServerRunning(8081))) { console.log('mms dev server is not running... launching!'); const { engines } = JSON.parse( - await fs.readFile(path.join(process.env.MMS_HOME, 'package.json'), 'utf8') + await asyncFs.readFile( + path.join(process.env.MMS_HOME, 'package.json'), + 'utf8' + ) ); const pnpmVersion = engines.pnpm ?? 'latest'; From 4d5ee40b3d43f7655603be43d1bc770c64ea0180 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 29 Sep 2025 13:15:05 +0200 Subject: [PATCH 09/17] undo typecheck change --- packages/compass-web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass-web/package.json b/packages/compass-web/package.json index 31a1ee5de24..814526e0e89 100644 --- a/packages/compass-web/package.json +++ b/packages/compass-web/package.json @@ -50,7 +50,7 @@ "analyze": "npm run webpack -- --mode production --analyze", "watch": "npm run webpack -- --mode development --watch", "sync": "node scripts/sync-dist-to-mms.mjs", - "typecheck": "tsc -p tsconfig-lint.json --noEmit", + "typecheck": "tsc -p tsconfig.json --noEmit", "eslint": "eslint-compass", "prettier": "prettier-compass", "lint": "npm run eslint . && npm run prettier -- --check .", From 39c037b0d3f6f3c058e2151028609ded51f839ca Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 30 Sep 2025 11:18:50 +0200 Subject: [PATCH 10/17] fix prefixing --- scripts/start.mts | 41 +++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/scripts/start.mts b/scripts/start.mts index ee0afcbd4ab..dcdaf084147 100644 --- a/scripts/start.mts +++ b/scripts/start.mts @@ -68,16 +68,17 @@ if ( // Check if we need prefixing (more than one target enabled) const enabledTargets = Object.values(targets).filter((t) => t.enabled); -const needsPrefixing = enabledTargets.length > 1; +const usePrefixing = enabledTargets.length > 1; +const startPrefix = usePrefixing ? `start | ` : ``; const subProcesses: child_process.ChildProcess[] = []; async function cleanup(signal: NodeJS.Signals) { for (const p of subProcesses) p.kill(signal); - console.log('\nstart | requested termination.'); + console.log(`\n${startPrefix}requested termination.`); await timers.setTimeout(10_000); const stillRunning = subProcesses.filter((p) => p.exitCode === null); for (const p of stillRunning) p.kill('SIGTERM'); - console.log('\nstart | done.'); + console.log(`\n${startPrefix}done.`); process.exit(0); } @@ -114,8 +115,7 @@ function spawnTarget( command: string, workspace: string, args: string[], - targetName: string, - usePrefixing: boolean + targetName: string ) { // Only set color-forcing env vars if user hasn't set color preferences const colorEnv: Record = {}; @@ -137,13 +137,13 @@ function spawnTarget( ...(args.length ? ['--', ...args] : []), ], { - stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr all piped + stdio: ['inherit', 'pipe', 'pipe'], env: { ...process.env, ...colorEnv }, }, ]; const paddedName = targetName.padEnd(8); - console.log(`start | ${spawnArgs[0]} ${spawnArgs[1].join(' ')}`); + console.log(`${startPrefix}${spawnArgs[0]} ${spawnArgs[1].join(' ')}`); const subProcess = child_process.spawn(...spawnArgs); // Set up stdout pipeline with error handling @@ -172,10 +172,12 @@ function spawnTarget( createPrefixTransform(paddedName), process.stderr, { end: false } - ).catch((err) => console.error(`start | stderr pipeline error:`, err)); + ).catch((err) => + console.error(`${startPrefix}stderr pipeline error:`, err) + ); } else { pipeline(subProcess.stderr, process.stderr, { end: false }).catch((err) => - console.error(`start | stderr pipeline error:`, err) + console.error(`${startPrefix}stderr pipeline error:`, err) ); } } @@ -185,25 +187,13 @@ function spawnTarget( if (targets.desktop.enabled) { subProcesses.push( - spawnTarget( - 'start', - 'mongodb-compass', - targets.desktop.args, - 'desktop', - needsPrefixing - ) + spawnTarget('start', 'mongodb-compass', targets.desktop.args, 'desktop') ); } if (targets.sync.enabled) { subProcesses.push( - spawnTarget( - 'sync', - '@mongodb-js/compass-web', - targets.sync.args, - 'sync', - needsPrefixing - ) + spawnTarget('sync', '@mongodb-js/compass-web', targets.sync.args, 'sync') ); } @@ -213,8 +203,7 @@ if (targets.sandbox.enabled) { 'start', '@mongodb-js/compass-web', targets.sandbox.args, - 'sandbox', - needsPrefixing + 'sandbox' ) ); } @@ -224,6 +213,6 @@ for (const subProcess of subProcesses) { if (subProcess.stdin) pipeline(process.stdin, subProcess.stdin, { end: false }).catch((err) => { if (err.code !== 'EPIPE' && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') - console.error(`start | stdin pipeline error:`, err); + console.error(`${startPrefix}stdin pipeline error:`, err); }); } From 9e545ab0b4adc4d924e658b1ffcb9118fafa365c Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 30 Sep 2025 11:20:54 +0200 Subject: [PATCH 11/17] use fetch head instead --- .../compass-web/scripts/sync-dist-to-mms.mjs | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mjs b/packages/compass-web/scripts/sync-dist-to-mms.mjs index 8fc7defcf56..3c58f9c587c 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mjs +++ b/packages/compass-web/scripts/sync-dist-to-mms.mjs @@ -4,7 +4,6 @@ import path from 'node:path'; import child_process from 'node:child_process'; import os from 'node:os'; import util from 'node:util'; -import net from 'node:net'; import timers from 'node:timers/promises'; if (!process.env.MMS_HOME) { @@ -13,25 +12,17 @@ if (!process.env.MMS_HOME) { ); } -function isDevServerRunning(port, host = '127.0.0.1') { - return new Promise((resolve) => { - const socket = new net.Socket(); - socket - .setTimeout(1000) - .on('connect', () => { - socket.destroy(); - resolve(true); +async function isDevServerRunning(port, host = '127.0.0.1') { + try { + return ( + await fetch(`http://${host}:${port}`, { + method: 'HEAD', + signal: AbortSignal.timeout(3000), }) - .on('error', () => { - socket.destroy(); - resolve(false); - }) - .on('timeout', () => { - socket.destroy(); - resolve(false); - }) - .connect(port, host); - }); + ).ok; + } catch (error) { + return false; + } } let devServer; @@ -112,6 +103,7 @@ fs.mkdirSync(tmpDir, { recursive: true }); fs.cpSync(destDir, tmpDir, { recursive: true }); let oneSec = null; +let queued = false; async function copyDist() { // If a copy is already in progress, return early (debounce) if (oneSec) return; From c24e8561cb0033de74518189d2bd65898ea17d12 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 30 Sep 2025 11:30:08 +0200 Subject: [PATCH 12/17] fix shortcuts --- scripts/start.mts | 84 ++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/scripts/start.mts b/scripts/start.mts index dcdaf084147..aeee0eae686 100644 --- a/scripts/start.mts +++ b/scripts/start.mts @@ -128,6 +128,12 @@ function spawnTarget( colorEnv.CLICOLOR_FORCE = '1'; } + // Desktop needs full terminal control for shortcuts, others need piped output for prefixing + const stdio: child_process.StdioOptions = + targetName === 'desktop' && !usePrefixing + ? 'inherit' + : ['inherit', 'pipe', 'pipe']; + const spawnArgs: Parameters = [ 'npm', [ @@ -137,7 +143,7 @@ function spawnTarget( ...(args.length ? ['--', ...args] : []), ], { - stdio: ['inherit', 'pipe', 'pipe'], + stdio, env: { ...process.env, ...colorEnv }, }, ]; @@ -146,39 +152,44 @@ function spawnTarget( console.log(`${startPrefix}${spawnArgs[0]} ${spawnArgs[1].join(' ')}`); const subProcess = child_process.spawn(...spawnArgs); - // Set up stdout pipeline with error handling - if (subProcess.stdout) { - subProcess.stdout.setEncoding('utf-8'); - if (usePrefixing) { - pipeline( - subProcess.stdout, - createPrefixTransform(paddedName), - process.stdout, - { end: false } - ).catch((err) => console.error(`start | stdout pipeline error:`, err)); - } else { - pipeline(subProcess.stdout, process.stdout, { end: false }).catch((err) => - console.error(`start | stdout pipeline error:`, err) - ); + // Only set up output piping if we're not using full inheritance + if (stdio !== 'inherit') { + // Set up stdout pipeline with error handling + if (subProcess.stdout) { + subProcess.stdout.setEncoding('utf-8'); + if (usePrefixing) { + pipeline( + subProcess.stdout, + createPrefixTransform(paddedName), + process.stdout, + { end: false } + ).catch((err) => + console.error(`start | stdout pipeline error:`, err) + ); + } else { + pipeline(subProcess.stdout, process.stdout, { end: false }).catch( + (err) => console.error(`start | stdout pipeline error:`, err) + ); + } } - } - // Set up stderr pipeline with error handling - if (subProcess.stderr) { - subProcess.stderr.setEncoding('utf-8'); - if (usePrefixing) { - pipeline( - subProcess.stderr, - createPrefixTransform(paddedName), - process.stderr, - { end: false } - ).catch((err) => - console.error(`${startPrefix}stderr pipeline error:`, err) - ); - } else { - pipeline(subProcess.stderr, process.stderr, { end: false }).catch((err) => - console.error(`${startPrefix}stderr pipeline error:`, err) - ); + // Set up stderr pipeline with error handling + if (subProcess.stderr) { + subProcess.stderr.setEncoding('utf-8'); + if (usePrefixing) { + pipeline( + subProcess.stderr, + createPrefixTransform(paddedName), + process.stderr, + { end: false } + ).catch((err) => + console.error(`${startPrefix}stderr pipeline error:`, err) + ); + } else { + pipeline(subProcess.stderr, process.stderr, { end: false }).catch( + (err) => console.error(`${startPrefix}stderr pipeline error:`, err) + ); + } } } @@ -207,12 +218,3 @@ if (targets.sandbox.enabled) { ) ); } - -// Forward stdin to all subprocesses using pipeline -for (const subProcess of subProcesses) { - if (subProcess.stdin) - pipeline(process.stdin, subProcess.stdin, { end: false }).catch((err) => { - if (err.code !== 'EPIPE' && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') - console.error(`${startPrefix}stdin pipeline error:`, err); - }); -} From c8e871794de8f6092150dc50aad0e71f34b14c72 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 30 Sep 2025 11:44:57 +0200 Subject: [PATCH 13/17] improve signal handling --- packages/compass-web/package.json | 2 +- ...c-dist-to-mms.mjs => sync-dist-to-mms.mts} | 111 ++++++++++-------- scripts/start.mts | 62 ++++++++-- 3 files changed, 116 insertions(+), 59 deletions(-) rename packages/compass-web/scripts/{sync-dist-to-mms.mjs => sync-dist-to-mms.mts} (74%) diff --git a/packages/compass-web/package.json b/packages/compass-web/package.json index 814526e0e89..34a801cfa74 100644 --- a/packages/compass-web/package.json +++ b/packages/compass-web/package.json @@ -49,7 +49,7 @@ "start": "electron ./scripts/electron-proxy.js", "analyze": "npm run webpack -- --mode production --analyze", "watch": "npm run webpack -- --mode development --watch", - "sync": "node scripts/sync-dist-to-mms.mjs", + "sync": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/sync-dist-to-mms.mts", "typecheck": "tsc -p tsconfig.json --noEmit", "eslint": "eslint-compass", "prettier": "prettier-compass", diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mjs b/packages/compass-web/scripts/sync-dist-to-mms.mts similarity index 74% rename from packages/compass-web/scripts/sync-dist-to-mms.mjs rename to packages/compass-web/scripts/sync-dist-to-mms.mts index 3c58f9c587c..37e01486aaf 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mjs +++ b/packages/compass-web/scripts/sync-dist-to-mms.mts @@ -12,7 +12,58 @@ if (!process.env.MMS_HOME) { ); } -async function isDevServerRunning(port, host = '127.0.0.1') { +// Set up early signal handling and cleanup +let devServer: child_process.ChildProcess | undefined; +let distWatcher: fs.FSWatcher | undefined; +let webpackWatchProcess: child_process.ChildProcess | undefined; +let tmpDir: string | undefined; +let destDir: string | undefined; + +const failProofRunner = () => + new (class FailProofRunner extends Array { + append(...fns: any[]) { + this.push(...fns); + return this; + } + + run() { + const errors = this.map((f) => { + try { + f(); + } catch (e) { + return e; + } + }).filter((e) => e); + + if (errors.length) { + fs.writeSync( + process.stdout.fd, + util.inspect(errors, { depth: 20 }) + '\n' + ); + } + + return errors.length; + } + })(); + +function cleanup(signalName: NodeJS.Signals): void { + console.log(`\nReceived ${signalName}, cleaning up...`); + const errorCount = failProofRunner() + .append(() => distWatcher?.close()) + .append(() => webpackWatchProcess?.kill(signalName)) + .append(() => devServer?.kill(signalName)) + .append(() => tmpDir && destDir && fs.cpSync(tmpDir, destDir, { recursive: true })) + .append(() => tmpDir && fs.rmSync(tmpDir, { recursive: true, force: true })) + .run(); + fs.writeSync(process.stdout.fd, 'Exit compass-web sync...\n'); + process.exit(errorCount); +} + +// Set up signal handlers immediately +process.on('SIGINT', () => cleanup('SIGINT')); +process.on('SIGTERM', () => cleanup('SIGTERM')); + +async function isDevServerRunning(port: number, host: string = '127.0.0.1'): Promise { try { return ( await fetch(`http://${host}:${port}`, { @@ -25,7 +76,6 @@ async function isDevServerRunning(port, host = '127.0.0.1') { } } -let devServer; if (!(await isDevServerRunning(8081))) { console.log('mms dev server is not running... launching!'); @@ -82,7 +132,7 @@ if (!(await isDevServerRunning(8081))) { const srcDir = path.resolve(import.meta.dirname, '..', 'dist'); -const destDir = path.dirname( +destDir = path.dirname( child_process.execFileSync( 'node', ['-e', "console.log(require.resolve('@mongodb-js/compass-web'))"], @@ -90,23 +140,23 @@ const destDir = path.dirname( ) ); -const tmpDir = path.join( +tmpDir = path.join( os.tmpdir(), `mongodb-js--compass-web-${Date.now().toString(36)}` ); fs.mkdirSync(srcDir, { recursive: true }); -// Create a copy of current dist that will be overriden by link, we'll restore +// Create a copy of current dist that will be overridden by link, we'll restore // it when we are done fs.mkdirSync(tmpDir, { recursive: true }); fs.cpSync(destDir, tmpDir, { recursive: true }); -let oneSec = null; -let queued = false; -async function copyDist() { +let oneSec: Promise | null = null; +async function copyDist(): Promise { // If a copy is already in progress, return early (debounce) if (oneSec) return; + if (!destDir) throw new Error('destDir not initialized'); fs.cpSync(srcDir, destDir, { recursive: true }); oneSec = timers.setTimeout(1000); await oneSec; @@ -119,49 +169,8 @@ async function copyDist() { // monorepo instead of mms one. To work around that we are just watching for any // file changes in the dist folder and copying them as-is to whatever place // compass-web was installed in mms node_modules -const distWatcher = fs.watch(srcDir, () => void copyDist()); +distWatcher = fs.watch(srcDir, () => void copyDist()); -const webpackWatchProcess = child_process.spawn('npm', ['run', 'watch'], { +webpackWatchProcess = child_process.spawn('npm', ['run', 'watch'], { stdio: 'inherit', }); - -const failProofRunner = () => - new (class FailProofRunner extends Array { - append(...fns) { - this.push(...fns); - return this; - } - - run() { - const errors = this.map((f) => { - try { - f(); - } catch (e) { - return e; - } - }).filter((e) => e); - - if (errors.length) { - fs.writeSync( - process.stdout.fd, - util.inspect(errors, { depth: 20 }) + '\n' - ); - } - - return errors.length; - } - })(); - -function cleanup(signalName) { - const errorCount = failProofRunner() - .append(() => distWatcher.close()) - .append(() => webpackWatchProcess.kill(signalName)) - .append(() => devServer?.kill(signalName)) - .append(() => fs.cpSync(tmpDir, destDir, { recursive: true })) - .append(() => fs.rmSync(tmpDir, { recursive: true, force: true })) - .run(); - fs.writeSync(process.stdout.fd, 'Exit compass-web sync...\n'); - process.exit(errorCount); -} - -process.on('SIGINT', cleanup).on('SIGTERM', cleanup); diff --git a/scripts/start.mts b/scripts/start.mts index aeee0eae686..311d6057801 100644 --- a/scripts/start.mts +++ b/scripts/start.mts @@ -72,17 +72,63 @@ const usePrefixing = enabledTargets.length > 1; const startPrefix = usePrefixing ? `start | ` : ``; const subProcesses: child_process.ChildProcess[] = []; +let isCleaningUp = false; + async function cleanup(signal: NodeJS.Signals) { - for (const p of subProcesses) p.kill(signal); - console.log(`\n${startPrefix}requested termination.`); - await timers.setTimeout(10_000); + if (isCleaningUp) return; // Prevent multiple cleanup calls + isCleaningUp = true; + + console.log(`\n${startPrefix}received ${signal}, terminating processes...`); + + // Kill all processes immediately with the received signal + for (const p of subProcesses) { + if (p.pid && p.exitCode === null) { + try { + process.kill(-p.pid, signal); // Kill entire process group + } catch { + p.kill(signal); // Fallback to killing just the process + } + } + } + + // Wait a shorter time for graceful shutdown + await timers.setTimeout(3000); + + // Force kill any remaining processes const stillRunning = subProcesses.filter((p) => p.exitCode === null); - for (const p of stillRunning) p.kill('SIGTERM'); - console.log(`\n${startPrefix}done.`); - process.exit(0); + if (stillRunning.length > 0) { + console.log( + `${startPrefix}force killing ${stillRunning.length} remaining processes...` + ); + for (const p of stillRunning) { + if (p.pid) { + try { + process.kill(-p.pid, 'SIGKILL'); // Kill entire process group + } catch { + p.kill('SIGKILL'); // Fallback + } + } + } + } + + console.log(`${startPrefix}done.`); + process.exit(signal === 'SIGTERM' ? 0 : 1); } -process.once('SIGINT', cleanup).once('SIGTERM', cleanup); +// Handle signals properly even when child processes inherit stdio +process.on('SIGINT', () => cleanup('SIGINT')); +process.on('SIGTERM', () => cleanup('SIGTERM')); + +// Ensure we exit cleanly on uncaught exceptions +process.on('uncaughtException', (err) => { + console.error('Uncaught exception:', err); + cleanup('SIGTERM'); +}); + +process.on('unhandledRejection', (err) => { + console.error('Unhandled rejection:', err); + cleanup('SIGTERM'); +}); // Helper function to create a transform stream that prefixes lines function createPrefixTransform(prefix: string) { @@ -145,6 +191,8 @@ function spawnTarget( { stdio, env: { ...process.env, ...colorEnv }, + // Create a new process group so we can kill the entire tree + detached: true, }, ]; From e47e12fa321a60b433fe12035bd14c658b9e2205 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 30 Sep 2025 11:49:46 +0200 Subject: [PATCH 14/17] fix debouncer --- .../compass-web/scripts/sync-dist-to-mms.mts | 73 ++++++++++--------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mts b/packages/compass-web/scripts/sync-dist-to-mms.mts index 37e01486aaf..30f479bcd47 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mts +++ b/packages/compass-web/scripts/sync-dist-to-mms.mts @@ -16,8 +16,25 @@ if (!process.env.MMS_HOME) { let devServer: child_process.ChildProcess | undefined; let distWatcher: fs.FSWatcher | undefined; let webpackWatchProcess: child_process.ChildProcess | undefined; -let tmpDir: string | undefined; -let destDir: string | undefined; + +const tmpDir = path.join( + os.tmpdir(), + `mongodb-js--compass-web-${Date.now().toString(36)}` +); +const srcDir = path.resolve(import.meta.dirname, '..', 'dist'); +const destDir = path.dirname( + child_process.execFileSync( + 'node', + ['-e', "console.log(require.resolve('@mongodb-js/compass-web'))"], + { cwd: process.env.MMS_HOME, encoding: 'utf-8' } + ) +); + +fs.mkdirSync(srcDir, { recursive: true }); +// Create a copy of current dist that will be overridden by link, we'll restore +// it when we are done +fs.mkdirSync(tmpDir, { recursive: true }); +fs.cpSync(destDir, tmpDir, { recursive: true }); const failProofRunner = () => new (class FailProofRunner extends Array { @@ -52,8 +69,8 @@ function cleanup(signalName: NodeJS.Signals): void { .append(() => distWatcher?.close()) .append(() => webpackWatchProcess?.kill(signalName)) .append(() => devServer?.kill(signalName)) - .append(() => tmpDir && destDir && fs.cpSync(tmpDir, destDir, { recursive: true })) - .append(() => tmpDir && fs.rmSync(tmpDir, { recursive: true, force: true })) + .append(() => fs.cpSync(tmpDir, destDir, { recursive: true })) + .append(() => fs.rmSync(tmpDir, { recursive: true, force: true })) .run(); fs.writeSync(process.stdout.fd, 'Exit compass-web sync...\n'); process.exit(errorCount); @@ -63,7 +80,10 @@ function cleanup(signalName: NodeJS.Signals): void { process.on('SIGINT', () => cleanup('SIGINT')); process.on('SIGTERM', () => cleanup('SIGTERM')); -async function isDevServerRunning(port: number, host: string = '127.0.0.1'): Promise { +async function isDevServerRunning( + port: number, + host: string = '127.0.0.1' +): Promise { try { return ( await fetch(`http://${host}:${port}`, { @@ -130,36 +150,23 @@ if (!(await isDevServerRunning(8081))) { console.log('Skipping running MMS dev server...'); } -const srcDir = path.resolve(import.meta.dirname, '..', 'dist'); - -destDir = path.dirname( - child_process.execFileSync( - 'node', - ['-e', "console.log(require.resolve('@mongodb-js/compass-web'))"], - { cwd: process.env.MMS_HOME, encoding: 'utf-8' } - ) -); - -tmpDir = path.join( - os.tmpdir(), - `mongodb-js--compass-web-${Date.now().toString(36)}` -); - -fs.mkdirSync(srcDir, { recursive: true }); - -// Create a copy of current dist that will be overridden by link, we'll restore -// it when we are done -fs.mkdirSync(tmpDir, { recursive: true }); -fs.cpSync(destDir, tmpDir, { recursive: true }); - let oneSec: Promise | null = null; +let pendingCopy = false; + async function copyDist(): Promise { - // If a copy is already in progress, return early (debounce) - if (oneSec) return; - if (!destDir) throw new Error('destDir not initialized'); - fs.cpSync(srcDir, destDir, { recursive: true }); - oneSec = timers.setTimeout(1000); - await oneSec; + // If a copy is already in progress, mark that we need another copy + if (oneSec) { + pendingCopy = true; + return; + } + // Keep copying until there are no more pending requests + do { + pendingCopy = false; + fs.cpSync(srcDir, destDir, { recursive: true }); + oneSec = timers.setTimeout(1000); + await oneSec; + } while (pendingCopy); + oneSec = null; } From 255706ee7570b3aa5fc9f478b675d53899f74672 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 1 Oct 2025 01:33:08 +0200 Subject: [PATCH 15/17] rm eslint change --- packages/compass-web/.eslintrc.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/compass-web/.eslintrc.js b/packages/compass-web/.eslintrc.js index 970d914f9ab..f64a0ab086d 100644 --- a/packages/compass-web/.eslintrc.js +++ b/packages/compass-web/.eslintrc.js @@ -6,17 +6,4 @@ module.exports = { tsconfigRootDir: __dirname, project: ['./tsconfig.json'], }, - overrides: [ - { - files: ['**/*.mjs'], - parserOptions: { - ecmaVersion: 2023, - sourceType: 'module', - }, - env: { - es2023: true, - node: true, - }, - }, - ], }; From add4ce63fbdc355a017150d684f3fdac09aab971 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 2 Oct 2025 15:05:13 +0200 Subject: [PATCH 16/17] chore: nits --- packages/compass-web/scripts/sync-dist-to-mms.mts | 1 + scripts/start.mts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mts b/packages/compass-web/scripts/sync-dist-to-mms.mts index 30f479bcd47..b0a234ef3f8 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mts +++ b/packages/compass-web/scripts/sync-dist-to-mms.mts @@ -128,6 +128,7 @@ if (!(await isDevServerRunning(8081))) { env: { ...process.env, NODE_OPTIONS: mergedNodeOptions, + npm_config_engine_strict: `${false}`, }, stdio: 'inherit', } diff --git a/scripts/start.mts b/scripts/start.mts index 311d6057801..e5b323ce077 100644 --- a/scripts/start.mts +++ b/scripts/start.mts @@ -92,7 +92,7 @@ async function cleanup(signal: NodeJS.Signals) { } // Wait a shorter time for graceful shutdown - await timers.setTimeout(3000); + await timers.setTimeout(10_000); // Force kill any remaining processes const stillRunning = subProcesses.filter((p) => p.exitCode === null); From 47d21c7ffd9859714c7805562c2df5072abd0580 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 2 Oct 2025 15:22:32 +0200 Subject: [PATCH 17/17] use node execPath --- packages/compass-web/scripts/sync-dist-to-mms.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass-web/scripts/sync-dist-to-mms.mts b/packages/compass-web/scripts/sync-dist-to-mms.mts index b0a234ef3f8..1aeeadf7c01 100644 --- a/packages/compass-web/scripts/sync-dist-to-mms.mts +++ b/packages/compass-web/scripts/sync-dist-to-mms.mts @@ -24,7 +24,7 @@ const tmpDir = path.join( const srcDir = path.resolve(import.meta.dirname, '..', 'dist'); const destDir = path.dirname( child_process.execFileSync( - 'node', + process.execPath, ['-e', "console.log(require.resolve('@mongodb-js/compass-web'))"], { cwd: process.env.MMS_HOME, encoding: 'utf-8' } )