From ba6a3a0a5b4af4de31df92a90b63adc9db860e85 Mon Sep 17 00:00:00 2001 From: Kingston Tam Date: Tue, 24 Sep 2024 20:32:11 +0000 Subject: [PATCH 1/7] fix: Allow ignore patterns to apply to absolute path instead of relative path + Upgrade chokidar to v4 for a smaller footprint + Add picomatch for glob matching + Add test for parent directory ignores --- package.json | 4 ++- pnpm-lock.yaml | 72 ++++++++++++++++++++++++----------------- src/watch/index.ts | 32 ++++++++++-------- tests/specs/watch.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index ea85dfb5c..8c3939b36 100644 --- a/package.json +++ b/package.json @@ -87,10 +87,11 @@ "@ampproject/remapping": "^2.3.0", "@types/cross-spawn": "^6.0.6", "@types/node": "^20.14.9", + "@types/picomatch": "^3.0.1", "@types/split2": "^4.2.3", "append-transform": "^2.0.0", "cachedir": "^2.4.0", - "chokidar": "^3.6.0", + "chokidar": "^4.0.1", "clean-pkg-json": "^1.2.0", "cleye": "^1.3.2", "cross-spawn": "^7.0.3", @@ -107,6 +108,7 @@ "memfs": "^4.9.3", "node-pty": "^1.0.0", "outdent": "^0.8.0", + "picomatch": "^4.0.2", "pkgroll": "^2.4.1", "proxyquire": "^2.1.3", "simple-git-hooks": "^2.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3241c307..a31602ae4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: '@types/node': specifier: ^20.14.9 version: 20.14.9 + '@types/picomatch': + specifier: ^3.0.1 + version: 3.0.1 '@types/split2': specifier: ^4.2.3 version: 4.2.3 @@ -38,8 +41,8 @@ importers: specifier: ^2.4.0 version: 2.4.0 chokidar: - specifier: ^3.6.0 - version: 3.6.0 + specifier: ^4.0.1 + version: 4.0.1 clean-pkg-json: specifier: ^1.2.0 version: 1.2.0 @@ -88,6 +91,9 @@ importers: outdent: specifier: ^0.8.0 version: 0.8.0 + picomatch: + specifier: ^4.0.2 + version: 4.0.2 pkgroll: specifier: ^2.4.1 version: 2.4.1(typescript@5.5.2) @@ -998,6 +1004,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/picomatch@3.0.1': + resolution: {integrity: sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==} + '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -1345,10 +1354,6 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1422,6 +1427,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -1913,10 +1922,6 @@ packages: resolution: {integrity: sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==} engines: {node: '>=0.10.0'} - fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2037,6 +2042,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported global-cache-dir@6.0.0: resolution: {integrity: sha512-UOwXU6ulg3VQsSyKf0QAVcW4EFq3hFehFHV/ne76iQ9FAw4ZpXHXsmw8AwUueGI13y4apVML/Pb+njilLn/RCw==} @@ -2151,6 +2157,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2545,14 +2552,14 @@ packages: micromark@2.11.4: resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} - micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - micromatch@4.0.7: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2979,6 +2986,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.1: + resolution: {integrity: sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==} + engines: {node: '>= 14.16.0'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3041,6 +3052,7 @@ packages: rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rollup@4.17.1: @@ -4294,6 +4306,8 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/picomatch@3.0.1': {} + '@types/prop-types@15.7.12': optional: true @@ -4723,10 +4737,6 @@ snapshots: dependencies: balanced-match: 1.0.2 - braces@3.0.2: - dependencies: - fill-range: 7.0.1 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -4799,7 +4809,7 @@ snapshots: chokidar@3.6.0: dependencies: anymatch: 3.1.3 - braces: 3.0.2 + braces: 3.0.3 glob-parent: 5.1.2 is-binary-path: 2.1.0 is-glob: 4.0.3 @@ -4808,6 +4818,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.1: + dependencies: + readdirp: 4.0.1 + ci-info@3.8.0: {} ci-info@4.0.0: {} @@ -5502,7 +5516,7 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 + micromatch: 4.0.7 fast-json-stable-stringify@2.1.0: {} @@ -5538,10 +5552,6 @@ snapshots: is-object: 1.0.2 merge-descriptors: 1.0.3 - fill-range@7.0.1: - dependencies: - to-regex-range: 5.0.1 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -6001,7 +6011,7 @@ snapshots: '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 @@ -6095,7 +6105,7 @@ snapshots: execa: 8.0.1 lilconfig: 3.1.2 listr2: 8.2.3 - micromatch: 4.0.7 + micromatch: 4.0.8 pidtree: 0.6.0 string-argv: 0.3.2 yaml: 2.4.5 @@ -6234,12 +6244,12 @@ snapshots: transitivePeerDependencies: - supports-color - micromatch@4.0.5: + micromatch@4.0.7: dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 - micromatch@4.0.7: + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 @@ -6644,6 +6654,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.1: {} + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.11.0 @@ -7005,7 +7017,7 @@ snapshots: is-glob: 4.0.3 jiti: 1.21.0 lilconfig: 2.1.0 - micromatch: 4.0.5 + micromatch: 4.0.7 normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 diff --git a/src/watch/index.ts b/src/watch/index.ts index 826bce1c3..bc5ebfd13 100644 --- a/src/watch/index.ts +++ b/src/watch/index.ts @@ -4,6 +4,7 @@ import { constants as osConstants } from 'node:os'; import path from 'node:path'; import { command } from 'cleye'; import { watch } from 'chokidar'; +import picomatch from 'picomatch'; import { lightMagenta, lightGreen, yellow } from 'kolorist'; import { run } from '../run.js'; import { @@ -81,6 +82,22 @@ export const watchCommand = command({ const server = await createIpcServer(); + const isIgnoreMatch = picomatch([ + // Hidden directories like .git + '**/.*/**', + + // Hidden files (e.g. logs or temp files) + '**/.*', + + // 3rd party packages + '**/{node_modules,bower_components,vendor}/**', + + // allow for relative exclude patterns, e.g. --exclude ../directory + ...options.exclude.map(pattern => (pattern.startsWith('**') || path.isAbsolute(pattern) + ? pattern + : path.join(process.cwd(), pattern))), + ]); + server.on('data', (data) => { // Collect run-time dependencies to watch if ( @@ -97,7 +114,7 @@ export const watchCommand = command({ : data.path ); - if (path.isAbsolute(dependencyPath)) { + if (path.isAbsolute(dependencyPath) && !isIgnoreMatch(dependencyPath)) { watcher.add(dependencyPath); } } @@ -215,18 +232,7 @@ export const watchCommand = command({ { cwd: process.cwd(), ignoreInitial: true, - ignored: [ - // Hidden directories like .git - '**/.*/**', - - // Hidden files (e.g. logs or temp files) - '**/.*', - - // 3rd party packages - '**/{node_modules,bower_components,vendor}/**', - - ...options.exclude, - ], + ignored: file => isIgnoreMatch(file), ignorePermissionErrors: true, }, ).on('all', reRun); diff --git a/tests/specs/watch.ts b/tests/specs/watch.ts index 35bd4f7e8..66b0740f9 100644 --- a/tests/specs/watch.ts +++ b/tests/specs/watch.ts @@ -1,4 +1,5 @@ import { setTimeout } from 'node:timers/promises'; +import path from 'node:path'; import { testSuite, expect } from 'manten'; import { createFixture } from 'fs-fixture'; import stripAnsi from 'strip-ansi'; @@ -355,6 +356,82 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { expect(p.all).not.toMatch('fail'); expect(p.stderr).toBe(''); }, 10_000); + + test('with parent directory', async ({ onTestFail }) => { + const entryFile = 'process-directory/index.js'; + const fileA = 'file-a.js'; + const fileB = 'directory/file-b.js'; + const depA = 'node_modules/a/index.js'; + + await using fixtureGlob = await createFixture({ + [fileA]: 'export default "logA"', + [fileB]: 'export default "logB"', + [depA]: 'export default "logC"', + [entryFile]: ` + import valueA from '../${fileA}' + import valueB from '../${fileB}' + import valueC from '../${depA}' + console.log(valueA, valueB, valueC) + `.trim(), + }); + + const tsxProcess = tsx( + [ + 'watch', + '--clear-screen=false', + `--ignore=../${fileA}`, + '--ignore=abra.js', + '--exclude=../directory/*', + 'index.js', + ], + path.join(fixtureGlob.path, 'process-directory'), + ); + + onTestFail(async () => { + // If timed out, force kill process + if (tsxProcess.exitCode === null) { + console.log('Force killing hanging process\n\n'); + tsxProcess.kill(); + console.log({ + tsxProcess: await tsxProcess, + }); + } + }); + + const negativeSignal = 'fail'; + + await processInteract( + tsxProcess.stdout!, + [ + async (data) => { + if (data.includes(negativeSignal)) { + throw new Error('should not log ignored file'); + } + + if (data === 'logA logB logC\n') { + // These changes should not trigger a re-run + await Promise.all([ + fixtureGlob.writeFile(fileA, `export default "${negativeSignal}"`), + fixtureGlob.writeFile(fileB, `export default "${negativeSignal}"`), + fixtureGlob.writeFile(depA, `export default "${negativeSignal}"`), + ]); + + await setTimeout(1000); + fixtureGlob.writeFile(entryFile, 'console.log("TERMINATE")'); + return true; + } + }, + data => data === 'TERMINATE\n', + ], + 9000, + ); + + tsxProcess.kill(); + + const p = await tsxProcess; + expect(p.all).not.toMatch('fail'); + expect(p.stderr).toBe(''); + }, 10_000); }); }); }); From 29f53d471ca62c9f0c8373f6a11e6b2a197def31 Mon Sep 17 00:00:00 2001 From: Kingston Tam Date: Wed, 25 Sep 2024 10:28:48 +0000 Subject: [PATCH 2/7] Create separate watcher for include patterns to support glob includes --- package.json | 4 ++++ pnpm-lock.yaml | 22 ++++++++++++++++++++++ src/watch/index.ts | 47 ++++++++++++++++++++++++++++++++++------------ src/watch/utils.ts | 8 ++++++++ 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 8c3939b36..6907bdaa7 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,8 @@ "devDependencies": { "@ampproject/remapping": "^2.3.0", "@types/cross-spawn": "^6.0.6", + "@types/glob-parent": "^5.1.3", + "@types/is-glob": "^4.0.4", "@types/node": "^20.14.9", "@types/picomatch": "^3.0.1", "@types/split2": "^4.2.3", @@ -100,6 +102,8 @@ "fs-fixture": "^2.4.0", "fs-require": "^1.6.0", "get-node": "^15.0.1", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", "kolorist": "^1.8.0", "lint-staged": "^15.2.7", "lintroll": "^1.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a31602ae4..e0ce1173a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,12 @@ importers: '@types/cross-spawn': specifier: ^6.0.6 version: 6.0.6 + '@types/glob-parent': + specifier: ^5.1.3 + version: 5.1.3 + '@types/is-glob': + specifier: ^4.0.4 + version: 4.0.4 '@types/node': specifier: ^20.14.9 version: 20.14.9 @@ -67,6 +73,12 @@ importers: get-node: specifier: ^15.0.1 version: 15.0.1 + glob-parent: + specifier: ^6.0.2 + version: 6.0.2 + is-glob: + specifier: ^4.0.3 + version: 4.0.3 kolorist: specifier: ^1.8.0 version: 1.8.0 @@ -968,9 +980,15 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/glob-parent@5.1.3': + resolution: {integrity: sha512-p+NciRH8TRvrgISOCQ55CP+lktMmDpOXsp4spULIIz0L4aJ6G9zFX+N0UZ2xulmJRgaQLRxXIp4xHdL6YOQjDg==} + '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/is-glob@4.0.4': + resolution: {integrity: sha512-3mFBtIPQ0TQetKRDe94g8YrxJZxdMillMGegyv6zRBXvq4peRRhf2wLZ/Dl53emtTsC29dQQBwYvovS20yXpiQ==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -4271,8 +4289,12 @@ snapshots: '@types/estree@1.0.5': {} + '@types/glob-parent@5.1.3': {} + '@types/http-cache-semantics@4.0.4': {} + '@types/is-glob@4.0.4': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': diff --git a/src/watch/index.ts b/src/watch/index.ts index bc5ebfd13..91f52a9ec 100644 --- a/src/watch/index.ts +++ b/src/watch/index.ts @@ -6,6 +6,8 @@ import { command } from 'cleye'; import { watch } from 'chokidar'; import picomatch from 'picomatch'; import { lightMagenta, lightGreen, yellow } from 'kolorist'; +import globParent from 'glob-parent'; +import isGlob from 'is-glob'; import { run } from '../run.js'; import { removeArgvFlags, @@ -16,6 +18,7 @@ import { clearScreen, debounce, log, + resolveGlobPattern, } from './utils.js'; const flags = { @@ -82,7 +85,7 @@ export const watchCommand = command({ const server = await createIpcServer(); - const isIgnoreMatch = picomatch([ + const isDefaultIgnore = picomatch([ // Hidden directories like .git '**/.*/**', @@ -91,13 +94,14 @@ export const watchCommand = command({ // 3rd party packages '**/{node_modules,bower_components,vendor}/**', - - // allow for relative exclude patterns, e.g. --exclude ../directory - ...options.exclude.map(pattern => (pattern.startsWith('**') || path.isAbsolute(pattern) - ? pattern - : path.join(process.cwd(), pattern))), ]); + const resolvedIncludes = options.include.map(resolveGlobPattern); + const isOptionsInclude = picomatch(resolvedIncludes); + + const resolvedExcludes = options.exclude.map(resolveGlobPattern); + const isOptionsExclude = picomatch(resolvedExcludes); + server.on('data', (data) => { // Collect run-time dependencies to watch if ( @@ -114,7 +118,12 @@ export const watchCommand = command({ : data.path ); - if (path.isAbsolute(dependencyPath) && !isIgnoreMatch(dependencyPath)) { + if ( + path.isAbsolute(dependencyPath) + && !isOptionsInclude(dependencyPath) + && !isOptionsExclude(dependencyPath) + && !isDefaultIgnore(dependencyPath) + ) { watcher.add(dependencyPath); } } @@ -225,18 +234,32 @@ export const watchCommand = command({ * As an alternative, we watch cwd and all run-time dependencies */ const watcher = watch( - [ - ...argv._, - ...options.include, - ], + argv._, { cwd: process.cwd(), ignoreInitial: true, - ignored: file => isIgnoreMatch(file), ignorePermissionErrors: true, + // ignore all files that are by default ignored or explicitly excluded + ignored: file => isDefaultIgnore(file) || isOptionsExclude(file), }, ).on('all', reRun); + if (resolvedIncludes.length > 0) { + const globParents = resolvedIncludes.map(pattern => (isGlob(pattern) + ? globParent(pattern) + : pattern)); + + watch(globParents, { + cwd: process.cwd(), + ignoreInitial: true, + ignorePermissionErrors: true, + // ignore all files not in includes or explicitly excluded + // we need to make sure not to ignore directories otherwise chokidar won't check for it + ignored: file => !globParents.includes(file) + && (!isOptionsInclude(file) || isOptionsExclude(file)), + }).on('all', reRun); + } + // On "Return" key process.stdin.on('data', () => reRun('Return key')); }); diff --git a/src/watch/utils.ts b/src/watch/utils.ts index 852f74aa8..b245fdc66 100644 --- a/src/watch/utils.ts +++ b/src/watch/utils.ts @@ -1,3 +1,4 @@ +import path from 'node:path'; import { gray, lightCyan } from 'kolorist'; const currentTime = () => (new Date()).toLocaleTimeString(); @@ -29,3 +30,10 @@ export const debounce = void>( ); } as T; }; + +export const resolveGlobPattern = (pattern: string): string => { + if (path.isAbsolute(pattern)) { + return pattern; + } + return path.join(process.cwd(), pattern); +}; From 9112e93cd13a8541fd2e4672a05b267001607a84 Mon Sep 17 00:00:00 2001 From: Kingston Tam Date: Wed, 25 Sep 2024 10:33:01 +0000 Subject: [PATCH 3/7] Update documentation --- docs/watch-mode.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/watch-mode.md b/docs/watch-mode.md index 7e99718aa..5d5394417 100644 --- a/docs/watch-mode.md +++ b/docs/watch-mode.md @@ -23,10 +23,10 @@ By default, _tsx_ watches all imported files, except those in the following dire ## Ignoring files -To exclude specific files or directories from being watched, use the `--ignore` flag: +To exclude specific files from being watched, use the `--exclude` flag: ```sh -tsx watch --ignore ./ignore-me.js --ignore ./ignore-me-too.js ./file.ts +tsx watch --exclude ./ignore-me.js --exclude ./ignore-me-too.js ./file.ts ``` ### Using glob patterns @@ -34,7 +34,15 @@ tsx watch --ignore ./ignore-me.js --ignore ./ignore-me-too.js ./file.ts Glob patterns allow you to define a set of files or directories to ignore. To prevent your shell from expanding the glob patterns, wrap them in quotes: ```sh -tsx watch --ignore "./data/**/*" ./file.ts +tsx watch --exclude "./data/**/*" ./file.ts +``` + +## Including files to watch + +To include specific files or directories to watch, use the `--include` flag: + +```sh +tsx watch --include ./other-dep.txt --include "./other-deps/*" ./file.ts ``` ## Tips From 910604af3a633d5786c4c7dd48e4fbd46dc868c1 Mon Sep 17 00:00:00 2001 From: Kingston Tam Date: Thu, 26 Sep 2024 22:39:28 +0000 Subject: [PATCH 4/7] Use picomatch for matching glob parent to prevent cross-platform differences --- src/watch/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/watch/index.ts b/src/watch/index.ts index 91f52a9ec..a3bae56b5 100644 --- a/src/watch/index.ts +++ b/src/watch/index.ts @@ -249,13 +249,15 @@ export const watchCommand = command({ ? globParent(pattern) : pattern)); + const isGlobParent = picomatch(globParents); + watch(globParents, { cwd: process.cwd(), ignoreInitial: true, ignorePermissionErrors: true, // ignore all files not in includes or explicitly excluded // we need to make sure not to ignore directories otherwise chokidar won't check for it - ignored: file => !globParents.includes(file) + ignored: file => !isGlobParent(file) && (!isOptionsInclude(file) || isOptionsExclude(file)), }).on('all', reRun); } From 70b9d2e884d4a7fafb5b29ebe6deef255e6f8fa7 Mon Sep 17 00:00:00 2001 From: Kingston Date: Mon, 30 Sep 2024 13:10:42 +0200 Subject: [PATCH 5/7] Fix Windows compatability by using normalize-path to normalize path to UNIX paths --- package.json | 2 ++ pnpm-lock.yaml | 11 +++++++++++ src/watch/index.ts | 4 +--- src/watch/utils.ts | 5 +++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6907bdaa7..5ff3e3754 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@types/glob-parent": "^5.1.3", "@types/is-glob": "^4.0.4", "@types/node": "^20.14.9", + "@types/normalize-path": "^3.0.2", "@types/picomatch": "^3.0.1", "@types/split2": "^4.2.3", "append-transform": "^2.0.0", @@ -111,6 +112,7 @@ "manten": "^1.3.0", "memfs": "^4.9.3", "node-pty": "^1.0.0", + "normalize-path": "^3.0.0", "outdent": "^0.8.0", "picomatch": "^4.0.2", "pkgroll": "^2.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0ce1173a..fad811545 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: '@types/node': specifier: ^20.14.9 version: 20.14.9 + '@types/normalize-path': + specifier: ^3.0.2 + version: 3.0.2 '@types/picomatch': specifier: ^3.0.1 version: 3.0.1 @@ -100,6 +103,9 @@ importers: node-pty: specifier: ^1.0.0 version: 1.0.0 + normalize-path: + specifier: ^3.0.0 + version: 3.0.0 outdent: specifier: ^0.8.0 version: 0.8.0 @@ -1022,6 +1028,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/normalize-path@3.0.2': + resolution: {integrity: sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA==} + '@types/picomatch@3.0.1': resolution: {integrity: sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==} @@ -4328,6 +4337,8 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/normalize-path@3.0.2': {} + '@types/picomatch@3.0.1': {} '@types/prop-types@15.7.12': diff --git a/src/watch/index.ts b/src/watch/index.ts index a3bae56b5..91f52a9ec 100644 --- a/src/watch/index.ts +++ b/src/watch/index.ts @@ -249,15 +249,13 @@ export const watchCommand = command({ ? globParent(pattern) : pattern)); - const isGlobParent = picomatch(globParents); - watch(globParents, { cwd: process.cwd(), ignoreInitial: true, ignorePermissionErrors: true, // ignore all files not in includes or explicitly excluded // we need to make sure not to ignore directories otherwise chokidar won't check for it - ignored: file => !isGlobParent(file) + ignored: file => !globParents.includes(file) && (!isOptionsInclude(file) || isOptionsExclude(file)), }).on('all', reRun); } diff --git a/src/watch/utils.ts b/src/watch/utils.ts index b245fdc66..cc542518a 100644 --- a/src/watch/utils.ts +++ b/src/watch/utils.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import { gray, lightCyan } from 'kolorist'; +import normalizePath from 'normalize-path'; const currentTime = () => (new Date()).toLocaleTimeString(); @@ -33,7 +34,7 @@ export const debounce = void>( export const resolveGlobPattern = (pattern: string): string => { if (path.isAbsolute(pattern)) { - return pattern; + return normalizePath(pattern); } - return path.join(process.cwd(), pattern); + return normalizePath(path.join(process.cwd(), pattern)); }; From 2470fcad60745f88c120c550c0f71fe363f747cd Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Wed, 16 Oct 2024 10:23:32 +0900 Subject: [PATCH 6/7] wip --- src/watch/index.ts | 17 ++++++++++++----- tests/specs/watch.ts | 17 +++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/watch/index.ts b/src/watch/index.ts index 91f52a9ec..8629ade9e 100644 --- a/src/watch/index.ts +++ b/src/watch/index.ts @@ -245,9 +245,11 @@ export const watchCommand = command({ ).on('all', reRun); if (resolvedIncludes.length > 0) { - const globParents = resolvedIncludes.map(pattern => (isGlob(pattern) - ? globParent(pattern) - : pattern)); + const globParents = resolvedIncludes.map(pattern => ( + isGlob(pattern) + ? globParent(pattern) + : pattern + )); watch(globParents, { cwd: process.cwd(), @@ -255,8 +257,13 @@ export const watchCommand = command({ ignorePermissionErrors: true, // ignore all files not in includes or explicitly excluded // we need to make sure not to ignore directories otherwise chokidar won't check for it - ignored: file => !globParents.includes(file) - && (!isOptionsInclude(file) || isOptionsExclude(file)), + ignored: file => ( + !globParents.includes(file) + && ( + !isOptionsInclude(file) + || isOptionsExclude(file) + ) + ), }).on('all', reRun); } diff --git a/tests/specs/watch.ts b/tests/specs/watch.ts index 66b0740f9..acae26fa5 100644 --- a/tests/specs/watch.ts +++ b/tests/specs/watch.ts @@ -225,7 +225,8 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { describe('include', ({ test }) => { test('file path & glob', async () => { const entryFile = 'index.js'; - const fileA = 'file-a'; + // Watches hidden file + const fileA = '.file-a'; const fileB = 'directory/file-b'; await using fixture = await createFixture({ [entryFile]: ` @@ -363,7 +364,7 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { const fileB = 'directory/file-b.js'; const depA = 'node_modules/a/index.js'; - await using fixtureGlob = await createFixture({ + await using fixture = await createFixture({ [fileA]: 'export default "logA"', [fileB]: 'export default "logB"', [depA]: 'export default "logC"', @@ -380,11 +381,11 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { 'watch', '--clear-screen=false', `--ignore=../${fileA}`, - '--ignore=abra.js', + '--ignore=doesnt-exist.js', '--exclude=../directory/*', 'index.js', ], - path.join(fixtureGlob.path, 'process-directory'), + fixture.getPath('process-directory'), ); onTestFail(async () => { @@ -411,13 +412,13 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { if (data === 'logA logB logC\n') { // These changes should not trigger a re-run await Promise.all([ - fixtureGlob.writeFile(fileA, `export default "${negativeSignal}"`), - fixtureGlob.writeFile(fileB, `export default "${negativeSignal}"`), - fixtureGlob.writeFile(depA, `export default "${negativeSignal}"`), + fixture.writeFile(fileA, `export default "${negativeSignal}"`), + fixture.writeFile(fileB, `export default "${negativeSignal}"`), + fixture.writeFile(depA, `export default "${negativeSignal}"`), ]); await setTimeout(1000); - fixtureGlob.writeFile(entryFile, 'console.log("TERMINATE")'); + fixture.writeFile(entryFile, 'console.log("TERMINATE")'); return true; } }, From 5bb6d3ed52d3a771281c5bd0c02d77533e5d3872 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Wed, 16 Oct 2024 13:08:34 +0900 Subject: [PATCH 7/7] wip --- tests/specs/watch.ts | 56 ++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/tests/specs/watch.ts b/tests/specs/watch.ts index e4af07dbb..36abf2e5e 100644 --- a/tests/specs/watch.ts +++ b/tests/specs/watch.ts @@ -1,5 +1,4 @@ import { setTimeout } from 'node:timers/promises'; -import path from 'node:path'; import { testSuite, expect } from 'manten'; import { createFixture } from 'fs-fixture'; import stripAnsi from 'strip-ansi'; @@ -225,8 +224,7 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { describe('include', ({ test }) => { test('file path & glob', async () => { const entryFile = 'index.js'; - // Watches hidden file - const fileA = '.file-a'; + const fileA = '.file-a'; // Watches hidden files const fileB = 'directory/file-b'; await using fixture = await createFixture({ [entryFile]: ` @@ -363,7 +361,7 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { const fileB = 'directory/file-b.js'; const depA = 'node_modules/a/index.js'; - await using fixture = await createFixture({ + await using fixtureGlob = await createFixture({ [fileA]: 'export default "logA"', [fileB]: 'export default "logB"', [depA]: 'export default "logC"', @@ -380,11 +378,10 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { 'watch', '--clear-screen=false', `--ignore=../${fileA}`, - '--ignore=doesnt-exist.js', - '--exclude=../directory/*', + `--exclude=../${fileB}`, 'index.js', ], - fixture.getPath('process-directory'), + fixtureGlob.getPath('process-directory'), ); onTestFail(async () => { @@ -400,37 +397,36 @@ export default testSuite(async ({ describe }, { tsx }: NodeApis) => { const negativeSignal = 'fail'; - await processInteract( - tsxProcess.stdout!, - [ - async (data) => { - if (data.includes(negativeSignal)) { - throw new Error('should not log ignored file'); - } + await expect( + processInteract( + tsxProcess.stdout!, + [ + async (data) => { + if (data !== 'logA logB logC\n') { + return; + } - if (data === 'logA logB logC\n') { // These changes should not trigger a re-run await Promise.all([ - fixture.writeFile(fileA, `export default "${negativeSignal}"`), - fixture.writeFile(fileB, `export default "${negativeSignal}"`), - fixture.writeFile(depA, `export default "${negativeSignal}"`), + fixtureGlob.writeFile(fileA, `export default "${negativeSignal}"`), + fixtureGlob.writeFile(fileB, `export default "${negativeSignal}"`), + fixtureGlob.writeFile(depA, `export default "${negativeSignal}"`), ]); - - await setTimeout(1000); - fixture.writeFile(entryFile, 'console.log("TERMINATE")'); return true; - } - }, - data => data === 'TERMINATE\n', - ], - 9000, - ); + }, + (data) => { + if (data.includes(negativeSignal)) { + throw new Error('Unexpected re-run'); + } + }, + ], + 2000, + ), + ).rejects.toThrow('Timeout'); // Watch should not trigger tsxProcess.kill(); - const p = await tsxProcess; - expect(p.all).not.toMatch('fail'); - expect(p.stderr).toBe(''); + await tsxProcess; }, 10_000); }); });