diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3d545c8..90ae1f278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,13 @@ * **Potentially breaking bug fix:** `math.unit()` now wraps multiple denominator units in parentheses. For example, `px/(em*em)` instead of `px/em*em`. +### Command-Line Interface + +* Use `@parcel/watcher` to watch the filesystem when running from JavaScript and + not using `--poll`. This should mitigate more frequent failures users have + been seeing since version 4.0.0 of Chokidar, our previous watching tool, was + released. + ### JS API * Fix `SassColor.interpolate()` to allow an undefined `options` parameter, as diff --git a/lib/src/io/js.dart b/lib/src/io/js.dart index 973b62f1f..e8b2e7722 100644 --- a/lib/src/io/js.dart +++ b/lib/src/io/js.dart @@ -16,6 +16,7 @@ import 'package:watcher/watcher.dart'; import '../exception.dart'; import '../js/chokidar.dart'; +import '../js/parcel_watcher.dart'; @JS('process') external final Process? _nodeJsProcess; // process is null in the browser @@ -248,39 +249,65 @@ int get exitCode => _process?.exitCode ?? 0; set exitCode(int code) => _process?.exitCode = code; -Future> watchDir(String path, {bool poll = false}) { +Future> watchDir(String path, {bool poll = false}) async { if (!isNodeJs) { throw UnsupportedError("watchDir() is only supported on Node.js"); } - var watcher = chokidar.watch(path, ChokidarOptions(usePolling: poll)); // Don't assign the controller until after the ready event fires. Otherwise, // Chokidar will give us a bunch of add events for files that already exist. StreamController? controller; - watcher - ..on( - 'add', - allowInterop((String path, [void _]) => - controller?.add(WatchEvent(ChangeType.ADD, path)))) - ..on( - 'change', - allowInterop((String path, [void _]) => - controller?.add(WatchEvent(ChangeType.MODIFY, path)))) - ..on( - 'unlink', - allowInterop((String path) => - controller?.add(WatchEvent(ChangeType.REMOVE, path)))) - ..on('error', allowInterop((Object error) => controller?.addError(error))); - - var completer = Completer>(); - watcher.on('ready', allowInterop(() { - // dart-lang/sdk#45348 - var stream = (controller = StreamController(onCancel: () { - watcher.close(); + if (poll) { + var watcher = chokidar.watch(path, ChokidarOptions(usePolling: true)); + watcher + ..on( + 'add', + allowInterop((String path, [void _]) => + controller?.add(WatchEvent(ChangeType.ADD, path)))) + ..on( + 'change', + allowInterop((String path, [void _]) => + controller?.add(WatchEvent(ChangeType.MODIFY, path)))) + ..on( + 'unlink', + allowInterop((String path) => + controller?.add(WatchEvent(ChangeType.REMOVE, path)))) + ..on( + 'error', allowInterop((Object error) => controller?.addError(error))); + + var completer = Completer>(); + watcher.on('ready', allowInterop(() { + // dart-lang/sdk#45348 + var stream = (controller = StreamController(onCancel: () { + watcher.close(); + })) + .stream; + completer.complete(stream); + })); + + return completer.future; + } else { + var subscription = await ParcelWatcher.subscribeFuture(path, + (Object? error, List events) { + if (error != null) { + controller?.addError(error); + } else { + for (var event in events) { + switch (event.type) { + case 'create': + controller?.add(WatchEvent(ChangeType.ADD, event.path)); + case 'update': + controller?.add(WatchEvent(ChangeType.MODIFY, event.path)); + case 'delete': + controller?.add(WatchEvent(ChangeType.REMOVE, event.path)); + } + } + } + }); + + return (controller = StreamController(onCancel: () { + subscription.unsubscribe(); })) .stream; - completer.complete(stream); - })); - - return completer.future; + } } diff --git a/lib/src/js/parcel_watcher.dart b/lib/src/js/parcel_watcher.dart new file mode 100644 index 000000000..1cf698a23 --- /dev/null +++ b/lib/src/js/parcel_watcher.dart @@ -0,0 +1,44 @@ +// Copyright 2024 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:js/js.dart'; +import 'package:node_interop/js.dart'; +import 'package:node_interop/util.dart'; + +@JS() +class ParcelWatcherSubscription { + external void unsubscribe(); +} + +@JS() +class ParcelWatcherEvent { + external String get type; + external String get path; +} + +/// The @parcel/watcher module. +/// +/// See [the docs on npm](https://www.npmjs.com/package/@parcel/watcher). +@JS('parcel_watcher') +class ParcelWatcher { + external static Promise subscribe(String path, Function callback); + static Future subscribeFuture(String path, + void Function(Object? error, List) callback) => + promiseToFuture( + subscribe(path, allowInterop((Object? error, List events) { + callback(error, events.cast()); + }))); + + external static Promise getEventsSince(String path, String snapshotPath); + static Future> getEventsSinceFuture( + String path, String snapshotPath) async { + List events = + await promiseToFuture(getEventsSince(path, snapshotPath)); + return events.cast(); + } + + external static Promise writeSnapshot(String path, String snapshotPath); + static Future writeSnapshotFuture(String path, String snapshotPath) => + promiseToFuture(writeSnapshot(path, snapshotPath)); +} diff --git a/package.json b/package.json index 84f259e39..14b800cf2 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ ], "name": "sass", "devDependencies": { + "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", "immutable": "^4.0.0", "intercept-stdout": "^0.1.2" diff --git a/package/package.json b/package/package.json index e1581c448..881e3351c 100644 --- a/package/package.json +++ b/package/package.json @@ -17,6 +17,7 @@ "node": ">=14.0.0" }, "dependencies": { + "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" diff --git a/tool/grind.dart b/tool/grind.dart index 0cea7264b..b5e735d3e 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -34,6 +34,7 @@ void main(List args) { pkg.homebrewFormula.value = "Formula/sass.rb"; pkg.homebrewEditFormula.value = _updateHomebrewLanguageRevision; pkg.jsRequires.value = [ + pkg.JSRequire("@parcel/watcher", target: pkg.JSRequireTarget.cli), pkg.JSRequire("immutable", target: pkg.JSRequireTarget.all), pkg.JSRequire("chokidar", target: pkg.JSRequireTarget.cli), pkg.JSRequire("readline", target: pkg.JSRequireTarget.cli),