@angular/build 22.3.0-next.1 → 22.3.0-next.2

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 (123) hide show
  1. package/package.json +13 -13
  2. package/src/builders/application/build-action.js +2 -2
  3. package/src/builders/application/build-action.js.map +1 -1
  4. package/src/builders/application/index.js +8 -31
  5. package/src/builders/application/index.js.map +1 -1
  6. package/src/builders/application/options.d.ts +1 -3
  7. package/src/builders/application/options.js +1 -1
  8. package/src/builders/application/options.js.map +1 -1
  9. package/src/builders/application/results.d.ts +1 -1
  10. package/src/builders/dev-server/vite/utils.js +1 -1
  11. package/src/builders/dev-server/vite/utils.js.map +1 -1
  12. package/src/builders/extract-i18n/application-extraction.js +2 -1
  13. package/src/builders/extract-i18n/application-extraction.js.map +1 -1
  14. package/src/builders/karma/assets-middleware.js +1 -1
  15. package/src/builders/karma/assets-middleware.js.map +1 -1
  16. package/src/builders/library/builder-state.d.ts +115 -0
  17. package/src/builders/library/builder-state.js +282 -0
  18. package/src/builders/library/builder-state.js.map +1 -0
  19. package/src/builders/library/builder.js +101 -89
  20. package/src/builders/library/builder.js.map +1 -1
  21. package/src/builders/library/execution-result.d.ts +35 -0
  22. package/src/builders/library/execution-result.js +67 -0
  23. package/src/builders/library/execution-result.js.map +1 -0
  24. package/src/builders/library/index.d.ts +1 -1
  25. package/src/builders/library/index.js.map +1 -1
  26. package/src/builders/library/options.d.ts +9 -0
  27. package/src/builders/library/options.js +29 -14
  28. package/src/builders/library/options.js.map +1 -1
  29. package/src/builders/library/pipeline/assets.d.ts +3 -3
  30. package/src/builders/library/pipeline/assets.js +12 -16
  31. package/src/builders/library/pipeline/assets.js.map +1 -1
  32. package/src/builders/library/pipeline/build-action.d.ts +11 -24
  33. package/src/builders/library/pipeline/build-action.js +76 -125
  34. package/src/builders/library/pipeline/build-action.js.map +1 -1
  35. package/src/builders/library/pipeline/bundler.d.ts +3 -3
  36. package/src/builders/library/pipeline/bundler.js +55 -33
  37. package/src/builders/library/pipeline/bundler.js.map +1 -1
  38. package/src/builders/library/pipeline/compilation.d.ts +7 -26
  39. package/src/builders/library/pipeline/compilation.js +59 -69
  40. package/src/builders/library/pipeline/compilation.js.map +1 -1
  41. package/src/builders/library/pipeline/entry-points.d.ts +26 -6
  42. package/src/builders/library/pipeline/entry-points.js +82 -21
  43. package/src/builders/library/pipeline/entry-points.js.map +1 -1
  44. package/src/builders/library/pipeline/package-manifests.d.ts +2 -1
  45. package/src/builders/library/pipeline/package-manifests.js +50 -1
  46. package/src/builders/library/pipeline/package-manifests.js.map +1 -1
  47. package/src/builders/library/pipeline/stylesheet-bundler.d.ts +4 -1
  48. package/src/builders/library/pipeline/stylesheet-bundler.js +1 -1
  49. package/src/builders/library/pipeline/stylesheet-bundler.js.map +1 -1
  50. package/src/builders/library/pipeline/utils.d.ts +19 -11
  51. package/src/builders/library/pipeline/utils.js +27 -11
  52. package/src/builders/library/pipeline/utils.js.map +1 -1
  53. package/src/builders/library/schema.json +2 -4
  54. package/src/builders/library/types.d.ts +62 -22
  55. package/src/builders/unit-test/runners/vitest/executor.js +0 -5
  56. package/src/builders/unit-test/runners/vitest/executor.js.map +1 -1
  57. package/src/builders/unit-test/runners/vitest/plugins.js +13 -1
  58. package/src/builders/unit-test/runners/vitest/plugins.js.map +1 -1
  59. package/src/builders/unit-test/test-discovery.js +4 -2
  60. package/src/builders/unit-test/test-discovery.js.map +1 -1
  61. package/src/private.d.ts +1 -1
  62. package/src/private.js +3 -3
  63. package/src/private.js.map +1 -1
  64. package/src/tools/angular/angular-host.d.ts +1 -1
  65. package/src/tools/angular/angular-host.js +5 -2
  66. package/src/tools/angular/angular-host.js.map +1 -1
  67. package/src/tools/angular/compilation/angular-compilation.d.ts +1 -0
  68. package/src/tools/angular/compilation/angular-compilation.js.map +1 -1
  69. package/src/tools/angular/compilation/aot-compilation.js +325 -80
  70. package/src/tools/angular/compilation/aot-compilation.js.map +1 -1
  71. package/src/tools/angular/compilation/compiler-options.js +4 -1
  72. package/src/tools/angular/compilation/compiler-options.js.map +1 -1
  73. package/src/tools/angular/compilation/jit-compilation.js +5 -4
  74. package/src/tools/angular/compilation/jit-compilation.js.map +1 -1
  75. package/src/tools/angular/compilation/parallel-worker.js +1 -0
  76. package/src/tools/angular/compilation/parallel-worker.js.map +1 -1
  77. package/src/tools/angular/compilation/typescript-compilation.d.ts +2 -0
  78. package/src/tools/angular/compilation/typescript-compilation.js +44 -5
  79. package/src/tools/angular/compilation/typescript-compilation.js.map +1 -1
  80. package/src/tools/esbuild/bundler-context.js +5 -1
  81. package/src/tools/esbuild/bundler-context.js.map +1 -1
  82. package/src/tools/esbuild/persistent-load-result-cache.js +2 -4
  83. package/src/tools/esbuild/persistent-load-result-cache.js.map +1 -1
  84. package/src/tools/esbuild/utils.d.ts +1 -2
  85. package/src/tools/esbuild/utils.js +0 -13
  86. package/src/tools/esbuild/utils.js.map +1 -1
  87. package/src/tools/sass/rebasing-importer.d.ts +1 -1
  88. package/src/tools/sass/rebasing-importer.js +6 -3
  89. package/src/tools/sass/rebasing-importer.js.map +1 -1
  90. package/src/tools/vite/middlewares/assets-middleware.js +1 -1
  91. package/src/tools/vite/middlewares/assets-middleware.js.map +1 -1
  92. package/src/utils/concurrency.d.ts +4 -0
  93. package/src/utils/concurrency.js +5 -0
  94. package/src/utils/concurrency.js.map +1 -1
  95. package/src/utils/delete-output-dir.js +3 -2
  96. package/src/utils/delete-output-dir.js.map +1 -1
  97. package/src/utils/disk-files.d.ts +30 -0
  98. package/src/utils/disk-files.js +68 -0
  99. package/src/utils/disk-files.js.map +1 -0
  100. package/src/utils/index-file/nonce.d.ts +2 -1
  101. package/src/utils/index-file/nonce.js +22 -4
  102. package/src/utils/index-file/nonce.js.map +1 -1
  103. package/src/utils/normalize-asset-patterns.d.ts +18 -3
  104. package/src/utils/normalize-asset-patterns.js +34 -67
  105. package/src/utils/normalize-asset-patterns.js.map +1 -1
  106. package/src/utils/normalize-cache.js +2 -2
  107. package/src/utils/normalize-cache.js.map +1 -1
  108. package/src/utils/postcss-configuration.d.ts +2 -2
  109. package/src/utils/postcss-configuration.js +3 -3
  110. package/src/utils/postcss-configuration.js.map +1 -1
  111. package/src/utils/resolve-assets.js +28 -9
  112. package/src/utils/resolve-assets.js.map +1 -1
  113. package/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.d.ts +1 -1
  114. package/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.js +3 -3
  115. package/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.js.map +1 -1
  116. package/src/utils/server-rendering/esm-in-memory-loader/register-hooks.js +3 -2
  117. package/src/utils/server-rendering/esm-in-memory-loader/register-hooks.js.map +1 -1
  118. package/src/utils/test-files.js +5 -57
  119. package/src/utils/test-files.js.map +1 -1
  120. package/src/utils/watcher.d.ts +53 -0
  121. package/src/utils/watcher.js +404 -76
  122. package/src/utils/watcher.js.map +1 -1
  123. package/src/builders/library/pipeline/types.d.ts +0 -54
@@ -43,7 +43,7 @@ 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;
@@ -362,122 +362,450 @@ function extractNodeModulesPackageDir(posixPath) {
362
362
  }
363
363
  return `${nodeModulesPath}/${segments[0]}`;
364
364
  }
365
+ /**
366
+ * Maximum number of concurrent external directory subscriptions before coalescing
367
+ * sibling/cousin directories into common ancestor directories.
368
+ * Kept well below macOS's ~512 per-process FSEvents stream limit.
369
+ */
370
+ const DEFAULT_MAX_EXTERNAL_SUBSCRIPTIONS = 256;
371
+ /**
372
+ * Checks whether any strict ancestor of the given trie node is already marked as a target.
373
+ */
374
+ function hasTargetAncestor(node) {
375
+ let current = node.parent;
376
+ while (current) {
377
+ if (current.isTarget) {
378
+ return true;
379
+ }
380
+ current = current.parent;
381
+ }
382
+ return false;
383
+ }
384
+ /**
385
+ * Manages `@parcel/watcher` directory subscriptions for watched files located outside the
386
+ * primary `workspaceRoot`.
387
+ *
388
+ * Because `@parcel/watcher` watches directories recursively and macOS limits each process to
389
+ * ~512 concurrent `FSEvents` streams, this manager:
390
+ * - Normalizes external `node_modules` file paths to their package root directory.
391
+ * - Batches synchronous `watchFile` and `removeFile` calls within a microtask before subscribing.
392
+ * - Uses a directory prefix tree to subsume nested child directories and iteratively coalesce
393
+ * deepest branching subdirectories into common ancestors when the subscription budget is exceeded.
394
+ * - Dynamically lowers its subscription budget and coalesces further if `subscribe()` fails on
395
+ * an existing directory due to OS stream limits.
396
+ */
365
397
  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) {
398
+ /** Active `@parcel/watcher` subscriptions keyed by subscribed directory lookup key. */
399
+ #extraSubscriptions = new Map();
400
+ /** Reference-counted watched files grouped by their target external directory lookup key. */
401
+ #externalDirFiles = new Map();
402
+ /** Reverse lookup mapping each watched file lookup key to its target directory lookup key. */
403
+ #fileToDirKey = new Map();
404
+ /** The `@parcel/watcher` module instance. */
405
+ #parcelWatcher;
406
+ /** Watcher configuration options including ignore patterns. */
407
+ #options;
408
+ /** Normalized POSIX path of the primary watched workspace root. */
409
+ #rootDirPosix;
410
+ /** Case-normalized lookup key of the primary watched workspace root. */
411
+ #rootDirLookupKey;
412
+ /** Whether the underlying filesystem is case-sensitive. */
413
+ #isCaseSensitive;
414
+ /** Callback invoked when `@parcel/watcher` emits filesystem events. */
415
+ #handleEvents;
416
+ /** Current maximum number of concurrent external directory subscriptions allowed before coalescing. */
417
+ #maxSubscriptions;
418
+ /** Whether external directory state has changed and requires a reconciliation pass. */
419
+ #needsReconcile = false;
420
+ /** Whether obsolete subscriptions should be unsubscribed before creating new ones (used after stream limit errors). */
421
+ #shrinkBeforeSubscribe = false;
422
+ /** In-flight promise for the currently scheduled or running reconciliation loop. */
423
+ #reconcilePromise;
424
+ /** Whether this manager has been closed. */
425
+ #isClosed = false;
426
+ /**
427
+ * Creates a new external directory subscription manager for `@parcel/watcher`.
428
+ *
429
+ * @param parcelWatcher The `@parcel/watcher` module.
430
+ * @param options Watcher configuration options.
431
+ * @param rootDirPosix Normalized POSIX path of the primary workspace root.
432
+ * @param rootDirLookupKey Case-normalized lookup key of the primary workspace root.
433
+ * @param isCaseSensitive Whether the filesystem at the workspace root is case-sensitive.
434
+ * @param handleEvents Callback to process incoming `@parcel/watcher` events.
435
+ * @param maxSubscriptions Maximum number of concurrent external directory subscriptions before coalescing.
436
+ */
437
+ constructor(parcelWatcher, options, rootDirPosix, rootDirLookupKey, isCaseSensitive, handleEvents, maxSubscriptions = DEFAULT_MAX_EXTERNAL_SUBSCRIPTIONS) {
438
+ this.#parcelWatcher = parcelWatcher;
439
+ this.#options = options;
440
+ this.#rootDirPosix = rootDirPosix;
441
+ this.#rootDirLookupKey = rootDirLookupKey;
442
+ this.#isCaseSensitive = isCaseSensitive;
443
+ this.#handleEvents = handleEvents;
444
+ this.#maxSubscriptions = Math.max(1, maxSubscriptions);
445
+ }
446
+ /**
447
+ * Registers a file path to be watched if it resides outside the primary workspace root,
448
+ * and schedules an asynchronous subscription reconciliation pass if needed.
449
+ *
450
+ * @param posixPath Normalized POSIX file path preserving original casing.
451
+ * @param lookupKey Case-normalized lookup key for the file path.
452
+ */
453
+ watchFile(posixPath, lookupKey) {
454
+ if (this.#isClosed ||
455
+ isPathInside(lookupKey, this.#rootDirLookupKey) ||
456
+ lookupKey === this.#rootDirLookupKey) {
381
457
  return;
382
458
  }
383
- const dirPath = getDirectoryPath(posixPath);
384
- const dirKey = getDirectoryPath(lookupKey);
385
- let dirEntry = this.externalDirFiles.get(dirKey);
459
+ const pkgDir = extractNodeModulesPackageDir(posixPath);
460
+ const dirPath = pkgDir ?? getDirectoryPath(posixPath);
461
+ const dirKey = pkgDir
462
+ ? toLookupKey(pkgDir, this.#isCaseSensitive)
463
+ : getDirectoryPath(lookupKey);
464
+ this.#fileToDirKey.set(lookupKey, dirKey);
465
+ let dirEntry = this.#externalDirFiles.get(dirKey);
466
+ const isNewDir = !dirEntry;
386
467
  if (!dirEntry) {
387
468
  dirEntry = { dirPath, files: new Set() };
388
- this.externalDirFiles.set(dirKey, dirEntry);
469
+ this.#externalDirFiles.set(dirKey, dirEntry);
389
470
  }
390
471
  dirEntry.files.add(lookupKey);
391
- await this.ensureDirWatched(dirPath, dirKey);
472
+ if (isNewDir || !this.#isCoveredByActiveSubscription(dirKey)) {
473
+ this.#scheduleReconcile();
474
+ }
392
475
  }
476
+ /**
477
+ * Unregisters a previously watched external file and schedules a reconciliation pass
478
+ * if it was the last watched file in its target directory.
479
+ *
480
+ * @param lookupKey Case-normalized lookup key for the file path to remove.
481
+ */
393
482
  removeFile(lookupKey) {
394
- if (isPathInside(lookupKey, this.rootDirLookupKey) || lookupKey === this.rootDirLookupKey) {
483
+ const dirKey = this.#fileToDirKey.get(lookupKey);
484
+ if (!dirKey) {
395
485
  return;
396
486
  }
397
- const dirKey = getDirectoryPath(lookupKey);
398
- const dirEntry = this.externalDirFiles.get(dirKey);
487
+ this.#fileToDirKey.delete(lookupKey);
488
+ const dirEntry = this.#externalDirFiles.get(dirKey);
399
489
  if (dirEntry) {
400
490
  dirEntry.files.delete(lookupKey);
401
491
  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
- }
492
+ this.#externalDirFiles.delete(dirKey);
493
+ this.#scheduleReconcile();
413
494
  }
414
495
  }
415
496
  }
497
+ /**
498
+ * Waits for any scheduled or in-progress subscription reconciliation pass to complete.
499
+ */
500
+ async flush() {
501
+ if (this.#reconcilePromise) {
502
+ await this.#reconcilePromise;
503
+ }
504
+ }
505
+ /**
506
+ * Closes the manager, awaiting any pending reconciliation and unsubscribing all active
507
+ * external directory subscriptions.
508
+ */
416
509
  async close() {
510
+ this.#isClosed = true;
511
+ this.#needsReconcile = false;
417
512
  try {
418
- if (this.pendingSubscriptions.size > 0) {
419
- await Promise.allSettled(Array.from(this.pendingSubscriptions.values()));
513
+ if (this.#reconcilePromise) {
514
+ await this.#reconcilePromise.catch(() => { });
420
515
  }
421
- if (this.extraSubscriptions.size > 0) {
422
- await Promise.allSettled(Array.from(this.extraSubscriptions.values()).map((sub) => sub.unsubscribe()));
516
+ if (this.#extraSubscriptions.size > 0) {
517
+ await Promise.allSettled(Array.from(this.#extraSubscriptions.values(), (sub) => sub.unsubscribe()));
423
518
  }
424
519
  }
425
520
  finally {
426
- this.extraSubscriptions.clear();
427
- this.pendingSubscriptions.clear();
428
- this.externalDirFiles.clear();
521
+ this.#extraSubscriptions.clear();
522
+ this.#externalDirFiles.clear();
523
+ this.#fileToDirKey.clear();
429
524
  }
430
525
  }
431
- isCoveredByExistingExternal(dirLookupKey) {
432
- for (const existingDir of this.extraSubscriptions.keys()) {
433
- if (dirLookupKey === existingDir || isPathInside(dirLookupKey, existingDir)) {
434
- return true;
526
+ /**
527
+ * Schedules a microtask to reconcile active `@parcel/watcher` subscriptions with the
528
+ * current set of external directories.
529
+ */
530
+ #scheduleReconcile() {
531
+ if (this.#isClosed) {
532
+ return;
533
+ }
534
+ this.#needsReconcile = true;
535
+ this.#reconcilePromise ??= Promise.resolve()
536
+ .then(() => this.#runReconcileLoop())
537
+ .finally(() => {
538
+ this.#reconcilePromise = undefined;
539
+ });
540
+ }
541
+ /**
542
+ * Continuously executes reconciliation passes until no further changes are pending.
543
+ */
544
+ async #runReconcileLoop() {
545
+ while (this.#needsReconcile && !this.#isClosed) {
546
+ this.#needsReconcile = false;
547
+ await this.#reconcileOnce();
548
+ }
549
+ }
550
+ /**
551
+ * Performs a single reconciliation pass: computes the desired coalesced directory set,
552
+ * subscribes to newly required directories, unsubscribes obsolete directories, and
553
+ * triggers fallback coalescing if OS stream limits are encountered.
554
+ */
555
+ async #reconcileOnce() {
556
+ const desiredDirs = this.#computeDesiredDirectories();
557
+ if (this.#shrinkBeforeSubscribe) {
558
+ this.#shrinkBeforeSubscribe = false;
559
+ const toUnsubscribe = [];
560
+ for (const [existingKey, sub] of this.#extraSubscriptions.entries()) {
561
+ if (!desiredDirs.has(existingKey)) {
562
+ this.#extraSubscriptions.delete(existingKey);
563
+ toUnsubscribe.push(sub.unsubscribe().catch(() => { }));
564
+ }
565
+ }
566
+ if (toUnsubscribe.length > 0) {
567
+ await Promise.allSettled(toUnsubscribe);
568
+ }
569
+ }
570
+ if (this.#isClosed) {
571
+ return;
572
+ }
573
+ const toSubscribe = [];
574
+ for (const [dirKey, dirPath] of desiredDirs.entries()) {
575
+ if (!this.#extraSubscriptions.has(dirKey)) {
576
+ toSubscribe.push([dirKey, dirPath]);
435
577
  }
436
578
  }
437
- for (const pendingDir of this.pendingSubscriptions.keys()) {
438
- if (dirLookupKey === pendingDir || isPathInside(dirLookupKey, pendingDir)) {
579
+ let hadExistingDirFailure = false;
580
+ if (toSubscribe.length > 0) {
581
+ await Promise.allSettled(toSubscribe.map(async ([dirKey, dirPath]) => {
582
+ const ignore = isPathInside(this.#rootDirLookupKey, dirKey)
583
+ ? [...(this.#options?.ignored ?? []), this.#rootDirPosix, `${this.#rootDirPosix}/**`]
584
+ : this.#options?.ignored;
585
+ try {
586
+ const sub = await this.#parcelWatcher.subscribe(dirPath, (err, events) => {
587
+ if (!err) {
588
+ this.#handleEvents(events);
589
+ }
590
+ }, { ignore });
591
+ if (this.#isClosed) {
592
+ await sub.unsubscribe().catch(() => { });
593
+ return;
594
+ }
595
+ this.#extraSubscriptions.set(dirKey, sub);
596
+ }
597
+ catch {
598
+ // Ignore errors for missing directories; if the directory exists on disk,
599
+ // mark failure so the manager can coalesce further if over OS stream limits.
600
+ if (!this.#isClosed && fs.existsSync(dirPath)) {
601
+ hadExistingDirFailure = true;
602
+ }
603
+ }
604
+ }));
605
+ }
606
+ if (this.#isClosed) {
607
+ return;
608
+ }
609
+ // Unsubscribe any previous subscriptions that are no longer desired and whose
610
+ // remaining watched files (if any) are now covered by another active subscription.
611
+ for (const [existingKey, sub] of this.#extraSubscriptions.entries()) {
612
+ if (!desiredDirs.has(existingKey) && this.#canRemoveSubscription(existingKey)) {
613
+ this.#extraSubscriptions.delete(existingKey);
614
+ sub.unsubscribe().catch(() => { });
615
+ }
616
+ }
617
+ // If subscribing to an existing directory failed (e.g. macOS FSEvents per-process limit),
618
+ // dynamically lower the subscription budget and re-reconcile to coalesce into ancestors.
619
+ if (hadExistingDirFailure && desiredDirs.size > 1 && this.#maxSubscriptions > 1) {
620
+ const nextMax = this.#extraSubscriptions.size > 0
621
+ ? Math.max(1, Math.floor(this.#extraSubscriptions.size / 2))
622
+ : Math.max(1, Math.floor(Math.min(this.#maxSubscriptions, desiredDirs.size) / 2));
623
+ if (nextMax < desiredDirs.size && nextMax < this.#maxSubscriptions) {
624
+ this.#maxSubscriptions = nextMax;
625
+ this.#shrinkBeforeSubscribe = true;
626
+ this.#needsReconcile = true;
627
+ }
628
+ }
629
+ }
630
+ /**
631
+ * Checks whether a directory lookup key is already covered by an active subscription.
632
+ */
633
+ #isCoveredByActiveSubscription(dirLookupKey) {
634
+ for (const existingDir of this.#extraSubscriptions.keys()) {
635
+ if (dirLookupKey === existingDir || isPathInside(dirLookupKey, existingDir)) {
439
636
  return true;
440
637
  }
441
638
  }
442
639
  return false;
443
640
  }
444
- async ensureDirWatched(dirPath, dirKey) {
445
- if (this.isCoveredByExistingExternal(dirKey)) {
446
- return;
641
+ /**
642
+ * Determines whether an existing subscription can be safely removed without leaving
643
+ * any remaining watched target directories uncovered.
644
+ */
645
+ #canRemoveSubscription(subKey) {
646
+ for (const targetDirKey of this.#externalDirFiles.keys()) {
647
+ if (targetDirKey === subKey || isPathInside(targetDirKey, subKey)) {
648
+ let coveredByOther = false;
649
+ for (const otherSubKey of this.#extraSubscriptions.keys()) {
650
+ if (otherSubKey !== subKey &&
651
+ (targetDirKey === otherSubKey || isPathInside(targetDirKey, otherSubKey))) {
652
+ coveredByOther = true;
653
+ break;
654
+ }
655
+ }
656
+ if (!coveredByOther) {
657
+ return false;
658
+ }
659
+ }
660
+ }
661
+ return true;
662
+ }
663
+ /**
664
+ * Computes the minimal set of external directories to subscribe to (mapped from `dirKey` to `dirPath`),
665
+ * subsuming nested directories and coalescing branching ancestors to stay within `#maxSubscriptions`.
666
+ */
667
+ #computeDesiredDirectories() {
668
+ if (this.#externalDirFiles.size === 0) {
669
+ return new Map();
447
670
  }
448
- const subPromise = this.parcelWatcher.subscribe(dirPath, (err, events) => {
449
- if (!err) {
450
- this.handleEvents(events);
671
+ const nodesByKey = new Map();
672
+ const roots = [];
673
+ for (const [dirKey, { dirPath }] of this.#externalDirFiles.entries()) {
674
+ const chain = [];
675
+ let currentKey = dirKey;
676
+ let currentPath = dirPath;
677
+ while (true) {
678
+ chain.push({ dirKey: currentKey, dirPath: currentPath });
679
+ const parentKey = getDirectoryPath(currentKey);
680
+ if (parentKey === currentKey) {
681
+ break;
682
+ }
683
+ currentKey = parentKey;
684
+ currentPath = getDirectoryPath(currentPath);
451
685
  }
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(() => { });
686
+ let parentNode;
687
+ for (let i = chain.length - 1; i >= 0; i--) {
688
+ const { dirKey: nodeKey, dirPath: nodePath } = chain[i];
689
+ const depth = chain.length - 1 - i;
690
+ let node = nodesByKey.get(nodeKey);
691
+ if (!node) {
692
+ node = {
693
+ dirKey: nodeKey,
694
+ dirPath: nodePath,
695
+ depth,
696
+ parent: parentNode,
697
+ children: new Map(),
698
+ isTarget: false,
699
+ leafCount: 0,
700
+ };
701
+ nodesByKey.set(nodeKey, node);
702
+ if (parentNode) {
703
+ parentNode.children.set(nodeKey, node);
704
+ }
705
+ else {
706
+ roots.push(node);
466
707
  }
467
708
  }
709
+ parentNode = node;
468
710
  }
469
- else {
470
- sub.unsubscribe().catch(() => { });
711
+ if (parentNode) {
712
+ parentNode.isTarget = true;
471
713
  }
472
714
  }
473
- catch {
474
- // Ignore subscription errors for missing or restricted external directories
715
+ const initSubtree = (node) => {
716
+ if (node.isTarget) {
717
+ node.children.clear();
718
+ node.leafCount = 1;
719
+ return 1;
720
+ }
721
+ let count = 0;
722
+ for (const child of node.children.values()) {
723
+ count += initSubtree(child);
724
+ }
725
+ node.leafCount = count;
726
+ return count;
727
+ };
728
+ let totalLeaves = 0;
729
+ for (const root of roots) {
730
+ totalLeaves += initSubtree(root);
475
731
  }
476
- finally {
477
- this.pendingSubscriptions.delete(dirKey);
732
+ if (totalLeaves > this.#maxSubscriptions) {
733
+ // Tier 1: non-ancestors of workspaceRoot (e.g., <monorepo>/libs, <monorepo>/node_modules)
734
+ // Tier 2: ancestors of workspaceRoot (e.g., <monorepo> when sibling apps/libs are directly under it)
735
+ const tier1ByDepth = new Map();
736
+ const tier2ByDepth = new Map();
737
+ const collectInternalNodes = (node) => {
738
+ if (node.isTarget) {
739
+ return;
740
+ }
741
+ if (node.parent !== undefined && node.leafCount > 1) {
742
+ const isWorkspaceAncestor = isPathInside(this.#rootDirLookupKey, node.dirKey);
743
+ const bucketMap = isWorkspaceAncestor ? tier2ByDepth : tier1ByDepth;
744
+ let bucket = bucketMap.get(node.depth);
745
+ if (!bucket) {
746
+ bucket = [];
747
+ bucketMap.set(node.depth, bucket);
748
+ }
749
+ bucket.push(node);
750
+ }
751
+ for (const child of node.children.values()) {
752
+ collectInternalNodes(child);
753
+ }
754
+ };
755
+ for (const root of roots) {
756
+ collectInternalNodes(root);
757
+ }
758
+ const collapseTier = (byDepth) => {
759
+ const sortedLevels = Array.from(byDepth.entries()).sort(([a], [b]) => b - a);
760
+ for (const [, nodes] of sortedLevels) {
761
+ if (totalLeaves <= this.#maxSubscriptions) {
762
+ return;
763
+ }
764
+ const candidates = nodes
765
+ .filter((n) => n.leafCount > 1 && !hasTargetAncestor(n))
766
+ .sort((a, b) => b.leafCount - a.leafCount || a.dirKey.localeCompare(b.dirKey));
767
+ for (const candidate of candidates) {
768
+ if (totalLeaves <= this.#maxSubscriptions) {
769
+ return;
770
+ }
771
+ if (candidate.leafCount <= 1 || hasTargetAncestor(candidate)) {
772
+ continue;
773
+ }
774
+ const reduction = candidate.leafCount - 1;
775
+ candidate.isTarget = true;
776
+ candidate.children.clear();
777
+ candidate.leafCount = 1;
778
+ totalLeaves -= reduction;
779
+ let ancestor = candidate.parent;
780
+ while (ancestor) {
781
+ ancestor.leafCount -= reduction;
782
+ ancestor = ancestor.parent;
783
+ }
784
+ }
785
+ }
786
+ };
787
+ collapseTier(tier1ByDepth);
788
+ if (totalLeaves > this.#maxSubscriptions) {
789
+ collapseTier(tier2ByDepth);
790
+ }
791
+ }
792
+ const desired = new Map();
793
+ const collectTargets = (node) => {
794
+ if (node.isTarget) {
795
+ desired.set(node.dirKey, node.dirPath);
796
+ return;
797
+ }
798
+ for (const child of node.children.values()) {
799
+ collectTargets(child);
800
+ }
801
+ };
802
+ for (const root of roots) {
803
+ collectTargets(root);
478
804
  }
805
+ return desired;
479
806
  }
480
807
  }
808
+ exports.ParcelExternalManager = ParcelExternalManager;
481
809
  async function createParcelWatcher(options, parcelWatcher) {
482
810
  const watchedFiles = new Set();
483
811
  const queue = new WatcherQueue();
@@ -516,7 +844,7 @@ async function createParcelWatcher(options, parcelWatcher) {
516
844
  }, {
517
845
  ignore: options?.ignored,
518
846
  });
519
- const externalManager = new ParcelExternalManager(parcelWatcher, options, rootDirLookupKey, handleEvents);
847
+ const externalManager = new ParcelExternalManager(parcelWatcher, options, rootDirPosix, rootDirLookupKey, isCaseSensitive, handleEvents);
520
848
  const buildWatcher = {
521
849
  [Symbol.asyncIterator]() {
522
850
  return this;
@@ -531,7 +859,7 @@ async function createParcelWatcher(options, parcelWatcher) {
531
859
  const lookupKey = toLookupKey(posixPath, isCaseSensitive);
532
860
  if (!watchedFiles.has(lookupKey)) {
533
861
  watchedFiles.add(lookupKey);
534
- void externalManager.ensureWatched(posixPath, lookupKey);
862
+ externalManager.watchFile(posixPath, lookupKey);
535
863
  }
536
864
  }
537
865
  },