Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable watch root #9424

Merged
merged 12 commits into from
Dec 19, 2023
2 changes: 1 addition & 1 deletion packages/core/core/src/Parcel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably also update this so it works for one-shot builds

let events = await options.inputFS.getEventsSince(

idk if watchRoot is the most obvious name in that case but not sure about another one. projectRoot is a different thing and affects more stuff so would need to be something different.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Yeah the name because less intuitive when used in this context :|

this.#watchEvents.emit({error: err});
Expand Down
2 changes: 1 addition & 1 deletion packages/core/core/src/RequestTracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -1217,7 +1217,7 @@ async function loadRequestGraph(options): Async<RequestGraph> {
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,
);
Expand Down
9 changes: 9 additions & 0 deletions packages/core/core/src/resolveOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/core/core/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export type ParcelOptions = {|

shouldDisableCache: boolean,
cacheDir: FilePath,
watchDir: FilePath,
mode: BuildMode,
hmrOptions: ?HMROptions,
shouldContentHash: boolean,
Expand Down
1 change: 1 addition & 0 deletions packages/core/core/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ cache.ensure();

export const DEFAULT_OPTIONS: ParcelOptions = {
cacheDir: path.join(__dirname, '.parcel-cache'),
watchDir: __dirname,
entries: [],
logLevel: 'info',
targets: undefined,
Expand Down
68 changes: 68 additions & 0 deletions packages/core/integration-tests/test/monorepos.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
assertBundles,
inputFS,
outputFS,
fsFixture,
ncp,
run,
overlayFS,
Expand Down Expand Up @@ -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;
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions packages/core/parcel/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ const commonOptions = {
'--config <path>':
'specify which config to use. can be a path or a package name',
'--cache-dir <path>': 'set the cache directory. defaults to ".parcel-cache"',
'--watch-dir <path>':
mischnic marked this conversation as resolved.
Show resolved Hide resolved
'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)',
Expand Down Expand Up @@ -473,6 +475,7 @@ async function normalizeOptions(
return {
shouldDisableCache: command.cache === false,
cacheDir: command.cacheDir,
watchDir: command.watchDir,
config: command.config,
mode,
hmrOptions,
Expand Down
1 change: 1 addition & 0 deletions packages/core/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export type InitialParcelOptions = {|

+shouldDisableCache?: boolean,
+cacheDir?: FilePath,
+watchDir?: FilePath,
+mode?: BuildMode,
+hmrOptions?: ?HMROptions,
+shouldContentHash?: boolean,
Expand Down
Loading