diff --git a/packages/nx/src/command-line/watch.ts b/packages/nx/src/command-line/watch.ts index 17fe96ce69f69..67f93b13dcc03 100644 --- a/packages/nx/src/command-line/watch.ts +++ b/packages/nx/src/command-line/watch.ts @@ -17,13 +17,13 @@ export interface WatchArguments { const DEFAULT_PROJECT_NAME_ENV = 'NX_PROJECT_NAME'; const DEFAULT_FILE_CHANGES_ENV = 'NX_FILE_CHANGES'; -class BatchCommandRunner { - running = false; +export class BatchFunctionRunner { + private running = false; - pendingProjects = new Set(); - pendingFiles = new Set(); + private pendingProjects = new Set(); + private pendingFiles = new Set(); - private get _verbose() { + protected get _verbose() { return process.env.NX_VERBOSE_LOGGING === 'true'; } @@ -32,9 +32,10 @@ class BatchCommandRunner { } constructor( - private command: string, - private projectNameEnv: string = DEFAULT_PROJECT_NAME_ENV, - private fileChangesEnv: string = DEFAULT_FILE_CHANGES_ENV + private callback: ( + projects: Set, + files: Set + ) => Promise ) {} enqueue(projectNames: string[], fileChanges: ChangedFile[]) { @@ -48,60 +49,77 @@ class BatchCommandRunner { return this.process(); } - async process() { + private async process() { if (!this.running && this.hasPending) { this.running = true; - // process all pending commands together - const envs = this.createCommandEnvironments(); + // Clone the pending projects and files before clearing + const projects = new Set(this.pendingProjects); + const files = new Set(this.pendingFiles); - this._verbose && - output.logSingleLine( - 'about to run commands with these environments: ' + - JSON.stringify(envs) - ); + // Clear the pending projects and files + this.pendingProjects.clear(); + this.pendingFiles.clear(); - return this.run(envs).then(() => { + return this.callback(projects, files).then(() => { this.running = false; - this._verbose && - output.logSingleLine('running complete, processing the next batch'); this.process(); }); } else { this._verbose && this.running && - output.logSingleLine('waiting for commands to finish executing'); + output.logSingleLine('waiting for function to finish executing'); this._verbose && !this.hasPending && - output.logSingleLine('no more commands to process'); + output.logSingleLine('no more function to process'); } } +} + +class BatchCommandRunner extends BatchFunctionRunner { + constructor( + private command: string, + private projectNameEnv: string = DEFAULT_PROJECT_NAME_ENV, + private fileChangesEnv: string = DEFAULT_FILE_CHANGES_ENV + ) { + super((projects, files) => { + // process all pending commands together + const envs = this.createCommandEnvironments(projects, files); + + return this.run(envs); + }); + } - createCommandEnvironments(): Record[] { + private createCommandEnvironments( + projects: Set, + files: Set + ): Record[] { const commandsToRun = []; - if (this.pendingProjects.size > 0) { - this.pendingProjects.forEach((projectName) => { + if (projects.size > 0) { + projects.forEach((projectName) => { commandsToRun.push({ [this.projectNameEnv]: projectName, - [this.fileChangesEnv]: Array.from(this.pendingFiles).join(' '), + [this.fileChangesEnv]: Array.from(files).join(' '), }); }); } else { commandsToRun.push({ [this.projectNameEnv]: '', - [this.fileChangesEnv]: Array.from(this.pendingFiles).join(' '), + [this.fileChangesEnv]: Array.from(files).join(' '), }); } - this.pendingProjects.clear(); - this.pendingFiles.clear(); - return commandsToRun; } async run(envs: Record[]) { + this._verbose && + output.logSingleLine( + 'about to run commands with these environments: ' + JSON.stringify(envs) + ); + return Promise.all( envs.map((env) => { return new Promise((resolve, reject) => { @@ -123,7 +141,11 @@ class BatchCommandRunner { }); }); }) - ); + ).then((r) => { + this._verbose && + output.logSingleLine('running complete, processing the next batch'); + return r; + }); } } diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 3e7bab3969050..0d9e3539fed01 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -36,14 +36,12 @@ "autoprefixer": "^10.4.9", "babel-loader": "^9.1.2", "chalk": "^4.1.0", - "chokidar": "^3.5.1", "copy-webpack-plugin": "^10.2.4", "css-loader": "^6.4.0", "css-minimizer-webpack-plugin": "^3.4.1", "dotenv": "~10.0.0", "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "7.2.13", - "ignore": "^5.0.4", "less": "4.1.3", "less-loader": "11.1.0", "license-webpack-plugin": "^4.0.2", diff --git a/packages/webpack/src/plugins/webpack-nx-build-coordination-plugin.ts b/packages/webpack/src/plugins/webpack-nx-build-coordination-plugin.ts index 2864c747a5508..a3ee1afc4a6ca 100644 --- a/packages/webpack/src/plugins/webpack-nx-build-coordination-plugin.ts +++ b/packages/webpack/src/plugins/webpack-nx-build-coordination-plugin.ts @@ -1,10 +1,8 @@ -import { watch } from 'chokidar'; -import { execSync } from 'child_process'; -import { workspaceLayout } from '@nrwl/devkit'; -import { joinPathFragments } from '@nrwl/devkit'; -import ignore from 'ignore'; -import { readFileSync } from 'fs'; +import { exec } from 'child_process'; import type { Compiler } from 'webpack'; +import { daemonClient } from 'nx/src/daemon/client/client'; +import { BatchFunctionRunner } from 'nx/src/command-line/watch'; +import { output } from 'nx/src/utils/output'; export class WebpackNxBuildCoordinationPlugin { private currentlyRunning: 'none' | 'nx-build' | 'webpack-build' = 'none'; @@ -31,9 +29,23 @@ export class WebpackNxBuildCoordinationPlugin { }); } - startWatchingBuildableLibs() { - createFileWatcher(process.cwd(), () => { - this.buildChangedProjects(); + async startWatchingBuildableLibs() { + const unregisterFileWatcher = await createFileWatcher( + () => this.buildChangedProjects(), + () => { + output.error({ + title: 'Watch connection closed', + bodyLines: [ + 'The daemon has closed the connection to this watch process.', + 'Please restart your watch command.', + ], + }); + process.exit(1); + } + ); + + process.on('exit', () => { + unregisterFileWatcher(); }); } @@ -42,46 +54,46 @@ export class WebpackNxBuildCoordinationPlugin { await sleep(50); } this.currentlyRunning = 'nx-build'; - try { - execSync(this.buildCmd, { stdio: [0, 1, 2] }); - // eslint-disable-next-line no-empty - } catch (e) {} - this.currentlyRunning = 'none'; + return new Promise((res) => { + try { + const cp = exec(this.buildCmd); + + cp.stdout.pipe(process.stdout); + cp.stderr.pipe(process.stderr); + cp.on('exit', () => { + res(); + }); + cp.on('error', () => { + res(); + }); + // eslint-disable-next-line no-empty + } catch (e) { + res(); + } finally { + this.currentlyRunning = 'none'; + } + }); } } function sleep(time: number) { return new Promise((resolve) => setTimeout(resolve, time)); } - -function getIgnoredGlobs(root: string) { - const ig = ignore(); - try { - ig.add(readFileSync(`${root}/.gitignore`, 'utf-8')); - } catch {} - try { - ig.add(readFileSync(`${root}/.nxignore`, 'utf-8')); - } catch {} - return ig; -} - -function createFileWatcher(root: string, changeHandler: () => void) { - const ignoredGlobs = getIgnoredGlobs(root); - const layout = workspaceLayout(); - - const watcher = watch( - [ - joinPathFragments(layout.appsDir, '**'), - joinPathFragments(layout.libsDir, '**'), - ], +async function createFileWatcher( + changeHandler: () => Promise, + onClose: () => void +) { + const runner = new BatchFunctionRunner(changeHandler); + return daemonClient.registerFileWatcher( { - cwd: root, - ignoreInitial: true, + watchProjects: 'all', + }, + (err, { changedProjects, changedFiles }) => { + if (err === 'closed') { + onClose(); + } + // Queue a build + runner.enqueue(changedProjects, changedFiles); } ); - watcher.on('all', (_event: string, path: string) => { - if (ignoredGlobs.ignores(path)) return; - changeHandler(); - }); - return { close: () => watcher.close() }; }