Skip to content

Commit f110caa

Browse files
committed
fix(typescript): gate temp outDir cleanup to non-watch; cleanup in closeWatcher; harden outDir exclusion\n\n- Clean temp outDir only when not watching; perform final cleanup in closeWatcher to avoid watch-mode churn\n- Robust containment check for excluding outDir using drive-root equality + path.relative + !isAbsolute (handles Windows cross-drive)\n- When filterRoot=false, use CWD fallback for containment to avoid over-excluding sources (e.g., outDir='.')\n- Docs: clarify cleanup timing in README (non-watch vs watch)
1 parent 777d3bc commit f110caa

File tree

3 files changed

+34
-11
lines changed

3 files changed

+34
-11
lines changed

packages/typescript/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Default: `null`
7979

8080
A [picomatch pattern](https://github.com/micromatch/picomatch), or array of patterns, which specifies the files in the build the plugin should operate on. By default all `.ts` and `.tsx` files are targeted. If your `tsconfig.json` (or plugin `compilerOptions`) sets `allowJs: true`, the default include expands to also cover `.js`, `.jsx`, `.mjs`, and `.cjs` files so that JavaScript sources are downleveled by TypeScript as well.
8181

82-
> Note: When `allowJs` is enabled and no `outDir` is configured, this plugin creates a temporary output directory for TypeScript emit to avoid TS5055 (overwriting input files). The directory is excluded from processing and removed after the build.
82+
> Note: When `allowJs` is enabled and no `outDir` is configured, this plugin creates a temporary output directory for TypeScript emit to avoid TS5055 (overwriting input files). The directory is excluded from processing and removed after the build in non‑watch runs, and when the watcher stops in watch mode.
8383
8484
### `filterRoot`
8585

packages/typescript/src/index.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,26 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
6969
const effectiveOutDir = parsedOptions.options.outDir
7070
? path.resolve(parsedOptions.options.outDir)
7171
: null;
72-
const filterBase = (filterRoot ?? parsedOptions.options.rootDir) || null;
72+
// Determine the base directory used for containment checks. When
73+
// `filterRoot === false`, Rollup's pattern resolver does not resolve
74+
// include/exclude patterns against a directory. In that edge case,
75+
// use CWD as a sensible fallback to avoid over-excluding sources
76+
// (e.g., when outDir='.'), while still preventing feedback loops.
77+
const willResolvePatterns = filterRoot !== false;
78+
const filterBase = willResolvePatterns ? filterRoot ?? parsedOptions.options.rootDir : null;
7379
const filterBaseAbs = filterBase ? path.resolve(filterBase) : null;
7480
if (effectiveOutDir) {
75-
// Avoid excluding sources: skip when the filter base (rootDir) lives inside outDir
76-
// Use path.relative for a robust cross-platform containment check.
77-
const outDirContainsFilterBase = filterBaseAbs
78-
? (() => {
79-
const rel = path.relative(effectiveOutDir, filterBaseAbs);
80-
// rel === '' -> same dir; rel starts with '..' -> outside
81-
return rel === '' || !(rel === '..' || rel.startsWith(`..${path.sep}`));
82-
})()
83-
: false;
81+
// Avoid excluding sources: skip when the filter base (rootDir) lives inside outDir.
82+
// Use a robust cross-platform containment check that handles Windows
83+
// different-drive cases where path.relative may return an absolute path.
84+
const baseForContainment = filterBaseAbs ?? process.cwd();
85+
const outDirContainsFilterBase = (() => {
86+
// Different roots (e.g., Windows drive letters) can never be parent/child
87+
if (path.parse(effectiveOutDir).root !== path.parse(baseForContainment).root) return false;
88+
const rel = path.relative(effectiveOutDir, baseForContainment);
89+
// rel === '' => same dir; if absolute or starts with '..', it's outside
90+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
91+
})();
8492
if (!outDirContainsFilterBase) {
8593
filterExclude.push(normalizePath(path.join(effectiveOutDir, '**')));
8694
}
@@ -146,7 +154,20 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
146154
// ESLint doesn't understand optional chaining
147155
// eslint-disable-next-line
148156
program?.close();
157+
if (autoOutDir) {
158+
try {
159+
fs.rmSync(autoOutDir, { recursive: true, force: true });
160+
} catch {
161+
// ignore cleanup failures
162+
}
163+
autoOutDir = null;
164+
}
149165
}
166+
},
167+
168+
// Ensure we clean up any auto-created outDir exactly once when a watch
169+
// session ends, avoiding churn during incremental rebuilds.
170+
closeWatcher() {
150171
if (autoOutDir) {
151172
try {
152173
fs.rmSync(autoOutDir, { recursive: true, force: true });

packages/typescript/test/fixtures/incremental-watch-off/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ const answer: AnswerToQuestion = '42';
44

55
// eslint-disable-next-line no-console
66
console.log(`the answer is ${answer}`);
7+
8+
export const REBUILD_WITH_WATCH_OFF = 1;

0 commit comments

Comments
 (0)