@angular/build 22.2.2 → 22.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/package.json +3 -3
  2. package/src/builders/application/index-html-generator.js +6 -1
  3. package/src/builders/application/index-html-generator.js.map +1 -1
  4. package/src/builders/application/options.js +2 -2
  5. package/src/builders/application/options.js.map +1 -1
  6. package/src/builders/application/schema.json +1 -1
  7. package/src/builders/dev-server/schema.d.ts +4 -3
  8. package/src/builders/dev-server/schema.json +2 -2
  9. package/src/builders/dev-server/vite/index.js +6 -0
  10. package/src/builders/dev-server/vite/index.js.map +1 -1
  11. package/src/builders/extract-i18n/application-extraction.js +1 -0
  12. package/src/builders/extract-i18n/application-extraction.js.map +1 -1
  13. package/src/builders/karma/schema.json +2 -2
  14. package/src/builders/unit-test/runners/karma/executor.js +1 -1
  15. package/src/builders/unit-test/runners/karma/executor.js.map +1 -1
  16. package/src/builders/unit-test/runners/vitest/browser-provider.js +2 -1
  17. package/src/builders/unit-test/runners/vitest/browser-provider.js.map +1 -1
  18. package/src/builders/unit-test/runners/vitest/executor.js +0 -5
  19. package/src/builders/unit-test/runners/vitest/executor.js.map +1 -1
  20. package/src/builders/unit-test/runners/vitest/plugins.js +12 -0
  21. package/src/builders/unit-test/runners/vitest/plugins.js.map +1 -1
  22. package/src/builders/unit-test/schema.d.ts +4 -4
  23. package/src/builders/unit-test/schema.json +1 -1
  24. package/src/builders/unit-test/test-discovery.js +4 -2
  25. package/src/builders/unit-test/test-discovery.js.map +1 -1
  26. package/src/tools/angular/angular-host.js +7 -2
  27. package/src/tools/angular/angular-host.js.map +1 -1
  28. package/src/tools/angular/compilation/aot-compilation.js +2 -1
  29. package/src/tools/angular/compilation/aot-compilation.js.map +1 -1
  30. package/src/tools/angular/compilation/compiler-options.d.ts +1 -0
  31. package/src/tools/angular/compilation/compiler-options.js +4 -2
  32. package/src/tools/angular/compilation/compiler-options.js.map +1 -1
  33. package/src/tools/angular/compilation/diagnostics.js +2 -2
  34. package/src/tools/angular/compilation/diagnostics.js.map +1 -1
  35. package/src/tools/esbuild/angular/compiler-plugin.js +2 -0
  36. package/src/tools/esbuild/angular/compiler-plugin.js.map +1 -1
  37. package/src/tools/esbuild/angular/component-stylesheets.js +14 -1
  38. package/src/tools/esbuild/angular/component-stylesheets.js.map +1 -1
  39. package/src/tools/esbuild/application-code-bundle.js +7 -5
  40. package/src/tools/esbuild/application-code-bundle.js.map +1 -1
  41. package/src/tools/esbuild/global-styles.js +3 -1
  42. package/src/tools/esbuild/global-styles.js.map +1 -1
  43. package/src/tools/esbuild/persistent-load-result-cache.js +2 -4
  44. package/src/tools/esbuild/persistent-load-result-cache.js.map +1 -1
  45. package/src/tools/esbuild/stylesheets/sass-language.js +18 -24
  46. package/src/tools/esbuild/stylesheets/sass-language.js.map +1 -1
  47. package/src/tools/sass/rebasing-importer.d.ts +1 -1
  48. package/src/tools/sass/rebasing-importer.js +6 -3
  49. package/src/tools/sass/rebasing-importer.js.map +1 -1
  50. package/src/utils/concurrency.d.ts +4 -0
  51. package/src/utils/concurrency.js +5 -0
  52. package/src/utils/concurrency.js.map +1 -1
  53. package/src/utils/index-file/nonce.d.ts +2 -1
  54. package/src/utils/index-file/nonce.js +22 -4
  55. package/src/utils/index-file/nonce.js.map +1 -1
  56. package/src/utils/load-proxy-config.js +27 -1
  57. package/src/utils/load-proxy-config.js.map +1 -1
  58. package/src/utils/normalize-cache.js +1 -1
  59. package/src/utils/resolve-assets.js +28 -9
  60. package/src/utils/resolve-assets.js.map +1 -1
  61. package/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.d.ts +1 -1
  62. package/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.js +3 -3
  63. package/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.js.map +1 -1
  64. package/src/utils/server-rendering/esm-in-memory-loader/register-hooks.js +3 -2
  65. package/src/utils/server-rendering/esm-in-memory-loader/register-hooks.js.map +1 -1
  66. package/src/utils/watcher.d.ts +53 -0
  67. package/src/utils/watcher.js +521 -200
  68. package/src/utils/watcher.js.map +1 -1
@@ -43,14 +43,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
43
43
  return (mod && mod.__esModule) ? mod : { "default": mod };
44
44
  };
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.ChangedFiles = void 0;
46
+ exports.ParcelExternalManager = exports.ChangedFiles = void 0;
47
47
  exports.setupWatcher = setupWatcher;
48
48
  exports.toPosixPathNormalized = toPosixPathNormalized;
49
49
  exports.getDirectoryPath = getDirectoryPath;
50
50
  exports.createWatcher = createWatcher;
51
51
  exports.isPathInside = isPathInside;
52
52
  exports.extractNodeModulesPackageDir = extractNodeModulesPackageDir;
53
- const node_events_1 = require("node:events");
54
53
  const fs = __importStar(require("node:fs"));
55
54
  const path = __importStar(require("node:path"));
56
55
  const picomatch_1 = __importDefault(require("picomatch"));
@@ -362,122 +361,450 @@ function extractNodeModulesPackageDir(posixPath) {
362
361
  }
363
362
  return `${nodeModulesPath}/${segments[0]}`;
364
363
  }
364
+ /**
365
+ * Maximum number of concurrent external directory subscriptions before coalescing
366
+ * sibling/cousin directories into common ancestor directories.
367
+ * Kept well below macOS's ~512 per-process FSEvents stream limit.
368
+ */
369
+ const DEFAULT_MAX_EXTERNAL_SUBSCRIPTIONS = 256;
370
+ /**
371
+ * Checks whether any strict ancestor of the given trie node is already marked as a target.
372
+ */
373
+ function hasTargetAncestor(node) {
374
+ let current = node.parent;
375
+ while (current) {
376
+ if (current.isTarget) {
377
+ return true;
378
+ }
379
+ current = current.parent;
380
+ }
381
+ return false;
382
+ }
383
+ /**
384
+ * Manages `@parcel/watcher` directory subscriptions for watched files located outside the
385
+ * primary `workspaceRoot`.
386
+ *
387
+ * Because `@parcel/watcher` watches directories recursively and macOS limits each process to
388
+ * ~512 concurrent `FSEvents` streams, this manager:
389
+ * - Normalizes external `node_modules` file paths to their package root directory.
390
+ * - Batches synchronous `watchFile` and `removeFile` calls within a microtask before subscribing.
391
+ * - Uses a directory prefix tree to subsume nested child directories and iteratively coalesce
392
+ * deepest branching subdirectories into common ancestors when the subscription budget is exceeded.
393
+ * - Dynamically lowers its subscription budget and coalesces further if `subscribe()` fails on
394
+ * an existing directory due to OS stream limits.
395
+ */
365
396
  class ParcelExternalManager {
366
- parcelWatcher;
367
- options;
368
- rootDirLookupKey;
369
- handleEvents;
370
- extraSubscriptions = new Map();
371
- pendingSubscriptions = new Map();
372
- externalDirFiles = new Map();
373
- constructor(parcelWatcher, options, rootDirLookupKey, handleEvents) {
374
- this.parcelWatcher = parcelWatcher;
375
- this.options = options;
376
- this.rootDirLookupKey = rootDirLookupKey;
377
- this.handleEvents = handleEvents;
378
- }
379
- async ensureWatched(posixPath, lookupKey) {
380
- if (isPathInside(lookupKey, this.rootDirLookupKey) || lookupKey === this.rootDirLookupKey) {
397
+ /** Active `@parcel/watcher` subscriptions keyed by subscribed directory lookup key. */
398
+ #extraSubscriptions = new Map();
399
+ /** Reference-counted watched files grouped by their target external directory lookup key. */
400
+ #externalDirFiles = new Map();
401
+ /** Reverse lookup mapping each watched file lookup key to its target directory lookup key. */
402
+ #fileToDirKey = new Map();
403
+ /** The `@parcel/watcher` module instance. */
404
+ #parcelWatcher;
405
+ /** Watcher configuration options including ignore patterns. */
406
+ #options;
407
+ /** Normalized POSIX path of the primary watched workspace root. */
408
+ #rootDirPosix;
409
+ /** Case-normalized lookup key of the primary watched workspace root. */
410
+ #rootDirLookupKey;
411
+ /** Whether the underlying filesystem is case-sensitive. */
412
+ #isCaseSensitive;
413
+ /** Callback invoked when `@parcel/watcher` emits filesystem events. */
414
+ #handleEvents;
415
+ /** Current maximum number of concurrent external directory subscriptions allowed before coalescing. */
416
+ #maxSubscriptions;
417
+ /** Whether external directory state has changed and requires a reconciliation pass. */
418
+ #needsReconcile = false;
419
+ /** Whether obsolete subscriptions should be unsubscribed before creating new ones (used after stream limit errors). */
420
+ #shrinkBeforeSubscribe = false;
421
+ /** In-flight promise for the currently scheduled or running reconciliation loop. */
422
+ #reconcilePromise;
423
+ /** Whether this manager has been closed. */
424
+ #isClosed = false;
425
+ /**
426
+ * Creates a new external directory subscription manager for `@parcel/watcher`.
427
+ *
428
+ * @param parcelWatcher The `@parcel/watcher` module.
429
+ * @param options Watcher configuration options.
430
+ * @param rootDirPosix Normalized POSIX path of the primary workspace root.
431
+ * @param rootDirLookupKey Case-normalized lookup key of the primary workspace root.
432
+ * @param isCaseSensitive Whether the filesystem at the workspace root is case-sensitive.
433
+ * @param handleEvents Callback to process incoming `@parcel/watcher` events.
434
+ * @param maxSubscriptions Maximum number of concurrent external directory subscriptions before coalescing.
435
+ */
436
+ constructor(parcelWatcher, options, rootDirPosix, rootDirLookupKey, isCaseSensitive, handleEvents, maxSubscriptions = DEFAULT_MAX_EXTERNAL_SUBSCRIPTIONS) {
437
+ this.#parcelWatcher = parcelWatcher;
438
+ this.#options = options;
439
+ this.#rootDirPosix = rootDirPosix;
440
+ this.#rootDirLookupKey = rootDirLookupKey;
441
+ this.#isCaseSensitive = isCaseSensitive;
442
+ this.#handleEvents = handleEvents;
443
+ this.#maxSubscriptions = Math.max(1, maxSubscriptions);
444
+ }
445
+ /**
446
+ * Registers a file path to be watched if it resides outside the primary workspace root,
447
+ * and schedules an asynchronous subscription reconciliation pass if needed.
448
+ *
449
+ * @param posixPath Normalized POSIX file path preserving original casing.
450
+ * @param lookupKey Case-normalized lookup key for the file path.
451
+ */
452
+ watchFile(posixPath, lookupKey) {
453
+ if (this.#isClosed ||
454
+ isPathInside(lookupKey, this.#rootDirLookupKey) ||
455
+ lookupKey === this.#rootDirLookupKey) {
381
456
  return;
382
457
  }
383
- const dirPath = getDirectoryPath(posixPath);
384
- const dirKey = getDirectoryPath(lookupKey);
385
- let dirEntry = this.externalDirFiles.get(dirKey);
458
+ const pkgDir = extractNodeModulesPackageDir(posixPath);
459
+ const dirPath = pkgDir ?? getDirectoryPath(posixPath);
460
+ const dirKey = pkgDir
461
+ ? toLookupKey(pkgDir, this.#isCaseSensitive)
462
+ : getDirectoryPath(lookupKey);
463
+ this.#fileToDirKey.set(lookupKey, dirKey);
464
+ let dirEntry = this.#externalDirFiles.get(dirKey);
465
+ const isNewDir = !dirEntry;
386
466
  if (!dirEntry) {
387
467
  dirEntry = { dirPath, files: new Set() };
388
- this.externalDirFiles.set(dirKey, dirEntry);
468
+ this.#externalDirFiles.set(dirKey, dirEntry);
389
469
  }
390
470
  dirEntry.files.add(lookupKey);
391
- await this.ensureDirWatched(dirPath, dirKey);
471
+ if (isNewDir || !this.#isCoveredByActiveSubscription(dirKey)) {
472
+ this.#scheduleReconcile();
473
+ }
392
474
  }
475
+ /**
476
+ * Unregisters a previously watched external file and schedules a reconciliation pass
477
+ * if it was the last watched file in its target directory.
478
+ *
479
+ * @param lookupKey Case-normalized lookup key for the file path to remove.
480
+ */
393
481
  removeFile(lookupKey) {
394
- if (isPathInside(lookupKey, this.rootDirLookupKey) || lookupKey === this.rootDirLookupKey) {
482
+ const dirKey = this.#fileToDirKey.get(lookupKey);
483
+ if (!dirKey) {
395
484
  return;
396
485
  }
397
- const dirKey = getDirectoryPath(lookupKey);
398
- const dirEntry = this.externalDirFiles.get(dirKey);
486
+ this.#fileToDirKey.delete(lookupKey);
487
+ const dirEntry = this.#externalDirFiles.get(dirKey);
399
488
  if (dirEntry) {
400
489
  dirEntry.files.delete(lookupKey);
401
490
  if (dirEntry.files.size === 0) {
402
- this.externalDirFiles.delete(dirKey);
403
- const sub = this.extraSubscriptions.get(dirKey);
404
- if (sub) {
405
- this.extraSubscriptions.delete(dirKey);
406
- sub.unsubscribe().catch(() => { });
407
- for (const [remainingDirKey, remainingDirEntry] of this.externalDirFiles.entries()) {
408
- if (!this.isCoveredByExistingExternal(remainingDirKey)) {
409
- this.ensureDirWatched(remainingDirEntry.dirPath, remainingDirKey).catch(() => { });
410
- }
411
- }
412
- }
491
+ this.#externalDirFiles.delete(dirKey);
492
+ this.#scheduleReconcile();
413
493
  }
414
494
  }
415
495
  }
496
+ /**
497
+ * Waits for any scheduled or in-progress subscription reconciliation pass to complete.
498
+ */
499
+ async flush() {
500
+ if (this.#reconcilePromise) {
501
+ await this.#reconcilePromise;
502
+ }
503
+ }
504
+ /**
505
+ * Closes the manager, awaiting any pending reconciliation and unsubscribing all active
506
+ * external directory subscriptions.
507
+ */
416
508
  async close() {
509
+ this.#isClosed = true;
510
+ this.#needsReconcile = false;
417
511
  try {
418
- if (this.pendingSubscriptions.size > 0) {
419
- await Promise.allSettled(Array.from(this.pendingSubscriptions.values()));
512
+ if (this.#reconcilePromise) {
513
+ await this.#reconcilePromise.catch(() => { });
420
514
  }
421
- if (this.extraSubscriptions.size > 0) {
422
- await Promise.allSettled(Array.from(this.extraSubscriptions.values()).map((sub) => sub.unsubscribe()));
515
+ if (this.#extraSubscriptions.size > 0) {
516
+ await Promise.allSettled(Array.from(this.#extraSubscriptions.values(), (sub) => sub.unsubscribe()));
423
517
  }
424
518
  }
425
519
  finally {
426
- this.extraSubscriptions.clear();
427
- this.pendingSubscriptions.clear();
428
- this.externalDirFiles.clear();
520
+ this.#extraSubscriptions.clear();
521
+ this.#externalDirFiles.clear();
522
+ this.#fileToDirKey.clear();
429
523
  }
430
524
  }
431
- isCoveredByExistingExternal(dirLookupKey) {
432
- for (const existingDir of this.extraSubscriptions.keys()) {
433
- if (dirLookupKey === existingDir || isPathInside(dirLookupKey, existingDir)) {
434
- return true;
525
+ /**
526
+ * Schedules a microtask to reconcile active `@parcel/watcher` subscriptions with the
527
+ * current set of external directories.
528
+ */
529
+ #scheduleReconcile() {
530
+ if (this.#isClosed) {
531
+ return;
532
+ }
533
+ this.#needsReconcile = true;
534
+ this.#reconcilePromise ??= Promise.resolve()
535
+ .then(() => this.#runReconcileLoop())
536
+ .finally(() => {
537
+ this.#reconcilePromise = undefined;
538
+ });
539
+ }
540
+ /**
541
+ * Continuously executes reconciliation passes until no further changes are pending.
542
+ */
543
+ async #runReconcileLoop() {
544
+ while (this.#needsReconcile && !this.#isClosed) {
545
+ this.#needsReconcile = false;
546
+ await this.#reconcileOnce();
547
+ }
548
+ }
549
+ /**
550
+ * Performs a single reconciliation pass: computes the desired coalesced directory set,
551
+ * subscribes to newly required directories, unsubscribes obsolete directories, and
552
+ * triggers fallback coalescing if OS stream limits are encountered.
553
+ */
554
+ async #reconcileOnce() {
555
+ const desiredDirs = this.#computeDesiredDirectories();
556
+ if (this.#shrinkBeforeSubscribe) {
557
+ this.#shrinkBeforeSubscribe = false;
558
+ const toUnsubscribe = [];
559
+ for (const [existingKey, sub] of this.#extraSubscriptions.entries()) {
560
+ if (!desiredDirs.has(existingKey)) {
561
+ this.#extraSubscriptions.delete(existingKey);
562
+ toUnsubscribe.push(sub.unsubscribe().catch(() => { }));
563
+ }
564
+ }
565
+ if (toUnsubscribe.length > 0) {
566
+ await Promise.allSettled(toUnsubscribe);
567
+ }
568
+ }
569
+ if (this.#isClosed) {
570
+ return;
571
+ }
572
+ const toSubscribe = [];
573
+ for (const [dirKey, dirPath] of desiredDirs.entries()) {
574
+ if (!this.#extraSubscriptions.has(dirKey)) {
575
+ toSubscribe.push([dirKey, dirPath]);
435
576
  }
436
577
  }
437
- for (const pendingDir of this.pendingSubscriptions.keys()) {
438
- if (dirLookupKey === pendingDir || isPathInside(dirLookupKey, pendingDir)) {
578
+ let hadExistingDirFailure = false;
579
+ if (toSubscribe.length > 0) {
580
+ await Promise.allSettled(toSubscribe.map(async ([dirKey, dirPath]) => {
581
+ const ignore = isPathInside(this.#rootDirLookupKey, dirKey)
582
+ ? [...(this.#options?.ignored ?? []), this.#rootDirPosix, `${this.#rootDirPosix}/**`]
583
+ : this.#options?.ignored;
584
+ try {
585
+ const sub = await this.#parcelWatcher.subscribe(dirPath, (err, events) => {
586
+ if (!err) {
587
+ this.#handleEvents(events);
588
+ }
589
+ }, { ignore });
590
+ if (this.#isClosed) {
591
+ await sub.unsubscribe().catch(() => { });
592
+ return;
593
+ }
594
+ this.#extraSubscriptions.set(dirKey, sub);
595
+ }
596
+ catch {
597
+ // Ignore errors for missing directories; if the directory exists on disk,
598
+ // mark failure so the manager can coalesce further if over OS stream limits.
599
+ if (!this.#isClosed && fs.existsSync(dirPath)) {
600
+ hadExistingDirFailure = true;
601
+ }
602
+ }
603
+ }));
604
+ }
605
+ if (this.#isClosed) {
606
+ return;
607
+ }
608
+ // Unsubscribe any previous subscriptions that are no longer desired and whose
609
+ // remaining watched files (if any) are now covered by another active subscription.
610
+ for (const [existingKey, sub] of this.#extraSubscriptions.entries()) {
611
+ if (!desiredDirs.has(existingKey) && this.#canRemoveSubscription(existingKey)) {
612
+ this.#extraSubscriptions.delete(existingKey);
613
+ sub.unsubscribe().catch(() => { });
614
+ }
615
+ }
616
+ // If subscribing to an existing directory failed (e.g. macOS FSEvents per-process limit),
617
+ // dynamically lower the subscription budget and re-reconcile to coalesce into ancestors.
618
+ if (hadExistingDirFailure && desiredDirs.size > 1 && this.#maxSubscriptions > 1) {
619
+ const nextMax = this.#extraSubscriptions.size > 0
620
+ ? Math.max(1, Math.floor(this.#extraSubscriptions.size / 2))
621
+ : Math.max(1, Math.floor(Math.min(this.#maxSubscriptions, desiredDirs.size) / 2));
622
+ if (nextMax < desiredDirs.size && nextMax < this.#maxSubscriptions) {
623
+ this.#maxSubscriptions = nextMax;
624
+ this.#shrinkBeforeSubscribe = true;
625
+ this.#needsReconcile = true;
626
+ }
627
+ }
628
+ }
629
+ /**
630
+ * Checks whether a directory lookup key is already covered by an active subscription.
631
+ */
632
+ #isCoveredByActiveSubscription(dirLookupKey) {
633
+ for (const existingDir of this.#extraSubscriptions.keys()) {
634
+ if (dirLookupKey === existingDir || isPathInside(dirLookupKey, existingDir)) {
439
635
  return true;
440
636
  }
441
637
  }
442
638
  return false;
443
639
  }
444
- async ensureDirWatched(dirPath, dirKey) {
445
- if (this.isCoveredByExistingExternal(dirKey)) {
446
- return;
640
+ /**
641
+ * Determines whether an existing subscription can be safely removed without leaving
642
+ * any remaining watched target directories uncovered.
643
+ */
644
+ #canRemoveSubscription(subKey) {
645
+ for (const targetDirKey of this.#externalDirFiles.keys()) {
646
+ if (targetDirKey === subKey || isPathInside(targetDirKey, subKey)) {
647
+ let coveredByOther = false;
648
+ for (const otherSubKey of this.#extraSubscriptions.keys()) {
649
+ if (otherSubKey !== subKey &&
650
+ (targetDirKey === otherSubKey || isPathInside(targetDirKey, otherSubKey))) {
651
+ coveredByOther = true;
652
+ break;
653
+ }
654
+ }
655
+ if (!coveredByOther) {
656
+ return false;
657
+ }
658
+ }
447
659
  }
448
- const subPromise = this.parcelWatcher.subscribe(dirPath, (err, events) => {
449
- if (!err) {
450
- this.handleEvents(events);
660
+ return true;
661
+ }
662
+ /**
663
+ * Computes the minimal set of external directories to subscribe to (mapped from `dirKey` to `dirPath`),
664
+ * subsuming nested directories and coalescing branching ancestors to stay within `#maxSubscriptions`.
665
+ */
666
+ #computeDesiredDirectories() {
667
+ if (this.#externalDirFiles.size === 0) {
668
+ return new Map();
669
+ }
670
+ const nodesByKey = new Map();
671
+ const roots = [];
672
+ for (const [dirKey, { dirPath }] of this.#externalDirFiles.entries()) {
673
+ const chain = [];
674
+ let currentKey = dirKey;
675
+ let currentPath = dirPath;
676
+ while (true) {
677
+ chain.push({ dirKey: currentKey, dirPath: currentPath });
678
+ const parentKey = getDirectoryPath(currentKey);
679
+ if (parentKey === currentKey) {
680
+ break;
681
+ }
682
+ currentKey = parentKey;
683
+ currentPath = getDirectoryPath(currentPath);
451
684
  }
452
- }, {
453
- ignore: this.options?.ignored,
454
- });
455
- this.pendingSubscriptions.set(dirKey, subPromise);
456
- try {
457
- const sub = await subPromise;
458
- this.pendingSubscriptions.delete(dirKey);
459
- if (this.externalDirFiles.has(dirKey) && !this.isCoveredByExistingExternal(dirKey)) {
460
- this.extraSubscriptions.set(dirKey, sub);
461
- // Subsume any nested child subscriptions that are now covered by this parent subscription
462
- for (const [childDir, childSub] of this.extraSubscriptions.entries()) {
463
- if (childDir !== dirKey && isPathInside(childDir, dirKey)) {
464
- this.extraSubscriptions.delete(childDir);
465
- childSub.unsubscribe().catch(() => { });
685
+ let parentNode;
686
+ for (let i = chain.length - 1; i >= 0; i--) {
687
+ const { dirKey: nodeKey, dirPath: nodePath } = chain[i];
688
+ const depth = chain.length - 1 - i;
689
+ let node = nodesByKey.get(nodeKey);
690
+ if (!node) {
691
+ node = {
692
+ dirKey: nodeKey,
693
+ dirPath: nodePath,
694
+ depth,
695
+ parent: parentNode,
696
+ children: new Map(),
697
+ isTarget: false,
698
+ leafCount: 0,
699
+ };
700
+ nodesByKey.set(nodeKey, node);
701
+ if (parentNode) {
702
+ parentNode.children.set(nodeKey, node);
703
+ }
704
+ else {
705
+ roots.push(node);
466
706
  }
467
707
  }
708
+ parentNode = node;
468
709
  }
469
- else {
470
- sub.unsubscribe().catch(() => { });
710
+ if (parentNode) {
711
+ parentNode.isTarget = true;
471
712
  }
472
713
  }
473
- catch {
474
- // Ignore subscription errors for missing or restricted external directories
714
+ const initSubtree = (node) => {
715
+ if (node.isTarget) {
716
+ node.children.clear();
717
+ node.leafCount = 1;
718
+ return 1;
719
+ }
720
+ let count = 0;
721
+ for (const child of node.children.values()) {
722
+ count += initSubtree(child);
723
+ }
724
+ node.leafCount = count;
725
+ return count;
726
+ };
727
+ let totalLeaves = 0;
728
+ for (const root of roots) {
729
+ totalLeaves += initSubtree(root);
475
730
  }
476
- finally {
477
- this.pendingSubscriptions.delete(dirKey);
731
+ if (totalLeaves > this.#maxSubscriptions) {
732
+ // Tier 1: non-ancestors of workspaceRoot (e.g., <monorepo>/libs, <monorepo>/node_modules)
733
+ // Tier 2: ancestors of workspaceRoot (e.g., <monorepo> when sibling apps/libs are directly under it)
734
+ const tier1ByDepth = new Map();
735
+ const tier2ByDepth = new Map();
736
+ const collectInternalNodes = (node) => {
737
+ if (node.isTarget) {
738
+ return;
739
+ }
740
+ if (node.parent !== undefined && node.leafCount > 1) {
741
+ const isWorkspaceAncestor = isPathInside(this.#rootDirLookupKey, node.dirKey);
742
+ const bucketMap = isWorkspaceAncestor ? tier2ByDepth : tier1ByDepth;
743
+ let bucket = bucketMap.get(node.depth);
744
+ if (!bucket) {
745
+ bucket = [];
746
+ bucketMap.set(node.depth, bucket);
747
+ }
748
+ bucket.push(node);
749
+ }
750
+ for (const child of node.children.values()) {
751
+ collectInternalNodes(child);
752
+ }
753
+ };
754
+ for (const root of roots) {
755
+ collectInternalNodes(root);
756
+ }
757
+ const collapseTier = (byDepth) => {
758
+ const sortedLevels = Array.from(byDepth.entries()).sort(([a], [b]) => b - a);
759
+ for (const [, nodes] of sortedLevels) {
760
+ if (totalLeaves <= this.#maxSubscriptions) {
761
+ return;
762
+ }
763
+ const candidates = nodes
764
+ .filter((n) => n.leafCount > 1 && !hasTargetAncestor(n))
765
+ .sort((a, b) => b.leafCount - a.leafCount || a.dirKey.localeCompare(b.dirKey));
766
+ for (const candidate of candidates) {
767
+ if (totalLeaves <= this.#maxSubscriptions) {
768
+ return;
769
+ }
770
+ if (candidate.leafCount <= 1 || hasTargetAncestor(candidate)) {
771
+ continue;
772
+ }
773
+ const reduction = candidate.leafCount - 1;
774
+ candidate.isTarget = true;
775
+ candidate.children.clear();
776
+ candidate.leafCount = 1;
777
+ totalLeaves -= reduction;
778
+ let ancestor = candidate.parent;
779
+ while (ancestor) {
780
+ ancestor.leafCount -= reduction;
781
+ ancestor = ancestor.parent;
782
+ }
783
+ }
784
+ }
785
+ };
786
+ collapseTier(tier1ByDepth);
787
+ if (totalLeaves > this.#maxSubscriptions) {
788
+ collapseTier(tier2ByDepth);
789
+ }
790
+ }
791
+ const desired = new Map();
792
+ const collectTargets = (node) => {
793
+ if (node.isTarget) {
794
+ desired.set(node.dirKey, node.dirPath);
795
+ return;
796
+ }
797
+ for (const child of node.children.values()) {
798
+ collectTargets(child);
799
+ }
800
+ };
801
+ for (const root of roots) {
802
+ collectTargets(root);
478
803
  }
804
+ return desired;
479
805
  }
480
806
  }
807
+ exports.ParcelExternalManager = ParcelExternalManager;
481
808
  async function createParcelWatcher(options, parcelWatcher) {
482
809
  const watchedFiles = new Set();
483
810
  const queue = new WatcherQueue();
@@ -516,7 +843,7 @@ async function createParcelWatcher(options, parcelWatcher) {
516
843
  }, {
517
844
  ignore: options?.ignored,
518
845
  });
519
- const externalManager = new ParcelExternalManager(parcelWatcher, options, rootDirLookupKey, handleEvents);
846
+ const externalManager = new ParcelExternalManager(parcelWatcher, options, rootDirPosix, rootDirLookupKey, isCaseSensitive, handleEvents);
520
847
  const buildWatcher = {
521
848
  [Symbol.asyncIterator]() {
522
849
  return this;
@@ -531,7 +858,7 @@ async function createParcelWatcher(options, parcelWatcher) {
531
858
  const lookupKey = toLookupKey(posixPath, isCaseSensitive);
532
859
  if (!watchedFiles.has(lookupKey)) {
533
860
  watchedFiles.add(lookupKey);
534
- void externalManager.ensureWatched(posixPath, lookupKey);
861
+ externalManager.watchFile(posixPath, lookupKey);
535
862
  }
536
863
  }
537
864
  },
@@ -559,119 +886,91 @@ async function createParcelWatcher(options, parcelWatcher) {
559
886
  };
560
887
  return buildWatcher;
561
888
  }
562
- /**
563
- * Manages selective node_modules watching for Chokidar.
564
- *
565
- * During Chokidar's initial scan, the allowed packages set is empty, causing Chokidar to prune the entire
566
- * node_modules directory immediately and avoid blocking the 'ready' event for minutes.
567
- * Packages are dynamically allowed and watched at the package root level as files are added via `watcher.add`.
568
- * Tracking files per package allows reference-counting so that package directories are unwatched when no
569
- * longer needed.
570
- */
571
- class ChokidarNodeModulesManager {
572
- isCaseSensitive;
573
- allowedPackages = new Map();
574
- constructor(isCaseSensitive) {
575
- this.isCaseSensitive = isCaseSensitive;
576
- }
577
- registerPackage(posixPath, lookupKey) {
578
- const packageDir = extractNodeModulesPackageDir(posixPath);
579
- if (!packageDir) {
580
- return { isPackage: false };
581
- }
582
- const packageKey = toLookupKey(packageDir, this.isCaseSensitive);
583
- let watchedInPkg = this.allowedPackages.get(packageKey);
584
- if (!watchedInPkg) {
585
- watchedInPkg = new Set();
586
- this.allowedPackages.set(packageKey, watchedInPkg);
587
- watchedInPkg.add(lookupKey);
588
- return { isPackage: true, newPkgDir: packageDir };
589
- }
590
- watchedInPkg.add(lookupKey);
591
- return { isPackage: true };
592
- }
593
- removePackageFile(posixPath, lookupKey) {
594
- const packageDir = extractNodeModulesPackageDir(posixPath);
595
- if (!packageDir) {
596
- return { isPackage: false };
597
- }
598
- const packageKey = toLookupKey(packageDir, this.isCaseSensitive);
599
- const watchedInPkg = this.allowedPackages.get(packageKey);
600
- if (watchedInPkg) {
601
- watchedInPkg.delete(lookupKey);
602
- if (watchedInPkg.size === 0) {
603
- this.allowedPackages.delete(packageKey);
604
- return { isPackage: true, unwatchPkgDir: packageDir };
605
- }
606
- }
607
- return { isPackage: true };
608
- }
609
- isIgnored(filePath) {
610
- // Fast path: avoid expensive normalization for non-node_modules files
611
- if (!filePath.includes(NODE_MODULES)) {
612
- return false;
613
- }
614
- const posixPath = toPosixPathNormalized(filePath);
615
- const lookupKey = toLookupKey(posixPath, this.isCaseSensitive);
616
- if (!`${lookupKey}/`.includes(NODE_MODULES_SEGMENT)) {
617
- return false;
618
- }
619
- if (this.allowedPackages.has(lookupKey)) {
620
- return false;
621
- }
622
- for (const pkg of this.allowedPackages.keys()) {
623
- if (isPathInside(lookupKey, pkg)) {
889
+ function createChokidarIgnored(ignoredPatterns, isCaseSensitive, watchedFiles) {
890
+ // Strip static node_modules ignore patterns (e.g. `**/node_modules/**` from setupWatcher).
891
+ // If left in place, picomatch would unconditionally ignore all node_modules files and prevent
892
+ // watching explicitly added files inside node_modules.
893
+ const customIgnored = ignoredPatterns
894
+ ?.filter((pattern) => {
895
+ const base = toPosixPathNormalized(pattern).replace(/\/\*\*$/, '');
896
+ return base !== NODE_MODULES && !base.endsWith(`/${NODE_MODULES}`);
897
+ })
898
+ .map((pattern) => {
899
+ if (/[*?[\]{}()]/.test(pattern)) {
900
+ const isMatch = (0, picomatch_1.default)(pattern, { dot: true });
901
+ return (filePath) => isMatch(toPosixPathNormalized(filePath));
902
+ }
903
+ return { path: toPosixPathNormalized(pattern), recursive: true };
904
+ }) ?? [];
905
+ // When watching a directory (e.g. projectRoot via NG_BUILD_WATCH_ROOT), prune `node_modules`
906
+ // directories unless explicitly added to `watchedFiles`. Explicitly added files inside
907
+ // `node_modules` do not end with `/node_modules` and remain watchable.
908
+ return [
909
+ ...customIgnored,
910
+ (filePath) => {
911
+ if (!filePath.includes(NODE_MODULES)) {
624
912
  return false;
625
913
  }
914
+ const posixPath = toPosixPathNormalized(filePath);
915
+ const lookupKey = toLookupKey(posixPath, isCaseSensitive);
916
+ return ((lookupKey === NODE_MODULES || lookupKey.endsWith(`/${NODE_MODULES}`)) &&
917
+ !watchedFiles.has(lookupKey));
918
+ },
919
+ ];
920
+ }
921
+ // Chokidar initializes file watchers asynchronously (`await stat(path)` followed by
922
+ // `fs.watch` or `fs.watchFile`). If a file is modified or recreated immediately after
923
+ // `watcher.add()` is called, Chokidar may capture the post-modification state as its initial
924
+ // baseline (or suppress the initial `'add'` event due to `ignoreInitial: true`) and miss the
925
+ // change. Re-statting after a short delay catches any modifications or recreations that occurred
926
+ // during asynchronous watcher registration, while `fileStats` deduplicates if Chokidar also fired.
927
+ async function checkChokidarBaselines(baselineStats, watchedFiles, fileStats, watcher, handleEvent) {
928
+ await Promise.all(Array.from(baselineStats, async ([lookupKey, baseline]) => {
929
+ if (!watchedFiles.has(lookupKey)) {
930
+ return;
626
931
  }
627
- return true;
628
- }
932
+ const currentStat = await fs.promises.stat(baseline.posixPath).catch(() => null);
933
+ if (!watchedFiles.has(lookupKey) || !currentStat?.isFile()) {
934
+ return;
935
+ }
936
+ if (baseline.mtimeMs === undefined || baseline.size === undefined) {
937
+ const recorded = fileStats.get(lookupKey);
938
+ if (!recorded) {
939
+ watcher.add(baseline.posixPath);
940
+ handleEvent('added', path.normalize(baseline.posixPath));
941
+ }
942
+ else if (currentStat.mtimeMs !== recorded.mtimeMs || currentStat.size !== recorded.size) {
943
+ handleEvent('modified', path.normalize(baseline.posixPath));
944
+ }
945
+ }
946
+ else if (currentStat.mtimeMs !== baseline.mtimeMs || currentStat.size !== baseline.size) {
947
+ handleEvent('modified', path.normalize(baseline.posixPath));
948
+ }
949
+ }));
629
950
  }
630
951
  async function createChokidarWatcher(options, chokidarModule) {
631
952
  const chokidar = chokidarModule ?? (await Promise.resolve().then(() => __importStar(require('chokidar'))));
632
953
  const watchedFiles = new Set();
633
954
  const fileStats = new Map();
634
- let queue;
635
- if (options?.polling) {
636
- const pollingInterval = options.interval ?? 100;
637
- const debounceMs = Math.min(250, Math.max(100, Math.ceil(pollingInterval * 1.5)));
638
- const maxWaitMs = Math.max(500, debounceMs * 3);
639
- queue = new WatcherQueue(debounceMs, maxWaitMs);
640
- }
641
- else {
642
- queue = new WatcherQueue();
643
- }
955
+ const pendingTimers = new Set();
956
+ const pollingInterval = options?.interval ?? 100;
957
+ const debounceMs = Math.min(250, Math.max(100, Math.ceil(pollingInterval * 1.5)));
958
+ const queue = options?.polling
959
+ ? new WatcherQueue(debounceMs, Math.max(500, debounceMs * 3))
960
+ : new WatcherQueue();
644
961
  const rootDir = options?.cwd ?? process.cwd();
645
962
  const isCaseSensitive = isFileSystemCaseSensitive(rootDir);
646
- const rootDirPosix = toPosixPathNormalized(rootDir);
647
- const rootDirLookupKey = toLookupKey(rootDirPosix, isCaseSensitive);
648
- const nodeModulesManager = new ChokidarNodeModulesManager(isCaseSensitive);
649
- // Strip static node_modules ignore patterns (e.g. `**/node_modules/**` from setupWatcher).
650
- // If left in place, picomatch would unconditionally ignore all node_modules files and override
651
- // our dynamic package allowlist above.
652
- const customIgnored = options?.ignored
653
- ?.filter((pattern) => !pattern.includes(NODE_MODULES))
654
- .map((pattern) => {
655
- if (/[*?[\]{}()]/.test(pattern)) {
656
- const isMatch = (0, picomatch_1.default)(pattern, { dot: true });
657
- return (filePath) => isMatch(toPosixPathNormalized(filePath));
658
- }
659
- return { path: toPosixPathNormalized(pattern), recursive: true };
660
- }) ?? [];
661
- const ignored = [...customIgnored, (filePath) => nodeModulesManager.isIgnored(filePath)];
662
- const watcher = chokidar.watch(rootDir, {
963
+ const ignored = createChokidarIgnored(options?.ignored, isCaseSensitive, watchedFiles);
964
+ const watcher = chokidar.watch([], {
663
965
  ignoreInitial: true,
664
966
  ignored,
665
967
  followSymlinks: options?.followSymlinks,
666
968
  usePolling: !!options?.polling,
667
969
  interval: options?.interval,
668
970
  });
971
+ // Prevent unhandled watcher errors (such as EMFILE or EACCES) from crashing the process.
972
+ watcher.on('error', () => { });
669
973
  const initTime = Date.now();
670
- // Wait for the watcher to complete its initial filesystem scan before returning.
671
- // With `ignoreInitial: true`, any file visited during the initial scan is treated as the initial baseline
672
- // and will not emit 'add' or 'change' events. Awaiting 'ready' ensures that rapid file modifications
673
- // made right after watcher setup (e.g. in rebuild tests) are not swallowed as initial files.
674
- await (0, node_events_1.once)(watcher, 'ready');
675
974
  const handleEvent = (type, rawPath) => {
676
975
  const posixPath = toPosixPathNormalized(rawPath);
677
976
  const lookupKey = toLookupKey(posixPath, isCaseSensitive);
@@ -684,13 +983,10 @@ async function createChokidarWatcher(options, chokidarModule) {
684
983
  return;
685
984
  }
686
985
  const stat = fs.statSync(rawPath, { throwIfNoEntry: false });
687
- if (!stat) {
688
- return;
689
- }
690
986
  // Ignore historical events from before watcher initialization, but allow a 1000 ms window
691
987
  // to account for coarse filesystem timestamp resolution (e.g., ext4/overlayfs integer second
692
988
  // mtime truncation on Linux) where files modified during startup may have truncated .000 ms mtimes.
693
- if (stat.mtimeMs < initTime - 1000) {
989
+ if (!stat || stat.mtimeMs < initTime - 1000) {
694
990
  return;
695
991
  }
696
992
  const previousStat = fileStats.get(lookupKey);
@@ -703,10 +999,45 @@ async function createChokidarWatcher(options, chokidarModule) {
703
999
  fileStats.set(lookupKey, { mtimeMs: stat.mtimeMs, size: stat.size });
704
1000
  queue.addChange(type, rawPath);
705
1001
  };
1002
+ const scheduleBaselineCheck = (baselineStats) => {
1003
+ if (baselineStats.size === 0) {
1004
+ return;
1005
+ }
1006
+ const timer = setTimeout(() => {
1007
+ pendingTimers.delete(timer);
1008
+ void checkChokidarBaselines(baselineStats, watchedFiles, fileStats, watcher, handleEvent);
1009
+ }, 50);
1010
+ timer.unref();
1011
+ pendingTimers.add(timer);
1012
+ };
706
1013
  watcher.on('add', (path) => handleEvent('added', path));
707
1014
  watcher.on('change', (path) => handleEvent('modified', path));
708
- watcher.on('unlink', (path) => handleEvent('removed', path));
709
- const buildWatcher = {
1015
+ watcher.on('unlink', (rawPath) => {
1016
+ const posixPath = toPosixPathNormalized(rawPath);
1017
+ const lookupKey = toLookupKey(posixPath, isCaseSensitive);
1018
+ if (!isPathWatched(lookupKey, watchedFiles)) {
1019
+ return;
1020
+ }
1021
+ // When Chokidar processes a file removal, it closes the underlying file watcher unless only a
1022
+ // single directory is tracked (`_watched.size === 1`). Re-adding a directly watched path
1023
+ // ensures Chokidar continues watching either the recreated file (if replaced within the atomic
1024
+ // window) or its parent directory for future recreation.
1025
+ if (watchedFiles.has(lookupKey)) {
1026
+ fileStats.delete(lookupKey);
1027
+ watcher.add(posixPath);
1028
+ const stat = fs.statSync(rawPath, { throwIfNoEntry: false });
1029
+ if (stat?.isFile()) {
1030
+ handleEvent('modified', rawPath);
1031
+ scheduleBaselineCheck(new Map([[lookupKey, { posixPath, mtimeMs: stat.mtimeMs, size: stat.size }]]));
1032
+ return;
1033
+ }
1034
+ handleEvent('removed', rawPath);
1035
+ scheduleBaselineCheck(new Map([[lookupKey, { posixPath }]]));
1036
+ return;
1037
+ }
1038
+ handleEvent('removed', rawPath);
1039
+ });
1040
+ return {
710
1041
  [Symbol.asyncIterator]() {
711
1042
  return this;
712
1043
  },
@@ -728,26 +1059,22 @@ async function createChokidarWatcher(options, chokidarModule) {
728
1059
  add(paths) {
729
1060
  const targets = typeof paths === 'string' ? [paths] : paths;
730
1061
  const newPaths = [];
1062
+ const baselineStats = new Map();
731
1063
  for (const p of targets) {
732
1064
  const posixPath = toPosixPathNormalized(p);
733
1065
  const lookupKey = toLookupKey(posixPath, isCaseSensitive);
734
1066
  if (!watchedFiles.has(lookupKey)) {
735
1067
  watchedFiles.add(lookupKey);
736
- // For files inside node_modules, register their package directory. Only newly encountered
737
- // package directories need to be added to Chokidar.
738
- const { isPackage, newPkgDir } = nodeModulesManager.registerPackage(posixPath, lookupKey);
739
- if (newPkgDir) {
740
- newPaths.push(newPkgDir);
741
- }
742
- else if (!isPackage &&
743
- !isPathInside(lookupKey, rootDirLookupKey) &&
744
- lookupKey !== rootDirLookupKey) {
745
- newPaths.push(posixPath);
1068
+ newPaths.push(posixPath);
1069
+ const stat = fs.statSync(posixPath, { throwIfNoEntry: false });
1070
+ if (stat?.isFile()) {
1071
+ baselineStats.set(lookupKey, { posixPath, mtimeMs: stat.mtimeMs, size: stat.size });
746
1072
  }
747
1073
  }
748
1074
  }
749
1075
  if (newPaths.length > 0) {
750
1076
  watcher.add(newPaths);
1077
+ scheduleBaselineCheck(baselineStats);
751
1078
  }
752
1079
  },
753
1080
  remove(paths) {
@@ -759,16 +1086,7 @@ async function createChokidarWatcher(options, chokidarModule) {
759
1086
  if (watchedFiles.has(lookupKey)) {
760
1087
  watchedFiles.delete(lookupKey);
761
1088
  fileStats.delete(lookupKey);
762
- // When the last watched file in a package is removed, unwatch the package directory.
763
- const { isPackage, unwatchPkgDir } = nodeModulesManager.removePackageFile(posixPath, lookupKey);
764
- if (unwatchPkgDir) {
765
- removePaths.push(unwatchPkgDir);
766
- }
767
- else if (!isPackage &&
768
- !isPathInside(lookupKey, rootDirLookupKey) &&
769
- lookupKey !== rootDirLookupKey) {
770
- removePaths.push(posixPath);
771
- }
1089
+ removePaths.push(posixPath);
772
1090
  }
773
1091
  }
774
1092
  if (removePaths.length > 0) {
@@ -776,6 +1094,10 @@ async function createChokidarWatcher(options, chokidarModule) {
776
1094
  }
777
1095
  },
778
1096
  async close() {
1097
+ for (const timer of pendingTimers) {
1098
+ clearTimeout(timer);
1099
+ }
1100
+ pendingTimers.clear();
779
1101
  try {
780
1102
  await watcher.close();
781
1103
  }
@@ -784,6 +1106,5 @@ async function createChokidarWatcher(options, chokidarModule) {
784
1106
  }
785
1107
  },
786
1108
  };
787
- return buildWatcher;
788
1109
  }
789
1110
  //# sourceMappingURL=watcher.js.map