diff --git a/forge.config.ts b/forge.config.ts index c6b7d186b0..3c058cb2a4 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -156,6 +156,14 @@ const config: ForgeConfig = { js: path.join(root, './src/isolated-run-button.ts'), name: 'isolated_run_button', }, + { + html: path.join(root, './static/process-monitor.html'), + js: path.join(root, './src/renderer/process-monitor-entry.tsx'), + name: 'process_monitor_window', + preload: { + js: path.join(root, 'src/preload/preload.ts'), + }, + }, ], }, }, diff --git a/package.json b/package.json index a9a8c04413..1a5317fcbd 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,8 @@ "node-watch": "^0.7.3", "p-debounce": "^2.0.0", "parse-env-string": "^1.0.1", + "pidtree": "^1.0.0", + "pidusage": "^4.0.1", "prettier": "^3.6.2", "react": "^16.14.0", "react-dom": "^16.14.0", @@ -94,6 +96,7 @@ "@types/fs-extra": "^9.0.7", "@types/node": "^22.19.1", "@types/parse-env-string": "^1.0.2", + "@types/pidusage": "^2.0.5", "@types/react": "^16.14.0", "@types/react-dom": "^16.9.11", "@types/react-window": "^1.8.8", diff --git a/src/interfaces.ts b/src/interfaces.ts index cc2d83d1a7..93d571695d 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -193,7 +193,8 @@ export type FiddleEvent = | 'toggle-monaco-option' | 'undo-in-editor' | 'version-download-progress' - | 'version-state-changed'; + | 'version-state-changed' + | 'process-metrics-update'; export interface MessageOptions { message: string; @@ -284,6 +285,7 @@ export enum GlobalSetting { localVersion = 'local-electron-versions', packageAuthor = 'packageAuthor', isShowingGistHistory = 'isShowingGistHistory', + isProcessMonitorEnabled = 'isProcessMonitorEnabled', packageManager = 'packageManager', showObsoleteVersions = 'showObsoleteVersions', showUndownloadedVersions = 'showUndownloadedVersions', @@ -332,8 +334,16 @@ export interface StartFiddleOptions { packageManager: IPackageManager; useSocketFirewall: boolean; isKeepingUserDataDirs: boolean; + isProcessMonitorEnabled: boolean; } export interface DownloadVersionParams { mirror: Mirrors; } +export interface ProcessMetric { + pid: number; + name?: string; + type?: string; + cpu: number; + memory: number; +} diff --git a/src/ipc-events.ts b/src/ipc-events.ts index 62514cc9e9..1a79dca50b 100644 --- a/src/ipc-events.ts +++ b/src/ipc-events.ts @@ -86,6 +86,7 @@ export enum IpcEvents { GITHUB_TOKEN_CHECK_AUTH = 'GITHUB_TOKEN_CHECK_AUTH', GITHUB_TOKEN_SIGN_IN = 'GITHUB_TOKEN_SIGN_IN', GITHUB_TOKEN_SIGN_OUT = 'GITHUB_TOKEN_SIGN_OUT', + PROCESS_METRICS_UPDATE = 'PROCESS_METRICS_UPDATE', } export const ipcMainEvents = [ diff --git a/src/less/components/process-monitor.less b/src/less/components/process-monitor.less new file mode 100644 index 0000000000..25364ff292 --- /dev/null +++ b/src/less/components/process-monitor.less @@ -0,0 +1,173 @@ +@import '../variables.less'; + +// ─── Body reset ──────────────────────────────────────────────────────────── +body { + margin: 0; + padding: 0; + background: @background-2; + overflow: hidden; + font-family: @fonts-common; + color: @text-color-1; +} + +// ─── Root container ──────────────────────────────────────────────────────── +.process-monitor { + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + background: @background-2; + color: @text-color-1; + overflow: hidden; + + // ─── Header bar ────────────────────────────────────────────────────── + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 14px; + background: @background-4; + border-bottom: @border; + user-select: none; + -webkit-app-region: drag; + flex-shrink: 0; + min-height: 36px; + } + + &__title { + font-size: 12px; + font-weight: 600; + color: @foreground-1; + letter-spacing: 0.04em; + text-transform: uppercase; + display: flex; + align-items: center; + gap: 7px; + } + + &__icon { + font-size: 14px; + opacity: 0.75; + color: @foreground-2; + } + + &__live-badge { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + color: #4caf7d; + animation: live-pulse 1.6s ease-in-out infinite; + text-transform: uppercase; + } + + // ─── Table area ────────────────────────────────────────────────────── + &__table-container { + flex: 1; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: @background-2; + } + + &::-webkit-scrollbar-thumb { + background: @border-color-1; + border-radius: 3px; + } + } + + &__table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + + thead tr { + background: @background-3; + } + + th { + padding: 6px 14px; + text-align: left; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.07em; + text-transform: uppercase; + color: @foreground-3; + border-bottom: @border; + } + + td { + padding: 7px 14px; + border-bottom: 1px solid @border-color-2; + color: @text-color-3; + white-space: nowrap; + } + + tbody tr:last-child td { + border-bottom: none; + } + } + + // ─── Rows ───────────────────────────────────────────────────────────── + &__row { + transition: background 0.1s ease; + + &:hover { + background: @background-1; + } + } + + // ─── Cell accents ───────────────────────────────────────────────────── + &__name { + color: @foreground-1; + font-weight: 500; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + } + + &__pid { + color: @foreground-3; + font-size: 11px; + font-family: 'Cascadia Code', 'Fira Code', monospace; + } + + &__cpu { + color: #4caf7d; + font-weight: 600; + font-family: 'Cascadia Code', 'Fira Code', monospace; + + &--high { + color: var(--error-color); + } + } + + &__memory { + color: @foreground-2; + font-family: 'Cascadia Code', 'Fira Code', monospace; + } + + // ─── Empty state ────────────────────────────────────────────────────── + &__empty { + text-align: center; + padding: 24px 14px !important; + color: @foreground-3; + font-style: italic; + font-size: 12px; + } +} + +// ─── Animations ───────────────────────────────────────────────────────────── +@keyframes live-pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.35; + } +} diff --git a/src/main/fiddle-core.ts b/src/main/fiddle-core.ts index eeda31e9f0..b2c58e01fd 100644 --- a/src/main/fiddle-core.ts +++ b/src/main/fiddle-core.ts @@ -1,6 +1,9 @@ import { ChildProcess } from 'node:child_process'; import * as fs from 'node:fs'; +import pidusage from 'pidusage'; +import pidtree from 'pidtree'; + import { ElectronVersions, Installer, Runner } from '@electron/fiddle-core'; import { BrowserWindow, @@ -14,6 +17,10 @@ import { import { ELECTRON_DOWNLOAD_PATH, ELECTRON_INSTALL_PATH } from './constants'; import { cleanupDirectory, deleteUserData, saveFilesToTemp } from './files'; import { ipcMainManager } from './ipc'; +import { + closeProcessMonitorWindow, + createProcessMonitorWindow, +} from './process-monitor-window'; import { ISOLATED_ACTIONS_SCHEME, getIsolatedRunButtonFrame, @@ -27,6 +34,7 @@ import { DownloadVersionParams, PACKAGE_NAME, PMOperationOptions, + ProcessMetric, ProgressObject, RunResult, StartFiddleOptions, @@ -165,6 +173,7 @@ async function startFiddleImpl(webContents: WebContents): Promise { env: rendererEnv, executionFlags, isKeepingUserDataDirs, + isProcessMonitorEnabled, modules, packageManager, useSocketFirewall, @@ -303,8 +312,51 @@ async function startFiddleImpl(webContents: WebContents): Promise { child.stdout?.on('data', (data) => pushOutput(webContents, data.toString())); child.stderr?.on('data', (data) => pushOutput(webContents, data.toString())); + // --- Process Monitor polling --- + let metricsInterval: ReturnType | null = null; + + if (isProcessMonitorEnabled && child.pid) { + const rootPid = child.pid; + + const monitorWindow = createProcessMonitorWindow(); + + metricsInterval = setInterval(async () => { + try { + // Get all descendant PIDs + const children = await pidtree(rootPid, { root: true }); + const pids = [rootPid, ...children]; + + // Get CPU + memory for each PID + const stats = await pidusage(pids); + const metrics: ProcessMetric[] = pids + .filter((pid) => stats[pid]) + .map((pid, idx) => ({ + pid, + type: idx === 0 ? 'Main' : `Renderer/Helper ${idx}`, + cpu: stats[pid].cpu, + // pidusage returns memory in bytes; convert to KB for display + memory: Math.round(stats[pid].memory / 1024), + })); + + if (!monitorWindow.isDestroyed()) { + monitorWindow.webContents.send( + IpcEvents.PROCESS_METRICS_UPDATE, + metrics, + ); + } + } catch { + // Process may have already exited; ignore errors. + } + }, 1500); + } + return new Promise((resolve) => { child.on('close', async (code, signal) => { + if (metricsInterval) { + clearInterval(metricsInterval); + metricsInterval = null; + closeProcessMonitorWindow(); + } fiddleProcesses.delete(webContents); await cleanup(); diff --git a/src/main/process-monitor-window.ts b/src/main/process-monitor-window.ts new file mode 100644 index 0000000000..628cb51071 --- /dev/null +++ b/src/main/process-monitor-window.ts @@ -0,0 +1,67 @@ +import { BrowserWindow } from 'electron'; + +// Global variables exposed by forge/webpack-plugin to reference +// the entry point of preload and index.html over http:// +declare const PROCESS_MONITOR_WINDOW_WEBPACK_ENTRY: string; +declare const PROCESS_MONITOR_WINDOW_PRELOAD_WEBPACK_ENTRY: string; + +let processMonitorWindow: BrowserWindow | null = null; + +/** + * Creates and shows the Process Monitor window. + */ +export function createProcessMonitorWindow(): BrowserWindow { + if (processMonitorWindow) { + // If it already exists, just show and focus it + if (processMonitorWindow.isMinimized()) processMonitorWindow.restore(); + processMonitorWindow.show(); + processMonitorWindow.focus(); + return processMonitorWindow; + } + + processMonitorWindow = new BrowserWindow({ + width: 450, + height: 350, + minWidth: 350, + minHeight: 200, + title: 'Process Monitor - Electron Fiddle', + alwaysOnTop: true, + skipTaskbar: true, + frame: false, + backgroundColor: '#12121c', + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: PROCESS_MONITOR_WINDOW_PRELOAD_WEBPACK_ENTRY, + }, + }); + + processMonitorWindow.loadURL(PROCESS_MONITOR_WINDOW_WEBPACK_ENTRY); + + processMonitorWindow.on('closed', () => { + processMonitorWindow = null; + }); + + // Prevent default window opening + processMonitorWindow.webContents.setWindowOpenHandler(() => { + return { action: 'deny' }; + }); + + return processMonitorWindow; +} + +/** + * Gets the current Process Monitor window, if any. + */ +export function getProcessMonitorWindow(): BrowserWindow | null { + return processMonitorWindow; +} + +/** + * Closes the Process Monitor window if it exists. + */ +export function closeProcessMonitorWindow() { + if (processMonitorWindow) { + processMonitorWindow.close(); + } +} diff --git a/src/preload/preload.ts b/src/preload/preload.ts index f803341539..79a43869b9 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -52,6 +52,7 @@ const channelMapping: Record = { 'undo-in-editor': IpcEvents.UNDO_IN_EDITOR, 'version-download-progress': IpcEvents.VERSION_DOWNLOAD_PROGRESS, 'version-state-changed': IpcEvents.VERSION_STATE_CHANGED, + 'process-metrics-update': IpcEvents.PROCESS_METRICS_UPDATE, } as const; function addEventListener( diff --git a/src/renderer/components/output-editors-wrapper.tsx b/src/renderer/components/output-editors-wrapper.tsx index b26f31b583..7d594cff2b 100644 --- a/src/renderer/components/output-editors-wrapper.tsx +++ b/src/renderer/components/output-editors-wrapper.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; +import { observer } from 'mobx-react'; import { reaction } from 'mobx'; import { Mosaic, MosaicNode, MosaicParent } from 'react-mosaic-component'; @@ -19,60 +20,62 @@ interface WrapperState { export type WrapperEditorId = 'output' | 'editors' | 'sidebar'; -export class OutputEditorsWrapper extends React.Component< - WrapperProps, - WrapperState -> { - private MOSAIC_ELEMENTS = { - output: , - editors: , - sidebar: , - }; +export const OutputEditorsWrapper = observer( + class OutputEditorsWrapper extends React.Component< + WrapperProps, + WrapperState + > { + private MOSAIC_ELEMENTS = { + output: , + editors: , + sidebar: , + }; - constructor(props: any) { - super(props); - this.state = { - mosaic: { - direction: 'column', - first: 'output', - second: { - direction: 'row', - first: 'sidebar', - second: 'editors', - splitPercentage: 15, + constructor(props: WrapperProps) { + super(props); + this.state = { + mosaic: { + direction: 'column', + first: 'output', + second: { + direction: 'row', + first: 'sidebar', + second: 'editors', + splitPercentage: 15, + }, + splitPercentage: 25, }, - splitPercentage: 25, - }, - focusable: true, - }; - reaction( - () => this.props.appState.isSettingsShowing, - (isSettingsShowing) => this.setState({ focusable: !isSettingsShowing }), - ); - } + focusable: true, + }; + reaction( + () => this.props.appState.isSettingsShowing, + (isSettingsShowing) => this.setState({ focusable: !isSettingsShowing }), + ); + } - public render() { - return ( - - renderTile={(id: string) => - this.MOSAIC_ELEMENTS[id as keyof typeof this.MOSAIC_ELEMENTS] - } - resize={{ minimumPaneSizePercentage: 15 }} - value={this.state.mosaic} - onChange={this.onChange} - className={!this.state.focusable ? 'tabbing-hidden' : undefined} - /> - ); - } + public render() { + return ( + + renderTile={(id: string) => + this.MOSAIC_ELEMENTS[id as keyof typeof this.MOSAIC_ELEMENTS] + } + resize={{ minimumPaneSizePercentage: 15 }} + value={this.state.mosaic} + onChange={this.onChange} + className={!this.state.focusable ? 'tabbing-hidden' : undefined} + /> + ); + } - private onChange = (rootNode: MosaicNode | null) => { - if (rootNode === null) return; - const isConsoleShowing = - (rootNode as MosaicParent).splitPercentage !== 0; + private onChange = (rootNode: MosaicNode | null) => { + if (rootNode === null) return; + const isConsoleShowing = + (rootNode as MosaicParent).splitPercentage !== 0; - if (isConsoleShowing !== this.props.appState.isConsoleShowing) { - this.props.appState.isConsoleShowing = isConsoleShowing; - } - this.setState({ mosaic: rootNode }); - }; -} + if (isConsoleShowing !== this.props.appState.isConsoleShowing) { + this.props.appState.isConsoleShowing = isConsoleShowing; + } + this.setState({ mosaic: rootNode }); + }; + }, +); diff --git a/src/renderer/components/process-monitor.tsx b/src/renderer/components/process-monitor.tsx new file mode 100644 index 0000000000..dc9a80b04d --- /dev/null +++ b/src/renderer/components/process-monitor.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; + +import { ProcessMetric } from '../../interfaces'; + +interface ProcessMonitorState { + metrics: ProcessMetric[]; + isRunning: boolean; +} + +/** + * A panel that shows the live process tree (PID, CPU%, Memory) for a + * running Fiddle. It is mounted in its own standalone window. + */ +export class ProcessMonitor extends React.Component< + Record, + ProcessMonitorState +> { + constructor(props: Record) { + super(props); + this.state = { metrics: [], isRunning: true }; + this.handleMetricsUpdate = this.handleMetricsUpdate.bind(this); + } + + public componentDidMount() { + window.ElectronFiddle.addEventListener( + 'process-metrics-update', + this.handleMetricsUpdate as () => void, + ); + } + + public componentWillUnmount() { + window.ElectronFiddle.removeAllListeners('process-metrics-update'); + } + + private handleMetricsUpdate(metrics: ProcessMetric[]) { + // If we receive an empty array, the Fiddle stopped. + this.setState({ metrics, isRunning: metrics.length > 0 }); + } + + private formatMemory(kb: number): string { + if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(1)} GB`; + if (kb >= 1024) return `${(kb / 1024).toFixed(1)} MB`; + return `${kb} KB`; + } + + private formatCpu(cpu: number): string { + return `${cpu.toFixed(1)}%`; + } + + public render() { + const { isRunning, metrics } = this.state; + + return ( +
+
+ + ⚙ + Process Monitor + + {isRunning && ( + ● LIVE + )} +
+
+ + + + + + + + + + + {metrics.length === 0 ? ( + + + + ) : ( + metrics.map((m) => ( + + + + + + + )) + )} + +
Type / NamePIDCPUMemory
+ {isRunning + ? 'Waiting for process data…' + : 'No Fiddle running.'} +
+ {m.type ?? m.name ?? 'Unknown'} + {m.pid} 50 ? 'process-monitor__cpu--high' : '' + }`} + > + {this.formatCpu(m.cpu)} + + {this.formatMemory(m.memory)} +
+
+
+ ); + } +} diff --git a/src/renderer/components/settings-execution.tsx b/src/renderer/components/settings-execution.tsx index 7dfe47203b..3de49109f5 100644 --- a/src/renderer/components/settings-execution.tsx +++ b/src/renderer/components/settings-execution.tsx @@ -59,6 +59,8 @@ export const ExecutionSettings = observer( this.handleElectronLoggingChange.bind(this); this.handleSocketFirewallChange = this.handleSocketFirewallChange.bind(this); + this.handleProcessMonitorChange = + this.handleProcessMonitorChange.bind(this); this.handleSettingsItemChange = this.handleSettingsItemChange.bind(this); this.addNewSettingsItem = this.addNewSettingsItem.bind(this); @@ -105,6 +107,16 @@ export const ExecutionSettings = observer( this.props.appState.isUsingSocketFirewall = checked; } + /** + * Handles a change on whether to enable the process monitor + */ + public handleProcessMonitorChange( + event: React.FormEvent, + ) { + const { checked } = event.currentTarget; + this.props.appState.isProcessMonitorEnabled = checked; + } + /** * Handles a change in the execution flags or environment variables * run with the Electron executable. @@ -386,6 +398,21 @@ export const ExecutionSettings = observer( /> +
+ + +

+ The Process Monitor allows you to view the system processes + spawned by your Fiddle, including their live CPU and memory + usage. +

+ +
+
); } diff --git a/src/renderer/process-monitor-entry.tsx b/src/renderer/process-monitor-entry.tsx new file mode 100644 index 0000000000..a433423616 --- /dev/null +++ b/src/renderer/process-monitor-entry.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { getCssStringForTheme } from '../utils/theme'; +import { defaultDark, defaultLight } from '../themes-defaults'; +import { PREFERS_DARK_MEDIA_QUERY } from '../constants'; +import { ProcessMonitor } from './components/process-monitor'; +import '../less/root.less'; + +/** + * Injects the Fiddle theme CSS variables into the #fiddle-theme style tag, + * exactly mirroring how app.tsx does it in the main window. + */ +function applyThemeCss(css: string) { + const tag = document.getElementById('fiddle-theme'); + if (tag) tag.innerHTML = css; +} + +async function bootstrapTheme() { + // Try to read the user's currently active theme from disk. + const themePath: string = window.ElectronFiddle.themePath; + let theme = null; + + if (themePath) { + theme = await window.ElectronFiddle.readThemeFile(themePath); + } + + // Fall back to the system preference if no custom theme is found. + if (!theme) { + theme = window.matchMedia(PREFERS_DARK_MEDIA_QUERY).matches + ? defaultDark + : defaultLight; + } + + applyThemeCss(getCssStringForTheme(theme)); +} + +// Reload theme whenever the main window changes it. +window.ElectronFiddle.addEventListener('theme-loaded', ((theme: { + common?: Record; +}) => { + if (theme && theme.common) { + applyThemeCss(getCssStringForTheme(theme as any)); + } +}) as () => void); + +bootstrapTheme(); + +const render = () => { + ReactDOM.render(, document.getElementById('app')); +}; + +render(); diff --git a/src/renderer/runner.ts b/src/renderer/runner.ts index 92a2c7078a..8ca92b136c 100644 --- a/src/renderer/runner.ts +++ b/src/renderer/runner.ts @@ -244,6 +244,7 @@ export class Runner { packageManager: appState.packageManager, useSocketFirewall: appState.isUsingSocketFirewall, isKeepingUserDataDirs: appState.isKeepingUserDataDirs, + isProcessMonitorEnabled: appState.isProcessMonitorEnabled, }; } diff --git a/src/renderer/state.ts b/src/renderer/state.ts index edd182d205..aaeb09c60d 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -175,6 +175,9 @@ export class AppState { public isUpdatingElectronVersions = false; public isDownloadingAll = false; public isDeletingAll = false; + public isProcessMonitorEnabled = !!( + this.retrieve(GlobalSetting.isProcessMonitorEnabled) ?? false + ); // -- Editor Values stored when we close the editor ------------------ private outputBuffer = ''; @@ -249,6 +252,7 @@ export class AppState { isDownloadingAll: observable, isShowingGistHistory: observable, isUsingSystemTheme: observable, + isProcessMonitorEnabled: observable, localPath: observable, modules: observable, output: observable, @@ -413,6 +417,7 @@ export class AppState { case GlobalSetting.isKeepingUserDataDirs: case GlobalSetting.isPublishingGistAsRevision: case GlobalSetting.isShowingGistHistory: + case GlobalSetting.isProcessMonitorEnabled: case GlobalSetting.isUsingSocketFirewall: case GlobalSetting.isUsingSystemTheme: case GlobalSetting.packageAuthor: @@ -542,6 +547,12 @@ export class AppState { autorun(() => this.save(GlobalSetting.electronMirror, this.electronMirror)); autorun(() => this.save(GlobalSetting.fontFamily, this.fontFamily)); autorun(() => this.save(GlobalSetting.fontSize, this.fontSize)); + autorun(() => + this.save( + GlobalSetting.isProcessMonitorEnabled, + this.isProcessMonitorEnabled, + ), + ); // Update our known versions this.updateElectronVersions(); diff --git a/static/process-monitor.html b/static/process-monitor.html new file mode 100644 index 0000000000..cb2b54039f --- /dev/null +++ b/static/process-monitor.html @@ -0,0 +1,16 @@ + + + + + + Process Monitor — Electron Fiddle + + + + +
+ + \ No newline at end of file diff --git a/tests/main/fiddle-core.spec.ts b/tests/main/fiddle-core.spec.ts index 903daf7b8e..fcfbe63600 100644 --- a/tests/main/fiddle-core.spec.ts +++ b/tests/main/fiddle-core.spec.ts @@ -82,6 +82,7 @@ function makeOptions( env: {}, executionFlags: [], isKeepingUserDataDirs: false, + isProcessMonitorEnabled: false, modules: [], packageManager: 'npm', useSocketFirewall: false, diff --git a/yarn.lock b/yarn.lock index 8067020772..91f990028a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2970,6 +2970,13 @@ __metadata: languageName: node linkType: hard +"@types/pidusage@npm:^2.0.5": + version: 2.0.5 + resolution: "@types/pidusage@npm:2.0.5" + checksum: 10c0/f4add16388280410604254c91499b873f95c98146c0a8016e09733d3d5c4362b7a5f56e2f1a5e9677772ab97ac37ffb1194d3ad43f872364d9d86856d4db414c + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.3 resolution: "@types/prop-types@npm:15.7.3" @@ -5181,6 +5188,7 @@ __metadata: "@types/fs-extra": "npm:^9.0.7" "@types/node": "npm:^22.19.1" "@types/parse-env-string": "npm:^1.0.2" + "@types/pidusage": "npm:^2.0.5" "@types/react": "npm:^16.14.0" "@types/react-dom": "npm:^16.9.11" "@types/react-window": "npm:^1.8.8" @@ -5214,6 +5222,8 @@ __metadata: oxlint: "npm:^1.81.0" p-debounce: "npm:^2.0.0" parse-env-string: "npm:^1.0.1" + pidtree: "npm:^1.0.0" + pidusage: "npm:^4.0.1" postcss: "npm:^8.5.23" postcss-less: "npm:^6.0.0" prettier: "npm:^3.6.2" @@ -10116,6 +10126,24 @@ __metadata: languageName: node linkType: hard +"pidtree@npm:^1.0.0": + version: 1.0.0 + resolution: "pidtree@npm:1.0.0" + bin: + pidtree: bin/pidtree.js + checksum: 10c0/0cd0bd222ffba553ad4807c6795988bd18b587e9c063149139fb8db78c3b747f43d3b1511372eeda6b29a1c96670d2ee17d65a33f3c54e15d98f800512e613cd + languageName: node + linkType: hard + +"pidusage@npm:^4.0.1": + version: 4.0.1 + resolution: "pidusage@npm:4.0.1" + dependencies: + safe-buffer: "npm:^5.2.1" + checksum: 10c0/d80984af6818d7f53c34dcb1e6d16de26230409e41b1a6ae01391a5b15e69b6c7b21d76b1362709e0ee1b5fb65744969c0c45d0a836571e1d059299b5776ab08 + languageName: node + linkType: hard + "pify@npm:^4.0.1": version: 4.0.1 resolution: "pify@npm:4.0.1" @@ -11163,7 +11191,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3