Fix wildcard directories being dropped for "./"-prefixed includes with dot-directory excludes#4529
Fix wildcard directories being dropped for "./"-prefixed includes with dot-directory excludes#4529jansedlon wants to merge 3 commits into
Conversation
…h dot-directory excludes getWildcardDirectories combined each include spec with the config directory using NormalizeSlashes, which leaves "./" segments intact (strada uses normalizePath here). The unnormalized spec (e.g. "/proj/apps/web/./app/**/*.ts") was then tested against the config's exclude matcher, and dot-directory exclude patterns such as "**/.*/", "**/.*", or ".*" matched the leftover literal "." segment, silently discarding every "./"-prefixed include spec. A config hitting this combination ended up with zero wildcard directories, so rootFilesWatch had no globs and PossiblyMatchesFileName could never match a newly created file. Creating a file in such a project never triggered PendingReloadFileNames; the stale config never claimed the file, and default-project resolution assigned it to an ancestor project (parsed fresh from disk, so it did contain the file) or to the inferred project. The file was then served with the wrong compiler options (missing lib globals like Temporal, wrong paths, degraded auto-imports) until the server was restarted. Fixes microsoft#3733 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes an issue in internal/tsoptions where ./-prefixed include globs could be incorrectly treated as matching dot-directory exclude patterns (e.g. **/.*/) due to a leftover literal ./ path segment, causing wildcard watch directories to be dropped.
Changes:
- Normalize combined
CurrentDirectory+includespecs usingtspath.NormalizePath(instead ofNormalizeSlashes) before applying exclude matching. - Add a regression unit test covering
./-prefixed includes with dot-directory excludes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| internal/tsoptions/wildcarddirectories.go | Normalize include specs to avoid false matches against dot-directory exclude patterns. |
| internal/tsoptions/wildcarddirectories_test.go | Add regression test for ./-prefixed include + **/.*/ exclude behavior. |
|
@microsoft-github-policy-service agree |
…3733) Covers the end-to-end failure mode from the issue: a newly created file in a project whose tsconfig combines "./"-prefixed include specs with a dot-directory exclude must be assigned to that configured project once its didOpen/create watch event is processed, rather than falling back to an ancestor or inferred project. Both event orderings are covered since the original bug reproduced regardless of ordering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Can you point to the Strada code, if this is just a porting bug? |
|
It should be here https://github.com/microsoft/TypeScript/blob/4d4f005c8541e0255a9d8791205fdce326e462bc/src/compiler/commandLineParser.ts#L4133 https://github.com/microsoft/TypeScript/blob/4d4f005c8541e0255a9d8791205fdce326e462bc/src/compiler/path.ts#L722 let simplified = path.replace(/\/\.\//g, "/"); // strips "/./" segments
if (simplified.startsWith("./")) simplified = simplified.slice(2);So in strada, |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes #3733
Sorry it took me so long to get back to it. I employed Fable 5 to debug the issue. I came up with a hypothesis and i made it verify each claim it made so that it's not AI slop PR. If this is not the root cause, hopefully you'll find something to start from.
Here's the explanation (AI disclaimer):
Found the root cause — it's neither the 12k files nor the monorepo. The crux is the tsconfig:
./-prefixedincludeentries combined with a dot-directoryexcludepattern ("**/.*/"). Two small tsconfigs and one source file reproduce it deterministically.Minimal repro
tsconfig.json(workspace root):
json { "compilerOptions": { "target": "ES2015", "lib": ["DOM"], "noEmit": true }, "include": ["apps"] } apps/web/tsconfig.json:
json { "compilerOptions": { "target": "ESNext", "lib": ["ESNext"], "noEmit": true }, "include": ["./app/**/*.ts"], "exclude": ["**/.*/"] } apps/web/app/existing.ts: any content.Open
existing.ts(loads theapps/webproject), then createapps/web/app/subdir/new_file.tscontainingconst b = Temporal.→ completions return 0 items and diagnostics showCannot find namespace 'Temporal'. It never recovers (verified by polling completions for 10s after thedidChangeWatchedFilescreate event); restarting the server fixes it. The server log shows the misassignment directly:
computeConfigFileName:: File: .../apps/web/app/subdir/new_file.ts:: Result: .../apps/web/tsconfig.json Found default configured project for .../apps/web/app/subdir/new_file.ts: .../tsconfig.json ← root project! It reproduces regardless of the ordering of
didOpenvs. the file-create watch event, so it's not the open-before-watch race — created-file detection is simply dead for these configs.Root cause
In
getWildcardDirectories(internal/tsoptions/wildcarddirectories.go), each include spec is made absolute withtspath.NormalizeSlashes(tspath.CombinePaths(configDir, spec)). Strada usesnormalizePath(combinePaths(...))here, which collapses./segments;NormalizeSlashesdoes not. So"./app/**/*.ts"becomes/…/apps/web/./app/**/*.ts— with a literal.path segment left in.Each include spec is then tested against the config's exclude matcher (excluded specs aren't watched).
"**/.*/"means "any path with a segment starting with." — and the leftover literal.segment matches it. Since every./-prefixed include spec matches, the config ends up with zero wildcard directories. ("**/.*"and".*"excludes trigger it identically; includes without the./prefix are unaffected.)With no wildcard directories,
PossiblyMatchesFileNamehas no globs, so when the Created watch event arrives, the config is never markedPendingReloadFileNamesand its cached file list never learns the new file exists. When the file is opened, the nearest config (correctly computed) doesn't contain it, so default-project resolution walks up and the root project — parsed fresh from disk after the file was created — claims it. The file is then served with the root project's options (lib: ["DOM"],target: ES2015, nopaths): noTemporal, wrong globals, and degraded auto-imports since the file isn't in the project that knows the app's sources and path aliases. If no ancestor config covers the file at all, it lands in the inferred project instead (which matches the original screenshot where only node_modules packages were suggested). Only a restart (full re-parse) recovers.The bug has been present since file watching was introduced (#806).
Fix
One line:
NormalizeSlashes→NormalizePathingetWildcardDirectories, matching strada. Verified: wildcard directories survive, the create event triggers a file-name reload, and the new file is assigned to the nested project immediately (both event orderings). Dot-directory exclusion semantics are unchanged (a file inapp/.hidden/is still excluded from the program).PR incoming with the fix plus regression tests (a unit test for
getWildcardDirectoriesand a project-level test covering both event orderings).