Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions tsc/internal/module/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,14 @@ func (r *DefaultResolver) ResolvePackageDirectory(moduleName string, containingF
compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference)
containingDirectory := containingFile.Directory()
state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r, nil)
if tspath.IsExternalModuleNameRelative(moduleName) {
packageDirectory := containingDirectory.ResolveDirectory(moduleName)
Comment thread
andrewbranch marked this conversation as resolved.
Resolved
if r.fs.DirectoryExists(packageDirectory) {
originalPath, resolvedPath := state.getOriginalAndResolvedFileName(tspath.RootedFilePathFromPath(packageDirectory.AsPath()))
return state.createResolvedModuleHandlingSymlink(&resolved{path: resolvedPath, originalPath: originalPath})
}
return nil
}
state.resolvePackageDirectoryOnly = true
if result := state.loadModuleFromNearestNodeModulesDirectory(false /*typesScopeOnly*/); result != nil && result.path != "" {
return state.createResolvedModuleHandlingSymlink(result)
Expand Down
8 changes: 7 additions & 1 deletion tsc/internal/project/configfileregistrybuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -799,12 +799,18 @@ func (c *configFileRegistryBuilder) IsContentMapperPackageAllowed(packageDirecto
if c.sessionOptions.InitialWorkspaceRoots == nil {
return true
}
caseSensitivity := c.fs.CaseSensitivity()
if slices.ContainsFunc(c.sessionOptions.InitialWorkspaceRoots, func(root tspath.RootedDirectoryPath) bool {
return caseSensitivity.ContainsPath(root, packageDirectory.AsPath())
}) {
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())
return caseSensitivity.ContainsPath(root, nodeModulesPath.AsPath())
})
}

Expand Down
90 changes: 89 additions & 1 deletion tsc/internal/project/contentmapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ func TestContentMapperPackageMustBeInWorkspace(t *testing.T) {
name string
projectDirectory string
workspaceRoots []string
mapperPackage string
mapperPath string
mapperTarget string
allowed bool
Expand All @@ -314,6 +315,30 @@ func TestContentMapperPackageMustBeInWorkspace(t *testing.T) {
mapperPath: "/home/trusted/node_modules/mapper/package.json",
allowed: true,
},
{
name: "directory mapper in workspace",
projectDirectory: "/home/trusted",
workspaceRoots: []string{"/home/trusted"},
mapperPackage: "./mapper",
mapperPath: "/home/trusted/mapper/package.json",
allowed: true,
},
{
name: "workspace directory mapper symlinked outside",
projectDirectory: "/home/trusted",
workspaceRoots: []string{"/home/trusted"},
mapperPackage: "./mapper",
mapperPath: "/home/trusted/mapper",
mapperTarget: "/home/outside/mapper",
allowed: true,
},
{
name: "directory mapper outside workspace",
projectDirectory: "/home/untrusted",
workspaceRoots: []string{"/home/trusted"},
mapperPackage: "./mapper",
mapperPath: "/home/untrusted/mapper/package.json",
},
{
name: "loose file outside workspace",
projectDirectory: "/home/untrusted",
Expand Down Expand Up @@ -357,10 +382,14 @@ func TestContentMapperPackageMustBeInWorkspace(t *testing.T) {
configPath := test.projectDirectory + "/tsconfig.json"
mainPath := test.projectDirectory + "/main.ts"
boxPath := test.projectDirectory + "/app.box"
mapperPackage := test.mapperPackage
if mapperPackage == "" {
mapperPackage = "mapper"
}
files := map[string]any{
configPath: `{
"compilerOptions": { "target": "es2020", "module": "esnext", "moduleResolution": "bundler" },
"contentMappers": [ { "package": "mapper", "extensions": [".box"] } ]
"contentMappers": [ { "package": "` + mapperPackage + `", "extensions": [".box"] } ]
}`,
mainPath: "import { version } from \"./app.box\";\nexport { version };\n",
boxPath: "export const version = #{target};\n",
Expand Down Expand Up @@ -491,6 +520,65 @@ func TestContentMapperPackageManifestChangeReloadsConfig(t *testing.T) {
assert.Assert(t, strings.Contains(boxFile.Text(), "export const version = 7;"), "expected fixed mapper manifest to be reloaded: %q", boxFile.Text())
}

func TestRelativeSymlinkedContentMapperPackageManifestChangeReloadsConfig(t *testing.T) {
t.Parallel()
const packageJsonPath = "/home/mapper/package.json"
files := map[string]any{
"/home/project/tsconfig.json": `{
"compilerOptions": { "target": "es2020", "module": "esnext", "moduleResolution": "bundler" },
"contentMappers": [{ "package": "./mapper", "extensions": [".box"] }]
}`,
"/home/project/mapper": vfstest.Symlink("/home/mapper"),
packageJsonPath: `{
"name": "mapper",
"version": "1.0.0",
"typescript": { "contentMapper": { "exec": ["compiler-test-mapper"] } }
}`,
"/home/project/app.box": "export const version = #{target};\n",
"/home/project/main.ts": "import { version } from \"./app.box\";\n",
}
caps := &lsproto.ResolvedClientCapabilities{}
caps.Workspace.DidChangeWatchedFiles.RelativePatternSupport = true
ctx := lsproto.WithClientCapabilities(context.Background(), caps)
init, utils := projecttestutil.GetSessionInitOptions(files, &project.SessionOptions{
CurrentDirectory: "/home/project",
DefaultLibraryPath: bundled.LibPath(),
TypingsLocation: projecttestutil.TestTypingsLocation,
PositionEncoding: lsproto.PositionEncodingKindUTF8,
RunExternalCode: true,
WatchEnabled: true,
}, nil)
init.BackgroundCtx = ctx
init.Spawner = contentmappertest.NewSpawner()
session := project.NewSession(init)
defer session.Close()

mainURI := lsproto.DocumentUri("file:///home/project/main.ts")
session.DidOpenFile(ctx, mainURI, 1, files["/home/project/main.ts"].(string), lsproto.LanguageKindTypeScript)
_, err := session.GetLanguageService(ctx, mainURI)
assert.NilError(t, err)
configuredProject := session.Snapshot().GetDefaultProject(mainURI)
assert.Assert(t, configuredProject != nil)
mappers := configuredProject.CommandLine.ContentMappers()
assert.Equal(t, len(mappers), 1)
assert.Equal(t, mappers[0].PackageDirectory, tspath.RootedDirectoryPath("/home/mapper"))
session.WaitForBackgroundTasks()
assert.Assert(t, utils.WatchesFile(packageJsonPath), "expected the mapper package manifest to be watched")

fixedManifest := contentmappertest.PackageJSON(contentmappertest.TransformingMapper)
assert.NilError(t, utils.FS().WriteFile(packageJsonPath, fixedManifest))
session.DidChangeWatchedFiles(ctx, []*lsproto.FileEvent{{
Uri: lsproto.DocumentUri("file://" + packageJsonPath),
Type: lsproto.FileChangeTypeChanged,
}})

languageService, err := session.GetLanguageService(ctx, mainURI)
assert.NilError(t, err)
boxFile := languageService.GetProgram().GetSourceFile("/home/project/app.box")
assert.Assert(t, boxFile != nil, "expected app.box in the rebuilt program")
assert.Assert(t, strings.Contains(boxFile.Text(), "export const version = 7;"), "expected fixed mapper manifest to be reloaded: %q", boxFile.Text())
}

func TestContentMapperSupplementalFileClonedOnEdit(t *testing.T) {
t.Parallel()
files := map[string]any{
Expand Down
4 changes: 2 additions & 2 deletions tsc/internal/tsoptions/contentmappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/vfs"
)

// resolveContentMapperManifest locates packageName in node_modules (walking up from the directory of
// containingFile via node module resolution) and reads its package.json to produce the mapper's manifest
// resolveContentMapperManifest locates packageName as a directory path relative to containingFile or in
// node_modules (walking up via node module resolution) and reads its package.json to produce the mapper's manifest
// and package directory. It never executes the package. On failure it returns a diagnostic describing why
// the mapper could not be resolved; on success the diagnostic is nil.
type contentMapperResolutionHost struct {
Expand Down
74 changes: 74 additions & 0 deletions tsc/internal/tsoptions/contentmappers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,77 @@ func TestResolveContentMapperManifest(t *testing.T) {
_, _, 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())
}

func TestResolveContentMapperManifestPaths(t *testing.T) {
t.Parallel()

for _, test := range []struct {
packageName string
packageDirectory tspath.RootedDirectoryPath
}{
{".", "/home/project"},
{"..", "/home"},
{"./mapper", "/home/project/mapper"},
{"../mapper", "/home/mapper"},
{`.\mapper`, "/home/project/mapper"},
{`..\mapper`, "/home/mapper"},
{"/home/mapper", "/home/mapper"},
{`C:\mapper`, "C:/mapper"},
} {
t.Run(test.packageName, func(t *testing.T) {
t.Parallel()
files := map[string]string{
"/home/node_modules/unrelated/package.json": `{
"name": "unrelated",
"typescript": { "contentMapper": { "exec": ["run"] } }
}`,
"/home/package.json": `{"name": "unrelated"}`,
}
files[test.packageDirectory.ResolveFile("package.json").AsString()] = `{
"name": "mapper",
"typescript": { "contentMapper": { "exec": ["run"] } }
}`
containingFile := tspath.RootedFilePath("/home/project/tsconfig.json")
if test.packageName == `C:\mapper` {
files = map[string]string{
"C:/mapper/package.json": files["C:/mapper/package.json"],
}
containingFile = "C:/project/tsconfig.json"
}
fs := vfstest.FromMap(files, tspath.CaseSensitive)
for _, allowed := range []bool{false, true} {
host := &contentMapperTestParseConfigHost{ParseConfigHost: NewDefaultParseConfigHost(fs), allowed: allowed}
manifest, packageDirectory, diagnostic := resolveContentMapperManifest(host, containingFile, test.packageName)
assert.Equal(t, host.packageDirectory, test.packageDirectory)
if allowed {
assert.Assert(t, diagnostic == nil)
assert.Equal(t, packageDirectory, test.packageDirectory)
assert.Equal(t, manifest.Name, "mapper")
assert.DeepEqual(t, manifest.Exec, []string{"run"})
} else {
assert.Assert(t, diagnostic != nil)
assert.Equal(t, diagnostic.Code(), diagnostics.The_content_mapper_package_0_is_outside_the_open_workspace_and_will_not_be_loaded.Code())
}
}
})
}
}

func TestResolveContentMapperManifestMissingPaths(t *testing.T) {
t.Parallel()

for _, packageName := range []string{"./does-not-exist", "../does-not-exist", "/does-not-exist", `C:\does-not-exist`, "../no-manifest", "../file"} {
t.Run(packageName, func(t *testing.T) {
t.Parallel()
fs := vfstest.FromMap(map[string]string{
"/home/package.json": `{"name": "unrelated"}`,
"/home/node_modules/unrelated/package.json": `{"name": "unrelated"}`,
"/home/no-manifest/index.js": "",
"/home/file": "",
}, tspath.CaseSensitive)
_, _, diagnostic := resolveContentMapperManifest(NewDefaultParseConfigHost(fs), "/home/project/tsconfig.json", packageName)
assert.Assert(t, diagnostic != nil)
assert.Equal(t, diagnostic.Code(), diagnostics.The_content_mapper_package_0_could_not_be_resolved.Code())
})
}
}
51 changes: 30 additions & 21 deletions tsc/internal/tsoptions/tsconfigparsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ import (
)

type extendsResult struct {
options *parsedCompilerOptions
include []any
exclude []any
files []any
contentMappers []any
compileOnSave bool
extendedSourceFiles collections.Set[tspath.RootedFilePath]
options *parsedCompilerOptions
include []any
exclude []any
files []any
contentMappers []any
contentMappersConfigFile tspath.RootedFilePath
compileOnSave bool
extendedSourceFiles collections.Set[tspath.RootedFilePath]
}

type configFileSpecs struct {
Expand Down Expand Up @@ -85,10 +86,11 @@ func (e *ExtendedConfigCacheEntry) ExtendedFileNames() []tspath.RootedFilePath {
}

type parsedTsconfig struct {
raw any
options *parsedCompilerOptions
typeAcquisition *core.TypeAcquisition
extendedConfigPaths []tspath.RootedFilePath
raw any
options *parsedCompilerOptions
typeAcquisition *core.TypeAcquisition
extendedConfigPaths []tspath.RootedFilePath
contentMappersConfigFile tspath.RootedFilePath
}

type parsedCompilerOptions struct {
Expand Down Expand Up @@ -199,10 +201,11 @@ func parseOwnConfigOfJsonSourceFile(
))
}
return &parsedTsconfig{
raw: json,
options: compilerOptions,
typeAcquisition: typeAcquisition,
extendedConfigPaths: extendedConfigPaths,
raw: json,
options: compilerOptions,
typeAcquisition: typeAcquisition,
extendedConfigPaths: extendedConfigPaths,
contentMappersConfigFile: configFileName,
}, errors
}

Expand Down Expand Up @@ -901,10 +904,11 @@ func parseOwnConfigOfJson(
errors = append(errors, err...)
}
parsedConfig := &parsedTsconfig{
raw: json,
options: options,
typeAcquisition: typeAcquisition,
extendedConfigPaths: extendedConfigPaths,
raw: json,
options: options,
typeAcquisition: typeAcquisition,
extendedConfigPaths: extendedConfigPaths,
contentMappersConfigFile: configFileName,
}
return parsedConfig, errors
}
Expand Down Expand Up @@ -1087,6 +1091,7 @@ func parseConfig(
setPropertyValue("files")
if extendedRawMap, ok := extendsRaw.(*collections.OrderedMap[string, any]); ok && extendedRawMap.Has("contentMappers") {
result.contentMappers, _ = extendedRawMap.GetOrZero("contentMappers").([]any)
result.contentMappersConfigFile = extendedConfig.contentMappersConfigFile
}
if extendedRawMap, ok := extendsRaw.(*collections.OrderedMap[string, any]); ok && extendedRawMap.Has("compileOnSave") {
if compileOnSave, ok := extendedRawMap.GetOrZero("compileOnSave").(bool); ok {
Expand Down Expand Up @@ -1120,6 +1125,7 @@ func parseConfig(
}
if result.contentMappers != nil && !ownConfig.raw.(*collections.OrderedMap[string, any]).Has("contentMappers") {
ownConfig.raw.(*collections.OrderedMap[string, any]).Set("contentMappers", result.contentMappers)
ownConfig.contentMappersConfigFile = result.contentMappersConfigFile
}
if result.compileOnSave && !ownConfig.raw.(*collections.OrderedMap[string, any]).Has("compileOnSave") {
ownConfig.raw.(*collections.OrderedMap[string, any]).Set("compileOnSave", result.compileOnSave)
Expand Down Expand Up @@ -1398,9 +1404,12 @@ func parseJsonConfigFileContentWorker(
} else if len(contentMappers) > 0 {
// Resolve each mapper's package.json now so its name, version, and run command are available to
// everything downstream (diagnostics, build-info staleness) without executing anything.
containingFile := configFileName
containingFile := parsedConfig.contentMappersConfigFile
if containingFile == "" {
containingFile = basePathForFileNames.ResolveFile("tsconfig.json")
containingFile = configFileName
if containingFile == "" {
containingFile = basePathForFileNames.ResolveFile("tsconfig.json")
}
}
resolvedContentMappers := make([]*contentmapper.Mapper, 0, len(contentMappers))
for j, mapper := range contentMappers {
Expand Down
Loading
Loading