| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 37cf399 commit 5194b41
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -49,6 +49,9 @@ export interface WatcherOptions { | |||
| 49 | 49 | cwd?: string; | |
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | + const NODE_MODULES = 'node_modules'; | ||
| 53 | + const NODE_MODULES_SEGMENT = `/${NODE_MODULES}/`; | ||
| 54 | + | ||
| 52 | 55 | // Watch workspace for package manager changes | |
| 53 | 56 | const packageWatchFiles = [ | |
| 54 | 57 | // manifest can affect module resolution | |
@@ -109,7 +112,7 @@ export async function setupWatcher(options: SetupWatcherOptions): Promise<BuildW | |||
| 109 | 112 | // Ignore all node modules directories to avoid excessive file watchers. | |
| 110 | 113 | // Package changes are handled below by watching manifest and lock files. | |
| 111 | 114 | // NOTE: this is not enabled when preserveSymlinks is true as this would break `npm link` usages. | |
| 112 | - ignored.push('**/node_modules/**'); | ||
| 115 | + ignored.push(`**/${NODE_MODULES}/**`); | ||
| 113 | 116 | } | |
| 114 | 117 | ||
| 115 | 118 | const watcher = await createWatcher({ | |
@@ -386,6 +389,36 @@ export function isPathInside(file: string, dir: string): boolean { | |||
| 386 | 389 | return file.startsWith(dirWithSlash); | |
| 387 | 390 | } | |
| 388 | 391 | ||
| 392 | + /** | ||
| 393 | + * Extracts the package directory inside node_modules for a given POSIX path. | ||
| 394 | + * | ||
| 395 | + * Input Expectations: | ||
| 396 | + * - `posixPath` must be a normalized POSIX-style path (using forward slashes '/'). | ||
| 397 | + */ | ||
| 398 | + export function extractNodeModulesPackageDir(posixPath: string): string | undefined { | ||
| 399 | + const index = posixPath.lastIndexOf(NODE_MODULES_SEGMENT); | ||
| 400 | + if (index === -1) { | ||
| 401 | + return undefined; | ||
| 402 | + } | ||
| 403 | + | ||
| 404 | + const nodeModulesPath = posixPath.slice(0, index + NODE_MODULES_SEGMENT.length - 1); | ||
| 405 | + const remainder = posixPath.slice(index + NODE_MODULES_SEGMENT.length); | ||
| 406 | + const segments = remainder.split('/'); | ||
| 407 | + if (segments.length === 0 || !segments[0]) { | ||
| 408 | + return undefined; | ||
| 409 | + } | ||
| 410 | + | ||
| 411 | + if (segments[0].startsWith('@')) { | ||
| 412 | + if (segments.length < 2 || !segments[1]) { | ||
| 413 | + return undefined; | ||
| 414 | + } | ||
| 415 | + | ||
| 416 | + return `${nodeModulesPath}/${segments[0]}/${segments[1]}`; | ||
| 417 | + } | ||
| 418 | + | ||
| 419 | + return `${nodeModulesPath}/${segments[0]}`; | ||
| 420 | + } | ||
| 421 | + | ||
| 389 | 422 | class ParcelExternalManager { | |
| 390 | 423 | private readonly extraSubscriptions = new Map<string, ParcelWatcher.AsyncSubscription>(); | |
| 391 | 424 | private readonly pendingSubscriptions = new Map< | |
@@ -625,6 +658,94 @@ async function createParcelWatcher( | |||
| 625 | 658 | return buildWatcher; | |
| 626 | 659 | } | |
| 627 | 660 | ||
| 661 | + /** | ||
| 662 | + * Manages selective node_modules watching for Chokidar. | ||
| 663 | + * | ||
| 664 | + * During Chokidar's initial scan, the allowed packages set is empty, causing Chokidar to prune the entire | ||
| 665 | + * node_modules directory immediately and avoid blocking the 'ready' event for minutes. | ||
| 666 | + * Packages are dynamically allowed and watched at the package root level as files are added via `watcher.add`. | ||
| 667 | + * Tracking files per package allows reference-counting so that package directories are unwatched when no | ||
| 668 | + * longer needed. | ||
| 669 | + */ | ||
| 670 | + class ChokidarNodeModulesManager { | ||
| 671 | + private readonly allowedPackages = new Map<string, Set<string>>(); | ||
| 672 | + | ||
| 673 | + constructor(private readonly isCaseSensitive: boolean) {} | ||
| 674 | + | ||
| 675 | + registerPackage( | ||
| 676 | + posixPath: string, | ||
| 677 | + lookupKey: string, | ||
| 678 | + ): { isPackage: boolean; newPkgDir?: string } { | ||
| 679 | + const packageDir = extractNodeModulesPackageDir(posixPath); | ||
| 680 | + if (!packageDir) { | ||
| 681 | + return { isPackage: false }; | ||
| 682 | + } | ||
| 683 | + | ||
| 684 | + const packageKey = toLookupKey(packageDir, this.isCaseSensitive); | ||
| 685 | + let watchedInPkg = this.allowedPackages.get(packageKey); | ||
| 686 | + if (!watchedInPkg) { | ||
| 687 | + watchedInPkg = new Set(); | ||
| 688 | + this.allowedPackages.set(packageKey, watchedInPkg); | ||
| 689 | + watchedInPkg.add(lookupKey); | ||
| 690 | + | ||
| 691 | + return { isPackage: true, newPkgDir: packageDir }; | ||
| 692 | + } | ||
| 693 | + | ||
| 694 | + watchedInPkg.add(lookupKey); | ||
| 695 | + | ||
| 696 | + return { isPackage: true }; | ||
| 697 | + } | ||
| 698 | + | ||
| 699 | + removePackageFile( | ||
| 700 | + posixPath: string, | ||
| 701 | + lookupKey: string, | ||
| 702 | + ): { isPackage: boolean; unwatchPkgDir?: string } { | ||
| 703 | + const packageDir = extractNodeModulesPackageDir(posixPath); | ||
| 704 | + if (!packageDir) { | ||
| 705 | + return { isPackage: false }; | ||
| 706 | + } | ||
| 707 | + | ||
| 708 | + const packageKey = toLookupKey(packageDir, this.isCaseSensitive); | ||
| 709 | + const watchedInPkg = this.allowedPackages.get(packageKey); | ||
| 710 | + if (watchedInPkg) { | ||
| 711 | + watchedInPkg.delete(lookupKey); | ||
| 712 | + if (watchedInPkg.size === 0) { | ||
| 713 | + this.allowedPackages.delete(packageKey); | ||
| 714 | + | ||
| 715 | + return { isPackage: true, unwatchPkgDir: packageDir }; | ||
| 716 | + } | ||
| 717 | + } | ||
| 718 | + | ||
| 719 | + return { isPackage: true }; | ||
| 720 | + } | ||
| 721 | + | ||
| 722 | + isIgnored(filePath: string): boolean { | ||
| 723 | + // Fast path: avoid expensive normalization for non-node_modules files | ||
| 724 | + if (!filePath.includes(NODE_MODULES)) { | ||
| 725 | + return false; | ||
| 726 | + } | ||
| 727 | + | ||
| 728 | + const posixPath = toPosixPathNormalized(filePath); | ||
| 729 | + const lookupKey = toLookupKey(posixPath, this.isCaseSensitive); | ||
| 730 | + | ||
| 731 | + if (!`${lookupKey}/`.includes(NODE_MODULES_SEGMENT)) { | ||
| 732 | + return false; | ||
| 733 | + } | ||
| 734 | + | ||
| 735 | + if (this.allowedPackages.has(lookupKey)) { | ||
| 736 | + return false; | ||
| 737 | + } | ||
| 738 | + | ||
| 739 | + for (const pkg of this.allowedPackages.keys()) { | ||
| 740 | + if (isPathInside(lookupKey, pkg)) { | ||
| 741 | + return false; | ||
| 742 | + } | ||
| 743 | + } | ||
| 744 | + | ||
| 745 | + return true; | ||
| 746 | + } | ||
| 747 | + } | ||
| 748 | + | ||
| 628 | 749 | async function createChokidarWatcher( | |
| 629 | 750 | options?: WatcherOptions, | |
| 630 | 751 | chokidarModule?: typeof Chokidar, | |
@@ -647,15 +768,25 @@ async function createChokidarWatcher( | |||
| 647 | 768 | const rootDirPosix = toPosixPathNormalized(rootDir); | |
| 648 | 769 | const rootDirLookupKey = toLookupKey(rootDirPosix, isCaseSensitive); | |
| 649 | 770 | ||
| 650 | - const ignored = options?.ignored?.map((pattern) => { | ||
| 651 | - if (/[*?[\]{}()]/.test(pattern)) { | ||
| 652 | - const isMatch = picomatch(pattern, { dot: true }); | ||
| 771 | + const nodeModulesManager = new ChokidarNodeModulesManager(isCaseSensitive); | ||
| 653 | 772 | ||
| 654 | - return (filePath: string) => isMatch(toPosixPathNormalized(filePath)); | ||
| 655 | - } | ||
| 773 | + // Strip static node_modules ignore patterns (e.g. `**/node_modules/**` from setupWatcher). | ||
| 774 | + // If left in place, picomatch would unconditionally ignore all node_modules files and override | ||
| 775 | + // our dynamic package allowlist above. | ||
| 776 | + const customIgnored = | ||
| 777 | + options?.ignored | ||
| 778 | + ?.filter((pattern) => !pattern.includes(NODE_MODULES)) | ||
| 779 | + .map((pattern) => { | ||
| 780 | + if (/[*?[\]{}()]/.test(pattern)) { | ||
| 781 | + const isMatch = picomatch(pattern, { dot: true }); | ||
| 656 | 782 | ||
| 657 | - return { path: toPosixPathNormalized(pattern), recursive: true }; | ||
| 658 | - }); | ||
| 783 | + return (filePath: string) => isMatch(toPosixPathNormalized(filePath)); | ||
| 784 | + } | ||
| 785 | + | ||
| 786 | + return { path: toPosixPathNormalized(pattern), recursive: true }; | ||
| 787 | + }) ?? []; | ||
| 788 | + | ||
| 789 | + const ignored = [...customIgnored, (filePath: string) => nodeModulesManager.isIgnored(filePath)]; | ||
| 659 | 790 | ||
| 660 | 791 | const watcher = chokidar.watch(rootDir, { | |
| 661 | 792 | ignoreInitial: true, | |
@@ -714,7 +845,17 @@ async function createChokidarWatcher( | |||
| 714 | 845 | const lookupKey = toLookupKey(posixPath, isCaseSensitive); | |
| 715 | 846 | if (!watchedFiles.has(lookupKey)) { | |
| 716 | 847 | watchedFiles.add(lookupKey); | |
| 717 | - if (!isPathInside(lookupKey, rootDirLookupKey) && lookupKey !== rootDirLookupKey) { | ||
| 848 | + | ||
| 849 | + // For files inside node_modules, register their package directory. Only newly encountered | ||
| 850 | + // package directories need to be added to Chokidar. | ||
| 851 | + const { isPackage, newPkgDir } = nodeModulesManager.registerPackage(posixPath, lookupKey); | ||
| 852 | + if (newPkgDir) { | ||
| 853 | + newPaths.push(newPkgDir); | ||
| 854 | + } else if ( | ||
| 855 | + !isPackage && | ||
| 856 | + !isPathInside(lookupKey, rootDirLookupKey) && | ||
| 857 | + lookupKey !== rootDirLookupKey | ||
| 858 | + ) { | ||
| 718 | 859 | newPaths.push(posixPath); | |
| 719 | 860 | } | |
| 720 | 861 | } | |
@@ -732,7 +873,19 @@ async function createChokidarWatcher( | |||
| 732 | 873 | const lookupKey = toLookupKey(posixPath, isCaseSensitive); | |
| 733 | 874 | if (watchedFiles.has(lookupKey)) { | |
| 734 | 875 | watchedFiles.delete(lookupKey); | |
| 735 | - if (!isPathInside(lookupKey, rootDirLookupKey) && lookupKey !== rootDirLookupKey) { | ||
| 876 | + | ||
| 877 | + // When the last watched file in a package is removed, unwatch the package directory. | ||
| 878 | + const { isPackage, unwatchPkgDir } = nodeModulesManager.removePackageFile( | ||
| 879 | + posixPath, | ||
| 880 | + lookupKey, | ||
| 881 | + ); | ||
| 882 | + if (unwatchPkgDir) { | ||
| 883 | + removePaths.push(unwatchPkgDir); | ||
| 884 | + } else if ( | ||
| 885 | + !isPackage && | ||
| 886 | + !isPathInside(lookupKey, rootDirLookupKey) && | ||
| 887 | + lookupKey !== rootDirLookupKey | ||
| 888 | + ) { | ||
| 736 | 889 | removePaths.push(posixPath); | |
| 737 | 890 | } | |
| 738 | 891 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments