From 0d482b5f1529d4048bdc66f0769ba0efb01bfa39 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 16 Sep 2026 16:10:13 -0700 Subject: [PATCH 1/3] In LSP, only load content mappers resolved within the workspace directory --- .../diagnosticMessages.generated.json | 1 + .../diagnostics/diagnosticMessages.json | 4 + .../diagnostics/diagnostics_generated.go | 3 + tsc/internal/fourslash/fourslash.go | 3 + tsc/internal/lsp/server.go | 20 +++ tsc/internal/lsp/server_contentmapper_test.go | 1 + tsc/internal/lsp/server_test.go | 33 +++++ .../project/configfileregistrybuilder.go | 4 +- tsc/internal/project/contentmapper_test.go | 125 ++++++++++++++++++ tsc/internal/project/session.go | 7 +- tsc/internal/project/snapshotfs.go | 26 +++- tsc/internal/tsoptions/contentmappers.go | 12 ++ 12 files changed, 230 insertions(+), 9 deletions(-) diff --git a/tsc/internal/diagnostics/diagnosticMessages.generated.json b/tsc/internal/diagnostics/diagnosticMessages.generated.json index 52c3ab10ac45e..27324cf0958bd 100644 --- a/tsc/internal/diagnostics/diagnosticMessages.generated.json +++ b/tsc/internal/diagnostics/diagnosticMessages.generated.json @@ -1951,6 +1951,7 @@ "Source_phase_imports_are_not_allowed_on_statements_that_compile_to_CommonJS_require_calls_18115": "Source phase imports are not allowed on statements that compile to CommonJS 'require' calls.", "This_initializer_has_a_Symbol_asyncDispose_method_Did_you_mean_to_use_await_using_18116": "This initializer has a '[Symbol.asyncDispose]()' method. Did you mean to use 'await using'?", "A_deferred_import_must_specify_a_namespace_binding_18117": "A deferred import must specify a namespace binding.", + "The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded_18118": "The content mapper package '{0}' is outside the open workspace and will not be loaded.", "nodenext_if_module_is_nodenext_node16_if_module_is_node16_or_node18_otherwise_bundler_69010": "`nodenext` if `module` is `nodenext`; `node16` if `module` is `node16` or `node18`; otherwise, `bundler`.", "File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module_80001": "File is a CommonJS module; it may be converted to an ES module.", "This_constructor_function_may_be_converted_to_a_class_declaration_80002": "This constructor function may be converted to a class declaration.", diff --git a/tsc/internal/diagnostics/diagnosticMessages.json b/tsc/internal/diagnostics/diagnosticMessages.json index 18e7048874771..dabd64fe37109 100644 --- a/tsc/internal/diagnostics/diagnosticMessages.json +++ b/tsc/internal/diagnostics/diagnosticMessages.json @@ -8728,6 +8728,10 @@ "category": "Error", "code": 18068 }, + "The content mapper package '{0}' is outside the open workspace and will not be loaded.": { + "category": "Error", + "code": 18118 + }, "The content mapper '{0}' failed to transform this file.": { "category": "Error", "code": 18069 diff --git a/tsc/internal/diagnostics/diagnostics_generated.go b/tsc/internal/diagnostics/diagnostics_generated.go index 9bca165033942..01b1dd6a3267d 100644 --- a/tsc/internal/diagnostics/diagnostics_generated.go +++ b/tsc/internal/diagnostics/diagnostics_generated.go @@ -3906,6 +3906,8 @@ var This_initializer_has_a_Symbol_asyncDispose_method_Did_you_mean_to_use_await_ var A_deferred_import_must_specify_a_namespace_binding = &Message{code: 18117, category: CategoryError, key: "A_deferred_import_must_specify_a_namespace_binding_18117", text: "A deferred import must specify a namespace binding."} +var The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded = &Message{code: 18118, category: CategoryError, key: "The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded_18118", text: "The content mapper package '{0}' is outside the open workspace and will not be loaded."} + var X_nodenext_if_module_is_nodenext_node16_if_module_is_node16_or_node18_otherwise_bundler = &Message{code: 69010, category: CategoryMessage, key: "nodenext_if_module_is_nodenext_node16_if_module_is_node16_or_node18_otherwise_bundler_69010", text: "`nodenext` if `module` is `nodenext`; `node16` if `module` is `node16` or `node18`; otherwise, `bundler`."} var File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module = &Message{code: 80001, category: CategorySuggestion, key: "File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module_80001", text: "File is a CommonJS module; it may be converted to an ES module."} @@ -6399,6 +6401,7 @@ var allMessages = [...]**Message{ &Source_phase_imports_are_not_allowed_on_statements_that_compile_to_CommonJS_require_calls, &This_initializer_has_a_Symbol_asyncDispose_method_Did_you_mean_to_use_await_using, &A_deferred_import_must_specify_a_namespace_binding, + &The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded, &X_nodenext_if_module_is_nodenext_node16_if_module_is_node16_or_node18_otherwise_bundler, &File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module, &This_constructor_function_may_be_converted_to_a_class_declaration, diff --git a/tsc/internal/fourslash/fourslash.go b/tsc/internal/fourslash/fourslash.go index 7c8fbe3191648..02420953fe9de 100644 --- a/tsc/internal/fourslash/fourslash.go +++ b/tsc/internal/fourslash/fourslash.go @@ -386,6 +386,9 @@ func (f *FourslashTest) initialize(t *testing.T, options *FourslashOptions) { InitializationOptions: initializationOptions, }, } + if options.RunExternalCode { + params.RootUri = lsproto.DocumentUriOrNull{DocumentUri: new(lsproto.DocumentUri("file:///"))} + } params.Capabilities = getCapabilitiesWithDefaults(options.Capabilities) f.capabilities = params.Capabilities resp, _, ok := f.client.SendRequest(t, lsproto.InitializeInfo, params) diff --git a/tsc/internal/lsp/server.go b/tsc/internal/lsp/server.go index dc90637966627..31299c6971a42 100644 --- a/tsc/internal/lsp/server.go +++ b/tsc/internal/lsp/server.go @@ -1744,6 +1744,8 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali s.logger.Logf("file watching: disabled (client lacks dynamic watch registration and builtin watcher backend is not fast-recursive)") } + workspaceRoots := getInitialWorkspaceRoots(s.initializeParams) + cwd := s.cwd if s.clientCapabilities.Workspace.WorkspaceFolders && s.initializeParams.WorkspaceFolders != nil && @@ -1777,6 +1779,7 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali DebounceDelay: 500 * time.Millisecond, PushDiagnosticsEnabled: !disablePushDiagnostics, RunExternalCode: runExternalCode, + InitialWorkspaceRoots: workspaceRoots, }, FS: s.fs, Logger: s.logger, @@ -1837,6 +1840,23 @@ func (s *Server) handleExit(ctx context.Context, _ lsproto.NoParams) error { return io.EOF } +func getInitialWorkspaceRoots(params *lsproto.InitializeParams) []tspath.RootedDirectoryPath { + workspaceRoots := []tspath.RootedDirectoryPath{} + if params.WorkspaceFolders != nil && params.WorkspaceFolders.WorkspaceFolders != nil { + for _, folder := range *params.WorkspaceFolders.WorkspaceFolders { + uri := lsproto.DocumentUri(folder.Uri) + if strings.HasPrefix(string(uri), "file://") { + workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPath(uri.Path())) + } + } + } else if params.RootUri.DocumentUri != nil && strings.HasPrefix(string(*params.RootUri.DocumentUri), "file://") { + workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPath(params.RootUri.DocumentUri.Path())) + } else if params.RootPath != nil && params.RootPath.String != nil && tspath.PathIsAbsolute(*params.RootPath.String) { + workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPath(tspath.RootedPathFromAbsolute(*params.RootPath.String))) + } + return workspaceRoots +} + func (s *Server) handleDidChangeWorkspaceConfiguration(ctx context.Context, params *lsproto.DidChangeConfigurationParams) error { if params.Settings == nil { return nil diff --git a/tsc/internal/lsp/server_contentmapper_test.go b/tsc/internal/lsp/server_contentmapper_test.go index d69572c77516a..2a8595d1c8da5 100644 --- a/tsc/internal/lsp/server_contentmapper_test.go +++ b/tsc/internal/lsp/server_contentmapper_test.go @@ -108,6 +108,7 @@ export const title = "Profile"; } initMsg, _, ok := client.SendRequest(t, lsproto.InitializeInfo, &lsproto.InitializeParams{ Capabilities: caps, + RootUri: lsproto.DocumentUriOrNull{DocumentUri: new(lsproto.DocumentUri("file:///home/project"))}, InitializationOptions: &lsproto.InitializationOptionsOrNull{InitializationOptions: &lsproto.InitializationOptions{ RunExternalCode: new(true), }}, diff --git a/tsc/internal/lsp/server_test.go b/tsc/internal/lsp/server_test.go index f31078c13993f..826bb41985d6b 100644 --- a/tsc/internal/lsp/server_test.go +++ b/tsc/internal/lsp/server_test.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" + "gotest.tools/v3/assert" ) type shutdownTestReader struct{} @@ -39,6 +40,38 @@ func (w *cancelingTestWriter) Write(msg *lsproto.Message) error { return nil } +func TestGetContentMapperWorkspaceRoots(t *testing.T) { + t.Parallel() + + t.Run("no workspace", func(t *testing.T) { + t.Parallel() + roots := getInitialWorkspaceRoots(&lsproto.InitializeParams{}) + assert.DeepEqual(t, roots, []tspath.RootedDirectoryPath{}) + }) + + t.Run("all file workspace folders", func(t *testing.T) { + t.Parallel() + folders := []*lsproto.WorkspaceFolder{ + {Uri: "file:///home/first", Name: "first"}, + {Uri: "untitled:ignored", Name: "ignored"}, + {Uri: "file:///home/second", Name: "second"}, + } + roots := getInitialWorkspaceRoots(&lsproto.InitializeParams{ + RootUri: lsproto.DocumentUriOrNull{DocumentUri: new(lsproto.DocumentUri("file:///fallback"))}, + WorkspaceFolders: &lsproto.WorkspaceFoldersOrNull{WorkspaceFolders: &folders}, + }) + assert.DeepEqual(t, roots, []tspath.RootedDirectoryPath{"/home/first", "/home/second"}) + }) + + t.Run("root URI fallback", func(t *testing.T) { + t.Parallel() + roots := getInitialWorkspaceRoots(&lsproto.InitializeParams{ + RootUri: lsproto.DocumentUriOrNull{DocumentUri: new(lsproto.DocumentUri("file:///home/root"))}, + }) + assert.DeepEqual(t, roots, []tspath.RootedDirectoryPath{"/home/root"}) + }) +} + // TestServerShutdownNoDeadlock verifies that operations after shutdown // don't block. func TestServerShutdownNoDeadlock(t *testing.T) { diff --git a/tsc/internal/project/configfileregistrybuilder.go b/tsc/internal/project/configfileregistrybuilder.go index df1bb93aa8a42..1d59eeab6ed05 100644 --- a/tsc/internal/project/configfileregistrybuilder.go +++ b/tsc/internal/project/configfileregistrybuilder.go @@ -48,9 +48,11 @@ func newConfigFileRegistryBuilder( customConfigFileName string, logger *logging.LogTree, ) *configFileRegistryBuilder { + sourceFS := newSourceFS(false, fs) + sourceFS.contentMapperWorkspaceRoots = sessionOptions.InitialWorkspaceRoots return &configFileRegistryBuilder{ hasRelativePatternCapability: hasRelativePatternCapability, - fs: newSourceFS(false, fs), + fs: sourceFS, isOpenFile: isOpenFile, base: oldConfigFileRegistry, sessionOptions: sessionOptions, diff --git a/tsc/internal/project/contentmapper_test.go b/tsc/internal/project/contentmapper_test.go index db1459c760465..fa793b9ecac5e 100644 --- a/tsc/internal/project/contentmapper_test.go +++ b/tsc/internal/project/contentmapper_test.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/bundled" "github.com/microsoft/TypeScript/tsc/internal/contentmapper" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/ls" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" @@ -294,6 +295,130 @@ func TestContentMapperInProject(t *testing.T) { }) } +func TestContentMapperPackageMustBeInWorkspace(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + projectDirectory string + workspaceRoots []string + mapperPath string + mapperTarget string + allowed bool + }{ + { + name: "trusted workspace", + projectDirectory: "/home/trusted", + workspaceRoots: []string{"/home/trusted"}, + mapperPath: "/home/trusted/node_modules/mapper/package.json", + allowed: true, + }, + { + name: "loose file outside workspace", + projectDirectory: "/home/untrusted", + workspaceRoots: []string{"/home/trusted"}, + mapperPath: "/home/untrusted/node_modules/mapper/package.json", + }, + { + name: "no workspace", + projectDirectory: "/home/project", + workspaceRoots: []string{}, + mapperPath: "/home/project/node_modules/mapper/package.json", + }, + { + name: "second multi-root workspace", + projectDirectory: "/home/second", + workspaceRoots: []string{"/home/first", "/home/second"}, + mapperPath: "/home/second/node_modules/mapper/package.json", + allowed: true, + }, + { + name: "trusted node_modules symlinked outside", + projectDirectory: "/home/trusted", + workspaceRoots: []string{"/home/trusted"}, + mapperPath: "/home/trusted/node_modules/mapper", + mapperTarget: "/home/outside/mapper", + allowed: true, + }, + { + name: "untrusted node_modules symlinked inside", + projectDirectory: "/home/untrusted", + workspaceRoots: []string{"/home/trusted"}, + mapperPath: "/home/untrusted/node_modules/mapper", + mapperTarget: "/home/trusted/mapper", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + configPath := test.projectDirectory + "/tsconfig.json" + mainPath := test.projectDirectory + "/main.ts" + boxPath := test.projectDirectory + "/app.box" + files := map[string]any{ + configPath: `{ + "compilerOptions": { "target": "es2020", "module": "esnext", "moduleResolution": "bundler" }, + "contentMappers": [ { "package": "mapper", "extensions": [".box"] } ] + }`, + mainPath: "import { version } from \"./app.box\";\nexport { version };\n", + boxPath: "export const version = #{target};\n", + } + if test.mapperTarget == "" { + files[test.mapperPath] = contentmappertest.PackageJSON(contentmappertest.TransformingMapper) + } else { + files[test.mapperPath] = vfstest.Symlink(test.mapperTarget) + files[test.mapperTarget+"/package.json"] = contentmappertest.PackageJSON(contentmappertest.TransformingMapper) + } + + workspaceRoots := make([]tspath.RootedDirectoryPath, len(test.workspaceRoots)) + for i, root := range test.workspaceRoots { + workspaceRoots[i] = tspath.RootedDirectoryPath(tspath.RootedPathFromAbsolute(root)) + } + currentDirectory := tspath.RootedDirectoryPath(tspath.RootedPathFromAbsolute(test.projectDirectory)) + boxFilePath := tspath.RootedFilePath(tspath.RootedPathFromAbsolute(boxPath)) + init, _ := projecttestutil.GetSessionInitOptions(files, &project.SessionOptions{ + CurrentDirectory: currentDirectory, + DefaultLibraryPath: bundled.LibPath(), + TypingsLocation: projecttestutil.TestTypingsLocation, + PositionEncoding: lsproto.PositionEncodingKindUTF8, + RunExternalCode: true, + InitialWorkspaceRoots: workspaceRoots, + }, nil) + spawner := &recordingContentMapperSpawner{inner: contentmappertest.NewSpawner()} + init.Spawner = spawner + session := project.NewSession(init) + defer session.Close() + + ctx := context.Background() + mainURI := lsproto.DocumentUri("file://" + mainPath) + session.DidOpenFile(ctx, mainURI, 1, files[mainPath].(string), lsproto.LanguageKindTypeScript) + languageService, err := session.GetLanguageService(ctx, mainURI) + assert.NilError(t, err) + + configuredProject := session.Snapshot().GetDefaultProject(mainURI) + assert.Assert(t, configuredProject != nil) + expectedMapperCount := 0 + if test.allowed { + expectedMapperCount = 1 + } + assert.Equal(t, len(configuredProject.CommandLine.ContentMappers()), expectedMapperCount) + if test.allowed { + boxFile := languageService.GetProgram().GetSourceFile(boxFilePath) + assert.Assert(t, boxFile != nil) + assert.Assert(t, strings.Contains(boxFile.Text(), "export const version = 7;")) + assert.Equal(t, spawner.spawns.Load(), int32(1)) + } else { + assert.Assert(t, languageService.GetProgram().GetSourceFile(boxFilePath) == nil) + assert.Equal(t, spawner.spawns.Load(), int32(0)) + assert.Assert(t, slices.ContainsFunc(configuredProject.CommandLine.GetConfigFileParsingDiagnostics(), func(diagnostic *ast.Diagnostic) bool { + return diagnostic.Code() == diagnostics.The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded.Code() + })) + } + }) + } +} + func TestContentMapperPackageManifestChangeReloadsConfig(t *testing.T) { t.Parallel() const packageJsonPath = "/home/mapper/package.json" diff --git a/tsc/internal/project/session.go b/tsc/internal/project/session.go index 9af28b85f6d05..e02dd45c3e842 100644 --- a/tsc/internal/project/session.go +++ b/tsc/internal/project/session.go @@ -76,9 +76,10 @@ type SessionOptions struct { PushDiagnosticsEnabled bool // RunExternalCode allows configured content mappers to run their (external) processes, // gated on workspace trust by the client. It corresponds to the --runExternalCode CLI flag. - RunExternalCode bool - DebounceDelay time.Duration - CheckerPoolOptions CheckerPoolOptions + RunExternalCode bool + InitialWorkspaceRoots []tspath.RootedDirectoryPath + DebounceDelay time.Duration + CheckerPoolOptions CheckerPoolOptions } type SessionInit struct { diff --git a/tsc/internal/project/snapshotfs.go b/tsc/internal/project/snapshotfs.go index 3e95fb3709231..756f7b8b84c6f 100644 --- a/tsc/internal/project/snapshotfs.go +++ b/tsc/internal/project/snapshotfs.go @@ -773,11 +773,12 @@ func (s *snapshotFSBuilder) convertOpenAndCloseToChanges(change FileChangeSummar // sourceFS is a vfs.FS that sources files from a FileSource and tracks seen files. type sourceFS struct { - tracking bool - missingDirectories *collections.SyncSet[tspath.PathKey] - seenFiles *collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] - source FileSource - caseSensitivity tspath.CaseSensitivity + tracking bool + missingDirectories *collections.SyncSet[tspath.PathKey] + seenFiles *collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] + source FileSource + caseSensitivity tspath.CaseSensitivity + contentMapperWorkspaceRoots []tspath.RootedDirectoryPath } func newSourceFS(tracking bool, source FileSource) *sourceFS { @@ -814,6 +815,21 @@ func (fs *sourceFS) SeenFile(path tspath.PathKey) bool { return ok } +func (fs *sourceFS) IsContentMapperPackageAllowed(packageDirectory tspath.RootedDirectoryPath) bool { + if fs.contentMapperWorkspaceRoots == nil { + return true + } + nodeModulesIndex := strings.LastIndex(packageDirectory.AsString(), "/node_modules/") + if nodeModulesIndex < 0 { + return false + } + nodeModulesDirectory := packageDirectory.AsString()[:nodeModulesIndex+len("/node_modules")] + nodeModulesPath := tspath.RootedDirectoryPath(tspath.RootedPath(nodeModulesDirectory)) + return slices.ContainsFunc(fs.contentMapperWorkspaceRoots, func(root tspath.RootedDirectoryPath) bool { + return fs.caseSensitivity.ContainsPath(root, nodeModulesPath.AsPath()) + }) +} + func (fs *sourceFS) SeenFileOrMissingParentDirectory(path tspath.PathKey) bool { if fs.seenFiles != nil { if _, ok := fs.seenFiles.Load(path); ok { diff --git a/tsc/internal/tsoptions/contentmappers.go b/tsc/internal/tsoptions/contentmappers.go index ab79a29e2af13..11adc42509a21 100644 --- a/tsc/internal/tsoptions/contentmappers.go +++ b/tsc/internal/tsoptions/contentmappers.go @@ -28,6 +28,10 @@ func (h *contentMapperResolutionHost) GetCurrentDirectory() tspath.RootedDirecto return h.currentDirectory } +type contentMapperPackagePolicy interface { + IsContentMapperPackageAllowed(packageDirectory tspath.RootedDirectoryPath) bool +} + func resolveContentMapperManifest(fs vfs.FS, containingFile tspath.RootedFilePath, packageName string) (contentmapper.Manifest, tspath.RootedDirectoryPath, *ast.Diagnostic) { resolver := module.NewResolver(module.ResolverOptions{ Host: &contentMapperResolutionHost{fs: fs, currentDirectory: containingFile.Directory()}, @@ -37,6 +41,14 @@ func resolveContentMapperManifest(fs vfs.FS, containingFile tspath.RootedFilePat if resolved == nil || resolved.ResolvedFileName == "" { return contentmapper.Manifest{}, "", ast.NewCompilerDiagnostic(diagnostics.The_content_mapper_package_0_could_not_be_resolved, packageName) } + logicalPackageDirectory := resolved.OriginalPath + if logicalPackageDirectory == "" { + logicalPackageDirectory = resolved.ResolvedFileName + } + logicalPackageDirectoryPath := tspath.RootedDirectoryPathFromPath(tspath.RootedPath(logicalPackageDirectory)) + if policy, ok := fs.(contentMapperPackagePolicy); ok && !policy.IsContentMapperPackageAllowed(logicalPackageDirectoryPath) { + return contentmapper.Manifest{}, "", ast.NewCompilerDiagnostic(diagnostics.The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded, packageName) + } packageDirectory := tspath.RootedDirectoryPathFromPath(tspath.RootedPath(resolved.ResolvedFileName)) packageJsonPath := packageDirectory.ResolveFile("package.json") From 4cd6440a32923a32eb17ec0d7ae56dd2d7939fb7 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 14 Oct 2026 10:56:00 -0700 Subject: [PATCH 2/3] Use typed paths for content mapper workspace checks Adapt the rebased workspace policy to the typed-path constructors and component splitting required by current main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tsc/internal/lsp/server.go | 6 +++--- tsc/internal/project/contentmapper_test.go | 6 +++--- tsc/internal/project/snapshotfs.go | 6 ++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tsc/internal/lsp/server.go b/tsc/internal/lsp/server.go index 31299c6971a42..d32af37a0544f 100644 --- a/tsc/internal/lsp/server.go +++ b/tsc/internal/lsp/server.go @@ -1846,13 +1846,13 @@ func getInitialWorkspaceRoots(params *lsproto.InitializeParams) []tspath.RootedD for _, folder := range *params.WorkspaceFolders.WorkspaceFolders { uri := lsproto.DocumentUri(folder.Uri) if strings.HasPrefix(string(uri), "file://") { - workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPath(uri.Path())) + workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPathFromPath(uri.Path())) } } } else if params.RootUri.DocumentUri != nil && strings.HasPrefix(string(*params.RootUri.DocumentUri), "file://") { - workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPath(params.RootUri.DocumentUri.Path())) + workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPathFromPath(params.RootUri.DocumentUri.Path())) } else if params.RootPath != nil && params.RootPath.String != nil && tspath.PathIsAbsolute(*params.RootPath.String) { - workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPath(tspath.RootedPathFromAbsolute(*params.RootPath.String))) + workspaceRoots = append(workspaceRoots, tspath.RootedDirectoryPathFromAbsolute(*params.RootPath.String)) } return workspaceRoots } diff --git a/tsc/internal/project/contentmapper_test.go b/tsc/internal/project/contentmapper_test.go index fa793b9ecac5e..95f2da159160a 100644 --- a/tsc/internal/project/contentmapper_test.go +++ b/tsc/internal/project/contentmapper_test.go @@ -373,10 +373,10 @@ func TestContentMapperPackageMustBeInWorkspace(t *testing.T) { workspaceRoots := make([]tspath.RootedDirectoryPath, len(test.workspaceRoots)) for i, root := range test.workspaceRoots { - workspaceRoots[i] = tspath.RootedDirectoryPath(tspath.RootedPathFromAbsolute(root)) + workspaceRoots[i] = tspath.RootedDirectoryPathFromAbsolute(root) } - currentDirectory := tspath.RootedDirectoryPath(tspath.RootedPathFromAbsolute(test.projectDirectory)) - boxFilePath := tspath.RootedFilePath(tspath.RootedPathFromAbsolute(boxPath)) + currentDirectory := tspath.RootedDirectoryPathFromAbsolute(test.projectDirectory) + boxFilePath := tspath.RootedFilePathFromAbsolute(boxPath) init, _ := projecttestutil.GetSessionInitOptions(files, &project.SessionOptions{ CurrentDirectory: currentDirectory, DefaultLibraryPath: bundled.LibPath(), diff --git a/tsc/internal/project/snapshotfs.go b/tsc/internal/project/snapshotfs.go index 756f7b8b84c6f..e406383870bc1 100644 --- a/tsc/internal/project/snapshotfs.go +++ b/tsc/internal/project/snapshotfs.go @@ -819,12 +819,10 @@ func (fs *sourceFS) IsContentMapperPackageAllowed(packageDirectory tspath.Rooted if fs.contentMapperWorkspaceRoots == nil { return true } - nodeModulesIndex := strings.LastIndex(packageDirectory.AsString(), "/node_modules/") - if nodeModulesIndex < 0 { + _, nodeModulesPath, ok := tspath.RootedFilePathFromPath(packageDirectory.AsPath()).SplitAtLastComponent("node_modules") + if !ok { return false } - nodeModulesDirectory := packageDirectory.AsString()[:nodeModulesIndex+len("/node_modules")] - nodeModulesPath := tspath.RootedDirectoryPath(tspath.RootedPath(nodeModulesDirectory)) return slices.ContainsFunc(fs.contentMapperWorkspaceRoots, func(root tspath.RootedDirectoryPath) bool { return fs.caseSensitivity.ContainsPath(root, nodeModulesPath.AsPath()) }) From 1b4346a7691a24f25c2922c1b1145bd3028d972e Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 15 Oct 2026 14:52:30 -0700 Subject: [PATCH 3/3] Put IsContentMapperPackageAllowed in a proper host interface --- tsc/internal/api/session.go | 4 +- tsc/internal/checker/checker_test.go | 10 +-- tsc/internal/compiler/host.go | 2 +- tsc/internal/compiler/program_test.go | 8 +- tsc/internal/execute/build/host.go | 2 +- tsc/internal/execute/tsc.go | 2 +- .../execute/tsc/extendedconfigcache_test.go | 8 +- .../execute/tsctests/tscbuild_test.go | 2 +- tsc/internal/execute/watcher.go | 2 +- tsc/internal/ls/findallreferences_test.go | 2 +- .../project/configfileregistrybuilder.go | 28 +++++-- .../project/extendedconfigcache_test.go | 2 +- tsc/internal/project/snapshotfs.go | 24 ++---- tsc/internal/testrunner/test_case_parser.go | 2 +- .../tsoptions/commandlineparser_test.go | 2 +- tsc/internal/tsoptions/contentmappers.go | 9 +-- tsc/internal/tsoptions/contentmappers_test.go | 76 +++++++++++++++++-- .../tsoptions/parsedcommandline_test.go | 12 +-- tsc/internal/tsoptions/tsconfigparsing.go | 47 +++++++++--- .../tsoptions/tsconfigparsing_test.go | 26 +++---- .../tsoptionstest/parsedcommandline.go | 2 +- 21 files changed, 178 insertions(+), 94 deletions(-) diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 93d6ab53c303d..20f13d4e89feb 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -2031,7 +2031,7 @@ func (s *Session) handleParseJsonConfigFileContent(ctx context.Context, params * parsedCommandLine := tsoptions.ParseJsonConfigFileContent( jsonValueToAny(params.JSON), - s.snapshotHost.FS(), + tsoptions.NewDefaultParseConfigHost(s.snapshotHost.FS()), basePath, nil, /*existingOptions*/ configFileName, @@ -2057,7 +2057,7 @@ func (s *Session) handleParseConfigFile(ctx context.Context, params *ParseConfig ) parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, - s.snapshotHost.FS(), + tsoptions.NewDefaultParseConfigHost(s.snapshotHost.FS()), configDir, nil, /*existingOptions*/ nil, /*existingOptionsRaw*/ diff --git a/tsc/internal/checker/checker_test.go b/tsc/internal/checker/checker_test.go index 31cea19751525..42ae368d35924 100644 --- a/tsc/internal/checker/checker_test.go +++ b/tsc/internal/checker/checker_test.go @@ -39,7 +39,7 @@ foo.bar;` fs = bundled.WrapFS(fs) host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, fs, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line") p := compiler.NewProgram(compiler.ProgramOptions{ @@ -87,7 +87,7 @@ export type E = D;`, host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, fs, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line") p := compiler.NewProgram(compiler.ProgramOptions{ @@ -121,7 +121,7 @@ func BenchmarkNewChecker(b *testing.B) { fs := bundled.WrapFS(osvfs.FS()) rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), &core.CompilerOptions{}, nil, fs, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") program := compiler.NewProgram(compiler.ProgramOptions{ Config: parsed, @@ -138,7 +138,7 @@ func BenchmarkCheck(b *testing.B) { fs := bundled.WrapFS(osvfs.FS()) rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), &core.CompilerOptions{}, nil, fs, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") program := compiler.NewProgram(compiler.ProgramOptions{ Config: parsed, @@ -161,7 +161,7 @@ func BenchmarkCheckFreshProgram(b *testing.B) { fs := bundled.WrapFS(osvfs.FS()) rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), &core.CompilerOptions{}, nil, fs, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") ctx := b.Context() diff --git a/tsc/internal/compiler/host.go b/tsc/internal/compiler/host.go index 6691eaf6ff70c..8ff15f75cce38 100644 --- a/tsc/internal/compiler/host.go +++ b/tsc/internal/compiler/host.go @@ -108,6 +108,6 @@ func (h *compilerHost) ContentMapperProject() contentmapper.Project { } func (h *compilerHost) GetResolvedProjectReference(fileName tspath.RootedFilePath, path tspath.PathKey) *tsoptions.ParsedCommandLine { - commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, h.fs, h.extendedConfigCache) + commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, tsoptions.NewDefaultParseConfigHost(h.fs), h.extendedConfigCache) return commandLine } diff --git a/tsc/internal/compiler/program_test.go b/tsc/internal/compiler/program_test.go index 20311ca1dafe2..4ef0537403903 100644 --- a/tsc/internal/compiler/program_test.go +++ b/tsc/internal/compiler/program_test.go @@ -103,7 +103,7 @@ import { value } from "./dep.js"; export const result = value;`, } fs := vfstest.FromMap(files, tspath.CaseSensitive) host := NewCompilerHost(fs, "/", nil, nil, nil) - config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", nil, nil, fs, nil) + config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", nil, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(t, len(diagnostics), 0) var pools, resolvers int tr := new(tracing.Tracing) @@ -209,7 +209,7 @@ func TestClonedProgramProjectReferenceResolution(t *testing.T) { host := NewCompilerHost(fs, "/", nil, nil, nil) config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", &core.CompilerOptions{ PreserveSymlinks: core.BoolToTristate(preserveSymlinks), - }, nil, fs, nil) + }, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(t, len(diagnostics), 0) p := NewProgram(ProgramOptions{Config: config, Host: host, UseSourceOfProjectReference: true}) assert.Assert(t, p.GetSourceFile("/reference/index.ts") != nil) @@ -500,7 +500,7 @@ func TestImportSourceProgram(t *testing.T) { } fs := vfstest.FromMap(files, tspath.CaseSensitive) host := NewCompilerHost(fs, "/", nil, nil, nil) - config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", nil, nil, fs, nil) + config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", nil, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(t, len(diagnostics), 0) program := NewProgram(ProgramOptions{Config: config, Host: host}) file := program.GetSourceFile("/src/index.ts") @@ -609,7 +609,7 @@ func BenchmarkNewProgram(b *testing.B) { rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) fs := bundled.WrapFS(osvfs.FS()) host := NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), nil, nil, fs, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(rootPath.ResolveFile("tsconfig.json"), nil, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") opts := ProgramOptions{ Config: parsed, diff --git a/tsc/internal/execute/build/host.go b/tsc/internal/execute/build/host.go index 41e30b1280122..5949d952d7043 100644 --- a/tsc/internal/execute/build/host.go +++ b/tsc/internal/execute/build/host.go @@ -73,7 +73,7 @@ func (h *host) GetResolvedProjectReference(fileName tspath.RootedFilePath, path wrapped.Set("compilerOptions", raw) commandLineRaw = wrapped } - commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, h.orchestrator.opts.Command.CompilerOptions, commandLineRaw, h.FS(), &h.extendedConfigCache) + commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, h.orchestrator.opts.Command.CompilerOptions, commandLineRaw, tsoptions.NewDefaultParseConfigHost(h.FS()), &h.extendedConfigCache) configTime := h.orchestrator.opts.Sys.Now().Sub(configStart) h.configTimes.Store(path, configTime) return commandLine diff --git a/tsc/internal/execute/tsc.go b/tsc/internal/execute/tsc.go index 4303ee466531d..c41e0bb25c921 100644 --- a/tsc/internal/execute/tsc.go +++ b/tsc/internal/execute/tsc.go @@ -216,7 +216,7 @@ func tscCompilation(ctx context.Context, sys tsc.System, commandLine *tsoptions. commandLineRaw = wrapped } path := sys.FS().CaseSensitivity().PathKey(tspath.RootedPath(configFileName)) - configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFilePath(configFileName, path, compilerOptionsFromCommandLine, commandLineRaw, sys.FS(), extendedConfigCache) + configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFilePath(configFileName, path, compilerOptionsFromCommandLine, commandLineRaw, tsoptions.NewDefaultParseConfigHost(sys.FS()), extendedConfigCache) compileTimes.ConfigTime = sys.Now().Sub(configStart) if len(errors) != 0 { // these are unrecoverable errors--exit to report them as diagnostics diff --git a/tsc/internal/execute/tsc/extendedconfigcache_test.go b/tsc/internal/execute/tsc/extendedconfigcache_test.go index 03ddb773b1305..50bd447bd92f7 100644 --- a/tsc/internal/execute/tsc/extendedconfigcache_test.go +++ b/tsc/internal/execute/tsc/extendedconfigcache_test.go @@ -26,7 +26,7 @@ func TestExtendedConfigCacheExtendsCircularity(t *testing.T) { fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := &tsc.ExtendedConfigCache{} - cmd, _ := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, fs, cache) + cmd, _ := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, tsoptions.NewDefaultParseConfigHost(fs), cache) if cmd == nil { t.Fatal("expected non-nil ParsedCommandLine") } @@ -46,7 +46,7 @@ func TestExtendedConfigCacheExtendsCircularity(t *testing.T) { fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := &tsc.ExtendedConfigCache{} - cmd, _ := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, fs, cache) + cmd, _ := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, tsoptions.NewDefaultParseConfigHost(fs), cache) if cmd == nil { t.Fatal("expected non-nil ParsedCommandLine") } @@ -67,7 +67,7 @@ func TestExtendedConfigCacheExtendsCircularity(t *testing.T) { fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := &tsc.ExtendedConfigCache{} - cmd, _ := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, fs, cache) + cmd, _ := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, tsoptions.NewDefaultParseConfigHost(fs), cache) if cmd == nil { t.Fatal("expected non-nil ParsedCommandLine") } @@ -86,7 +86,7 @@ func TestExtendedConfigCacheNullExtendsDoesNotPanic(t *testing.T) { fs := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) cache := &tsc.ExtendedConfigCache{} - cmd, _ := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, fs, cache) + cmd, _ := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, tsoptions.NewDefaultParseConfigHost(fs), cache) if cmd == nil { t.Fatal("expected non-nil ParsedCommandLine") } diff --git a/tsc/internal/execute/tsctests/tscbuild_test.go b/tsc/internal/execute/tsctests/tscbuild_test.go index 1e6c958bd396f..0a961e1a83ccd 100644 --- a/tsc/internal/execute/tsctests/tscbuild_test.go +++ b/tsc/internal/execute/tsctests/tscbuild_test.go @@ -2268,7 +2268,7 @@ func TestBuildOutputPaths(t *testing.T) { t.Run("GetOutputFileNames/"+s.subScenario, func(t *testing.T) { t.Parallel() sys := newTestSys(input, false) - config, _ := tsoptions.GetParsedCommandLineOfConfigFile("/home/src/workspaces/project/tsconfig.json", &core.CompilerOptions{}, nil, sys.FS(), nil) + config, _ := tsoptions.GetParsedCommandLineOfConfigFile("/home/src/workspaces/project/tsconfig.json", &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(sys.FS()), nil) assert.DeepEqual(t, slices.Collect(config.GetOutputFileNames()), s.expectedDtsNames) }) } diff --git a/tsc/internal/execute/watcher.go b/tsc/internal/execute/watcher.go index 880849560a782..aa1346ba8bba4 100644 --- a/tsc/internal/execute/watcher.go +++ b/tsc/internal/execute/watcher.go @@ -704,7 +704,7 @@ func (w *Watcher) recheckTsConfig(force bool) bool { func (w *Watcher) parseConfigFile() *tsoptions.ParsedCommandLine { extendedConfigCache := &tsc.ExtendedConfigCache{} - configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, w.compilerOptionsFromCommandLine, w.commandLineRaw, w.sys.FS(), extendedConfigCache) + configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, w.compilerOptionsFromCommandLine, w.commandLineRaw, tsoptions.NewDefaultParseConfigHost(w.sys.FS()), extendedConfigCache) if len(errors) > 0 { for _, e := range errors { w.reportDiagnostic(e) diff --git a/tsc/internal/ls/findallreferences_test.go b/tsc/internal/ls/findallreferences_test.go index 2065d683ab85f..1dd5cd5c5883b 100644 --- a/tsc/internal/ls/findallreferences_test.go +++ b/tsc/internal/ls/findallreferences_test.go @@ -52,7 +52,7 @@ func TestImplementationsWorklistDoesNotBlowUp(t *testing.T) { fs = bundled.WrapFS(fs) host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, fs, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) assert.Equal(t, len(errors), 0) program := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host}) program.BindSourceFiles() diff --git a/tsc/internal/project/configfileregistrybuilder.go b/tsc/internal/project/configfileregistrybuilder.go index 1d59eeab6ed05..3a76381512f4c 100644 --- a/tsc/internal/project/configfileregistrybuilder.go +++ b/tsc/internal/project/configfileregistrybuilder.go @@ -15,7 +15,10 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/vfs" ) -var _ tsoptions.ExtendedConfigCache = (*configFileRegistryBuilder)(nil) +var ( + _ tsoptions.ParseConfigHost = (*configFileRegistryBuilder)(nil) + _ tsoptions.ExtendedConfigCache = (*configFileRegistryBuilder)(nil) +) // configFileRegistryBuilder tracks changes made on top of a previous // configFileRegistry, producing a new clone with `finalize()` after @@ -48,11 +51,9 @@ func newConfigFileRegistryBuilder( customConfigFileName string, logger *logging.LogTree, ) *configFileRegistryBuilder { - sourceFS := newSourceFS(false, fs) - sourceFS.contentMapperWorkspaceRoots = sessionOptions.InitialWorkspaceRoots return &configFileRegistryBuilder{ hasRelativePatternCapability: hasRelativePatternCapability, - fs: sourceFS, + fs: newSourceFS(false, fs), isOpenFile: isOpenFile, base: oldConfigFileRegistry, sessionOptions: sessionOptions, @@ -157,7 +158,7 @@ func (c *configFileRegistryBuilder) reloadIfNeeded(entry *configFileEntry, fileN if c.sessionOptions.RunExternalCode { existingOptions = &core.CompilerOptions{RunExternalCode: core.TSTrue} } - entry.commandLine, _ = tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, existingOptions, nil /*optionsRaw*/, c.fs, c) + entry.commandLine, _ = tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, existingOptions, nil /*optionsRaw*/, c, c) c.updateExtendingConfigs(path, entry.commandLine, oldCommandLine) c.updateRootFilesWatch(fileName, entry) logger.Log("Finished loading config file") @@ -790,6 +791,23 @@ func (c *configFileRegistryBuilder) getAncestorConfigFileName(fileName tspath.Ro return result } +func (c *configFileRegistryBuilder) FS() vfs.FS { + return c.fs +} + +func (c *configFileRegistryBuilder) IsContentMapperPackageAllowed(packageDirectory tspath.RootedDirectoryPath) bool { + if c.sessionOptions.InitialWorkspaceRoots == nil { + return true + } + _, nodeModulesPath, ok := tspath.RootedFilePathFromPath(packageDirectory.AsPath()).SplitAtLastComponent("node_modules") + if !ok { + return false + } + return slices.ContainsFunc(c.sessionOptions.InitialWorkspaceRoots, func(root tspath.RootedDirectoryPath) bool { + return c.fs.CaseSensitivity().ContainsPath(root, nodeModulesPath.AsPath()) + }) +} + // GetExtendedConfig implements tsoptions.ExtendedConfigCache. func (c *configFileRegistryBuilder) GetExtendedConfig(fileName tspath.RootedFilePath, path tspath.PathKey, resolutionStack []tspath.PathKey, fs vfs.FS) *tsoptions.ExtendedConfigCacheEntry { var content string diff --git a/tsc/internal/project/extendedconfigcache_test.go b/tsc/internal/project/extendedconfigcache_test.go index 61fa5d68d111a..e42021743a0f3 100644 --- a/tsc/internal/project/extendedconfigcache_test.go +++ b/tsc/internal/project/extendedconfigcache_test.go @@ -203,7 +203,7 @@ func TestExtendedConfigCacheOwnership(t *testing.T) { fsFromMap := vfstest.FromMap(files, tspath.CaseInsensitive /*caseSensitivity*/) fs := bundled.WrapFS(fsFromMap) - cmd, diags := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, fs, nil /*extendedConfigCache*/) + cmd, diags := tsoptions.GetParsedCommandLineOfConfigFile("/project/tsconfig.json", nil, nil, tsoptions.NewDefaultParseConfigHost(fs), nil /*extendedConfigCache*/) assert.Equal(t, len(diags), 0) assert.Assert(t, cmd != nil) diff --git a/tsc/internal/project/snapshotfs.go b/tsc/internal/project/snapshotfs.go index e406383870bc1..3e95fb3709231 100644 --- a/tsc/internal/project/snapshotfs.go +++ b/tsc/internal/project/snapshotfs.go @@ -773,12 +773,11 @@ func (s *snapshotFSBuilder) convertOpenAndCloseToChanges(change FileChangeSummar // sourceFS is a vfs.FS that sources files from a FileSource and tracks seen files. type sourceFS struct { - tracking bool - missingDirectories *collections.SyncSet[tspath.PathKey] - seenFiles *collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] - source FileSource - caseSensitivity tspath.CaseSensitivity - contentMapperWorkspaceRoots []tspath.RootedDirectoryPath + tracking bool + missingDirectories *collections.SyncSet[tspath.PathKey] + seenFiles *collections.SyncMap[tspath.PathKey, tspath.RootedFilePath] + source FileSource + caseSensitivity tspath.CaseSensitivity } func newSourceFS(tracking bool, source FileSource) *sourceFS { @@ -815,19 +814,6 @@ func (fs *sourceFS) SeenFile(path tspath.PathKey) bool { return ok } -func (fs *sourceFS) IsContentMapperPackageAllowed(packageDirectory tspath.RootedDirectoryPath) bool { - if fs.contentMapperWorkspaceRoots == nil { - return true - } - _, nodeModulesPath, ok := tspath.RootedFilePathFromPath(packageDirectory.AsPath()).SplitAtLastComponent("node_modules") - if !ok { - return false - } - return slices.ContainsFunc(fs.contentMapperWorkspaceRoots, func(root tspath.RootedDirectoryPath) bool { - return fs.caseSensitivity.ContainsPath(root, nodeModulesPath.AsPath()) - }) -} - func (fs *sourceFS) SeenFileOrMissingParentDirectory(path tspath.PathKey) bool { if fs.seenFiles != nil { if _, ok := fs.seenFiles.Load(path); ok { diff --git a/tsc/internal/testrunner/test_case_parser.go b/tsc/internal/testrunner/test_case_parser.go index e5773e71e92f6..dfd3c5df996f2 100644 --- a/tsc/internal/testrunner/test_case_parser.go +++ b/tsc/internal/testrunner/test_case_parser.go @@ -94,7 +94,7 @@ func makeUnitsFromTest(code string, fileName string) testCaseContent { configDir := configFileName.Directory() tsConfig = tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, - configFS, + tsoptions.NewDefaultParseConfigHost(configFS), configDir, existingOptions, nil, /*existingOptionsRaw*/ diff --git a/tsc/internal/tsoptions/commandlineparser_test.go b/tsc/internal/tsoptions/commandlineparser_test.go index d2fc1bea1635b..2ac2b5eedce8a 100644 --- a/tsc/internal/tsoptions/commandlineparser_test.go +++ b/tsc/internal/tsoptions/commandlineparser_test.go @@ -196,7 +196,7 @@ func TestCustomConditionsNullOverride(t *testing.T) { "/project/tsconfig.json", cmdLine.CompilerOptions(), wrappedRaw, - fs, + tsoptions.NewDefaultParseConfigHost(fs), nil, ) diff --git a/tsc/internal/tsoptions/contentmappers.go b/tsc/internal/tsoptions/contentmappers.go index 7ff81da2d1cc5..e3d2a4bc04c10 100644 --- a/tsc/internal/tsoptions/contentmappers.go +++ b/tsc/internal/tsoptions/contentmappers.go @@ -32,11 +32,8 @@ func (h *contentMapperResolutionHost) GetCurrentDirectory() tspath.RootedDirecto return h.currentDirectory } -type contentMapperPackagePolicy interface { - IsContentMapperPackageAllowed(packageDirectory tspath.RootedDirectoryPath) bool -} - -func resolveContentMapperManifest(fs vfs.FS, containingFile tspath.RootedFilePath, packageName string) (contentmapper.Manifest, tspath.RootedDirectoryPath, *ast.Diagnostic) { +func resolveContentMapperManifest(host ParseConfigHost, containingFile tspath.RootedFilePath, packageName string) (contentmapper.Manifest, tspath.RootedDirectoryPath, *ast.Diagnostic) { + fs := host.FS() resolver := module.NewResolver(module.ResolverOptions{ Host: &contentMapperResolutionHost{fs: fs, currentDirectory: containingFile.Directory()}, CompilerOptions: &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindBundler}, @@ -50,7 +47,7 @@ func resolveContentMapperManifest(fs vfs.FS, containingFile tspath.RootedFilePat logicalPackageDirectory = resolved.ResolvedFileName } logicalPackageDirectoryPath := tspath.RootedDirectoryPathFromPath(tspath.RootedPath(logicalPackageDirectory)) - if policy, ok := fs.(contentMapperPackagePolicy); ok && !policy.IsContentMapperPackageAllowed(logicalPackageDirectoryPath) { + if !host.IsContentMapperPackageAllowed(logicalPackageDirectoryPath) { return contentmapper.Manifest{}, "", ast.NewCompilerDiagnostic(diagnostics.The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded, packageName) } packageDirectory := tspath.RootedDirectoryPathFromPath(tspath.RootedPath(resolved.ResolvedFileName)) diff --git a/tsc/internal/tsoptions/contentmappers_test.go b/tsc/internal/tsoptions/contentmappers_test.go index 2c1566165c674..68137b0ac1203 100644 --- a/tsc/internal/tsoptions/contentmappers_test.go +++ b/tsc/internal/tsoptions/contentmappers_test.go @@ -118,6 +118,66 @@ func TestContentMapperExtensionRewritesAreCached(t *testing.T) { //nolint:parall } } +type contentMapperTestParseConfigHost struct { + ParseConfigHost + allowed bool + packageDirectory tspath.RootedDirectoryPath +} + +func (h *contentMapperTestParseConfigHost) IsContentMapperPackageAllowed(packageDirectory tspath.RootedDirectoryPath) bool { + h.packageDirectory = packageDirectory + return h.allowed +} + +func TestParseConfigHostContentMapperAuthorization(t *testing.T) { + t.Parallel() + + for _, allowed := range []bool{false, true} { + name := "denied" + if allowed { + name = "allowed" + } + t.Run(name, func(t *testing.T) { + t.Parallel() + fs := vfstest.FromMap(map[string]string{ + "/project/tsconfig.json": `{ + "compilerOptions": { "noLib": true }, + "contentMappers": [{ "package": "mapper", "extensions": [".vue"] }] + }`, + "/project/index.ts": "", + "/project/node_modules/mapper/package.json": `{ + "name": "mapper", + "typescript": { "contentMapper": { "exec": ["run"] } } + }`, + }, tspath.CaseSensitive) + host := &contentMapperTestParseConfigHost{ + ParseConfigHost: NewDefaultParseConfigHost(fs), + allowed: allowed, + } + commandLine, errors := GetParsedCommandLineOfConfigFile( + "/project/tsconfig.json", + &core.CompilerOptions{RunExternalCode: core.TSTrue}, + nil, + host, + nil, + ) + assert.Equal(t, len(errors), 0) + assert.Equal(t, host.packageDirectory, tspath.RootedDirectoryPath("/project/node_modules/mapper")) + if allowed { + assert.Equal(t, len(commandLine.ContentMappers()), 1) + assert.DeepEqual(t, commandLine.ContentMapperExtensions(), []string{".vue"}) + assert.Equal(t, len(commandLine.GetConfigFileParsingDiagnostics()), 0) + } else { + assert.Equal(t, len(commandLine.ContentMappers()), 0) + assert.Equal(t, len(commandLine.ContentMapperExtensions()), 0) + diagnosticsFound := commandLine.GetConfigFileParsingDiagnostics() + assert.Equal(t, len(diagnosticsFound), 1) + assert.Equal(t, diagnosticsFound[0].Code(), diagnostics.The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded.Code()) + } + }) + } +} + func TestResolveContentMapperManifestRejectsMalformedOutputExtensions(t *testing.T) { t.Parallel() for _, value := range []string{`"invalid"`, `null`, `[]`, `{".vue": 1}`, `{".vue": null}`, `{".vue/foo": ".js"}`, `{".vue\\foo": ".js"}`, `{".vue": ".js/foo"}`, `{".vue": ".js\\foo"}`} { @@ -129,7 +189,7 @@ func TestResolveContentMapperManifestRejectsMalformedOutputExtensions(t *testing "typescript": { "contentMapper": { "exec": ["run"], "outputExtensions": ` + value + ` } } }`, }, tspath.CaseSensitive) - _, _, diagnostic := resolveContentMapperManifest(fs, "/home/project/tsconfig.json", "mapper") + _, _, diagnostic := resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/tsconfig.json", "mapper") assert.Assert(t, diagnostic != nil, "expected malformed outputExtensions to be rejected") }) } @@ -169,7 +229,7 @@ func TestResolveContentMapperManifest(t *testing.T) { }, tspath.CaseSensitive /*caseSensitivity*/) // Name, version, and the verbatim exec argv are preserved. - manifest, packageDirectory, diagnostic := resolveContentMapperManifest(fs, "/home/project/tsconfig.json", "vue-ts-mapper") + manifest, packageDirectory, diagnostic := resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/tsconfig.json", "vue-ts-mapper") assert.Assert(t, diagnostic == nil) assert.Equal(t, manifest.Name, "vue-ts-mapper") assert.Equal(t, manifest.Version, "1.2.3") @@ -179,34 +239,34 @@ func TestResolveContentMapperManifest(t *testing.T) { assert.DeepEqual(t, manifest.DefaultOutputExtensions, map[string]string{".vue": ".js"}) // Resolution walks up node_modules; a package with no version resolves to a name and empty version. - manifest, _, diagnostic = resolveContentMapperManifest(fs, "/home/project/src/tsconfig.json", "@scope/noversion") + manifest, _, diagnostic = resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/src/tsconfig.json", "@scope/noversion") assert.Assert(t, diagnostic == nil) assert.Equal(t, manifest.Name, "@scope/noversion") assert.Equal(t, manifest.Version, "") // A package that is not installed reports a resolution diagnostic. - _, _, diagnostic = resolveContentMapperManifest(fs, "/home/project/tsconfig.json", "missing-mapper") + _, _, diagnostic = resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/tsconfig.json", "missing-mapper") assert.Assert(t, diagnostic != nil) assert.Equal(t, diagnostic.Code(), diagnostics.The_content_mapper_package_0_could_not_be_resolved.Code()) // A package whose package.json has no name reports a diagnostic. - _, packageDirectory, diagnostic = resolveContentMapperManifest(fs, "/home/project/tsconfig.json", "no-name") + _, packageDirectory, diagnostic = resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/tsconfig.json", "no-name") assert.Assert(t, diagnostic != nil) assert.Equal(t, packageDirectory, tspath.RootedDirectoryPath("/home/project/node_modules/no-name")) assert.Equal(t, diagnostic.Code(), diagnostics.The_package_json_of_the_content_mapper_package_0_does_not_specify_a_name.Code()) // A package that does not declare a "typescript.contentMapper" object reports a diagnostic. - _, _, diagnostic = resolveContentMapperManifest(fs, "/home/project/tsconfig.json", "no-manifest") + _, _, diagnostic = resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/tsconfig.json", "no-manifest") assert.Assert(t, diagnostic != nil) assert.Equal(t, diagnostic.Code(), diagnostics.The_package_json_of_the_content_mapper_package_0_does_not_declare_a_typescript_contentMapper_object.Code()) // A "typescript.contentMapper" with no "exec", or an "exec" of the wrong type, reports a diagnostic. for _, pkg := range []string{"no-exec", "bad-exec"} { - _, _, diagnostic = resolveContentMapperManifest(fs, "/home/project/tsconfig.json", pkg) + _, _, diagnostic = resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/tsconfig.json", pkg) assert.Assert(t, diagnostic != nil, "expected a diagnostic for %s", pkg) assert.Equal(t, diagnostic.Code(), diagnostics.The_typescript_contentMapper_exec_of_the_content_mapper_package_0_must_be_a_non_empty_array_of_strings.Code()) } - _, _, diagnostic = resolveContentMapperManifest(fs, "/home/project/tsconfig.json", "bad-output-extension") + _, _, diagnostic = resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/tsconfig.json", "bad-output-extension") assert.Equal(t, diagnostic.Code(), diagnostics.The_typescript_contentMapper_outputExtensions_of_the_content_mapper_package_0_contains_an_invalid_mapping_from_1_to_2_Extensions_must_be_non_empty_and_begin_with_a.Code()) } diff --git a/tsc/internal/tsoptions/parsedcommandline_test.go b/tsc/internal/tsoptions/parsedcommandline_test.go index 86ca7c74c6c92..9e836a394af81 100644 --- a/tsc/internal/tsoptions/parsedcommandline_test.go +++ b/tsc/internal/tsoptions/parsedcommandline_test.go @@ -181,7 +181,7 @@ func TestParsedCommandLine(t *testing.T) { tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(tspath.RootedFilePathFromNormalized(configFileName), "/dev/tsconfig.json", jsonText) parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( tsconfigSourceFile, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/dev", &core.CompilerOptions{RunExternalCode: core.TSTrue}, nil, @@ -201,7 +201,7 @@ func TestParsedCommandLine(t *testing.T) { }, tspath.CaseInsensitive) insensitiveCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( tsconfigSourceFile, - insensitiveFS, + tsoptions.NewDefaultParseConfigHost(insensitiveFS), "/dev", &core.CompilerOptions{RunExternalCode: core.TSTrue}, nil, @@ -222,7 +222,7 @@ func TestParsedCommandLine(t *testing.T) { ) sensitiveCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( tsconfigSourceFile, - tsoptionstest.NewVFS(nil, tspath.CaseSensitive), + tsoptions.NewDefaultParseConfigHost(tsoptionstest.NewVFS(nil, tspath.CaseSensitive)), "/dev", nil, nil, @@ -231,7 +231,7 @@ func TestParsedCommandLine(t *testing.T) { ) _ = tsoptions.ParseJsonSourceFileConfigFileContent( tsconfigSourceFile, - tsoptionstest.NewVFS(nil, tspath.CaseInsensitive), + tsoptions.NewDefaultParseConfigHost(tsoptionstest.NewVFS(nil, tspath.CaseInsensitive)), "/dev", nil, nil, @@ -252,7 +252,7 @@ func TestParsedCommandLine(t *testing.T) { tspath.CaseSensitive.PathKey(tspath.RootedPath("/dev/tsconfig.json")), `{ "files": ["src/a.ts"], "include": ["src/*.ts"] }`, ), - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/dev", nil, nil, @@ -270,7 +270,7 @@ func TestParsedCommandLine(t *testing.T) { tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(tspath.RootedFilePathFromNormalized(configFileName), tspath.PathKeyFromCanonical(configFileName), `{}`) parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( tsconfigSourceFile, - tsoptionstest.NewVFS(map[string]string{}, tspath.CaseSensitive), + tsoptions.NewDefaultParseConfigHost(tsoptionstest.NewVFS(map[string]string{}, tspath.CaseSensitive)), "/dev", nil, nil, diff --git a/tsc/internal/tsoptions/tsconfigparsing.go b/tsc/internal/tsoptions/tsconfigparsing.go index d08c8a9a9600b..a01e1e1444970 100644 --- a/tsc/internal/tsoptions/tsconfigparsing.go +++ b/tsc/internal/tsoptions/tsconfigparsing.go @@ -622,9 +622,31 @@ func ParseConfigFileTextToJson(fileName tspath.RootedFilePath, path tspath.PathK return config, errors } +type ParseConfigHost interface { + FS() vfs.FS + IsContentMapperPackageAllowed(packageDirectory tspath.RootedDirectoryPath) bool +} + +type defaultParseConfigHost struct { + fs vfs.FS +} + +// NewDefaultParseConfigHost leaves external code authorization to the runExternalCode option. +func NewDefaultParseConfigHost(fs vfs.FS) ParseConfigHost { + return &defaultParseConfigHost{fs: fs} +} + +func (h *defaultParseConfigHost) FS() vfs.FS { + return h.fs +} + +func (h *defaultParseConfigHost) IsContentMapperPackageAllowed(packageDirectory tspath.RootedDirectoryPath) bool { + return true +} + func ParseJsonSourceFileConfigFileContent( sourceFile *TsConfigSourceFile, - fs vfs.FS, + host ParseConfigHost, basePath tspath.RootedDirectoryPath, existingOptions *core.CompilerOptions, existingOptionsRaw *collections.OrderedMap[string, any], @@ -632,7 +654,7 @@ func ParseJsonSourceFileConfigFileContent( extendedConfigCache ExtendedConfigCache, ) *ParsedCommandLine { // tracing?.push(tracing.Phase.Parse, "parseJsonSourceFileConfigFileContent", { path: sourceFile.fileName }); - result := parseJsonConfigFileContentWorker(nil /*json*/, sourceFile, fs, basePath, existingOptions, existingOptionsRaw, sourceFile.SourceFile.FileName(), resolutionStack, extendedConfigCache) + result := parseJsonConfigFileContentWorker(nil /*json*/, sourceFile, host, basePath, existingOptions, existingOptionsRaw, sourceFile.SourceFile.FileName(), resolutionStack, extendedConfigCache) // tracing?.pop(); return result } @@ -765,15 +787,15 @@ func convertPropertyValueToJson(sourceFile *ast.SourceFile, valueExpression *ast // ParseJsonConfigFileContent parses the contents of a config file (tsconfig.json). // jsonNode: The contents of the config file to parse -// fs: File system used to resolve and enumerate config inputs. +// host: File system and content mapper authorization used to parse config inputs. // basePath: A root directory to resolve relative path entries in the config file to. e.g. outDir -func ParseJsonConfigFileContent(json any, fs vfs.FS, basePath tspath.RootedDirectoryPath, existingOptions *core.CompilerOptions, configFileName tspath.RootedFilePath, resolutionStack []tspath.PathKey, extendedConfigCache ExtendedConfigCache) *ParsedCommandLine { +func ParseJsonConfigFileContent(json any, host ParseConfigHost, basePath tspath.RootedDirectoryPath, existingOptions *core.CompilerOptions, configFileName tspath.RootedFilePath, resolutionStack []tspath.PathKey, extendedConfigCache ExtendedConfigCache) *ParsedCommandLine { normalized := normalizeJsonValue(json) jsonObject, ok := normalized.(*collections.OrderedMap[string, any]) if !ok { jsonObject = &collections.OrderedMap[string, any]{} } - result := parseJsonConfigFileContentWorker(jsonObject, nil /*sourceFile*/, fs, basePath, existingOptions, nil /*existingOptionsRaw*/, configFileName, resolutionStack, extendedConfigCache) + result := parseJsonConfigFileContentWorker(jsonObject, nil /*sourceFile*/, host, basePath, existingOptions, nil /*existingOptionsRaw*/, configFileName, resolutionStack, extendedConfigCache) return result } @@ -1120,7 +1142,7 @@ func isStringValue(value any) bool { func parseJsonConfigFileContentWorker( json *collections.OrderedMap[string, any], sourceFile *TsConfigSourceFile, - fs vfs.FS, + host ParseConfigHost, basePath tspath.RootedDirectoryPath, existingOptions *core.CompilerOptions, existingOptionsRaw *collections.OrderedMap[string, any], @@ -1128,6 +1150,7 @@ func parseJsonConfigFileContentWorker( resolutionStack []tspath.PathKey, extendedConfigCache ExtendedConfigCache, ) *ParsedCommandLine { + fs := host.FS() debug.Assert((json == nil && sourceFile != nil) || (json != nil && sourceFile == nil)) baseDirectory := basePath @@ -1362,7 +1385,7 @@ func parseJsonConfigFileContentWorker( } resolvedContentMappers := make([]*contentmapper.Mapper, 0, len(contentMappers)) for j, mapper := range contentMappers { - manifest, packageDirectory, diagnostic := resolveContentMapperManifest(fs, containingFile, mapper.Package) + manifest, packageDirectory, diagnostic := resolveContentMapperManifest(host, containingFile, mapper.Package) mapper.PackageDirectory = packageDirectory if diagnostic != nil { errors = append(errors, setContentMapperDiagnosticLocation(diagnostic, contentMapperSourceFile, getContentMapperSyntax(contentMapperSourceFile, contentMapperIndices[j], "package"))) @@ -1956,10 +1979,10 @@ func GetParsedCommandLineOfConfigFile( configFileName tspath.RootedFilePath, options *core.CompilerOptions, optionsRaw *collections.OrderedMap[string, any], - fs vfs.FS, + host ParseConfigHost, extendedConfigCache ExtendedConfigCache, ) (*ParsedCommandLine, []*ast.Diagnostic) { - return GetParsedCommandLineOfConfigFilePath(configFileName, fs.CaseSensitivity().PathKey(configFileName.AsPath()), options, optionsRaw, fs, extendedConfigCache) + return GetParsedCommandLineOfConfigFilePath(configFileName, host.FS().CaseSensitivity().PathKey(configFileName.AsPath()), options, optionsRaw, host, extendedConfigCache) } func GetParsedCommandLineOfConfigFilePath( @@ -1967,11 +1990,11 @@ func GetParsedCommandLineOfConfigFilePath( path tspath.PathKey, options *core.CompilerOptions, optionsRaw *collections.OrderedMap[string, any], - fs vfs.FS, + host ParseConfigHost, extendedConfigCache ExtendedConfigCache, ) (*ParsedCommandLine, []*ast.Diagnostic) { errors := []*ast.Diagnostic{} - configFileText, errors := tryReadFile(configFileName, fs.ReadFile, errors) + configFileText, errors := tryReadFile(configFileName, host.FS().ReadFile, errors) if len(errors) > 0 { // these are unrecoverable errors--exit to report them as diagnostics return nil, errors @@ -1982,7 +2005,7 @@ func GetParsedCommandLineOfConfigFilePath( // tsConfigSourceFile.originalFileName = tsConfigSourceFile.FileName() return ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, - fs, + host, configFileName.Directory(), options, optionsRaw, diff --git a/tsc/internal/tsoptions/tsconfigparsing_test.go b/tsc/internal/tsoptions/tsconfigparsing_test.go index 673fd8e4b7a8b..b537a6f0c664f 100644 --- a/tsc/internal/tsoptions/tsconfigparsing_test.go +++ b/tsc/internal/tsoptions/tsconfigparsing_test.go @@ -871,7 +871,7 @@ func TestParseJsonConfigFileContentAcceptsJsonRepresentations(t *testing.T) { t.Parallel() parsed := tsoptions.ParseJsonConfigFileContent( json, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/project", nil, "/project/tsconfig.json", @@ -898,7 +898,7 @@ func TestParseJsonConfigFileContentPreservesRaw(t *testing.T) { "customSetting": map[string]any{"enabled": true}, "compileOnSave": true, }, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/project", nil, "/project/tsconfig.json", @@ -925,7 +925,7 @@ func TestParseJsonConfigFileContentHandlesNullArrayElements(t *testing.T) { t.Parallel() parsed := tsoptions.ParseJsonConfigFileContent( map[string]any{property: []any{nil}}, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/project", nil, "/project/tsconfig.json", @@ -946,7 +946,7 @@ func TestParseJsonConfigFileContentDefaultsCompileOnSaveToFalse(t *testing.T) { }, tspath.CaseSensitive /*caseSensitivity*/) parsed := tsoptions.ParseJsonConfigFileContent( map[string]any{"files": []any{"index.ts"}}, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/project", nil, "/project/tsconfig.json", @@ -963,7 +963,7 @@ func getParsedWithJsonApi(config testConfig, fs vfs.FS, basePath tspath.RootedDi parsed, _ := tsoptions.ParseConfigFileTextToJson(configFileName, path, config.jsonText) return tsoptions.ParseJsonConfigFileContent( parsed, - fs, + tsoptions.NewDefaultParseConfigHost(fs), basePath, config.existingOptions, configFileName, @@ -1002,7 +1002,7 @@ func TestParseJsonSourceFileConfigFileContentReportsInvalidExtendedConfig(t *tes parsed := tsoptions.ParseJsonSourceFileConfigFileContent( configFile, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/project", nil, nil, @@ -1045,7 +1045,7 @@ func TestParseJsonSourceFileConfigFileContentWithEmptyExtendedConfig(t *testing. parsed := tsoptions.ParseJsonSourceFileConfigFileContent( configFile, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/project", nil, nil, @@ -1621,7 +1621,7 @@ func getParsedWithJsonSourceFileApi(config testConfig, fs vfs.FS, basePath tspat } return tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, - fs, + tsoptions.NewDefaultParseConfigHost(fs), basePath, config.existingOptions, nil, @@ -1815,7 +1815,7 @@ func parseSrcCompiler(tb testing.TB) *tsoptions.ParsedCommandLine { ) parsed := tsoptions.ParseJsonSourceFileConfigFileContent( configFile, - fs, + tsoptions.NewDefaultParseConfigHost(fs), compilerDir, nil, nil, @@ -1858,7 +1858,7 @@ func BenchmarkParseSrcCompiler(b *testing.B) { for b.Loop() { tsoptions.ParseJsonSourceFileConfigFileContent( configFile, - fs, + tsoptions.NewDefaultParseConfigHost(fs), compilerDir, nil, nil, @@ -1917,7 +1917,7 @@ func TestExtendedConfigErrorsAppearOnCacheHit(t *testing.T) { } return tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/", nil, nil, @@ -1960,7 +1960,7 @@ func TestExtendedConfigErrorsAppearOnCacheHit(t *testing.T) { } return tsoptions.ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, - fs, + tsoptions.NewDefaultParseConfigHost(fs), "/", nil, nil, @@ -2002,7 +2002,7 @@ func TestExtendedConfigConfigDirPathsAreNotCached(t *testing.T) { cache := &memoCache{} parseConfig := func(configFileName tspath.RootedFilePath) *tsoptions.ParsedCommandLine { - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(configFileName, nil, nil, fs, cache) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(configFileName, nil, nil, tsoptions.NewDefaultParseConfigHost(fs), cache) assert.Assert(t, len(errors) == 0, "unexpected errors parsing %s: %v", configFileName, errors) return parsed } diff --git a/tsc/internal/tsoptions/tsoptionstest/parsedcommandline.go b/tsc/internal/tsoptions/tsoptionstest/parsedcommandline.go index 2ac529946a65c..f4d032727ba88 100644 --- a/tsc/internal/tsoptions/tsoptionstest/parsedcommandline.go +++ b/tsc/internal/tsoptions/tsoptionstest/parsedcommandline.go @@ -10,5 +10,5 @@ func GetParsedCommandLine(t assert.TestingT, jsonText string, files map[string]s fs := NewVFS(files, caseSensitivity) configFileName := currentDirectory.ResolveFile("tsconfig.json") tsconfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(configFileName, caseSensitivity.PathKey(tspath.RootedPath(configFileName)), jsonText) - return tsoptions.ParseJsonSourceFileConfigFileContent(tsconfigSourceFile, fs, currentDirectory, nil, nil, nil, nil) + return tsoptions.ParseJsonSourceFileConfigFileContent(tsconfigSourceFile, tsoptions.NewDefaultParseConfigHost(fs), currentDirectory, nil, nil, nil, nil) }