vendor tsgo
This commit is contained in:
79
tools/tsgo/internal/module/cache.go
Normal file
79
tools/tsgo/internal/module/cache.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/packagejson"
|
||||
)
|
||||
|
||||
type ModeAwareCache[T any] map[ModeAwareCacheKey]T
|
||||
|
||||
type moduleResolutionCacheKey struct {
|
||||
containingDirectory string
|
||||
moduleName string
|
||||
resolutionMode core.ResolutionMode
|
||||
redirectConfigName string
|
||||
}
|
||||
|
||||
type moduleResolutionCache struct {
|
||||
cache collections.SyncMap[moduleResolutionCacheKey, *ResolvedModule]
|
||||
}
|
||||
|
||||
func (c *moduleResolutionCache) Get(key moduleResolutionCacheKey) (*ResolvedModule, bool) {
|
||||
return c.cache.Load(key)
|
||||
}
|
||||
|
||||
func (c *moduleResolutionCache) Set(key moduleResolutionCacheKey, value *ResolvedModule) {
|
||||
c.cache.LoadOrStore(key, value)
|
||||
}
|
||||
|
||||
type typeRefDirectiveResolutionCacheKey struct {
|
||||
containingDirectory string
|
||||
typeReferenceName string
|
||||
resolutionMode core.ResolutionMode
|
||||
redirectConfigName string
|
||||
fromInferredTypesContainingFile bool
|
||||
}
|
||||
|
||||
type typeRefDirectiveResolutionCache struct {
|
||||
cache collections.SyncMap[typeRefDirectiveResolutionCacheKey, *ResolvedTypeReferenceDirective]
|
||||
}
|
||||
|
||||
func (c *typeRefDirectiveResolutionCache) Get(key typeRefDirectiveResolutionCacheKey) (*ResolvedTypeReferenceDirective, bool) {
|
||||
return c.cache.Load(key)
|
||||
}
|
||||
|
||||
func (c *typeRefDirectiveResolutionCache) Set(key typeRefDirectiveResolutionCacheKey, value *ResolvedTypeReferenceDirective) {
|
||||
c.cache.Store(key, value)
|
||||
}
|
||||
|
||||
type caches struct {
|
||||
packageJsonInfoCache *packagejson.InfoCache
|
||||
|
||||
moduleResolutionCache moduleResolutionCache
|
||||
typeRefDirectiveResolutionCache typeRefDirectiveResolutionCache
|
||||
|
||||
// Cached representation for `core.CompilerOptions.paths`.
|
||||
// Doesn't handle other path patterns like in `typesVersions`.
|
||||
parsedPatternsForPathsOnce sync.Once
|
||||
parsedPatternsForPaths *ParsedPatterns
|
||||
}
|
||||
|
||||
func newCaches(
|
||||
currentDirectory string,
|
||||
useCaseSensitiveFileNames bool,
|
||||
options *core.CompilerOptions,
|
||||
) caches {
|
||||
return caches{
|
||||
packageJsonInfoCache: packagejson.NewInfoCache(currentDirectory, useCaseSensitiveFileNames),
|
||||
}
|
||||
}
|
||||
|
||||
func getRedirectConfigName(redirect ResolvedProjectReference) string {
|
||||
if redirect == nil {
|
||||
return ""
|
||||
}
|
||||
return redirect.ConfigName()
|
||||
}
|
||||
2368
tools/tsgo/internal/module/resolver.go
Normal file
2368
tools/tsgo/internal/module/resolver.go
Normal file
File diff suppressed because it is too large
Load Diff
416
tools/tsgo/internal/module/resolver_test.go
Normal file
416
tools/tsgo/internal/module/resolver_test.go
Normal file
@@ -0,0 +1,416 @@
|
||||
package module_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
)
|
||||
|
||||
type resolutionHostStub struct {
|
||||
fs vfs.FS
|
||||
cwd string
|
||||
}
|
||||
|
||||
func (h *resolutionHostStub) FS() vfs.FS { return h.fs }
|
||||
func (h *resolutionHostStub) GetCurrentDirectory() string { return h.cwd }
|
||||
|
||||
// Regression test for https://github.com/microsoft/typescript-go/issues/3526.
|
||||
//
|
||||
// Resolving a node_modules import with a trailing slash (e.g. `pkg/`) must
|
||||
// produce the same result as without one.
|
||||
func TestResolveModuleNameTrailingSlash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := vfstest.FromMap(map[string]string{
|
||||
"/repo/node_modules/pkg/package.json": `{"name":"pkg","main":"main.js","types":"main.d.ts"}`,
|
||||
"/repo/node_modules/pkg/main.d.ts": "export const x: number;",
|
||||
"/repo/node_modules/pkg/main.js": "exports.x = 1;",
|
||||
"/repo/src/file.ts": "",
|
||||
}, true)
|
||||
host := &resolutionHostStub{fs: fs, cwd: "/repo"}
|
||||
opts := &core.CompilerOptions{
|
||||
ModuleResolution: core.ModuleResolutionKindBundler,
|
||||
Module: core.ModuleKindESNext,
|
||||
Target: core.ScriptTargetESNext,
|
||||
}
|
||||
resolver := module.NewResolver(host, opts, "", "")
|
||||
|
||||
for _, name := range []string{"pkg", "pkg/"} {
|
||||
r, _ := resolver.ResolveModuleName(name, "/repo/src/file.ts", core.ModuleKindESNext, nil)
|
||||
if !r.IsResolved() {
|
||||
t.Errorf("%q failed to resolve", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// blockingFS wraps a vfs.FS and forces FileExists calls for `targetPath` to
|
||||
// block on `gate` until released. Each caller sends on `arrived` when it
|
||||
// reaches the gate. This is used to deterministically reproduce the
|
||||
// `package.json` info-cache insert race described in
|
||||
// https://github.com/microsoft/typescript-go/issues/3526.
|
||||
type blockingFS struct {
|
||||
vfs.FS
|
||||
targetPath string
|
||||
gate chan struct{}
|
||||
arrived chan struct{} // each blocked goroutine sends one value
|
||||
}
|
||||
|
||||
// waitForSignal waits for a synchronization point in these race regression
|
||||
// tests and converts deadlocks into deterministic test failures.
|
||||
func waitForSignal(t *testing.T, ch <-chan struct{}, description string) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
case <-t.Context().Done():
|
||||
t.Fatalf("timed out waiting for %s", description)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *blockingFS) FileExists(path string) bool {
|
||||
if path == f.targetPath {
|
||||
f.arrived <- struct{}{}
|
||||
<-f.gate
|
||||
}
|
||||
return f.FS.FileExists(path)
|
||||
}
|
||||
|
||||
// flipFileExistsFS wraps a vfs.FS and returns false for the first
|
||||
// FileExists call to `targetPath`, then true for the second. Both calls
|
||||
// signal arrival via channel then block until released via their respective
|
||||
// gate channels. ReadFile for the target path also signals arrival then
|
||||
// blocks, so the "file doesn't exist" Set completes before the "file exists"
|
||||
// Set (reproducing the LoadOrStore race).
|
||||
type flipFileExistsFS struct {
|
||||
vfs.FS
|
||||
targetPath string
|
||||
callCount atomic.Int32
|
||||
firstArrived chan struct{} // closed when the first FileExists caller arrives
|
||||
secondArrived chan struct{} // closed when the second FileExists caller arrives
|
||||
firstGate chan struct{}
|
||||
secondGate chan struct{}
|
||||
readArrived chan struct{} // closed when ReadFile caller arrives
|
||||
readGate chan struct{}
|
||||
}
|
||||
|
||||
func (f *flipFileExistsFS) FileExists(path string) bool {
|
||||
if path == f.targetPath {
|
||||
n := f.callCount.Add(1)
|
||||
if n == 1 {
|
||||
close(f.firstArrived)
|
||||
<-f.firstGate
|
||||
return false // first caller: simulate "file not yet visible"
|
||||
}
|
||||
if n == 2 {
|
||||
close(f.secondArrived)
|
||||
<-f.secondGate
|
||||
return f.FS.FileExists(path) // second caller: file is visible
|
||||
}
|
||||
}
|
||||
return f.FS.FileExists(path)
|
||||
}
|
||||
|
||||
func (f *flipFileExistsFS) ReadFile(path string) (string, bool) {
|
||||
if path == f.targetPath {
|
||||
close(f.readArrived)
|
||||
<-f.readGate
|
||||
}
|
||||
return f.FS.ReadFile(path)
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/microsoft/typescript-go/issues/3526.
|
||||
//
|
||||
// Two goroutines resolve the same package via specifiers that differ only by
|
||||
// a trailing slash (`pkg` and `pkg/`). A blocking FS holds both at the
|
||||
// `FileExists` check for `package.json` — *after* each has confirmed a
|
||||
// `package.json` info-cache miss but *before* either has called `Set`. When
|
||||
// released, both proceed to `LoadOrStore` and one of them loses. Without the
|
||||
// fix, the loser receives the winner's `InfoCacheEntry` whose
|
||||
// `PackageDirectory` doesn't match its own `candidate` (because one spelling
|
||||
// has a trailing slash and the other doesn't), and
|
||||
// `loadNodeModuleFromDirectoryWorker`'s `ComparePaths` check skips loading
|
||||
// the package's `main`/`types`. With no `index.*` present, resolution falls
|
||||
// through to "unresolved" — the phantom TS2307 the issue describes. This
|
||||
// test deterministically fails when the fix is reverted.
|
||||
func TestResolveModuleNameTrailingSlashRace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const pkgJSONPath = "/repo/node_modules/pkg/package.json"
|
||||
files := map[string]string{
|
||||
// `types` points at a file that is not discoverable through any
|
||||
// fallback path: there is no `index.*` and no `main`. The only way
|
||||
// to resolve `pkg` (or `pkg/`) is via the package.json `types` field
|
||||
// inside `loadNodeModuleFromDirectoryWorker`, which is exactly the
|
||||
// step that the bug skips when `candidate` and
|
||||
// `packageInfo.PackageDirectory` mismatch.
|
||||
pkgJSONPath: `{"name":"pkg","types":"./typings/index.d.ts"}`,
|
||||
"/repo/node_modules/pkg/typings/index.d.ts": "export const x: number;",
|
||||
// Distinct containing files so each `ResolveModuleName` call has a
|
||||
// unique module-resolution-cache key.
|
||||
"/repo/src/a/file.ts": "",
|
||||
"/repo/src/b/file.ts": "",
|
||||
}
|
||||
fs := &blockingFS{
|
||||
FS: vfstest.FromMap(files, true),
|
||||
targetPath: pkgJSONPath,
|
||||
gate: make(chan struct{}),
|
||||
arrived: make(chan struct{}, 2),
|
||||
}
|
||||
host := &resolutionHostStub{fs: fs, cwd: "/repo"}
|
||||
opts := &core.CompilerOptions{
|
||||
ModuleResolution: core.ModuleResolutionKindBundler,
|
||||
Module: core.ModuleKindESNext,
|
||||
Target: core.ScriptTargetESNext,
|
||||
}
|
||||
resolver := module.NewResolver(host, opts, "", "")
|
||||
|
||||
type resolutionResult struct {
|
||||
name string
|
||||
resolved bool
|
||||
}
|
||||
results := make(chan resolutionResult, 2)
|
||||
var wg sync.WaitGroup
|
||||
for _, name := range []string{"pkg", "pkg/"} {
|
||||
containingFile := "/repo/src/a/file.ts"
|
||||
if strings.HasSuffix(name, "/") {
|
||||
containingFile = "/repo/src/b/file.ts"
|
||||
}
|
||||
wg.Go(func() {
|
||||
r, _ := resolver.ResolveModuleName(name, containingFile, core.ModuleKindESNext, nil)
|
||||
results <- resolutionResult{name, r.IsResolved()}
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for both goroutines to reach the FileExists gate, guaranteeing
|
||||
// both have observed a package.json info-cache miss.
|
||||
waitForSignal(t, fs.arrived, "first FileExists gate arrival")
|
||||
waitForSignal(t, fs.arrived, "second FileExists gate arrival")
|
||||
close(fs.gate)
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
for r := range results {
|
||||
if !r.resolved {
|
||||
t.Errorf("%q failed to resolve", r.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/microsoft/typescript-go/issues/1290.
|
||||
//
|
||||
// Two goroutines resolve `pkg/sub` concurrently. Both miss the package.json
|
||||
// info-cache for the root package directory. A `flipFileExistsFS` forces the
|
||||
// first goroutine's `FileExists` to return false (simulating the file not yet
|
||||
// being visible), so it stores a nil-Contents cache entry. The second
|
||||
// goroutine's `FileExists` returns true, but its `Set` call (`LoadOrStore`)
|
||||
// returns the first goroutine's nil-Contents entry. Without the `Exists()`
|
||||
// guard on the `typesVersions` lookup, `packageInfo.Contents.GetVersionPaths`
|
||||
// dereferences nil and panics. With the guard the nil-Contents entry is safely
|
||||
// skipped.
|
||||
func TestResolveSubpathNilContentsRace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const rootPkgJSON = "/repo/node_modules/pkg/package.json"
|
||||
files := map[string]string{
|
||||
rootPkgJSON: `{"name":"pkg","version":"1.0.0"}`,
|
||||
"/repo/node_modules/pkg/sub/index.d.ts": "export declare const sub: number;",
|
||||
"/repo/node_modules/pkg/sub/index.js": "exports.sub = 1;",
|
||||
"/repo/src/a/file.ts": "",
|
||||
"/repo/src/b/file.ts": "",
|
||||
}
|
||||
fs := &flipFileExistsFS{
|
||||
FS: vfstest.FromMap(files, true),
|
||||
targetPath: rootPkgJSON,
|
||||
firstArrived: make(chan struct{}),
|
||||
secondArrived: make(chan struct{}),
|
||||
firstGate: make(chan struct{}),
|
||||
secondGate: make(chan struct{}),
|
||||
readArrived: make(chan struct{}),
|
||||
readGate: make(chan struct{}),
|
||||
}
|
||||
host := &resolutionHostStub{fs: fs, cwd: "/repo"}
|
||||
opts := &core.CompilerOptions{
|
||||
ModuleResolution: core.ModuleResolutionKindBundler,
|
||||
Module: core.ModuleKindESNext,
|
||||
Target: core.ScriptTargetESNext,
|
||||
}
|
||||
resolver := module.NewResolver(host, opts, "", "")
|
||||
|
||||
var panicked atomic.Bool
|
||||
type resolutionResult struct {
|
||||
containingFile string
|
||||
resolved bool
|
||||
}
|
||||
results := make(chan resolutionResult, 2)
|
||||
var wg sync.WaitGroup
|
||||
// Two goroutines both resolve "pkg/sub". Each calls getPackageJsonInfo
|
||||
// for the root package directory, reaching FileExists for rootPkgJSON.
|
||||
for _, containingFile := range []string{"/repo/src/a/file.ts", "/repo/src/b/file.ts"} {
|
||||
wg.Go(func() {
|
||||
resolved := false
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicked.Store(true)
|
||||
}
|
||||
results <- resolutionResult{containingFile: containingFile, resolved: resolved}
|
||||
}()
|
||||
r, _ := resolver.ResolveModuleName("pkg/sub", containingFile, core.ModuleKindESNext, nil)
|
||||
resolved = r.IsResolved()
|
||||
})
|
||||
}
|
||||
|
||||
// Phase 1: Wait for both goroutines to reach FileExists for the root
|
||||
// package.json, guaranteeing both have observed a cache miss.
|
||||
waitForSignal(t, fs.firstArrived, "first root package.json FileExists arrival")
|
||||
waitForSignal(t, fs.secondArrived, "second root package.json FileExists arrival")
|
||||
|
||||
// Phase 2: Release the first FileExists caller (returns false).
|
||||
// It enters the "file not found" branch and stores a nil-Contents entry
|
||||
// via Set — this is nearly instant (no ReadFile).
|
||||
close(fs.firstGate)
|
||||
|
||||
// Phase 3: Release the second FileExists caller (returns true).
|
||||
// It proceeds to ReadFile, which we gate separately to ensure the first
|
||||
// goroutine's nil-Contents Set has completed.
|
||||
close(fs.secondGate)
|
||||
|
||||
// Phase 4: Wait for the second goroutine to reach ReadFile, then release.
|
||||
// By this point the first goroutine has stored its nil-Contents entry.
|
||||
// The second goroutine's Set (LoadOrStore) will return that stale entry.
|
||||
waitForSignal(t, fs.readArrived, "root package.json ReadFile arrival")
|
||||
close(fs.readGate)
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
if panicked.Load() {
|
||||
t.Fatal("resolver panicked due to nil Contents dereference in loadModuleFromSpecificNodeModulesDirectory")
|
||||
}
|
||||
for r := range results {
|
||||
if !r.resolved {
|
||||
t.Fatalf("%q failed to resolve pkg/sub", r.containingFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNodeModuleFromPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
isFolder bool
|
||||
want string
|
||||
}{
|
||||
{"file in package", "/a/node_modules/b/lib/index.d.ts", false, "/a/node_modules/b"},
|
||||
{"file in scoped package", "/a/node_modules/@scope/b/lib/index.d.ts", false, "/a/node_modules/@scope/b"},
|
||||
{"folder subpath", "/a/node_modules/b/lib/File", true, "/a/node_modules/b"},
|
||||
{"folder subpath scoped", "/a/node_modules/@scope/b/lib/File", true, "/a/node_modules/@scope/b"},
|
||||
{"package root folder", "/a/node_modules/b", true, "/a/node_modules/b"},
|
||||
{"scoped package root folder", "/a/node_modules/@scope/b", true, "/a/node_modules/@scope/b"},
|
||||
// A bare scope directory has no package name; must not panic (https://github.com/microsoft/typescript-go/issues/4373).
|
||||
{"scope-only folder", "/a/node_modules/@scope", true, "/a/node_modules/@scope"},
|
||||
{"types scope-only folder", "/a/node_modules/@types", true, "/a/node_modules/@types"},
|
||||
{"not in node_modules", "/a/src/index.ts", false, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := module.ParseNodeModuleFromPath(tt.path, tt.isFolder); got != tt.want {
|
||||
t.Errorf("ParseNodeModuleFromPath(%q, %v) = %q, want %q", tt.path, tt.isFolder, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/microsoft/typescript-go/issues/4478.
|
||||
//
|
||||
// While resolving a package with peerDependencies, two goroutines look up the
|
||||
// peer package's package.json concurrently. A `flipFileExistsFS` forces the
|
||||
// first lookup to cache a nil-Contents entry and the second lookup to receive
|
||||
// that stale entry from `Set`. The resolver must not dereference the peer
|
||||
// package.json contents unless the entry actually exists.
|
||||
func TestResolvePeerDependencyNilContentsRace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const peerPkgJSON = "/repo/node_modules/peer/package.json"
|
||||
files := map[string]string{
|
||||
"/repo/node_modules/pkg/package.json": `{"name":"pkg","version":"1.0.0","types":"index.d.ts","peerDependencies":{"peer":"*"}}`,
|
||||
"/repo/node_modules/pkg/index.d.ts": "export declare const x: number;",
|
||||
peerPkgJSON: `{"name":"peer","version":"2.0.0"}`,
|
||||
"/repo/src/a/file.ts": "",
|
||||
"/repo/src/b/file.ts": "",
|
||||
}
|
||||
fs := &flipFileExistsFS{
|
||||
FS: vfstest.FromMap(files, true),
|
||||
targetPath: peerPkgJSON,
|
||||
firstArrived: make(chan struct{}),
|
||||
secondArrived: make(chan struct{}),
|
||||
firstGate: make(chan struct{}),
|
||||
secondGate: make(chan struct{}),
|
||||
readArrived: make(chan struct{}),
|
||||
readGate: make(chan struct{}),
|
||||
}
|
||||
host := &resolutionHostStub{fs: fs, cwd: "/repo"}
|
||||
opts := &core.CompilerOptions{
|
||||
ModuleResolution: core.ModuleResolutionKindBundler,
|
||||
Module: core.ModuleKindESNext,
|
||||
Target: core.ScriptTargetESNext,
|
||||
}
|
||||
resolver := module.NewResolver(host, opts, "", "")
|
||||
|
||||
var panicked atomic.Bool
|
||||
type resolutionResult struct {
|
||||
containingFile string
|
||||
resolved bool
|
||||
}
|
||||
results := make(chan resolutionResult, 2)
|
||||
var wg sync.WaitGroup
|
||||
for _, containingFile := range []string{"/repo/src/a/file.ts", "/repo/src/b/file.ts"} {
|
||||
wg.Go(func() {
|
||||
resolved := false
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicked.Store(true)
|
||||
}
|
||||
results <- resolutionResult{containingFile: containingFile, resolved: resolved}
|
||||
}()
|
||||
r, _ := resolver.ResolveModuleName("pkg", containingFile, core.ModuleKindESNext, nil)
|
||||
resolved = r.IsResolved()
|
||||
})
|
||||
}
|
||||
|
||||
waitForSignal(t, fs.firstArrived, "first peer package.json FileExists arrival")
|
||||
waitForSignal(t, fs.secondArrived, "second peer package.json FileExists arrival")
|
||||
close(fs.firstGate)
|
||||
var firstResult resolutionResult
|
||||
select {
|
||||
case result := <-results:
|
||||
firstResult = result
|
||||
case <-t.Context().Done():
|
||||
t.Fatal("timed out waiting for first peer package.json lookup to finish")
|
||||
}
|
||||
close(fs.secondGate)
|
||||
waitForSignal(t, fs.readArrived, "peer package.json ReadFile arrival")
|
||||
close(fs.readGate)
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
if panicked.Load() {
|
||||
t.Fatal("resolver panicked due to nil Contents dereference in readPackageJsonPeerDependencies")
|
||||
}
|
||||
if !firstResult.resolved {
|
||||
t.Fatalf("%q failed to resolve pkg", firstResult.containingFile)
|
||||
}
|
||||
for r := range results {
|
||||
if !r.resolved {
|
||||
t.Fatalf("%q failed to resolve pkg", r.containingFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
136
tools/tsgo/internal/module/types.go
Normal file
136
tools/tsgo/internal/module/types.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
type ResolutionHost interface {
|
||||
FS() vfs.FS
|
||||
GetCurrentDirectory() string
|
||||
}
|
||||
|
||||
type ModeAwareCacheKey struct {
|
||||
Name string
|
||||
Mode core.ResolutionMode
|
||||
}
|
||||
|
||||
type ResolvedProjectReference interface {
|
||||
ConfigName() string
|
||||
CompilerOptions() *core.CompilerOptions
|
||||
}
|
||||
|
||||
type NodeResolutionFeatures int32
|
||||
|
||||
const (
|
||||
NodeResolutionFeaturesImports NodeResolutionFeatures = 1 << iota
|
||||
NodeResolutionFeaturesSelfName
|
||||
NodeResolutionFeaturesExports
|
||||
NodeResolutionFeaturesExportsPatternTrailers
|
||||
// allowing `#/` root imports in package.json imports field
|
||||
// not supported until mass adoption - https://github.com/nodejs/node/pull/60864
|
||||
NodeResolutionFeaturesImportsPatternRoot
|
||||
|
||||
NodeResolutionFeaturesNone NodeResolutionFeatures = 0
|
||||
NodeResolutionFeaturesAll = NodeResolutionFeaturesImports | NodeResolutionFeaturesSelfName | NodeResolutionFeaturesExports | NodeResolutionFeaturesExportsPatternTrailers | NodeResolutionFeaturesImportsPatternRoot
|
||||
NodeResolutionFeaturesNode16Default = NodeResolutionFeaturesImports | NodeResolutionFeaturesSelfName | NodeResolutionFeaturesExports | NodeResolutionFeaturesExportsPatternTrailers
|
||||
NodeResolutionFeaturesNodeNextDefault = NodeResolutionFeaturesAll
|
||||
NodeResolutionFeaturesBundlerDefault = NodeResolutionFeaturesImports | NodeResolutionFeaturesSelfName | NodeResolutionFeaturesExports | NodeResolutionFeaturesExportsPatternTrailers | NodeResolutionFeaturesImportsPatternRoot
|
||||
)
|
||||
|
||||
type PackageId struct {
|
||||
Name string
|
||||
SubModuleName string
|
||||
Version string
|
||||
PeerDependencies string
|
||||
}
|
||||
|
||||
func (p *PackageId) String() string {
|
||||
return fmt.Sprintf("%s@%s%s", p.PackageName(), p.Version, p.PeerDependencies)
|
||||
}
|
||||
|
||||
func (p *PackageId) PackageName() string {
|
||||
if p.SubModuleName != "" {
|
||||
return p.Name + "/" + p.SubModuleName
|
||||
}
|
||||
return p.Name
|
||||
}
|
||||
|
||||
type ResolvedModule struct {
|
||||
ResolutionDiagnostics []*ast.Diagnostic
|
||||
ResolvedFileName string
|
||||
OriginalPath string
|
||||
Extension string
|
||||
ResolvedUsingTsExtension bool
|
||||
PackageId PackageId
|
||||
IsExternalLibraryImport bool
|
||||
AlternateResult string
|
||||
}
|
||||
|
||||
func (r *ResolvedModule) IsResolved() bool {
|
||||
return r != nil && r.ResolvedFileName != ""
|
||||
}
|
||||
|
||||
type ResolvedTypeReferenceDirective struct {
|
||||
ResolutionDiagnostics []*ast.Diagnostic
|
||||
Primary bool
|
||||
ResolvedFileName string
|
||||
OriginalPath string
|
||||
PackageId PackageId
|
||||
IsExternalLibraryImport bool
|
||||
}
|
||||
|
||||
func (r *ResolvedTypeReferenceDirective) IsResolved() bool {
|
||||
return r.ResolvedFileName != ""
|
||||
}
|
||||
|
||||
type extensions int32
|
||||
|
||||
const (
|
||||
extensionsTypeScript extensions = 1 << iota
|
||||
extensionsJavaScript
|
||||
extensionsDeclaration
|
||||
extensionsJson
|
||||
|
||||
extensionsImplementationFiles = extensionsTypeScript | extensionsJavaScript
|
||||
)
|
||||
|
||||
func (e extensions) String() string {
|
||||
result := make([]string, 0, bits.OnesCount(uint(e)))
|
||||
if e&extensionsTypeScript != 0 {
|
||||
result = append(result, "TypeScript")
|
||||
}
|
||||
if e&extensionsJavaScript != 0 {
|
||||
result = append(result, "JavaScript")
|
||||
}
|
||||
if e&extensionsDeclaration != 0 {
|
||||
result = append(result, "Declaration")
|
||||
}
|
||||
if e&extensionsJson != 0 {
|
||||
result = append(result, "JSON")
|
||||
}
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func (e extensions) Array() []string {
|
||||
result := []string{}
|
||||
if e&extensionsTypeScript != 0 {
|
||||
result = append(result, tspath.SupportedTSImplementationExtensions...)
|
||||
}
|
||||
if e&extensionsJavaScript != 0 {
|
||||
result = append(result, tspath.SupportedJSExtensionsFlat...)
|
||||
}
|
||||
if e&extensionsDeclaration != 0 {
|
||||
result = append(result, tspath.SupportedDeclarationExtensions...)
|
||||
}
|
||||
if e&extensionsJson != 0 {
|
||||
result = append(result, tspath.ExtensionJson)
|
||||
}
|
||||
return result
|
||||
}
|
||||
197
tools/tsgo/internal/module/util.go
Normal file
197
tools/tsgo/internal/module/util.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/semver"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
var typeScriptVersion = semver.MustParse(core.Version())
|
||||
|
||||
const InferredTypesContainingFile = "__inferred type names__.ts"
|
||||
|
||||
func IsApplicableVersionedTypesKey(key string) bool {
|
||||
if !strings.HasPrefix(key, "types@") {
|
||||
return false
|
||||
}
|
||||
range_, ok := semver.TryParseVersionRange(key[len("types@"):])
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return range_.Test(&typeScriptVersion)
|
||||
}
|
||||
|
||||
func ParseNodeModuleFromPath(resolved string, isFolder bool) string {
|
||||
path := tspath.NormalizePath(resolved)
|
||||
idx := strings.LastIndex(path, "/node_modules/")
|
||||
if idx == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
indexAfterNodeModules := idx + len("/node_modules/")
|
||||
indexAfterPackageName := moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules, isFolder)
|
||||
if path[indexAfterNodeModules] == '@' {
|
||||
indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName, isFolder)
|
||||
}
|
||||
return path[:indexAfterPackageName]
|
||||
}
|
||||
|
||||
func ParsePackageName(moduleName string) (packageName, rest string) {
|
||||
idx := strings.Index(moduleName, "/")
|
||||
if len(moduleName) > 0 && moduleName[0] == '@' {
|
||||
offset := idx + 1
|
||||
idx = strings.Index(moduleName[offset:], "/")
|
||||
if idx != -1 {
|
||||
idx += offset
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
return moduleName, ""
|
||||
}
|
||||
return moduleName[:idx], moduleName[idx+1:]
|
||||
}
|
||||
|
||||
func MangleScopedPackageName(packageName string) string {
|
||||
if len(packageName) > 0 && packageName[0] == '@' {
|
||||
idx := strings.Index(packageName, "/")
|
||||
if idx == -1 {
|
||||
return packageName
|
||||
}
|
||||
return packageName[1:idx] + "__" + packageName[idx+1:]
|
||||
}
|
||||
return packageName
|
||||
}
|
||||
|
||||
func UnmangleScopedPackageName(packageName string) string {
|
||||
before, after, ok := strings.Cut(packageName, "__")
|
||||
if ok {
|
||||
return "@" + before + "/" + after
|
||||
}
|
||||
return packageName
|
||||
}
|
||||
|
||||
func GetTypesPackageName(packageName string) string {
|
||||
return "@types/" + MangleScopedPackageName(packageName)
|
||||
}
|
||||
|
||||
func GetPackageNameFromTypesPackageName(mangledName string) string {
|
||||
withoutAtTypePrefix := strings.TrimPrefix(mangledName, "@types/")
|
||||
if withoutAtTypePrefix != mangledName {
|
||||
return UnmangleScopedPackageName(withoutAtTypePrefix)
|
||||
}
|
||||
return mangledName
|
||||
}
|
||||
|
||||
func ComparePatternKeys(a, b string) int {
|
||||
aPatternIndex := strings.Index(a, "*")
|
||||
bPatternIndex := strings.Index(b, "*")
|
||||
baseLenA := len(a)
|
||||
if aPatternIndex != -1 {
|
||||
baseLenA = aPatternIndex + 1
|
||||
}
|
||||
baseLenB := len(b)
|
||||
if bPatternIndex != -1 {
|
||||
baseLenB = bPatternIndex + 1
|
||||
}
|
||||
|
||||
if baseLenA > baseLenB {
|
||||
return -1
|
||||
}
|
||||
if baseLenB > baseLenA {
|
||||
return 1
|
||||
}
|
||||
if aPatternIndex == -1 {
|
||||
return 1
|
||||
}
|
||||
if bPatternIndex == -1 {
|
||||
return -1
|
||||
}
|
||||
if len(a) > len(b) {
|
||||
return -1
|
||||
}
|
||||
if len(b) > len(a) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Returns a DiagnosticMessage if we won't include a resolved module due to its extension.
|
||||
// The DiagnosticMessage's parameters are the imported module name, and the filename it resolved to.
|
||||
// This returns a diagnostic even if the module will be an untyped module.
|
||||
func GetResolutionDiagnostic(options *core.CompilerOptions, resolvedModule *ResolvedModule, file *ast.SourceFile) *diagnostics.Message {
|
||||
needJsx := func() *diagnostics.Message {
|
||||
if options.Jsx != core.JsxEmitNone {
|
||||
return nil
|
||||
}
|
||||
return diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set
|
||||
}
|
||||
|
||||
needAllowJs := func() *diagnostics.Message {
|
||||
if options.GetAllowJS() || !options.NoImplicitAny.DefaultIfUnknown(options.Strict).IsTrue() {
|
||||
return nil
|
||||
}
|
||||
return diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type
|
||||
}
|
||||
|
||||
needResolveJsonModule := func() *diagnostics.Message {
|
||||
if options.GetResolveJsonModule() {
|
||||
return nil
|
||||
}
|
||||
return diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used
|
||||
}
|
||||
|
||||
needAllowArbitraryExtensions := func() *diagnostics.Message {
|
||||
if file.IsDeclarationFile || options.AllowArbitraryExtensions.IsTrue() {
|
||||
return nil
|
||||
}
|
||||
return diagnostics.Module_0_was_resolved_to_1_but_allowArbitraryExtensions_is_not_set
|
||||
}
|
||||
|
||||
switch resolvedModule.Extension {
|
||||
case tspath.ExtensionTs, tspath.ExtensionDts,
|
||||
tspath.ExtensionMts, tspath.ExtensionDmts,
|
||||
tspath.ExtensionCts, tspath.ExtensionDcts:
|
||||
// These are always allowed.
|
||||
return nil
|
||||
case tspath.ExtensionTsx:
|
||||
return needJsx()
|
||||
case tspath.ExtensionJsx:
|
||||
if message := needJsx(); message != nil {
|
||||
return message
|
||||
}
|
||||
return needAllowJs()
|
||||
case tspath.ExtensionJs, tspath.ExtensionMjs, tspath.ExtensionCjs:
|
||||
return needAllowJs()
|
||||
case tspath.ExtensionJson:
|
||||
return needResolveJsonModule()
|
||||
default:
|
||||
return needAllowArbitraryExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
// TryGetJSExtensionForFile maps TS/JS/DTS extensions to the output JS-side extension.
|
||||
// Returns an empty string if the extension is unsupported.
|
||||
func TryGetJSExtensionForFile(fileName string, options *core.CompilerOptions) string {
|
||||
ext := tspath.TryGetExtensionFromPath(fileName)
|
||||
switch ext {
|
||||
case tspath.ExtensionTs, tspath.ExtensionDts:
|
||||
return tspath.ExtensionJs
|
||||
case tspath.ExtensionTsx:
|
||||
if options.Jsx == core.JsxEmitPreserve {
|
||||
return tspath.ExtensionJsx
|
||||
}
|
||||
return tspath.ExtensionJs
|
||||
case tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionJson:
|
||||
return ext
|
||||
case tspath.ExtensionDmts, tspath.ExtensionMts, tspath.ExtensionMjs:
|
||||
return tspath.ExtensionMjs
|
||||
case tspath.ExtensionDcts, tspath.ExtensionCts, tspath.ExtensionCjs:
|
||||
return tspath.ExtensionCjs
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user