Skip to content

Fix wildcard directories being dropped for "./"-prefixed includes with dot-directory excludes#4529

Open
jansedlon wants to merge 3 commits into
microsoft:mainfrom
jansedlon:main
Open

Fix wildcard directories being dropped for "./"-prefixed includes with dot-directory excludes#4529
jansedlon wants to merge 3 commits into
microsoft:mainfrom
jansedlon:main

Conversation

@jansedlon

Copy link
Copy Markdown

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: ./-prefixed include entries combined with a dot-directory exclude pattern ("**/.*/"). 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 the apps/web project), then create apps/web/app/subdir/new_file.ts containing const b = Temporal. → completions return 0 items and diagnostics show Cannot find namespace 'Temporal'. It never recovers (verified by polling completions for 10s after the didChangeWatchedFiles create 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 didOpen vs. 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 with tspath.NormalizeSlashes(tspath.CombinePaths(configDir, spec)). Strada uses normalizePath(combinePaths(...)) here, which collapses ./ segments; NormalizeSlashes does 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, PossiblyMatchesFileName has no globs, so when the Created watch event arrives, the config is never marked PendingReloadFileNames and 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, no paths): no Temporal, 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: NormalizeSlashesNormalizePath in getWildcardDirectories, 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 in app/.hidden/ is still excluded from the program).

PR incoming with the fix plus regression tests (a unit test for getWildcardDirectories and a project-level test covering both event orderings).

…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>
Copilot AI review requested due to automatic review settings July 3, 2026 07:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + include specs using tspath.NormalizePath (instead of NormalizeSlashes) 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.

Comment thread internal/tsoptions/wildcarddirectories_test.go
@jansedlon

Copy link
Copy Markdown
Author

@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>
@jakebailey

Copy link
Copy Markdown
Member

Can you point to the Strada code, if this is just a porting bug?

@jansedlon

Copy link
Copy Markdown
Author

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 normalizePath delegates to simpleNormalizePath, which contains the exact mechanism that prevents the bug:

let simplified = path.replace(/\/\.\//g, "/");   // strips "/./" segments
if (simplified.startsWith("./")) simplified = simplified.slice(2);

So in strada, /…/apps/web/./app/**/*.ts becomes /…/apps/web/app/**/*.ts before the exclude regex tests it — the phantom . segment that **/.*/ matches never exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No autocomplete for local symbols and some global types when creating a new file

3 participants