Extension Version
Latest (post #3516, merged Apr 23 2026) — e.g. 0.20260424.x or later
VS Code Version
1.100+
Operating system Version
macOS (but the absolute-path problem affects all platforms)
Steps to reproduce
- Commit a
.vscode/settings.json that includes "typescript.native-preview.tsdk": "./node_modules/@typescript/native-preview" (relative path, so it works for the whole team regardless of machine)
- Open the workspace in VS Code with the TypeScript Native Preview extension installed
- See the prompt: "This workspace contains a TypeScript Native Preview version (x.y.z). Would you like to use the workspace version?"
- Click Allow
- Check
git status — .vscode/settings.json is now dirty
Issue
Expected: The extension recognises the pre-existing relative tsdk path and either skips the prompt or, if the user confirms, leaves the setting value unchanged (relative paths committed to source control must stay relative so they work on every developer's machine).
Actual: Clicking Allow overwrites the relative path with a machine-specific absolute path, e.g.:
"typescript.native-preview.tsdk": "/Users/alice/projects/myrepo/node_modules/@typescript/native-preview"
This dirtied file cannot be committed — it would break the setting for every other team member. The prompt also re-fires on every workspace open because the extension never marks it as "already opted in" when the tsdk setting is set via a relative path.
Contrast with the built-in extension: "js/ts.tsdk.path": "./node_modules/typescript/lib" has worked reliably with this committed-settings workflow for years and does not exhibit this problem.
Root cause analysis
⚠️ Disclaimer: the following analysis was assisted by AI (Claude Sonnet 4.5) and may contain inaccuracies. It is provided as a starting point for investigation, not as a definitive diagnosis.
Bug 1 — promptUseWorkspaceVersion writes an absolute path when the user clicks Allow
In _extension/src/session.ts, findWorkspaceNativePreviewPackages() constructs tsdkPath using vscode.Uri.joinPath(folder.uri, ...).fsPath, which is always an absolute filesystem path:
// _extension/src/session.ts – findWorkspaceNativePreviewPackages()
const packagePath = vscode.Uri.joinPath(folder.uri, "node_modules", "@typescript", "native-preview");
tsdkPath: path.normalize(packagePath.fsPath), // ← always absolute
When the user clicks Allow, that absolute value is written back to the workspace setting:
// _extension/src/session.ts – promptUseWorkspaceVersion()
await config.update("tsdk", wsVersion.tsdkPath, vscode.ConfigurationTarget.Workspace);
// ↑ writes e.g. "/Users/alice/projects/myrepo/node_modules/@typescript/native-preview"
Bug 2 — The prompt fires even when tsdk is already set and resolves to the same package
The prompt checks only whether useWorkspaceTsdkStorageKey (in workspaceState) is true; it does not check whether the tsdk setting already resolves to a valid local install. So a developer who already has "./node_modules/@typescript/native-preview" in their committed settings still sees the prompt every time and risks dirtying the file.
How the built-in TypeScript extension avoids both problems
The built-in extension (extensions/typescript-language-features) uses two design choices:
-
pathLabel separation: TypeScriptVersion stores both the resolved absolute path (used for execution) and the original _pathLabel (used for config writes). The version picker writes version.pathLabel, so a relative path stays relative.
-
The prompt does not write to the setting: promptUseWorkspaceTsdk in versionManager.ts only calls workspaceState.update(useWorkspaceTsdkStorageKey, true) — it never calls config.update(). The in-memory active version is updated; the setting is left untouched.
// extensions/typescript-language-features/src/tsServer/versionManager.ts
if (result === allowIt) {
await this.workspaceState.update(useWorkspaceTsdkStorageKey, true);
this.updateActiveVersion(workspaceVersion); // ← no config.update()
}
Suggested fixes
Fix 1 — Don't write to the setting in promptUseWorkspaceVersion
Mirror the built-in extension: when the user clicks Allow, update workspaceState but do not call config.update(). The relative path already committed in .vscode/settings.json is sufficient.
Fix 2 — Treat a pre-existing tsdk that resolves to a local install as already opted-in
Before showing the prompt, check whether the current tsdk setting (after relative-path resolution) already points to the same package that was detected. If so, skip the prompt.
Fix 3 — When the version picker must write back, prefer a relative path
If the selected version lives inside a workspace folder, compute path.relative(folder.uri.fsPath, absoluteTsdkPath) before calling config.update(). This mirrors how pathLabel works in the built-in extension.
Extension Version
Latest (post #3516, merged Apr 23 2026) — e.g. 0.20260424.x or later
VS Code Version
1.100+
Operating system Version
macOS (but the absolute-path problem affects all platforms)
Steps to reproduce
.vscode/settings.jsonthat includes"typescript.native-preview.tsdk": "./node_modules/@typescript/native-preview"(relative path, so it works for the whole team regardless of machine)git status—.vscode/settings.jsonis now dirtyIssue
Expected: The extension recognises the pre-existing relative
tsdkpath and either skips the prompt or, if the user confirms, leaves the setting value unchanged (relative paths committed to source control must stay relative so they work on every developer's machine).Actual: Clicking Allow overwrites the relative path with a machine-specific absolute path, e.g.:
This dirtied file cannot be committed — it would break the setting for every other team member. The prompt also re-fires on every workspace open because the extension never marks it as "already opted in" when the
tsdksetting is set via a relative path.Contrast with the built-in extension:
"js/ts.tsdk.path": "./node_modules/typescript/lib"has worked reliably with this committed-settings workflow for years and does not exhibit this problem.Root cause analysis
Bug 1 —
promptUseWorkspaceVersionwrites an absolute path when the user clicks AllowIn
_extension/src/session.ts,findWorkspaceNativePreviewPackages()constructstsdkPathusingvscode.Uri.joinPath(folder.uri, ...).fsPath, which is always an absolute filesystem path:When the user clicks Allow, that absolute value is written back to the workspace setting:
Bug 2 — The prompt fires even when
tsdkis already set and resolves to the same packageThe prompt checks only whether
useWorkspaceTsdkStorageKey(inworkspaceState) istrue; it does not check whether thetsdksetting already resolves to a valid local install. So a developer who already has"./node_modules/@typescript/native-preview"in their committed settings still sees the prompt every time and risks dirtying the file.How the built-in TypeScript extension avoids both problems
The built-in extension (
extensions/typescript-language-features) uses two design choices:pathLabelseparation:TypeScriptVersionstores both the resolved absolutepath(used for execution) and the original_pathLabel(used for config writes). The version picker writesversion.pathLabel, so a relative path stays relative.The prompt does not write to the setting:
promptUseWorkspaceTsdkinversionManager.tsonly callsworkspaceState.update(useWorkspaceTsdkStorageKey, true)— it never callsconfig.update(). The in-memory active version is updated; the setting is left untouched.Suggested fixes
Fix 1 — Don't write to the setting in
promptUseWorkspaceVersionMirror the built-in extension: when the user clicks Allow, update
workspaceStatebut do not callconfig.update(). The relative path already committed in.vscode/settings.jsonis sufficient.Fix 2 — Treat a pre-existing
tsdkthat resolves to a local install as already opted-inBefore showing the prompt, check whether the current
tsdksetting (after relative-path resolution) already points to the same package that was detected. If so, skip the prompt.Fix 3 — When the version picker must write back, prefer a relative path
If the selected version lives inside a workspace folder, compute
path.relative(folder.uri.fsPath, absoluteTsdkPath)before callingconfig.update(). This mirrors howpathLabelworks in the built-in extension.