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

Add node.js only parcel watcher watchman back-end #9789

Merged
merged 31 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift click to select a range
68958a9
Stub out `@parcel/watcher` watchman drop-in backed by JS
jdlm-stripe Jun 5, 2024
a282c5b
Start to migrate to node.js watcher
pyamada-atlassian Jun 12, 2024
64e6361
Try to make node.js watcher available under feature-flag
pyamada-atlassian Jul 1, 2024
3ae0619
Stub out `@parcel/watcher` watchman drop-in backed by JS
jdlm-stripe Jun 5, 2024
5099a0d
Start to migrate to node.js watcher
pyamada-atlassian Jul 8, 2024
0ec8769
Fix linter errors
pyamada-atlassian Jul 8, 2024
7897f60
Update package.json to build ts files
pyamada-atlassian Jul 8, 2024
29ca761
Merge remote-tracking branch 'origin/v2' into pyamada-nodejs-watchman…
pyamada-atlassian Jul 17, 2024
f36b8db
Update lockfile
pyamada-atlassian Jul 17, 2024
b3f905a
Remove .gitignore file
pyamada-atlassian Jul 17, 2024
3d7fa92
Remove redundant workspace
pyamada-atlassian Jul 17, 2024
b276ada
Merge branch 'v2' into pyamada-nodejs-watchman-watcher
pyamada-atlassian Jul 25, 2024
9434979
Fix flow problems
pyamada-atlassian Jul 25, 2024
7e9c770
Ignore tracing errors
pyamada-atlassian Jul 26, 2024
0444200
Merge branch 'v2' into pyamada-nodejs-watchman-watcher
mattcompiles Aug 5, 2024
36432fc
Migrate watchman client to flow
mattcompiles Aug 5, 2024
c4d9ba7
WIP
mattcompiles Aug 6, 2024
1ba32b1
Remove rust changes
mattcompiles Aug 6, 2024
2db5632
Remove ts build stuff
mattcompiles Aug 6, 2024
5f0960f
Rename feature flag
mattcompiles Aug 6, 2024
fff66f6
Remove extra import
mattcompiles Aug 6, 2024
3d3605e
Remove .gitignore
mattcompiles Aug 6, 2024
7c30863
Fix directory watching
mattcompiles Aug 6, 2024
3303b50
Merge branch 'v2' into pyamada-nodejs-watchman-watcher
mattcompiles Aug 6, 2024
55696b2
Fix typo
mattcompiles Aug 6, 2024
8453b49
Fix flow errors
mattcompiles Aug 6, 2024
a0d0374
Fix snapshot awaiting
mattcompiles Aug 6, 2024
9e3979b
Refactor watcher
mattcompiles Aug 6, 2024
b69a4a7
Fix package registry
mattcompiles Aug 6, 2024
ad6bed2
Make watchman watcher get conditionally required
mattcompiles Aug 6, 2024
3b67301
Remove unused feature flag dep
mattcompiles Aug 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 8,12 @@
},
"private": true,
"workspaces": [
"packages/*/*"
"packages/*/*",
"packages/utils/parcel-watcher-watchman-js"
yamadapc marked this conversation as resolved.
Show resolved Hide resolved
],
"scripts": {
"build": "yarn build-bundles && cross-env NODE_ENV=production PARCEL_BUILD_ENV=production gulp",
"build-bundles": "rimraf --glob packages/*/*/lib && lerna run dev:prepare && cross-env NODE_ENV=production PARCEL_BUILD_ENV=production PARCEL_SELF_BUILD=true parcel build packages/core/{fs,codeframe,package-manager,utils} packages/reporters/{cli,dev-server} packages/utils/{parcel-lsp,parcel-lsp-protocol}",
"build-bundles": "rimraf --glob packages/*/*/lib && lerna run dev:prepare && lerna run build-ts && cross-env NODE_ENV=production PARCEL_BUILD_ENV=production PARCEL_SELF_BUILD=true parcel build packages/core/{fs,codeframe,package-manager,utils} packages/reporters/{cli,dev-server} packages/utils/{parcel-lsp,parcel-lsp-protocol}",
"build-ts": "lerna run build-ts && lerna run check-ts",
"build-native": "node scripts/build-native.js",
"build-native-release": "node scripts/build-native.js --release",
Expand All @@ -25,11 26,12 @@
"link-all": "node scripts/link-all.js packages",
"unlink-all": "node scripts/unlink-all.js packages",
"check": "flow check",
"flow": "lerna run build-ts && flow",
"lint": "eslint . && prettier \"./packages/*/*/{src,bin,test}/**/*.{js,json,md}\" --list-different && cargo fmt --all -- --check",
"prepublishOnly": "yarn adjust-versions && yarn build && yarn build-ts",
"test:unit": "cross-env NODE_ENV=test mocha --timeout 5000 && cargo test",
"test:integration": "yarn workspace @parcel/integration-tests test",
"test:integration-ci": "yarn workspace @parcel/integration-tests test-ci",
"test:unit": "lerna run build-ts && cross-env NODE_ENV=test mocha --timeout 5000 && cargo test",
mattcompiles marked this conversation as resolved.
Show resolved Hide resolved
"test:integration": "lerna run build-ts && yarn workspace @parcel/integration-tests test",
"test:integration-ci": "lerna run build-ts && yarn workspace @parcel/integration-tests test-ci",
"test": "yarn test:unit && yarn test:integration",
"dev:release": "SKIP_PLUGIN_COMPATIBILITY_CHECK=true lerna publish -y --canary --preid dev --dist-tag=dev --exact --force-publish=* --no-git-tag-version --no-push",
"canary:release": "SKIP_PLUGIN_COMPATIBILITY_CHECK=true lerna publish -y --canary --preid canary --dist-tag=canary --exact --force-publish=* --no-git-tag-version --no-push",
Expand Down
1 change: 1 addition & 0 deletions packages/core/cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 25,7 @@
"check-ts": "tsc --noEmit index.d.ts"
},
"dependencies": {
"@parcel/feature-flags": "2.12.0",
"@parcel/fs": "2.12.0",
"@parcel/logger": "2.12.0",
"@parcel/utils": "2.12.0",
Expand Down
1 change: 1 addition & 0 deletions packages/core/feature-flags/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 10,7 @@ export const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
configKeyInvalidation: false,
parcelV3: false,
randomLargeBlobKeys: false,
useNodeWatcher: false,
};

let featureFlagValues: FeatureFlags = {...DEFAULT_FEATURE_FLAGS};
Expand Down
4 changes: 4 additions & 0 deletions packages/core/feature-flags/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 13,10 @@ export type FeatureFlags = {|
* Rust backed requests
*/
parcelV3: boolean,
/**
* Use node.js implementation of @parcel/watcher watchman backend
*/
useNodeWatcher: boolean,
/**
* Store large blobs on randomly generated keys
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/core/fs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 48,8 @@
"check-ts": "tsc --noEmit index.d.ts"
},
"dependencies": {
"@parcel/feature-flags": "2.12.0",
"@parcel/watcher-watchman-js": "2.12.0",
Copy link
Member

Choose a reason for hiding this comment

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

How can we make this dependency opt-in? Peer dep? try...catch around a require?

Copy link
Contributor

Choose a reason for hiding this comment

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

The package is so small (less than 50KB) that I wasn't going to worry about it. I can try make it opt in though.

"@parcel/rust": "2.12.0",
"@parcel/types-internal": "2.12.0",
"@parcel/utils": "2.12.0",
Expand Down
15 changes: 12 additions & 3 deletions packages/core/fs/src/NodeFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 21,9 @@ import {tmpdir} from 'os';
import {promisify} from 'util';
import {registerSerializableClass} from '@parcel/core';
import {hashFile} from '@parcel/utils';
import {getFeatureFlag} from '@parcel/feature-flags';
import watcher from '@parcel/watcher';
import * as nodeWatcher from '@parcel/watcher-watchman-js';
import packageJSON from '../package.json';

import * as searchNative from '@parcel/rust';
Expand Down Expand Up @@ -163,23 165,29 @@ export class NodeFS implements FileSystem {
fn: (err: ?Error, events: Array<Event>) => mixed,
opts: WatcherOptions,
): Promise<AsyncSubscription> {
return watcher.subscribe(dir, fn, opts);
return getFeatureFlag('useNodeWatcher')
? nodeWatcher.subscribe(dir, fn, opts)
: watcher.subscribe(dir, fn, opts);
}

getEventsSince(
dir: FilePath,
snapshot: FilePath,
opts: WatcherOptions,
): Promise<Array<Event>> {
return watcher.getEventsSince(dir, snapshot, opts);
return getFeatureFlag('useNodeWatcher')
? nodeWatcher.getEventsSince(dir, snapshot, opts)
: watcher.getEventsSince(dir, snapshot, opts);
}

async writeSnapshot(
dir: FilePath,
snapshot: FilePath,
opts: WatcherOptions,
): Promise<void> {
await watcher.writeSnapshot(dir, snapshot, opts);
(await getFeatureFlag('useNodeWatcher'))
? nodeWatcher.writeSnapshot(dir, snapshot, opts)
: watcher.writeSnapshot(dir, snapshot, opts);
}

static deserialize(): NodeFS {
Expand Down Expand Up @@ -229,6 237,7 @@ try {
}

let useOsTmpDir;

function shouldUseOsTmpDir(filePath) {
if (useOsTmpDir != null) {
return useOsTmpDir;
Expand Down
1 change: 1 addition & 0 deletions packages/utils/parcel-watcher-watchman-js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 1 @@
/lib
yamadapc marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions packages/utils/parcel-watcher-watchman-js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 1,7 @@
# @parcel/watcher-watchman-js

This package acts as a drop-in replacements for `@parcel/watcher` but only for
watchman. It's a temporary workaround for some bugs and feature gaps with the
C watchman client.

It is intended to replace `@parcel/watcher` within node_modules.
22 changes: 22 additions & 0 deletions packages/utils/parcel-watcher-watchman-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 1,22 @@
{
"name": "@parcel/watcher-watchman-js",
"version": "2.12.0",
yamadapc marked this conversation as resolved.
Show resolved Hide resolved
"main": "lib/index.js",
"scripts": {
"build": "tsc",
"build-ts": "tsc",
"lint": "eslint src --ext ts"
},
"dependencies": {
"fb-watchman": "^2.0.2",
"@parcel/watcher": "^2.4.1"
},
"devDependencies": {
"@types/node": ">= 18",
"@types/fb-watchman": "2.0.4",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"eslint": "^8.41.0",
"typescript": ">=3.0.0"
}
}
Empty file.
8 changes: 8 additions & 0 deletions packages/utils/parcel-watcher-watchman-js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,8 @@
import {createWrapper} from './wrapper';

const wrapper = createWrapper();

export const writeSnapshot = wrapper.writeSnapshot.bind(wrapper);
export const getEventsSince = wrapper.getEventsSince.bind(wrapper);
export const subscribe = wrapper.subscribe.bind(wrapper);
export const unsubscribe = wrapper.unsubscribe.bind(wrapper);
170 changes: 170 additions & 0 deletions packages/utils/parcel-watcher-watchman-js/src/wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 1,170 @@
import * as fs from 'fs';
import * as path from 'path';
import * as watchman from 'fb-watchman';
import type {
Options,
getEventsSince,
subscribe,
unsubscribe,
writeSnapshot,
Event,
SubscribeCallback,
AsyncSubscription,
} from '@parcel/watcher';

export interface Watcher {
getEventsSince: typeof getEventsSince;
subscribe: typeof subscribe;
unsubscribe: typeof unsubscribe;
writeSnapshot: typeof writeSnapshot;
}

let client: watchman.Client | null = null;
function getClient() {
if (!client) {
client = new watchman.Client();
client.capabilityCheck(
{optional: [], required: ['relative_root']},
function (error, resp) {
if (error) {
throw error;
}
},
);
}
return client;
}

function commandAsync(args: any[]): Promise<any> {
return new Promise((resolve, reject) => {
const client = getClient();
client.command(args, (err: Error | null | undefined, response: any) => {
if (err) reject(err);
else resolve(response);
});
});
}

class ParcelWatcherWatchmanJS implements Watcher {
subscriptionName: string;

constructor() {
this.subscriptionName = 'parcel-watcher-subscription-' Date.now();
}

// Types should match @parcel/watcher/index.js.flow
async writeSnapshot(
dir: string,
snapshot: string,
opts?: Options,
): Promise<string> {
const response = await commandAsync(['clock', dir]);
fs.writeFileSync(path.resolve(snapshot), response.clock, {
encoding: 'utf-8',
});
return response.clock;
}

async getEventsSince(
dir: string,
snapshot: string,
opts?: Options,
): Promise<Event[]> {
// TODO: Handle ignore patterns here
const clock = fs.readFileSync(path.resolve(snapshot), {
encoding: 'utf-8',
});

const response = await commandAsync([
'query',
dir,
{
expression: [
'not',
[
'anyof',
['dirname', '.hg'],
['dirname', '.git'],
['dirname', '.parcel-cache'],
['match', 'node_modules/**/*'],
],
],
fields: ['name', 'mode', 'exists', 'new'],
since: clock,
},
]);

return (response.files || []).map((file: any) => ({
path: file.name,
type: file.new ? 'create' : file.exists ? 'update' : 'delete',
}));
}

async subscribe(
dir: string,
fn: SubscribeCallback,
opts?: Options,
): Promise<AsyncSubscription> {
const {subscriptionName} = this;
const {clock} = await commandAsync(['clock', dir]);

await commandAsync([
'subscribe',
dir,
subscriptionName,
{
// `defer` can be used here if you want to pause the
// notification stream until something has finished.
//
// https://facebook.github.io/watchman/docs/cmd/subscribe#defer
// defer: ['my-company-example'],
expression: [
'not',
[
'anyof',
['dirname', '.hg'],
['dirname', '.git'],
['dirname', '.parcel-cache'],
['match', 'node_modules/**/*'],
],
],
fields: ['name', 'mode', 'exists', 'new'],
since: clock,
},
]);

getClient().on('subscription', function (resp) {
if (!resp.files || resp.subscription !== subscriptionName) {
return;
}

fn(
null /* err */,
resp.files.map((file: any) => {
return {
path: path.join(dir, file.name),
type: file.new ? 'create' : file.exists ? 'update' : 'delete',
};
}),
);
});

return {
async unsubscribe() {
return await commandAsync(['unsubscribe', dir, subscriptionName]);
},
};
}

async unsubscribe(
dir: string,
_fn: SubscribeCallback,
_opts?: Options,
): Promise<void> {
await commandAsync(['unsubscribe', dir, this.subscriptionName]);
}
}

export function createWrapper(): Watcher {
return new ParcelWatcherWatchmanJS();
}
14 changes: 14 additions & 0 deletions packages/utils/parcel-watcher-watchman-js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "lib",
"lib": ["es6"],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"allowJs": true
},
"include": ["src/*"],
"exclude": ["node_modules", ".vscode-test"]
}
Loading
Loading