diff --git a/packages/core/core/src/Parcel.js b/packages/core/core/src/Parcel.js index fffb97ae3e6..560afb6df09 100644 --- a/packages/core/core/src/Parcel.js +++ b/packages/core/core/src/Parcel.js @@ -389,7 +389,7 @@ export default class Parcel { let resolvedOptions = nullthrows(this.#resolvedOptions); let opts = getWatcherOptions(resolvedOptions); let sub = await resolvedOptions.inputFS.watch( - resolvedOptions.projectRoot, + resolvedOptions.watchDir, (err, events) => { if (err) { this.#watchEvents.emit({error: err}); diff --git a/packages/core/core/src/RequestTracker.js b/packages/core/core/src/RequestTracker.js index c46c7e99a60..3dea1f54f27 100644 --- a/packages/core/core/src/RequestTracker.js +++ b/packages/core/core/src/RequestTracker.js @@ -1217,7 +1217,7 @@ async function loadRequestGraph(options): Async { let snapshotKey = hashString(`${cacheKey}:snapshot`); let snapshotPath = path.join(options.cacheDir, snapshotKey + '.txt'); let events = await options.inputFS.getEventsSince( - options.projectRoot, + options.watchDir, snapshotPath, opts, ); diff --git a/packages/core/core/src/resolveOptions.js b/packages/core/core/src/resolveOptions.js index 4fa491584e3..ede438b117c 100644 --- a/packages/core/core/src/resolveOptions.js +++ b/packages/core/core/src/resolveOptions.js @@ -97,6 +97,14 @@ export default async function resolveOptions( ? path.resolve(outputCwd, initialOptions.cacheDir) : path.resolve(projectRoot, DEFAULT_CACHE_DIRNAME); + // Make the root watch directory configurable. This is useful in some cases + // where symlinked dependencies outside the project root need to trigger HMR + // updates. Default to the project root if not provided. + let watchDir = + initialOptions.watchDir != null + ? path.resolve(initialOptions.watchDir) + : projectRoot; + let cache = initialOptions.cache ?? (outputFS instanceof NodeFS @@ -180,6 +188,7 @@ export default async function resolveOptions( shouldProfile: initialOptions.shouldProfile ?? false, shouldTrace: initialOptions.shouldTrace ?? false, cacheDir, + watchDir, entries: entries.map(e => toProjectPath(projectRoot, e)), targets: initialOptions.targets, logLevel: initialOptions.logLevel ?? 'info', diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index ee155bdcadf..119da4de624 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -266,6 +266,7 @@ export type ParcelOptions = {| shouldDisableCache: boolean, cacheDir: FilePath, + watchDir: FilePath, mode: BuildMode, hmrOptions: ?HMROptions, shouldContentHash: boolean, diff --git a/packages/core/core/test/test-utils.js b/packages/core/core/test/test-utils.js index 6ed4367e94d..4eda2d81f8d 100644 --- a/packages/core/core/test/test-utils.js +++ b/packages/core/core/test/test-utils.js @@ -17,6 +17,7 @@ cache.ensure(); export const DEFAULT_OPTIONS: ParcelOptions = { cacheDir: path.join(__dirname, '.parcel-cache'), + watchDir: __dirname, entries: [], logLevel: 'info', targets: undefined, diff --git a/packages/core/integration-tests/test/monorepos.js b/packages/core/integration-tests/test/monorepos.js index 1e0127f228c..478c835572b 100644 --- a/packages/core/integration-tests/test/monorepos.js +++ b/packages/core/integration-tests/test/monorepos.js @@ -6,6 +6,7 @@ import { assertBundles, inputFS, outputFS, + fsFixture, ncp, run, overlayFS, @@ -875,4 +876,71 @@ describe('monorepos', function () { inputFS.chdir(oldcwd); } }); + + // This test ensures that workspace linked dependency changes are correctly + // detected in watch mode when `watchDir` is set to the monorepo root. + it('should correctly detect changes when watchDir is higher up in a project-only lockfile monorepo', async () => { + const dir = path.join(__dirname, 'project-specific-lockfiles'); + overlayFS.mkdirp(dir); + + await fsFixture(overlayFS, dir)` + packages + app + package.json: + { "name": "app" } + pnpm-lock.yaml: + lockfileVersion: 5.4 + index.js: + import {msg} from 'lib'; + console.log(msg); + node_modules + lib -> ${path.join( + __dirname, + 'project-specific-lockfiles', + 'packages', + 'lib', + )} + lib + package.json: + { "name": "lib" } + pnpm-lock.yaml: + lockfileVersion: 5.4 + index.js: + export const msg = "initial";`; + + let b = await bundler(path.join(dir, 'packages', 'app', 'index.js'), { + inputFS: overlayFS, + watchDir: path.join(dir), + }); + + let builds = 0; + + return new Promise((resolve, reject) => { + // 1. Increment the build counter and modify `packages/lib/index.js` which + // should trigger a subsquent build. + // + // 2. Ensure the changed asset was detected and built + b.watch(async (err, buildEvent) => { + builds++; + + if (builds < 2) { + await overlayFS.writeFile( + path.join(dir, 'packages', 'lib', 'index.js'), + 'export const msg = "changed-NcMB9nA7"', + ); + } else { + const values = buildEvent?.changedAssets?.values(); + if (values != null) { + const code = await Array.from(values)[0].getCode(); + assert(code.includes('changed-NcMB9nA7')); + resolve(); + } else { + reject(new Error('Changed assets missing.')); + } + } + }).then(sub => { + subscription = sub; + }); + }); + }); }); diff --git a/packages/core/integration-tests/test/project-specific-lockfiles/packages/app/.gitkeep b/packages/core/integration-tests/test/project-specific-lockfiles/packages/app/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/integration-tests/test/project-specific-lockfiles/packages/app/README.md b/packages/core/integration-tests/test/project-specific-lockfiles/packages/app/README.md new file mode 100644 index 00000000000..f9c89973a58 --- /dev/null +++ b/packages/core/integration-tests/test/project-specific-lockfiles/packages/app/README.md @@ -0,0 +1,3 @@ +This directory exists for the `should correctly detect changes when watchDir is higher up in a project-only lockfile monorepo` test. + +Without it I was getting a `Uncaught Error: No such file or directory` error stemming from [this line](https://github.com/parcel-bundler/parcel/blob/3b798e0456bbef951c684d43f96fda1fea386f62/packages/core/fs/src/OverlayFS.js#L375). The test itself doesn't care about watching the readable file system but it was necessary because `.watch` on `OverlayFS` subscribes to both the readable and writable filesystems. diff --git a/packages/core/parcel/src/cli.js b/packages/core/parcel/src/cli.js index 1e41e1e079d..6d01426e99b 100755 --- a/packages/core/parcel/src/cli.js +++ b/packages/core/parcel/src/cli.js @@ -73,6 +73,8 @@ const commonOptions = { '--config ': 'specify which config to use. can be a path or a package name', '--cache-dir ': 'set the cache directory. defaults to ".parcel-cache"', + '--watch-dir ': + 'set the root watch directory. defaults to nearest lockfile or source control dir.', '--no-source-maps': 'disable sourcemaps', '--target [name]': [ 'only build given target(s)', @@ -473,6 +475,7 @@ async function normalizeOptions( return { shouldDisableCache: command.cache === false, cacheDir: command.cacheDir, + watchDir: command.watchDir, config: command.config, mode, hmrOptions, diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 666eb9dc5c3..0d739ef183e 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -294,6 +294,7 @@ export type InitialParcelOptions = {| +shouldDisableCache?: boolean, +cacheDir?: FilePath, + +watchDir?: FilePath, +mode?: BuildMode, +hmrOptions?: ?HMROptions, +shouldContentHash?: boolean,