diff --git a/_extension/src/client.ts b/_extension/src/client.ts index b759501e97..916bedec52 100644 --- a/_extension/src/client.ts +++ b/_extension/src/client.ts @@ -28,7 +28,14 @@ export class Client { this.clientOptions = { documentSelector: [ ...jsTsLanguageModes.map(language => ({ scheme: "file", language })), - ...jsTsLanguageModes.map(language => ({ scheme: "untitled", language })), + ...jsTsLanguageModes.map(language => ({ + scheme: "untitled", + language, + })), + ...jsTsLanguageModes.map(language => ({ + scheme: "zip", + language, + })), ], outputChannel: this.outputChannel, traceOutputChannel: this.traceOutputChannel, diff --git a/cmd/tsgo/sys.go b/cmd/tsgo/sys.go index e5b2b8d569..e423fff24a 100644 --- a/cmd/tsgo/sys.go +++ b/cmd/tsgo/sys.go @@ -8,9 +8,11 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/execute/tsc" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "golang.org/x/term" ) @@ -20,6 +22,7 @@ type osSys struct { defaultLibraryPath string cwd string start time.Time + pnpApi *pnp.PnpApi } func (s *osSys) SinceStart() time.Duration { @@ -59,6 +62,10 @@ func (s *osSys) GetEnvironmentVariable(name string) string { return os.Getenv(name) } +func (s *osSys) PnpApi() *pnp.PnpApi { + return s.pnpApi +} + func newSystem() *osSys { cwd, err := os.Getwd() if err != nil { @@ -66,11 +73,19 @@ func newSystem() *osSys { os.Exit(int(tsc.ExitStatusInvalidProject_OutputsSkipped)) } + var fs vfs.FS = osvfs.FS() + + pnpApi := pnp.InitPnpApi(fs, tspath.NormalizePath(cwd)) + if pnpApi != nil { + fs = pnpvfs.From(fs) + } + return &osSys{ cwd: tspath.NormalizePath(cwd), - fs: bundled.WrapFS(osvfs.FS()), + fs: bundled.WrapFS(fs), defaultLibraryPath: bundled.LibPath(), writer: os.Stdout, start: time.Now(), + pnpApi: pnpApi, } } diff --git a/internal/api/server.go b/internal/api/server.go index 5a3ce4213b..68e4a9141c 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -15,10 +15,12 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" ) //go:generate go tool golang.org/x/tools/cmd/stringer -type=MessageType -output=stringer_generated.go @@ -93,12 +95,19 @@ func NewServer(options *ServerOptions) *Server { panic("Cwd is required") } + var fs vfs.FS = osvfs.FS() + + pnpApi := pnp.InitPnpApi(fs, options.Cwd) + if pnpApi != nil { + fs = pnpvfs.From(fs) + } + server := &Server{ r: bufio.NewReader(options.In), w: bufio.NewWriter(options.Out), stderr: options.Err, cwd: options.Cwd, - fs: bundled.WrapFS(osvfs.FS()), + fs: bundled.WrapFS(fs), defaultLibraryPath: options.DefaultLibraryPath, } logger := logging.NewLogger(options.Err) diff --git a/internal/checker/checker_test.go b/internal/checker/checker_test.go index c5029ecbc9..c9041c6606 100644 --- a/internal/checker/checker_test.go +++ b/internal/checker/checker_test.go @@ -36,7 +36,7 @@ foo.bar;` fs = bundled.WrapFS(fs) cd := "/" - host := compiler.NewCompilerHost(cd, fs, bundled.LibPath(), nil, nil) + host := compiler.NewCompilerHost(cd, fs, bundled.LibPath(), nil, nil, nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, host, nil) assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line") @@ -70,7 +70,7 @@ func TestCheckSrcCompiler(t *testing.T) { rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler") - host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil) + host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil, nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, host, nil) assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line") p := compiler.NewProgram(compiler.ProgramOptions{ @@ -87,7 +87,7 @@ func BenchmarkNewChecker(b *testing.B) { rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler") - host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil) + host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil, nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, host, nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") p := compiler.NewProgram(compiler.ProgramOptions{ diff --git a/internal/compiler/emitHost.go b/internal/compiler/emitHost.go index fcc0406342..b408ee7f5c 100644 --- a/internal/compiler/emitHost.go +++ b/internal/compiler/emitHost.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/outputpaths" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/transformers/declarations" "github.com/microsoft/typescript-go/internal/tsoptions" @@ -24,6 +25,7 @@ type EmitHost interface { GetCurrentDirectory() string CommonSourceDirectory() string IsEmitBlocked(file string) bool + PnpApi() *pnp.PnpApi } var _ EmitHost = (*emitHost)(nil) @@ -107,6 +109,7 @@ func (host *emitHost) Options() *core.CompilerOptions { return host.program.Opti func (host *emitHost) SourceFiles() []*ast.SourceFile { return host.program.SourceFiles() } func (host *emitHost) GetCurrentDirectory() string { return host.program.GetCurrentDirectory() } func (host *emitHost) CommonSourceDirectory() string { return host.program.CommonSourceDirectory() } +func (host *emitHost) PnpApi() *pnp.PnpApi { return host.program.PnpApi() } func (host *emitHost) UseCaseSensitiveFileNames() bool { return host.program.UseCaseSensitiveFileNames() } diff --git a/internal/compiler/host.go b/internal/compiler/host.go index 68f3cf620a..e6eea42955 100644 --- a/internal/compiler/host.go +++ b/internal/compiler/host.go @@ -4,6 +4,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" @@ -17,6 +18,7 @@ type CompilerHost interface { Trace(msg string) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine + PnpApi() *pnp.PnpApi } var _ CompilerHost = (*compilerHost)(nil) @@ -27,6 +29,7 @@ type compilerHost struct { defaultLibraryPath string extendedConfigCache tsoptions.ExtendedConfigCache trace func(msg string) + pnpApi *pnp.PnpApi } func NewCachedFSCompilerHost( @@ -34,9 +37,10 @@ func NewCachedFSCompilerHost( fs vfs.FS, defaultLibraryPath string, extendedConfigCache tsoptions.ExtendedConfigCache, + pnpApi *pnp.PnpApi, trace func(msg string), ) CompilerHost { - return NewCompilerHost(currentDirectory, cachedvfs.From(fs), defaultLibraryPath, extendedConfigCache, trace) + return NewCompilerHost(currentDirectory, cachedvfs.From(fs), defaultLibraryPath, extendedConfigCache, pnpApi, trace) } func NewCompilerHost( @@ -44,17 +48,20 @@ func NewCompilerHost( fs vfs.FS, defaultLibraryPath string, extendedConfigCache tsoptions.ExtendedConfigCache, + pnpApi *pnp.PnpApi, trace func(msg string), ) CompilerHost { if trace == nil { trace = func(msg string) {} } + return &compilerHost{ currentDirectory: currentDirectory, fs: fs, defaultLibraryPath: defaultLibraryPath, extendedConfigCache: extendedConfigCache, trace: trace, + pnpApi: pnpApi, } } @@ -70,6 +77,10 @@ func (h *compilerHost) GetCurrentDirectory() string { return h.currentDirectory } +func (h *compilerHost) PnpApi() *pnp.PnpApi { + return h.pnpApi +} + func (h *compilerHost) Trace(msg string) { h.trace(msg) } diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 794d2fa96d..236974c5ce 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -20,6 +20,7 @@ import ( "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/sourcemap" @@ -78,6 +79,11 @@ func (p *Program) GetCurrentDirectory() string { return p.Host().GetCurrentDirectory() } +// PnpApi implements checker.Program. +func (p *Program) PnpApi() *pnp.PnpApi { + return p.Host().PnpApi() +} + // GetGlobalTypingsCacheLocation implements checker.Program. func (p *Program) GetGlobalTypingsCacheLocation() string { return "" // !!! see src/tsserver/nodeServer.ts for strada's node-specific implementation diff --git a/internal/compiler/program_test.go b/internal/compiler/program_test.go index ae3a967b30..7d9a6f604b 100644 --- a/internal/compiler/program_test.go +++ b/internal/compiler/program_test.go @@ -243,7 +243,7 @@ func TestProgram(t *testing.T) { CompilerOptions: &opts, }, }, - Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil), + Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil, nil), }) actualFiles := []string{} @@ -280,7 +280,7 @@ func BenchmarkNewProgram(b *testing.B) { CompilerOptions: &opts, }, }, - Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil), + Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil, nil), } for b.Loop() { @@ -297,7 +297,7 @@ func BenchmarkNewProgram(b *testing.B) { fs := osvfs.FS() fs = bundled.WrapFS(fs) - host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil) + host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil, nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), nil, host, nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") diff --git a/internal/compiler/projectreferencedtsfakinghost.go b/internal/compiler/projectreferencedtsfakinghost.go index 916dce2225..b88482c838 100644 --- a/internal/compiler/projectreferencedtsfakinghost.go +++ b/internal/compiler/projectreferencedtsfakinghost.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" @@ -42,6 +43,11 @@ func (h *projectReferenceDtsFakingHost) GetCurrentDirectory() string { return h.host.GetCurrentDirectory() } +// PnpApi implements module.ResolutionHost. +func (h *projectReferenceDtsFakingHost) PnpApi() *pnp.PnpApi { + return h.host.PnpApi() +} + type projectReferenceDtsFakingVfs struct { projectReferenceFileMapper *projectReferenceFileMapper dtsDirectories collections.Set[tspath.Path] diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index ab1936ca76..17b51ba5c3 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -300,7 +301,7 @@ func (options *CompilerOptions) GetStrictOptionValue(value Tristate) bool { return options.Strict == TSTrue } -func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) (result []string, fromConfig bool) { +func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string, pnpApi *pnp.PnpApi) (result []string, fromConfig bool) { if options.TypeRoots != nil { return options.TypeRoots, true } @@ -316,6 +317,17 @@ func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) ( } } + nmTypes, nmFromConfig := options.GetNodeModulesTypeRoots(baseDir) + + if pnpApi != nil { + typeRoots, fromConfig := pnpApi.AppendPnpTypeRoots(nmTypes, baseDir, nmFromConfig) + return typeRoots, fromConfig + } + + return nmTypes, nmFromConfig +} + +func (options *CompilerOptions) GetNodeModulesTypeRoots(baseDir string) (result []string, fromConfig bool) { typeRoots := make([]string, 0, strings.Count(baseDir, "/")) tspath.ForEachAncestorDirectory(baseDir, func(dir string) (any, bool) { typeRoots = append(typeRoots, tspath.CombinePaths(dir, "node_modules", "@types")) diff --git a/internal/execute/build/compilerHost.go b/internal/execute/build/compilerHost.go index f11f06b9fc..4c6189d245 100644 --- a/internal/execute/build/compilerHost.go +++ b/internal/execute/build/compilerHost.go @@ -3,6 +3,7 @@ package build import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" @@ -27,6 +28,10 @@ func (h *compilerHost) GetCurrentDirectory() string { return h.host.GetCurrentDirectory() } +func (h *compilerHost) PnpApi() *pnp.PnpApi { + return h.host.PnpApi() +} + func (h *compilerHost) Trace(msg string) { h.trace(msg) } diff --git a/internal/execute/build/host.go b/internal/execute/build/host.go index 91f50aa59c..451e7afce4 100644 --- a/internal/execute/build/host.go +++ b/internal/execute/build/host.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/execute/incremental" "github.com/microsoft/typescript-go/internal/execute/tsc" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" @@ -45,6 +46,10 @@ func (h *host) GetCurrentDirectory() string { return h.host.GetCurrentDirectory() } +func (h *host) PnpApi() *pnp.PnpApi { + return h.host.PnpApi() +} + func (h *host) Trace(msg string) { panic("build.Orchestrator.host does not support tracing, use a different host for tracing") } diff --git a/internal/execute/build/orchestrator.go b/internal/execute/build/orchestrator.go index d521031780..27d138c151 100644 --- a/internal/execute/build/orchestrator.go +++ b/internal/execute/build/orchestrator.go @@ -387,6 +387,7 @@ func NewOrchestrator(opts Options) *Orchestrator { orchestrator.opts.Sys.FS(), orchestrator.opts.Sys.DefaultLibraryPath(), nil, + orchestrator.opts.Sys.PnpApi(), nil, ), mTimes: &collections.SyncMap[tspath.Path, time.Time]{}, diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index 65bf8d43bb..e85adb835d 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -245,7 +245,7 @@ func performIncrementalCompilation( compileTimes *tsc.CompileTimes, testing tsc.CommandLineTesting, ) tsc.CommandLineResult { - host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(sys, testing)) + host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache, sys.PnpApi(), getTraceFromSys(sys, testing)) buildInfoReadStart := sys.Now() oldProgram := incremental.ReadBuildInfoProgram(config, incremental.NewBuildInfoReader(host), host) compileTimes.BuildInfoReadTime = sys.Now().Sub(buildInfoReadStart) @@ -288,7 +288,7 @@ func performCompilation( compileTimes *tsc.CompileTimes, testing tsc.CommandLineTesting, ) tsc.CommandLineResult { - host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(sys, testing)) + host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache, sys.PnpApi(), getTraceFromSys(sys, testing)) // todo: cache, statistics, tracing parseStart := sys.Now() program := compiler.NewProgram(compiler.ProgramOptions{ diff --git a/internal/execute/tsc/compile.go b/internal/execute/tsc/compile.go index 4adc1df9e9..d17566b2c1 100644 --- a/internal/execute/tsc/compile.go +++ b/internal/execute/tsc/compile.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/execute/incremental" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" ) @@ -21,6 +22,8 @@ type System interface { GetWidthOfTerminal() int GetEnvironmentVariable(name string) string + PnpApi() *pnp.PnpApi + Now() time.Time SinceStart() time.Duration } diff --git a/internal/execute/tsctests/sys.go b/internal/execute/tsctests/sys.go index 199fce8e76..dc2322765f 100644 --- a/internal/execute/tsctests/sys.go +++ b/internal/execute/tsctests/sys.go @@ -16,12 +16,14 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/execute/incremental" "github.com/microsoft/typescript-go/internal/execute/tsc" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/testutil/harnessutil" "github.com/microsoft/typescript-go/internal/testutil/stringtestutil" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/iovfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" ) @@ -86,13 +88,14 @@ func (t *TestClock) SinceStart() time.Duration { func NewTscSystem(files FileMap, useCaseSensitiveFileNames bool, cwd string) *testSys { clock := &TestClock{start: time.Now()} - return &testSys{ - fs: &testFs{ - FS: vfstest.FromMapWithClock(files, useCaseSensitiveFileNames, clock), - }, - cwd: cwd, - clock: clock, + var fs vfs.FS = vfstest.FromMapWithClock(files, useCaseSensitiveFileNames, clock) + + pnpApi := pnp.InitPnpApi(fs, tspath.NormalizePath(cwd)) + if pnpApi != nil { + fs = pnpvfs.From(fs) } + + return &testSys{fs: &testFs{FS: fs}, cwd: cwd, clock: clock, pnpApi: pnpApi} } func newTestSys(tscInput *tscInput, forIncrementalCorrectness bool) *testSys { @@ -151,6 +154,8 @@ type testSys struct { cwd string env map[string]string clock *TestClock + + pnpApi *pnp.PnpApi } var ( @@ -200,6 +205,10 @@ func (s *testSys) GetCurrentDirectory() string { return s.cwd } +func (s *testSys) PnpApi() *pnp.PnpApi { + return s.pnpApi +} + func (s *testSys) Writer() io.Writer { return s.currentWrite } diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 1c1a97b238..5c8f332168 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -45,7 +45,7 @@ func createWatcher(sys tsc.System, configParseResult *tsoptions.ParsedCommandLin } func (w *Watcher) start() { - w.host = compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), nil, getTraceFromSys(w.sys, w.testing)) + w.host = compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), nil, w.sys.PnpApi(), getTraceFromSys(w.sys, w.testing)) w.program = incremental.ReadBuildInfoProgram(w.config, incremental.NewBuildInfoReader(w.host), w.host) if w.testing == nil { @@ -122,7 +122,7 @@ func (w *Watcher) hasErrorsInTsConfig() bool { } w.config = configParseResult } - w.host = compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(w.sys, w.testing)) + w.host = compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), extendedConfigCache, w.sys.PnpApi(), getTraceFromSys(w.sys, w.testing)) return false } diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index ec9ef60d6b..9902a3d69b 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -420,6 +420,11 @@ func (l *LanguageService) isImportable( // } fromPath := fromFile.FileName() + pnpApi := l.host.PnpApi() + if pnpApi != nil { + return pnpApi.IsImportable(fromPath, toFile.FileName()) + } + useCaseSensitiveFileNames := moduleSpecifierResolutionHost.UseCaseSensitiveFileNames() globalTypingsCache := l.GetProgram().GetGlobalTypingsCacheLocation() modulePaths := modulespecifiers.GetEachFileNameOfModule( diff --git a/internal/ls/host.go b/internal/ls/host.go index 8f517b787c..daeffd7711 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -4,10 +4,12 @@ import ( "github.com/microsoft/typescript-go/internal/format" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/sourcemap" ) type Host interface { + PnpApi() *pnp.PnpApi UseCaseSensitiveFileNames() bool ReadFile(path string) (contents string, ok bool) Converters() *lsconv.Converters diff --git a/internal/ls/lsconv/converters.go b/internal/ls/lsconv/converters.go index 3b290b446c..dc85df69d0 100644 --- a/internal/ls/lsconv/converters.go +++ b/internal/ls/lsconv/converters.go @@ -128,7 +128,14 @@ func FileNameToDocumentURI(fileName string) lsproto.DocumentUri { parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part)) } - return lsproto.DocumentUri("file://" + volume + strings.Join(parts, "/")) + var prefix string + if tspath.IsZipPath(fileName) { + prefix = "zip:" + } else { + prefix = "file:" + } + + return lsproto.DocumentUri(prefix + "//" + volume + strings.Join(parts, "/")) } func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter lsproto.Position) core.TextPos { diff --git a/internal/ls/lsconv/converters_test.go b/internal/ls/lsconv/converters_test.go index 30babd1df5..f3492a8dbb 100644 --- a/internal/ls/lsconv/converters_test.go +++ b/internal/ls/lsconv/converters_test.go @@ -31,6 +31,9 @@ func TestDocumentURIToFileName(t *testing.T) { {"file://localhost/c%24/GitDevelopment/express", "//localhost/c$/GitDevelopment/express"}, {"file:///c%3A/test%20with%20%2525/c%23code", "c:/test with %25/c#code"}, + {"zip:///path/to/archive.zip/file.ts", "/path/to/archive.zip/file.ts"}, + {"zip:///d:/work/tsgo932/lib/archive.zip/utils.ts", "d:/work/tsgo932/lib/archive.zip/utils.ts"}, + {"untitled:Untitled-1", "^/untitled/ts-nul-authority/Untitled-1"}, {"untitled:Untitled-1#fragment", "^/untitled/ts-nul-authority/Untitled-1#fragment"}, {"untitled:c:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"}, @@ -69,6 +72,9 @@ func TestFileNameToDocumentURI(t *testing.T) { {"//localhost/c$/GitDevelopment/express", "file://localhost/c%24/GitDevelopment/express"}, {"c:/test with %25/c#code", "file:///c%3A/test%20with%20%2525/c%23code"}, + {"/path/to/archive.zip/file.ts", "zip:///path/to/archive.zip/file.ts"}, + {"d:/work/tsgo932/lib/archive.zip/utils.ts", "zip:///d%3A/work/tsgo932/lib/archive.zip/utils.ts"}, + {"^/untitled/ts-nul-authority/Untitled-1", "untitled:Untitled-1"}, {"^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt", "untitled:c:/Users/jrieken/Code/abc.txt"}, {"^/untitled/ts-nul-authority///wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"}, diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 749ad880c6..eaee300342 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -522,6 +522,7 @@ func getStringLiteralCompletionsFromModuleNames( program *compiler.Program, ) *stringLiteralCompletions { // !!! needs `getModeForUsageLocationWorker` + // TODO investigate if we will need to update this for pnp, once available return nil } diff --git a/internal/lsp/lsproto/lsp.go b/internal/lsp/lsproto/lsp.go index dd172077c2..ea59298bcb 100644 --- a/internal/lsp/lsproto/lsp.go +++ b/internal/lsp/lsproto/lsp.go @@ -14,7 +14,7 @@ import ( type DocumentUri string // !!! func (uri DocumentUri) FileName() string { - if strings.HasPrefix(string(uri), "file://") { + if strings.HasPrefix(string(uri), "file://") || strings.HasPrefix(string(uri), "zip:") { parsed := core.Must(url.Parse(string(uri))) if parsed.Host != "" { return "//" + parsed.Host + parsed.Path diff --git a/internal/lsp/server.go b/internal/lsp/server.go index d5a2aca5a3..02f5ab7fe1 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -18,11 +18,13 @@ import ( "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "golang.org/x/sync/errgroup" "golang.org/x/text/language" ) @@ -701,6 +703,12 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali cwd = s.cwd } + fs := s.fs + pnpApi := pnp.InitPnpApi(fs, cwd) + if pnpApi != nil { + fs = pnpvfs.From(fs) + } + s.session = project.NewSession(&project.SessionInit{ Options: &project.SessionOptions{ CurrentDirectory: cwd, @@ -711,7 +719,7 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali LoggingEnabled: true, DebounceDelay: 500 * time.Millisecond, }, - FS: s.fs, + FS: fs, Logger: s.logger, Client: s, NpmExecutor: s, diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 1130604759..0f31ac568c 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -207,7 +207,7 @@ func (r *Resolver) ResolveTypeReferenceDirective( compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) containingDirectory := tspath.GetDirectoryPath(containingFile) - typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory()) + typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory(), r.host.PnpApi()) if traceBuilder != nil { traceBuilder.write(diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2.Format(typeReferenceDirectiveName, containingFile, strings.Join(typeRoots, ","))) traceBuilder.traceResolutionUsingProjectReference(redirectedReference) @@ -474,7 +474,7 @@ func (r *resolutionState) resolveNodeLikeWorker() *ResolvedModule { resolved := r.nodeLoadModuleByRelativeName(r.extensions, candidate, false, true) return r.createResolvedModule( resolved, - resolved != nil && strings.Contains(resolved.path, "/node_modules/"), + resolved != nil && (tspath.IsExternalLibraryImport(resolved.path)), ) } return r.createResolvedModule(nil, false) @@ -913,6 +913,11 @@ func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOn } func (r *resolutionState) loadModuleFromNearestNodeModulesDirectoryWorker(ext extensions, mode core.ResolutionMode, typesScopeOnly bool) *resolved { + if r.resolver.host.PnpApi() != nil { + // !!! stop at global cache + return r.loadModuleFromImmediateNodeModulesDirectoryPnP(ext, r.containingDirectory, typesScopeOnly) + } + result, _ := tspath.ForEachAncestorDirectory( r.containingDirectory, func(directory string) (result *resolved, stop bool) { @@ -952,11 +957,52 @@ func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions return continueSearching() } +/* +With Plug and Play, we directly resolve the path of the moduleName using the PnP API, instead of searching for it in the node_modules directory + +See github.com/microsoft/typescript-go/internal/pnp package for more details +*/ +func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectoryPnP(extensions extensions, directory string, typesScopeOnly bool) *resolved { + if !typesScopeOnly { + if packageResult := r.loadModuleFromPnpResolution(extensions, r.name, directory); !packageResult.shouldContinueSearching() { + return packageResult + } + } + + if extensions&extensionsDeclaration != 0 { + result := r.loadModuleFromPnpResolution(extensionsDeclaration, "@types/"+r.mangleScopedPackageName(r.name), directory) + + return result + } + + return nil +} + +func (r *resolutionState) loadModuleFromPnpResolution(ext extensions, moduleName string, issuer string) *resolved { + pnpApi := r.resolver.host.PnpApi() + + if pnpApi != nil { + packageName, rest := ParsePackageName(moduleName) + // TODO: bubble up yarn resolution errors, instead of _ + packageDirectory, _ := pnpApi.ResolveToUnqualified(packageName, issuer) + if packageDirectory != "" { + candidate := tspath.NormalizePath(tspath.CombinePaths(packageDirectory, rest)) + return r.loadModuleFromSpecificNodeModulesDirectoryImpl(ext, true /* nodeModulesDirectoryExists */, candidate, rest, packageDirectory) + } + } + + return nil +} + func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensions, moduleName string, nodeModulesDirectory string, nodeModulesDirectoryExists bool) *resolved { candidate := tspath.NormalizePath(tspath.CombinePaths(nodeModulesDirectory, moduleName)) packageName, rest := ParsePackageName(moduleName) packageDirectory := tspath.CombinePaths(nodeModulesDirectory, packageName) + return r.loadModuleFromSpecificNodeModulesDirectoryImpl(ext, nodeModulesDirectoryExists, candidate, rest, packageDirectory) +} + +func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectoryImpl(ext extensions, nodeModulesDirectoryExists bool, candidate string, rest string, packageDirectory string) *resolved { var rootPackageInfo *packagejson.InfoCacheEntry // First look for a nested package.json, as in `node_modules/foo/bar/package.json` packageInfo := r.getPackageJsonInfo(candidate, !nodeModulesDirectoryExists) @@ -1035,7 +1081,7 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensi } func (r *resolutionState) createResolvedModuleHandlingSymlink(resolved *resolved) *ResolvedModule { - isExternalLibraryImport := resolved != nil && strings.Contains(resolved.path, "/node_modules/") + isExternalLibraryImport := resolved != nil && (tspath.IsExternalLibraryImport(resolved.path)) if r.compilerOptions.PreserveSymlinks != core.TSTrue && isExternalLibraryImport && resolved.originalPath == "" && @@ -1083,7 +1129,7 @@ func (r *resolutionState) createResolvedTypeReferenceDirective(resolved *resolve resolvedTypeReferenceDirective.ResolvedFileName = resolved.path resolvedTypeReferenceDirective.Primary = primary resolvedTypeReferenceDirective.PackageId = resolved.packageId - resolvedTypeReferenceDirective.IsExternalLibraryImport = strings.Contains(resolved.path, "/node_modules/") + resolvedTypeReferenceDirective.IsExternalLibraryImport = tspath.IsExternalLibraryImport(resolved.path) if r.compilerOptions.PreserveSymlinks != core.TSTrue { originalPath, resolvedFileName := r.getOriginalAndResolvedFileName(resolved.path) @@ -1739,8 +1785,19 @@ func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packa } nodeModules := packageDirectory[:nodeModulesIndex+len("/node_modules")] + "/" builder := strings.Builder{} + pnpApi := r.resolver.host.PnpApi() for name := range peerDependencies.Value { - peerPackageJson := r.getPackageJsonInfo(nodeModules+name /*onlyRecordFailures*/, false) + var peerDependencyPath string + + if pnpApi != nil { + peerDependencyPath, _ = pnpApi.ResolveToUnqualified(name, packageDirectory) + } + + if peerDependencyPath == "" { + peerDependencyPath = nodeModules + name + } + + peerPackageJson := r.getPackageJsonInfo(peerDependencyPath, false /*onlyRecordFailures*/) if peerPackageJson != nil { version := peerPackageJson.Contents.Version.Value builder.WriteString("+") @@ -1983,7 +2040,7 @@ func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, host Resoluti } var result []string - typeRoots, _ := options.GetEffectiveTypeRoots(host.GetCurrentDirectory()) + typeRoots, _ := options.GetEffectiveTypeRoots(host.GetCurrentDirectory(), host.PnpApi()) for _, root := range typeRoots { if host.FS().DirectoryExists(root) { for _, typeDirectivePath := range host.FS().GetAccessibleEntries(root).Directories { diff --git a/internal/module/resolver_test.go b/internal/module/resolver_test.go index 37330dc8d5..a7917a0979 100644 --- a/internal/module/resolver_test.go +++ b/internal/module/resolver_test.go @@ -14,10 +14,12 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/jsonutil" "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/repo" "github.com/microsoft/typescript-go/internal/testutil/baseline" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" @@ -144,6 +146,7 @@ type vfsModuleResolutionHost struct { fs vfs.FS currentDirectory string traces []string + pnpApi *pnp.PnpApi } func fixRoot(path string) string { @@ -164,9 +167,17 @@ func newVFSModuleResolutionHost(files map[string]string, currentDirectory string } else if currentDirectory[0] != '/' { currentDirectory = "/.src/" + currentDirectory } + + var mapFS vfs.FS = vfstest.FromMap(fs, true /*useCaseSensitiveFileNames*/) + pnpApi := pnp.InitPnpApi(mapFS, tspath.NormalizePath(currentDirectory)) + if pnpApi != nil { + mapFS = pnpvfs.From(mapFS) + } + return &vfsModuleResolutionHost{ - fs: vfstest.FromMap(fs, true /*useCaseSensitiveFileNames*/), + fs: mapFS, currentDirectory: currentDirectory, + pnpApi: pnpApi, } } @@ -179,6 +190,11 @@ func (v *vfsModuleResolutionHost) GetCurrentDirectory() string { return v.currentDirectory } +// PnpApi implements ModuleResolutionHost. +func (v *vfsModuleResolutionHost) PnpApi() *pnp.PnpApi { + return v.pnpApi +} + // Trace implements ModuleResolutionHost. func (v *vfsModuleResolutionHost) Trace(msg string) { v.mu.Lock() diff --git a/internal/module/types.go b/internal/module/types.go index 7999e022a3..c01ce19aac 100644 --- a/internal/module/types.go +++ b/internal/module/types.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" ) @@ -14,6 +15,7 @@ import ( type ResolutionHost interface { FS() vfs.FS GetCurrentDirectory() string + PnpApi() *pnp.PnpApi } type ModeAwareCacheKey struct { diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index fd040fc14e..96aba57739 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -271,9 +271,14 @@ func GetEachFileNameOfModule( // so we only need to remove them from the realpath filenames. for _, p := range targets { if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { + IsInNodeModules := ContainsNodeModules(p) + if pnpApi := host.PnpApi(); pnpApi != nil { + IsInNodeModules = IsInNodeModules || pnpApi.IsInPnpModule(importingFileName, p) + } + results = append(results, ModulePath{ FileName: p, - IsInNodeModules: ContainsNodeModules(p), + IsInNodeModules: IsInNodeModules, IsRedirect: referenceRedirect == p, }) } @@ -314,9 +319,14 @@ func GetEachFileNameOfModule( if preferSymlinks { for _, p := range targets { if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { + IsInNodeModules := ContainsNodeModules(p) + if pnpApi := host.PnpApi(); pnpApi != nil { + IsInNodeModules = IsInNodeModules || pnpApi.IsInPnpModule(importingFileName, p) + } + results = append(results, ModulePath{ FileName: p, - IsInNodeModules: ContainsNodeModules(p), + IsInNodeModules: IsInNodeModules, IsRedirect: referenceRedirect == p, }) } @@ -681,6 +691,131 @@ func tryGetModuleNameFromRootDirs( return processEnding(shortest, allowedEndings, compilerOptions, host) } +// TODO: This code partially duplicates tryGetModuleNameAsNodeModule, is it better to keep it isolated from the node module version or should we merge them? +func tryGetModuleNameAsPnpPackage( + pathObj ModulePath, + info Info, + importingSourceFile SourceFileForSpecifierGeneration, + host ModuleSpecifierGenerationHost, + options *core.CompilerOptions, + userPreferences UserPreferences, + packageNameOnly bool, + overrideMode core.ResolutionMode, +) string { + pnpApi := host.PnpApi() + if pnpApi == nil { + return "" + } + + pnpPackageName := "" + fromLocator, _ := pnpApi.FindLocator(importingSourceFile.FileName()) + toLocator, _ := pnpApi.FindLocator(pathObj.FileName) + + // Don't use the package name when the imported file is inside + // the source directory (prefer a relative path instead) + if fromLocator == toLocator { + return "" + } + + if fromLocator != nil && toLocator != nil { + fromInfo := pnpApi.GetPackage(fromLocator) + + useToLocator := false + + for i := range fromInfo.PackageDependencies { + isAlias := fromInfo.PackageDependencies[i].IsAlias() + if isAlias && fromInfo.PackageDependencies[i].AliasName == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { + useToLocator = true + break + } else if fromInfo.PackageDependencies[i].Ident == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { + useToLocator = true + break + } + } + + if useToLocator { + pnpPackageName = toLocator.Name + } + } + + var parts *NodeModulePathParts + if toLocator != nil { + toInfo := pnpApi.GetPackage(toLocator) + packageRootAbsolutePath := pnpApi.GetPackageLocationAbsolutePath(toInfo) + parts = &NodeModulePathParts{ + TopLevelNodeModulesIndex: -1, + TopLevelPackageNameIndex: -1, + PackageRootIndex: len(packageRootAbsolutePath), + FileNameIndex: strings.LastIndex(pathObj.FileName, "/"), + } + } + + if parts == nil { + return "" + } + + // Simplify the full file path to something that can be resolved by Node. + preferences := getModuleSpecifierPreferences(userPreferences, host, options, importingSourceFile, "") + allowedEndings := preferences.getAllowedEndingsInPreferredOrder(core.ResolutionModeNone) + + moduleSpecifier := pathObj.FileName + isPackageRootPath := false + if !packageNameOnly { + packageRootIndex := parts.PackageRootIndex + var moduleFileName string + for true { + // If the module could be imported by a directory name, use that directory's name + pkgJsonResults := tryDirectoryWithPackageJson( + *parts, + pathObj, + importingSourceFile, + host, + overrideMode, + options, + allowedEndings, + pnpPackageName, + ) + moduleFileToTry := pkgJsonResults.moduleFileToTry + packageRootPath := pkgJsonResults.packageRootPath + blockedByExports := pkgJsonResults.blockedByExports + verbatimFromExports := pkgJsonResults.verbatimFromExports + if blockedByExports { + return "" // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution + } + if verbatimFromExports { + return moduleFileToTry + } + //} + if len(packageRootPath) > 0 { + moduleSpecifier = packageRootPath + isPackageRootPath = true + break + } + if len(moduleFileName) == 0 { + moduleFileName = moduleFileToTry + } + // try with next level of directory + packageRootIndex = core.IndexAfter(pathObj.FileName, "/", packageRootIndex+1) + if packageRootIndex == -1 { + moduleSpecifier = processEnding(moduleFileName, allowedEndings, options, host) + break + } + } + } + + if pathObj.IsRedirect && !isPackageRootPath { + return "" + } + + // If the module was found in @types, get the actual Node package name + nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] + if pnpPackageName != "" { + nodeModulesDirectoryName = pnpPackageName + moduleSpecifier[parts.PackageRootIndex:] + } + + return GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) +} + func tryGetModuleNameAsNodeModule( pathObj ModulePath, info Info, @@ -691,6 +826,11 @@ func tryGetModuleNameAsNodeModule( packageNameOnly bool, overrideMode core.ResolutionMode, ) string { + pnpModuleName := tryGetModuleNameAsPnpPackage(pathObj, info, importingSourceFile, host, options, userPreferences, packageNameOnly, overrideMode) + if pnpModuleName != "" { + return pnpModuleName + } + parts := GetNodeModulePathParts(pathObj.FileName) if parts == nil { return "" @@ -716,6 +856,7 @@ func tryGetModuleNameAsNodeModule( overrideMode, options, allowedEndings, + "", ) moduleFileToTry := pkgJsonResults.moduleFileToTry packageRootPath := pkgJsonResults.packageRootPath @@ -778,6 +919,7 @@ func tryDirectoryWithPackageJson( overrideMode core.ResolutionMode, options *core.CompilerOptions, allowedEndings []ModuleSpecifierEnding, + packageNameOverride string, ) pkgJsonDirAttemptResult { rootIdx := parts.PackageRootIndex if rootIdx == -1 { @@ -809,7 +951,10 @@ func tryDirectoryWithPackageJson( // name in the package.json content via url/filepath dependency specifiers. We need to // use the actual directory name, so don't look at `packageJsonContent.name` here. nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:] - packageName := GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) + packageName := packageNameOverride + if packageName == "" { + packageName = GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) + } conditions := module.GetConditions(options, importMode) var fromExports string diff --git a/internal/modulespecifiers/types.go b/internal/modulespecifiers/types.go index c83dde2d83..e9e95b770b 100644 --- a/internal/modulespecifiers/types.go +++ b/internal/modulespecifiers/types.go @@ -5,6 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -52,6 +53,8 @@ type ModuleSpecifierGenerationHost interface { UseCaseSensitiveFileNames() bool GetCurrentDirectory() string + PnpApi() *pnp.PnpApi + GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference GetRedirectTargets(path tspath.Path) []string GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go new file mode 100644 index 0000000000..8e75f2edc6 --- /dev/null +++ b/internal/pnp/manifestparser.go @@ -0,0 +1,324 @@ +package pnp + +import ( + "errors" + "fmt" + "strings" + + "github.com/go-json-experiment/json" + + "github.com/dlclark/regexp2" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type LinkType string + +const ( + LinkTypeSoft LinkType = "SOFT" + LinkTypeHard LinkType = "HARD" +) + +type PackageDependency struct { + Ident string + Reference string // Either the direct reference or alias reference + AliasName string // Empty if not an alias +} + +func (pd PackageDependency) IsAlias() bool { + return pd.AliasName != "" +} + +type PackageInfo struct { + PackageLocation string `json:"packageLocation"` + PackageDependencies []PackageDependency `json:"packageDependencies,omitempty"` + LinkType LinkType `json:"linkType,omitempty"` + DiscardFromLookup bool `json:"discardFromLookup,omitempty"` + PackagePeers []string `json:"packagePeers,omitempty"` +} + +type Locator struct { + Name string `json:"name"` + Reference string `json:"reference"` +} + +type FallbackExclusion struct { + Name string `json:"name"` + Entries []string `json:"entries"` +} + +type PackageTrieData struct { + ident string + reference string + info *PackageInfo +} + +type PackageRegistryTrie struct { + pathSegment string + childrenPathSegments map[string]*PackageRegistryTrie + packageData *PackageTrieData +} + +type PnpManifestData struct { + dirPath string + + ignorePatternData *regexp2.Regexp + enableTopLevelFallback bool + + fallbackPool [][2]string + fallbackExclusionMap map[string]*FallbackExclusion + + dependencyTreeRoots []Locator + + // Nested maps for package registry (ident -> reference -> PackageInfo) + packageRegistryMap map[string]map[string]*PackageInfo + packageRegistryTrie *PackageRegistryTrie +} + +func parseManifestFromPath(fs PnpApiFS, manifestDir string) (*PnpManifestData, error) { + pnpDataString := "" + + data, ok := fs.ReadFile(tspath.CombinePaths(manifestDir, ".pnp.data.json")) + if ok { + pnpDataString = data + } else { + pnpScriptString, ok := fs.ReadFile(tspath.CombinePaths(manifestDir, ".pnp.cjs")) + if !ok { + return nil, errors.New("failed to read .pnp.cjs file") + } + + manifestRegex := regexp2.MustCompile(`(const[ \r\n]+RAW_RUNTIME_STATE[ \r\n]*=[ \r\n]*|hydrateRuntimeState\(JSON\.parse\()'`, regexp2.None) + matches, err := manifestRegex.FindStringMatch(pnpScriptString) + if err != nil || matches == nil { + return nil, errors.New("We failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?") + } + + start := matches.Index + matches.Length + var b strings.Builder + b.Grow(len(pnpScriptString)) + for i := start; i < len(pnpScriptString); i++ { + if pnpScriptString[i] == '\'' { + break + } + + if pnpScriptString[i] != '\\' { + b.WriteByte(pnpScriptString[i]) + } + } + pnpDataString = b.String() + } + + return parseManifestFromData(pnpDataString, manifestDir) +} + +func parseManifestFromData(pnpDataString string, manifestDir string) (*PnpManifestData, error) { + var rawData map[string]interface{} + if err := json.Unmarshal([]byte(pnpDataString), &rawData); err != nil { + return nil, fmt.Errorf("failed to parse JSON PnP data: %w", err) + } + + pnpData, err := parsePnpManifest(rawData, manifestDir) + if err != nil { + return nil, fmt.Errorf("failed to parse PnP data: %w", err) + } + + return pnpData, nil +} + +// TODO add error handling for corrupted data +func parsePnpManifest(rawData map[string]interface{}, manifestDir string) (*PnpManifestData, error) { + data := &PnpManifestData{dirPath: manifestDir} + + if roots, ok := rawData["dependencyTreeRoots"].([]interface{}); ok { + for _, root := range roots { + if rootMap, ok := root.(map[string]interface{}); ok { + data.dependencyTreeRoots = append(data.dependencyTreeRoots, Locator{ + Name: getField(rootMap, "name", parseString), + Reference: getField(rootMap, "reference", parseString), + }) + } + } + } + + ignorePatternData := getField(rawData, "ignorePatternData", parseString) + if ignorePatternData != "" { + ignorePatternDataRegexp, err := regexp2.Compile(ignorePatternData, regexp2.None) + if err != nil { + return nil, fmt.Errorf("failed to compile ignore pattern data: %w", err) + } + + data.ignorePatternData = ignorePatternDataRegexp + } + + data.enableTopLevelFallback = getField(rawData, "enableTopLevelFallback", parseBool) + + data.fallbackPool = getField(rawData, "fallbackPool", parseStringPairs) + + data.fallbackExclusionMap = make(map[string]*FallbackExclusion) + + if exclusions, ok := rawData["fallbackExclusionList"].([]interface{}); ok { + for _, exclusion := range exclusions { + if exclusionArr, ok := exclusion.([]interface{}); ok && len(exclusionArr) == 2 { + name := parseString(exclusionArr[0]) + entries := parseStringArray(exclusionArr[1]) + exclusionEntry := &FallbackExclusion{ + Name: name, + Entries: entries, + } + data.fallbackExclusionMap[exclusionEntry.Name] = exclusionEntry + } + } + } + + data.packageRegistryMap = make(map[string]map[string]*PackageInfo) + + if registryData, ok := rawData["packageRegistryData"].([]interface{}); ok { + for _, entry := range registryData { + if entryArr, ok := entry.([]interface{}); ok && len(entryArr) == 2 { + ident := parseString(entryArr[0]) + + if data.packageRegistryMap[ident] == nil { + data.packageRegistryMap[ident] = make(map[string]*PackageInfo) + } + + if versions, ok := entryArr[1].([]interface{}); ok { + for _, version := range versions { + if versionArr, ok := version.([]interface{}); ok && len(versionArr) == 2 { + reference := parseString(versionArr[0]) + + if infoMap, ok := versionArr[1].(map[string]interface{}); ok { + packageInfo := &PackageInfo{ + PackageLocation: getField(infoMap, "packageLocation", parseString), + PackageDependencies: getField(infoMap, "packageDependencies", parsePackageDependencies), + LinkType: LinkType(getField(infoMap, "linkType", parseString)), + DiscardFromLookup: getField(infoMap, "discardFromLookup", parseBool), + PackagePeers: getField(infoMap, "packagePeers", parseStringArray), + } + + data.packageRegistryMap[ident][reference] = packageInfo + data.addPackageToTrie(ident, reference, packageInfo) + } + } + } + } + } + } + } + + return data, nil +} + +func (data *PnpManifestData) addPackageToTrie(ident string, reference string, packageInfo *PackageInfo) { + if data.packageRegistryTrie == nil { + data.packageRegistryTrie = &PackageRegistryTrie{ + pathSegment: "", + childrenPathSegments: make(map[string]*PackageRegistryTrie), + packageData: nil, + } + } + + packageData := &PackageTrieData{ + ident: ident, + reference: reference, + info: packageInfo, + } + + packagePath := tspath.RemoveTrailingDirectorySeparator(packageInfo.PackageLocation) + packagePathSegments := strings.Split(packagePath, "/") + + currentTrie := data.packageRegistryTrie + + for _, segment := range packagePathSegments { + if currentTrie.childrenPathSegments[segment] == nil { + currentTrie.childrenPathSegments[segment] = &PackageRegistryTrie{ + pathSegment: segment, + childrenPathSegments: make(map[string]*PackageRegistryTrie), + packageData: nil, + } + } + + currentTrie = currentTrie.childrenPathSegments[segment] + } + + currentTrie.packageData = packageData +} + +// Helper functions for parsing JSON values - following patterns from tsoptions.parseString, etc. +func parseString(value interface{}) string { + if str, ok := value.(string); ok { + return str + } + return "" +} + +func parseBool(value interface{}) bool { + if val, ok := value.(bool); ok { + return val + } + return false +} + +func parseStringArray(value interface{}) []string { + if arr, ok := value.([]interface{}); ok { + if arr == nil { + return nil + } + result := make([]string, 0, len(arr)) + for _, v := range arr { + if str, ok := v.(string); ok { + result = append(result, str) + } + } + return result + } + return nil +} + +func parseStringPairs(value interface{}) [][2]string { + var result [][2]string + if arr, ok := value.([]interface{}); ok { + for _, item := range arr { + if pair, ok := item.([]interface{}); ok && len(pair) == 2 { + result = append(result, [2]string{ + parseString(pair[0]), + parseString(pair[1]), + }) + } + } + } + return result +} + +func parsePackageDependencies(value interface{}) []PackageDependency { + var result []PackageDependency + if arr, ok := value.([]interface{}); ok { + for _, item := range arr { + if pair, ok := item.([]interface{}); ok && len(pair) == 2 { + ident := parseString(pair[0]) + + // Check if second element is string (simple reference) or array (alias) + if str, ok := pair[1].(string); ok { + result = append(result, PackageDependency{ + Ident: ident, + Reference: str, + AliasName: "", + }) + } else if aliasPair, ok := pair[1].([]interface{}); ok && len(aliasPair) == 2 { + result = append(result, PackageDependency{ + Ident: ident, + Reference: parseString(aliasPair[1]), + AliasName: parseString(aliasPair[0]), + }) + } + } + } + } + return result +} + +func getField[T any](m map[string]interface{}, key string, parser func(interface{}) T) T { + if val, exists := m[key]; exists { + return parser(val) + } + var zero T + return zero +} diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go new file mode 100644 index 0000000000..dccf186c22 --- /dev/null +++ b/internal/pnp/pnp.go @@ -0,0 +1,19 @@ +package pnp + +import "strings" + +func InitPnpApi(fs PnpApiFS, filePath string) *PnpApi { + pnpApi := &PnpApi{fs: fs, url: filePath} + + manifestData, err := pnpApi.findClosestPnpManifest() + if err == nil { + pnpApi.manifest = manifestData + return pnpApi + } + + return nil +} + +func IsPnpLoaderFile(path string) bool { + return strings.HasSuffix(path, ".pnp.cjs") +} diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go new file mode 100644 index 0000000000..eb33849b5a --- /dev/null +++ b/internal/pnp/pnpapi.go @@ -0,0 +1,341 @@ +package pnp + +/* + * Yarn Plug'n'Play (generally referred to as Yarn PnP) is the default installation strategy in modern releases of Yarn. + * Yarn PnP generates a single Node.js loader file in place of the typical node_modules folder. + * This loader file, named .pnp.cjs, contains all information about your project's dependency tree, informing your tools as to + * the location of the packages on the disk and letting them know how to resolve require and import calls. + * + * The full specification is available at https://yarnpkg.com/advanced/pnp-spec + */ + +import ( + "errors" + "fmt" + "strings" + + "github.com/microsoft/typescript-go/internal/tspath" +) + +type PnpApi struct { + fs PnpApiFS + url string + manifest *PnpManifestData +} + +// FS abstraction used by the PnpApi to access the file system +// We can't use the vfs.FS interface because it creates an import cycle: core -> pnp -> vfs -> core +type PnpApiFS interface { + FileExists(path string) bool + ReadFile(path string) (contents string, ok bool) +} + +func (p *PnpApi) RefreshManifest() error { + var newData *PnpManifestData + var err error + + if p.manifest == nil { + newData, err = p.findClosestPnpManifest() + } else { + newData, err = parseManifestFromPath(p.fs, p.manifest.dirPath) + } + + if err != nil { + return err + } + + p.manifest = newData + return nil +} + +func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (string, error) { + if p.manifest == nil { + panic("ResolveToUnqualified called with no PnP manifest available") + } + + ident, modulePath, err := p.ParseBareIdentifier(specifier) + if err != nil { + // Skipping resolution + return "", nil + } + + parentLocator, err := p.FindLocator(parentPath) + if err != nil || parentLocator == nil { + // Skipping resolution + return "", nil + } + + parentPkg := p.GetPackage(parentLocator) + + var referenceOrAlias *PackageDependency + for _, dep := range parentPkg.PackageDependencies { + if dep.Ident == ident { + referenceOrAlias = &dep + break + } + } + + // If not found, try fallback if enabled + if referenceOrAlias == nil { + if p.manifest.enableTopLevelFallback { + excluded := false + if exclusion, ok := p.manifest.fallbackExclusionMap[parentLocator.Name]; ok { + for _, entry := range exclusion.Entries { + if entry == parentLocator.Reference { + excluded = true + break + } + } + } + if !excluded { + fallback := p.ResolveViaFallback(ident) + if fallback != nil { + referenceOrAlias = fallback + } + } + } + } + + // undeclared dependency + if referenceOrAlias == nil { + if parentLocator.Name == "" { + return "", fmt.Errorf("Your application tried to access %s, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", ident, ident, parentPath) + } + return "", fmt.Errorf("%s tried to access %s, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", parentLocator.Name, ident, ident, parentPath) + } + + // unfulfilled peer dependency + if !referenceOrAlias.IsAlias() && referenceOrAlias.Reference == "" { + if parentLocator.Name == "" { + return "", fmt.Errorf("Your application tried to access %s (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed.\n\nRequired package: %s\nRequired by: %s", ident, ident, parentPath) + } + return "", fmt.Errorf("%s tried to access %s (a peer dependency) but it isn't provided by its ancestors/your application; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", parentLocator.Name, ident, ident, parentPath) + } + + var dependencyPkg *PackageInfo + if referenceOrAlias.IsAlias() { + dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.AliasName, Reference: referenceOrAlias.Reference}) + } else { + dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.Ident, Reference: referenceOrAlias.Reference}) + } + + return tspath.ResolvePath(p.manifest.dirPath, dependencyPkg.PackageLocation, modulePath), nil +} + +func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { + directoryPath := tspath.GetNormalizedAbsolutePath(p.url, "/") + + for { + pnpPath := tspath.CombinePaths(directoryPath, ".pnp.cjs") + if p.fs.FileExists(pnpPath) { + return parseManifestFromPath(p.fs, directoryPath) + } + + if tspath.IsDiskPathRoot(directoryPath) { + return nil, errors.New("no PnP manifest found") + } + + directoryPath = tspath.GetDirectoryPath(directoryPath) + } +} + +func (p *PnpApi) GetPackage(locator *Locator) *PackageInfo { + packageRegistryMap := p.manifest.packageRegistryMap + packageInfo, ok := packageRegistryMap[locator.Name][locator.Reference] + if !ok { + panic(locator.Name + " should have an entry in the package registry") + } + + return packageInfo +} + +func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { + relativePath := tspath.GetRelativePathFromDirectory(p.manifest.dirPath, parentPath, + tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}) + + if p.manifest.ignorePatternData != nil { + match, err := p.manifest.ignorePatternData.MatchString(relativePath) + if err != nil { + return nil, err + } + + if match { + return nil, nil + } + } + + var relativePathWithDot string + if strings.HasPrefix(relativePath, "../") { + relativePathWithDot = relativePath + } else { + relativePathWithDot = "./" + relativePath + } + + var bestLength int + var bestLocator *Locator + pathSegments := strings.Split(relativePathWithDot, "/") + currentTrie := p.manifest.packageRegistryTrie + + // Go down the trie, looking for the latest defined packageInfo that matches the path + for index, segment := range pathSegments { + currentTrie = currentTrie.childrenPathSegments[segment] + + if currentTrie == nil || currentTrie.childrenPathSegments == nil { + break + } + + if currentTrie.packageData != nil && index >= bestLength { + bestLength = index + bestLocator = &Locator{Name: currentTrie.packageData.ident, Reference: currentTrie.packageData.reference} + } + } + + if bestLocator == nil { + return nil, fmt.Errorf("no package found for path %s", relativePath) + } + + return bestLocator, nil +} + +func (p *PnpApi) ResolveViaFallback(name string) *PackageDependency { + topLevelPkg := p.GetPackage(&Locator{Name: "", Reference: ""}) + + if topLevelPkg != nil { + for _, dep := range topLevelPkg.PackageDependencies { + if dep.Ident == name { + return &dep + } + } + } + + for _, dep := range p.manifest.fallbackPool { + if dep[0] == name { + return &PackageDependency{ + Ident: dep[0], + Reference: dep[1], + AliasName: "", + } + } + } + + return nil +} + +func (p *PnpApi) ParseBareIdentifier(specifier string) (ident string, modulePath string, err error) { + if len(specifier) == 0 { + return "", "", fmt.Errorf("Empty specifier: %s", specifier) + } + + firstSlash := strings.Index(specifier, "/") + + if specifier[0] == '@' { + if firstSlash == -1 { + return "", "", fmt.Errorf("Invalid specifier: %s", specifier) + } + + secondSlash := strings.Index(specifier[firstSlash+1:], "/") + + if secondSlash == -1 { + ident = specifier + } else { + ident = specifier[:firstSlash+1+secondSlash] + } + } else { + firstSlash := strings.Index(specifier, "/") + + if firstSlash == -1 { + ident = specifier + } else { + ident = specifier[:firstSlash] + } + } + + modulePath = specifier[len(ident):] + + return ident, modulePath, nil +} + +func (p *PnpApi) GetPnpTypeRoots(currentDirectory string) []string { + if p.manifest == nil { + return []string{} + } + + currentDirectory = tspath.NormalizePath(currentDirectory) + + currentPackage, err := p.FindLocator(currentDirectory) + if err != nil { + return []string{} + } + + if currentPackage == nil { + return []string{} + } + + packageDependencies := p.GetPackage(currentPackage).PackageDependencies + + typeRoots := []string{} + for _, dep := range packageDependencies { + if strings.HasPrefix(dep.Ident, "@types/") && dep.Reference != "" { + packageInfo := p.GetPackage(&Locator{Name: dep.Ident, Reference: dep.Reference}) + typeRoots = append(typeRoots, tspath.GetDirectoryPath( + tspath.ResolvePath(p.manifest.dirPath, packageInfo.PackageLocation), + )) + } + } + + return typeRoots +} + +func (p *PnpApi) IsImportable(fromFileName string, toFileName string) bool { + fromLocator, errFromLocator := p.FindLocator(fromFileName) + toLocator, errToLocator := p.FindLocator(toFileName) + + if fromLocator == nil || toLocator == nil || errFromLocator != nil || errToLocator != nil { + return false + } + + fromInfo := p.GetPackage(fromLocator) + for _, dep := range fromInfo.PackageDependencies { + if dep.Reference == toLocator.Reference { + if dep.IsAlias() && dep.AliasName == toLocator.Name { + return true + } + + if dep.Ident == toLocator.Name { + return true + } + } + } + + return false +} + +func (p *PnpApi) GetPackageLocationAbsolutePath(packageInfo *PackageInfo) string { + if packageInfo == nil { + return "" + } + + packageLocation := packageInfo.PackageLocation + return tspath.ResolvePath(p.manifest.dirPath, packageLocation) +} + +func (p *PnpApi) IsInPnpModule(fromFileName string, toFileName string) bool { + fromLocator, _ := p.FindLocator(fromFileName) + toLocator, _ := p.FindLocator(toFileName) + // The targeted filename is in a pnp module different from the requesting filename + return fromLocator != nil && toLocator != nil && fromLocator.Name != toLocator.Name +} + +func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, baseDir string, nmFromConfig bool) ([]string, bool) { + pnpTypes := p.GetPnpTypeRoots(baseDir) + + if len(nmTypes) > 0 { + return append(nmTypes, pnpTypes...), nmFromConfig + } + + if len(pnpTypes) > 0 { + return pnpTypes, false + } + + return nil, false +} diff --git a/internal/project/compilerhost.go b/internal/project/compilerhost.go index d58b3522b8..97d19f81d8 100644 --- a/internal/project/compilerhost.go +++ b/internal/project/compilerhost.go @@ -6,10 +6,12 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" ) var _ compiler.CompilerHost = (*compilerHost)(nil) @@ -19,6 +21,8 @@ type compilerHost struct { currentDirectory string sessionOptions *SessionOptions + pnpApi *pnp.PnpApi + fs *snapshotFSBuilder compilerFS *compilerFS configFileRegistry *ConfigFileRegistry @@ -50,6 +54,11 @@ func newCompilerHost( builder *projectCollectionBuilder, logger *logging.LogTree, ) *compilerHost { + pnpApi := pnp.InitPnpApi(builder.fs.fs, currentDirectory) + if pnpApi != nil { + builder.fs.fs = pnpvfs.From(builder.fs.fs) + } + seenFiles := &collections.SyncSet[tspath.Path]{} compilerFS := &compilerFS{ source: &builderFileSource{ @@ -66,6 +75,8 @@ func newCompilerHost( compilerFS: compilerFS, seenFiles: seenFiles, + pnpApi: pnpApi, + fs: builder.fs, project: project, builder: builder, @@ -108,6 +119,11 @@ func (c *compilerHost) GetCurrentDirectory() string { return c.currentDirectory } +// PnpApi implements compiler.CompilerHost. +func (c *compilerHost) PnpApi() *pnp.PnpApi { + return c.pnpApi +} + // GetResolvedProjectReference implements compiler.CompilerHost. func (c *compilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { if c.builder == nil { diff --git a/internal/project/configfileregistrybuilder.go b/internal/project/configfileregistrybuilder.go index c8fc308500..d609e44a02 100644 --- a/internal/project/configfileregistrybuilder.go +++ b/internal/project/configfileregistrybuilder.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project/dirty" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/tsoptions" @@ -28,6 +29,8 @@ type configFileRegistryBuilder struct { extendedConfigCache *extendedConfigCache sessionOptions *SessionOptions + pnpApi *pnp.PnpApi + base *ConfigFileRegistry configs *dirty.SyncMap[tspath.Path, *configFileEntry] configFileNames *dirty.Map[tspath.Path, *configFileNames] @@ -38,6 +41,7 @@ func newConfigFileRegistryBuilder( oldConfigFileRegistry *ConfigFileRegistry, extendedConfigCache *extendedConfigCache, sessionOptions *SessionOptions, + pnpApi *pnp.PnpApi, logger *logging.LogTree, ) *configFileRegistryBuilder { return &configFileRegistryBuilder{ @@ -45,6 +49,7 @@ func newConfigFileRegistryBuilder( base: oldConfigFileRegistry, sessionOptions: sessionOptions, extendedConfigCache: extendedConfigCache, + pnpApi: pnpApi, configs: dirty.NewSyncMap(oldConfigFileRegistry.configs, nil), configFileNames: dirty.NewMap(oldConfigFileRegistry.configFileNames), @@ -570,6 +575,11 @@ func (c *configFileRegistryBuilder) GetCurrentDirectory() string { return c.sessionOptions.CurrentDirectory } +// PnpApi implements tsoptions.ParseConfigHost. +func (c *configFileRegistryBuilder) PnpApi() *pnp.PnpApi { + return c.pnpApi +} + // GetExtendedConfig implements tsoptions.ExtendedConfigCache. func (c *configFileRegistryBuilder) GetExtendedConfig(fileName string, path tspath.Path, parse func() *tsoptions.ExtendedConfigCacheEntry) *tsoptions.ExtendedConfigCacheEntry { fh := c.fs.GetFileByPath(fileName, path) diff --git a/internal/project/projectcollectionbuilder.go b/internal/project/projectcollectionbuilder.go index c7ed8bd2f9..2c99c62434 100644 --- a/internal/project/projectcollectionbuilder.go +++ b/internal/project/projectcollectionbuilder.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project/dirty" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/tsoptions" @@ -54,6 +55,7 @@ func newProjectCollectionBuilder( oldAPIOpenedProjects map[tspath.Path]struct{}, compilerOptionsForInferredProjects *core.CompilerOptions, sessionOptions *SessionOptions, + pnpApi *pnp.PnpApi, parseCache *ParseCache, extendedConfigCache *extendedConfigCache, ) *projectCollectionBuilder { @@ -65,7 +67,7 @@ func newProjectCollectionBuilder( parseCache: parseCache, extendedConfigCache: extendedConfigCache, base: oldProjectCollection, - configFileRegistryBuilder: newConfigFileRegistryBuilder(fs, oldConfigFileRegistry, extendedConfigCache, sessionOptions, nil), + configFileRegistryBuilder: newConfigFileRegistryBuilder(fs, oldConfigFileRegistry, extendedConfigCache, sessionOptions, pnpApi, nil), newSnapshotID: newSnapshotID, configuredProjects: dirty.NewSyncMap(oldProjectCollection.configuredProjects, nil), inferredProject: dirty.NewBox(oldProjectCollection.inferredProject), diff --git a/internal/project/session.go b/internal/project/session.go index 9e5a17aac6..85f3e4dea7 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -16,6 +16,7 @@ import ( "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/background" "github.com/microsoft/typescript-go/internal/project/logging" @@ -185,6 +186,11 @@ func (s *Session) GetCurrentDirectory() string { return s.options.CurrentDirectory } +// PnpApi implements module.ResolutionHost +func (s *Session) PnpApi() *pnp.PnpApi { + return s.snapshot.PnpApi() +} + // Gets current UserPreferences, always a copy func (s *Session) UserPreferences() *lsutil.UserPreferences { s.configRWMu.Lock() diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 29f3c374f2..1c299bb758 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -12,11 +12,13 @@ import ( "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/dirty" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" ) type Snapshot struct { @@ -39,6 +41,8 @@ type Snapshot struct { builderLogs *logging.LogTree apiError error + + pnpApi *pnp.PnpApi } // NewSnapshot @@ -53,6 +57,11 @@ func NewSnapshot( config Config, toPath func(fileName string) tspath.Path, ) *Snapshot { + pnpApi := pnp.InitPnpApi(fs.fs, sessionOptions.CurrentDirectory) + if pnpApi != nil { + fs.fs = pnpvfs.From(fs.fs) + } + s := &Snapshot{ id: id, @@ -64,6 +73,8 @@ func NewSnapshot( ProjectCollection: &ProjectCollection{toPath: toPath}, compilerOptionsForInferredProjects: compilerOptionsForInferredProjects, config: config, + + pnpApi: pnpApi, } s.converters = lsconv.NewConverters(s.sessionOptions.PositionEncoding, s.LSPLineMap) s.refCount.Store(1) @@ -114,6 +125,10 @@ func (s *Snapshot) UseCaseSensitiveFileNames() bool { return s.fs.fs.UseCaseSensitiveFileNames() } +func (s *Snapshot) PnpApi() *pnp.PnpApi { + return s.pnpApi +} + func (s *Snapshot) ReadFile(fileName string) (string, bool) { handle := s.GetFile(fileName) if handle == nil { @@ -227,6 +242,7 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma s.ProjectCollection.apiOpenedProjects, compilerOptionsForInferredProjects, s.sessionOptions, + s.pnpApi, session.parseCache, session.extendedConfigCache, ) diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go index 772eb169c0..816a2e0d96 100644 --- a/internal/testutil/harnessutil/harnessutil.go +++ b/internal/testutil/harnessutil/harnessutil.go @@ -23,12 +23,14 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/repo" "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/testutil" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" ) @@ -206,9 +208,21 @@ func CompileFilesEx( fs := vfstest.FromMap(testfs, harnessOptions.UseCaseSensitiveFileNames) fs = bundled.WrapFS(fs) + + pnpApi := pnp.InitPnpApi(fs, currentDirectory) + if pnpApi != nil { + fs = pnpvfs.From(fs) + t.Cleanup(func() { + err := fs.(*pnpvfs.PnpFS).ClearCache() + if err != nil { + t.Errorf("Failed to clear PnP cache: %v", err) + } + }) + } + fs = NewOutputRecorderFS(fs) - host := createCompilerHost(fs, bundled.LibPath(), currentDirectory) + host := createCompilerHost(fs, bundled.LibPath(), currentDirectory, pnpApi) var configFile *tsoptions.TsConfigSourceFile var errors []*ast.Diagnostic if tsconfig != nil { @@ -601,13 +615,13 @@ func (t *TracerForBaselining) Reset() { t.packageJsonCache = make(map[tspath.Path]bool) } -func createCompilerHost(fs vfs.FS, defaultLibraryPath string, currentDirectory string) *cachedCompilerHost { +func createCompilerHost(fs vfs.FS, defaultLibraryPath string, currentDirectory string, pnpApi *pnp.PnpApi) *cachedCompilerHost { tracer := NewTracerForBaselining(tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: fs.UseCaseSensitiveFileNames(), CurrentDirectory: currentDirectory, }, &strings.Builder{}) return &cachedCompilerHost{ - CompilerHost: compiler.NewCompilerHost(currentDirectory, fs, defaultLibraryPath, nil, tracer.Trace), + CompilerHost: compiler.NewCompilerHost(currentDirectory, fs, defaultLibraryPath, nil, pnpApi, tracer.Trace), tracer: tracer, } } diff --git a/internal/testutil/tsbaseline/js_emit_baseline.go b/internal/testutil/tsbaseline/js_emit_baseline.go index 8b51c19349..f5905e3658 100644 --- a/internal/testutil/tsbaseline/js_emit_baseline.go +++ b/internal/testutil/tsbaseline/js_emit_baseline.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/testutil/baseline" "github.com/microsoft/typescript-go/internal/testutil/harnessutil" "github.com/microsoft/typescript-go/internal/tspath" @@ -213,7 +214,7 @@ func prepareDeclarationCompilationContext( } addDtsFile := func(file *harnessutil.TestFile, dtsFiles []*harnessutil.TestFile) []*harnessutil.TestFile { - if tspath.IsDeclarationFileName(file.UnitName) || tspath.HasJSONFileExtension(file.UnitName) { + if tspath.IsDeclarationFileName(file.UnitName) || tspath.HasJSONFileExtension(file.UnitName) || pnp.IsPnpLoaderFile(file.UnitName) { dtsFiles = append(dtsFiles, file) } else if tspath.HasTSFileExtension(file.UnitName) || (tspath.HasJSFileExtension(file.UnitName) && options.GetAllowJS()) { declFile := findResultCodeFile(file.UnitName) diff --git a/internal/transformers/tstransforms/importelision_test.go b/internal/transformers/tstransforms/importelision_test.go index 3dc5c227cd..46d14db15a 100644 --- a/internal/transformers/tstransforms/importelision_test.go +++ b/internal/transformers/tstransforms/importelision_test.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/testutil/emittestutil" "github.com/microsoft/typescript-go/internal/testutil/parsetestutil" @@ -61,6 +62,10 @@ func (p *fakeProgram) GetCurrentDirectory() string { return "" } +func (p *fakeProgram) PnpApi() *pnp.PnpApi { + return nil +} + func (p *fakeProgram) GetGlobalTypingsCacheLocation() string { return "" } diff --git a/internal/tsoptions/tsconfigparsing.go b/internal/tsoptions/tsconfigparsing.go index 9ccef5af8d..d2d70a1a92 100644 --- a/internal/tsoptions/tsconfigparsing.go +++ b/internal/tsoptions/tsconfigparsing.go @@ -16,6 +16,7 @@ import ( "github.com/microsoft/typescript-go/internal/jsnum" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" ) @@ -672,6 +673,7 @@ func ParseConfigFileTextToJson(fileName string, path tspath.Path, jsonText strin type ParseConfigHost interface { FS() vfs.FS GetCurrentDirectory() string + PnpApi() *pnp.PnpApi } type resolverHost struct { diff --git a/internal/tsoptions/tsoptionstest/vfsparseconfighost.go b/internal/tsoptions/tsoptionstest/vfsparseconfighost.go index 249617083e..2f8e746380 100644 --- a/internal/tsoptions/tsoptionstest/vfsparseconfighost.go +++ b/internal/tsoptions/tsoptionstest/vfsparseconfighost.go @@ -1,9 +1,11 @@ package tsoptionstest import ( + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" ) @@ -21,6 +23,7 @@ func fixRoot(path string) string { type VfsParseConfigHost struct { Vfs vfs.FS CurrentDirectory string + pnpApi *pnp.PnpApi } var _ tsoptions.ParseConfigHost = (*VfsParseConfigHost)(nil) @@ -33,9 +36,20 @@ func (h *VfsParseConfigHost) GetCurrentDirectory() string { return h.CurrentDirectory } +func (h *VfsParseConfigHost) PnpApi() *pnp.PnpApi { + return h.pnpApi +} + func NewVFSParseConfigHost(files map[string]string, currentDirectory string, useCaseSensitiveFileNames bool) *VfsParseConfigHost { + var fs vfs.FS = vfstest.FromMap(files, useCaseSensitiveFileNames) + pnpApi := pnp.InitPnpApi(fs, tspath.NormalizePath(currentDirectory)) + if pnpApi != nil { + fs = pnpvfs.From(fs) + } + return &VfsParseConfigHost{ - Vfs: vfstest.FromMap(files, useCaseSensitiveFileNames), + Vfs: fs, CurrentDirectory: currentDirectory, + pnpApi: nil, } } diff --git a/internal/tspath/path.go b/internal/tspath/path.go index de09221507..c7dfb642b5 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -869,6 +869,17 @@ func IsExternalModuleNameRelative(moduleName string) bool { return PathIsRelative(moduleName) || IsRootedDiskPath(moduleName) } +func IsExternalLibraryImport(path string) bool { + // When PnP is enabled, some internal libraries can be resolved as virtual packages, which should be treated as external libraries + // Since these virtual pnp packages don't have a `/node_modules/` folder, we need to check for the presence of `/__virtual__/` in the path + // See https://yarnpkg.com/advanced/lexicon#virtual-package for more details + return strings.Contains(path, "/node_modules/") || isPnpVirtualPath(path) +} + +func isPnpVirtualPath(path string) bool { + return strings.Contains(path, "/__virtual__/") +} + type ComparePathsOptions struct { UseCaseSensitiveFileNames bool CurrentDirectory string @@ -1018,6 +1029,10 @@ func HasExtension(fileName string) bool { return strings.Contains(GetBaseFileName(fileName), ".") } +func IsZipPath(path string) bool { + return strings.Contains(path, ".zip/") || strings.HasSuffix(path, ".zip") +} + func SplitVolumePath(path string) (volume string, rest string, ok bool) { if len(path) >= 2 && IsVolumeCharacter(path[0]) && path[1] == ':' { return strings.ToLower(path[0:2]), path[2:], true diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go new file mode 100644 index 0000000000..c1ed29b109 --- /dev/null +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -0,0 +1,251 @@ +package pnpvfs + +import ( + "archive/zip" + "path" + "strconv" + "strings" + "sync" + "time" + + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/iovfs" +) + +type PnpFS struct { + fs vfs.FS + cachedZipReadersMap map[string]*zip.ReadCloser + cacheReaderMutex sync.Mutex +} + +var _ vfs.FS = (*PnpFS)(nil) + +func From(fs vfs.FS) *PnpFS { + pnpFS := &PnpFS{ + fs: fs, + cachedZipReadersMap: make(map[string]*zip.ReadCloser), + } + + return pnpFS +} + +func (pnpFS *PnpFS) DirectoryExists(path string) bool { + path, _, _ = resolveVirtual(path) + + if strings.HasSuffix(path, ".zip") { + return pnpFS.fs.FileExists(path) + } + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + + return fs.DirectoryExists(formattedPath) +} + +func (pnpFS *PnpFS) FileExists(path string) bool { + path, _, _ = resolveVirtual(path) + + if strings.HasSuffix(path, ".zip") { + return pnpFS.fs.FileExists(path) + } + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.FileExists(formattedPath) +} + +func (pnpFS *PnpFS) GetAccessibleEntries(path string) vfs.Entries { + path, hash, basePath := resolveVirtual(path) + + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) + entries := fs.GetAccessibleEntries(formattedPath) + + for i, dir := range entries.Directories { + fullPath := tspath.CombinePaths(zipPath+formattedPath, dir) + entries.Directories[i] = makeVirtualPath(basePath, hash, fullPath) + } + + for i, file := range entries.Files { + fullPath := tspath.CombinePaths(zipPath+formattedPath, file) + entries.Files[i] = makeVirtualPath(basePath, hash, fullPath) + } + + return entries +} + +func (pnpFS *PnpFS) ReadFile(path string) (contents string, ok bool) { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.ReadFile(formattedPath) +} + +func (pnpFS *PnpFS) Chtimes(path string, mtime time.Time, atime time.Time) error { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.Chtimes(formattedPath, mtime, atime) +} + +func (pnpFS *PnpFS) Realpath(path string) string { + path, hash, basePath := resolveVirtual(path) + + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) + fullPath := zipPath + fs.Realpath(formattedPath) + return makeVirtualPath(basePath, hash, fullPath) +} + +func (pnpFS *PnpFS) Remove(path string) error { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.Remove(formattedPath) +} + +func (pnpFS *PnpFS) Stat(path string) vfs.FileInfo { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.Stat(formattedPath) +} + +func (pnpFS *PnpFS) UseCaseSensitiveFileNames() bool { + // pnp fs is always case sensitive + return true +} + +func (pnpFS *PnpFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + root, hash, basePath := resolveVirtual(root) + + fs, formattedPath, zipPath := getMatchingFS(pnpFS, root) + return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error { + fullPath := zipPath + path + return walkFn(makeVirtualPath(basePath, hash, fullPath), d, err) + })) +} + +func (pnpFS *PnpFS) WriteFile(path string, data string, writeByteOrderMark bool) error { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) + if zipPath != "" { + panic("cannot write to zip file") + } + + return fs.WriteFile(formattedPath, data, writeByteOrderMark) +} + +func splitZipPath(path string) (string, string) { + parts := strings.Split(path, ".zip/") + if len(parts) < 2 { + return path, "/" + } + return parts[0] + ".zip", "/" + parts[1] +} + +func getMatchingFS(pnpFS *PnpFS, path string) (vfs.FS, string, string) { + if !tspath.IsZipPath(path) { + return pnpFS.fs, path, "" + } + + zipPath, internalPath := splitZipPath(path) + + zipStat := pnpFS.fs.Stat(zipPath) + if zipStat == nil { + return pnpFS.fs, path, "" + } + + var usedReader *zip.ReadCloser + + pnpFS.cacheReaderMutex.Lock() + defer pnpFS.cacheReaderMutex.Unlock() + + cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath] + if ok { + usedReader = cachedReader + } else { + zipReader, err := zip.OpenReader(zipPath) + if err != nil { + return pnpFS.fs, path, "" + } + + usedReader = zipReader + pnpFS.cachedZipReadersMap[zipPath] = usedReader + } + + return iovfs.From(usedReader, pnpFS.fs.UseCaseSensitiveFileNames()), internalPath, zipPath +} + +// Virtual paths are used to make different paths resolve to the same real file or folder, which is necessary in some cases when PnP is enabled +// See https://yarnpkg.com/advanced/lexicon#virtual-package and https://yarnpkg.com/advanced/pnpapi#resolvevirtual for more details +func resolveVirtual(path string) (realPath string, hash string, basePath string) { + idx := strings.Index(path, "/__virtual__/") + if idx == -1 { + return path, "", "" + } + + base := path[:idx] + rest := path[idx+len("/__virtual__/"):] + parts := strings.SplitN(rest, "/", 3) + if len(parts) < 3 { + // Not enough parts to match the pattern, return as is + return path, "", "" + } + hash = parts[0] + subpath := parts[2] + depth, err := strconv.Atoi(parts[1]) + if err != nil || depth < 0 { + // Invalid n, return as is + return path, "", "" + } + + basePath = path[:idx] + "/__virtual__" + + // Apply dirname n times to base + for range depth { + base = tspath.GetDirectoryPath(base) + } + // Join base and subpath + if base == "/" { + return "/" + subpath, hash, basePath + } + + return tspath.CombinePaths(base, subpath), hash, basePath +} + +func makeVirtualPath(basePath string, hash string, targetPath string) string { + if basePath == "" || hash == "" { + return targetPath + } + + relativePath := tspath.GetRelativePathFromDirectory( + tspath.GetDirectoryPath(basePath), + targetPath, + tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}) + + segments := strings.Split(relativePath, "/") + + depth := 0 + for depth < len(segments) && segments[depth] == ".." { + depth++ + } + + subPath := strings.Join(segments[depth:], "/") + + return path.Join(basePath, hash, strconv.Itoa(depth), subPath) +} + +func (pnpFS *PnpFS) ClearCache() error { + pnpFS.cacheReaderMutex.Lock() + defer pnpFS.cacheReaderMutex.Unlock() + + for _, reader := range pnpFS.cachedZipReadersMap { + err := reader.Close() + if err != nil { + return err + } + } + + pnpFS.cachedZipReadersMap = make(map[string]*zip.ReadCloser) + + return nil +} diff --git a/internal/vfs/pnpvfs/pnpvfs_test.go b/internal/vfs/pnpvfs/pnpvfs_test.go new file mode 100644 index 0000000000..f5d12fa775 --- /dev/null +++ b/internal/vfs/pnpvfs/pnpvfs_test.go @@ -0,0 +1,296 @@ +package pnpvfs_test + +import ( + "archive/zip" + "fmt" + "os" + "strings" + "testing" + + "github.com/microsoft/typescript-go/internal/testutil" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "gotest.tools/v3/assert" +) + +func createTestZip(t *testing.T, files map[string]string) string { + t.Helper() + + tmpDir := t.TempDir() + zipPath := tspath.CombinePaths(tmpDir, "test.zip") + + file, err := os.Create(zipPath) + assert.NilError(t, err) + defer file.Close() + + w := zip.NewWriter(file) + defer w.Close() + + for name, content := range files { + f, err := w.Create(name) + assert.NilError(t, err) + _, err = f.Write([]byte(content)) + assert.NilError(t, err) + } + + return zipPath +} + +func TestPnpVfs_BasicFileOperations(t *testing.T) { + t.Parallel() + + underlyingFS := vfstest.FromMap(map[string]string{ + "/project/src/index.ts": "export const hello = 'world';", + "/project/package.json": `{"name": "test"}`, + }, true) + + fs := pnpvfs.From(underlyingFS) + assert.Assert(t, fs.FileExists("/project/src/index.ts")) + assert.Assert(t, !fs.FileExists("/project/nonexistent.ts")) + + content, ok := fs.ReadFile("/project/src/index.ts") + assert.Assert(t, ok) + assert.Equal(t, "export const hello = 'world';", content) + + assert.Assert(t, fs.DirectoryExists("/project/src")) + assert.Assert(t, !fs.DirectoryExists("/project/nonexistent")) + + var files []string + err := fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + + assert.NilError(t, err) + assert.DeepEqual(t, files, []string{"/project/package.json", "/project/src/index.ts"}) + + err = fs.WriteFile("/project/src/index.ts", "export const hello = 'world2';", false) + assert.NilError(t, err) + + content, ok = fs.ReadFile("/project/src/index.ts") + assert.Assert(t, ok) + assert.Equal(t, "export const hello = 'world2';", content) +} + +func TestPnpVfs_ZipFileDetection(t *testing.T) { + t.Parallel() + + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + "package.json": `{"name": "test-project"}`, + } + + zipPath := createTestZip(t, zipFiles) + + underlyingFS := vfstest.FromMap(map[string]string{ + zipPath: "zip content placeholder", + }, true) + + fs := pnpvfs.From(underlyingFS) + + fmt.Println(zipPath) + assert.Assert(t, fs.FileExists(zipPath)) + + zipInternalPath := zipPath + "/src/index.ts" + assert.Assert(t, fs.FileExists(zipInternalPath)) + + content, ok := fs.ReadFile(zipInternalPath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["src/index.ts"]) +} + +func TestPnpVfs_ErrorHandling(t *testing.T) { + t.Parallel() + + fs := pnpvfs.From(osvfs.FS()) + + t.Run("NonexistentZipFile", func(t *testing.T) { + t.Parallel() + + result := fs.FileExists("/nonexistent/path/archive.zip/file.txt") + assert.Assert(t, !result) + + _, ok := fs.ReadFile("/nonexistent/archive.zip/file.txt") + assert.Assert(t, !ok) + }) + + t.Run("InvalidZipFile", func(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + fakePath := tspath.CombinePaths(tmpDir, "fake.zip") + err := os.WriteFile(fakePath, []byte("not a zip file"), 0o644) + assert.NilError(t, err) + + result := fs.FileExists(fakePath + "/file.txt") + assert.Assert(t, !result) + }) + + t.Run("WriteToZipFile", func(t *testing.T) { + t.Parallel() + + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + } + zipPath := createTestZip(t, zipFiles) + + testutil.AssertPanics(t, func() { + _ = fs.WriteFile(zipPath+"/src/index.ts", "hello, world", false) + }, "cannot write to zip file") + }) +} + +func TestPnpVfs_CaseSensitivity(t *testing.T) { + t.Parallel() + + sensitiveFS := pnpvfs.From(vfstest.FromMap(map[string]string{}, true)) + assert.Assert(t, sensitiveFS.UseCaseSensitiveFileNames()) + insensitiveFS := pnpvfs.From(vfstest.FromMap(map[string]string{}, false)) + // pnpvfs is always case sensitive + assert.Assert(t, insensitiveFS.UseCaseSensitiveFileNames()) +} + +func TestPnpVfs_FallbackToRegularFiles(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + regularFile := tspath.CombinePaths(tmpDir, "regular.ts") + err := os.WriteFile(regularFile, []byte("regular content"), 0o644) + assert.NilError(t, err) + + fs := pnpvfs.From(osvfs.FS()) + + assert.Assert(t, fs.FileExists(regularFile)) + + content, ok := fs.ReadFile(regularFile) + assert.Assert(t, ok) + assert.Equal(t, "regular content", content) + assert.Assert(t, fs.DirectoryExists(tmpDir)) +} + +func TestZipPath_Detection(t *testing.T) { + t.Parallel() + + testCases := []struct { + path string + shouldBeZip bool + }{ + {"/normal/path/file.txt", false}, + {"/path/to/archive.zip", true}, + {"/path/to/archive.zip/internal/file.txt", true}, + {"/path/archive.zip/nested/dir/file.ts", true}, + {"/path/file.zip.txt", false}, + {"/absolute/archive.zip", true}, + {"/absolute/archive.zip/file.txt", true}, + } + + for _, tc := range testCases { + t.Run(tc.path, func(t *testing.T) { + t.Parallel() + assert.Assert(t, tspath.IsZipPath(tc.path) == tc.shouldBeZip) + }) + } +} + +func TestPnpVfs_VirtualPathHandling(t *testing.T) { + t.Parallel() + + underlyingFS := vfstest.FromMap(map[string]string{ + "/project/packages/packageA/indexA.ts": "export const helloA = 'world';", + "/project/packages/packageA/package.json": `{"name": "packageA"}`, + "/project/packages/packageB/indexB.ts": "export const helloB = 'world';", + "/project/packages/packageB/package.json": `{"name": "packageB"}`, + }, true) + + fs := pnpvfs.From(underlyingFS) + assert.Assert(t, fs.FileExists("/project/packages/__virtual__/packageA-virtual-123456/0/packageA/package.json")) + assert.Assert(t, fs.FileExists("/project/packages/subfolder/__virtual__/packageA-virtual-123456/1/packageA/package.json")) + + content, ok := fs.ReadFile("/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json") + assert.Assert(t, ok) + assert.Equal(t, `{"name": "packageB"}`, content) + + assert.Assert(t, fs.DirectoryExists("/project/packages/__virtual__/packageB-virtual-123456/0/packageB")) + assert.Assert(t, !fs.DirectoryExists("/project/packages/__virtual__/packageB-virtual-123456/0/nonexistent")) + + entries := fs.GetAccessibleEntries("/project/packages/__virtual__/packageB-virtual-123456/0/packageB") + assert.DeepEqual(t, entries.Files, []string{ + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/indexB.ts", + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json", + }) + assert.DeepEqual(t, entries.Directories, []string(nil)) + + files := []string{} + err := fs.WalkDir("/project/packages/__virtual__/packageB-virtual-123456/0/packageB", func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + assert.NilError(t, err) + assert.DeepEqual(t, files, []string{ + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/indexB.ts", + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json", + }) +} + +func TestPnpVfs_RealZipIntegration(t *testing.T) { + t.Parallel() + + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + "src/utils/helpers.ts": "export function add(a: number, b: number) { return a + b; }", + "package.json": `{"name": "test-project", "version": "1.0.0"}`, + "tsconfig.json": `{"compilerOptions": {"target": "es2020"}}`, + } + + zipPath := createTestZip(t, zipFiles) + fs := pnpvfs.From(osvfs.FS()) + + assert.Assert(t, fs.FileExists(zipPath)) + + indexPath := zipPath + "/src/index.ts" + packagePath := zipPath + "/package.json" + assert.Assert(t, fs.FileExists(indexPath)) + assert.Assert(t, fs.FileExists(packagePath)) + assert.Assert(t, fs.DirectoryExists(zipPath+"/src")) + + content, ok := fs.ReadFile(indexPath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["src/index.ts"]) + + content, ok = fs.ReadFile(packagePath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["package.json"]) + + entries := fs.GetAccessibleEntries(zipPath) + assert.DeepEqual(t, entries.Files, []string{zipPath + "/package.json", zipPath + "/tsconfig.json"}) + assert.DeepEqual(t, entries.Directories, []string{zipPath + "/src"}) + + entries = fs.GetAccessibleEntries(zipPath + "/src") + assert.DeepEqual(t, entries.Files, []string{zipPath + "/src/index.ts"}) + assert.DeepEqual(t, entries.Directories, []string{zipPath + "/src/utils"}) + + assert.Equal(t, fs.Realpath(indexPath), indexPath) + + files := []string{} + err := fs.WalkDir(zipPath, func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + assert.NilError(t, err) + assert.DeepEqual(t, files, []string{zipPath + "/package.json", zipPath + "/src/index.ts", zipPath + "/src/utils/helpers.ts", zipPath + "/tsconfig.json"}) + + assert.Assert(t, fs.FileExists(zipPath+"/src/__virtual__/src-virtual-123456/0/index.ts")) + + splitZipPath := strings.Split(zipPath, "/") + beforeZipVirtualPath := strings.Join(splitZipPath[0:len(splitZipPath)-2], "/") + "/__virtual__/zip-virtual-123456/0/" + strings.Join(splitZipPath[len(splitZipPath)-2:], "/") + "/src/index.ts" + assert.Assert(t, fs.FileExists(beforeZipVirtualPath)) +} diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js new file mode 100644 index 0000000000..a949a85d46 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js @@ -0,0 +1,121 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:*" + } +} + +//// [package.json] +{ + "name": "package-a", + "exports": { + "./other-subpath": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "dependencies": { + "package-b": "workspace:*" + } +} + +//// [index.d.ts] +export interface BaseConfig { + timeout: number; + retries: number; +} + +export interface DataOptions { + format: "json" | "xml"; + encoding: string; +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; + options: DataOptions; +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; + +export declare function createServiceConfig(endpoint: string): ServiceConfig; + +//// [index.js] +exports.initializeService = function(url) {}; + + +//// [index.ts] +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; + +export function initializeService(url: string): ServiceConfig { + return createServiceConfig(url); +} + +export const factory = createServiceConfig; + +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.factory = void 0; +exports.initializeService = initializeService; +const other_subpath_1 = require("package-a/other-subpath"); +function initializeService(url) { + return (0, other_subpath_1.createServiceConfig)(url); +} +exports.factory = other_subpath_1.createServiceConfig; + + +//// [index.d.ts] +import type { ServiceConfig } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; +export declare function initializeService(url: string): ServiceConfig; +export declare const factory: typeof createServiceConfig; +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols new file mode 100644 index 0000000000..c99e148d54 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols @@ -0,0 +1,78 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +=== /src/index.ts === +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) +>ConfigFactory : Symbol(ConfigFactory, Decl(index.ts, 0, 28)) + +import { createServiceConfig } from 'package-a/other-subpath'; +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) + +export function initializeService(url: string): ServiceConfig { +>initializeService : Symbol(initializeService, Decl(index.ts, 1, 62)) +>url : Symbol(url, Decl(index.ts, 3, 34)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) + + return createServiceConfig(url); +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) +>url : Symbol(url, Decl(index.ts, 3, 34)) +} + +export const factory = createServiceConfig; +>factory : Symbol(factory, Decl(index.ts, 7, 12)) +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) + +export interface AppConfig { +>AppConfig : Symbol(AppConfig, Decl(index.ts, 7, 43)) + + service: ServiceConfig; +>service : Symbol(AppConfig.service, Decl(index.ts, 9, 28)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) + + debug: boolean; +>debug : Symbol(AppConfig.debug, Decl(index.ts, 10, 25)) +} + +=== /packages/package-a/index.d.ts === +export interface BaseConfig { +>BaseConfig : Symbol(BaseConfig, Decl(index.d.ts, 0, 0)) + + timeout: number; +>timeout : Symbol(BaseConfig.timeout, Decl(index.d.ts, 0, 29)) + + retries: number; +>retries : Symbol(BaseConfig.retries, Decl(index.d.ts, 1, 18)) +} + +export interface DataOptions { +>DataOptions : Symbol(DataOptions, Decl(index.d.ts, 3, 1)) + + format: "json" | "xml"; +>format : Symbol(DataOptions.format, Decl(index.d.ts, 5, 30)) + + encoding: string; +>encoding : Symbol(DataOptions.encoding, Decl(index.d.ts, 6, 25)) +} + +export interface ServiceConfig extends BaseConfig { +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) +>BaseConfig : Symbol(BaseConfig, Decl(index.d.ts, 0, 0)) + + endpoint: string; +>endpoint : Symbol(ServiceConfig.endpoint, Decl(index.d.ts, 10, 51)) + + options: DataOptions; +>options : Symbol(ServiceConfig.options, Decl(index.d.ts, 11, 19)) +>DataOptions : Symbol(DataOptions, Decl(index.d.ts, 3, 1)) +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; +>ConfigFactory : Symbol(ConfigFactory, Decl(index.d.ts, 13, 1)) +>endpoint : Symbol(endpoint, Decl(index.d.ts, 15, 29)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) + +export declare function createServiceConfig(endpoint: string): ServiceConfig; +>createServiceConfig : Symbol(createServiceConfig, Decl(index.d.ts, 15, 64)) +>endpoint : Symbol(endpoint, Decl(index.d.ts, 17, 44)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) + diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types new file mode 100644 index 0000000000..9e51fc8ba3 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types @@ -0,0 +1,65 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +=== /src/index.ts === +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +>ServiceConfig : ServiceConfig +>ConfigFactory : ConfigFactory + +import { createServiceConfig } from 'package-a/other-subpath'; +>createServiceConfig : (endpoint: string) => ServiceConfig + +export function initializeService(url: string): ServiceConfig { +>initializeService : (url: string) => ServiceConfig +>url : string + + return createServiceConfig(url); +>createServiceConfig(url) : ServiceConfig +>createServiceConfig : (endpoint: string) => ServiceConfig +>url : string +} + +export const factory = createServiceConfig; +>factory : (endpoint: string) => ServiceConfig +>createServiceConfig : (endpoint: string) => ServiceConfig + +export interface AppConfig { + service: ServiceConfig; +>service : ServiceConfig + + debug: boolean; +>debug : boolean +} + +=== /packages/package-a/index.d.ts === +export interface BaseConfig { + timeout: number; +>timeout : number + + retries: number; +>retries : number +} + +export interface DataOptions { + format: "json" | "xml"; +>format : "json" | "xml" + + encoding: string; +>encoding : string +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; +>endpoint : string + + options: DataOptions; +>options : DataOptions +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; +>ConfigFactory : ConfigFactory +>endpoint : string + +export declare function createServiceConfig(endpoint: string): ServiceConfig; +>createServiceConfig : (endpoint: string) => ServiceConfig +>endpoint : string + diff --git a/testdata/baselines/reference/compiler/pnpSimpleTest.js b/testdata/baselines/reference/compiler/pnpSimpleTest.js new file mode 100644 index 0000000000..4f17149ae5 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.js @@ -0,0 +1,110 @@ +//// [tests/cases/compiler/pnpSimpleTest.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "npm:1.0.0"], + ["package-b", "npm:2.0.0"] + ] + }] + ]], + ["package-a", [ + ["npm:1.0.0", { + "packageLocation": "./.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/", + "packageDependencies": [] + }] + ]], + ["package-b", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "dependencies": { + "package-a": "npm:1.0.0", + "package-b": "npm:2.0.0" + } +} + +//// [package.json] +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +//// [index.js] +exports.helperA = function(value) { + return "Helper A: " + value; +}; + +//// [index.d.ts] +export declare function helperA(value: string): string; + +//// [package.json] +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +//// [index.js] +exports.helperB = function(value) { + return "Helper B: " + value; +}; + +//// [index.d.ts] +export declare function helperB(value: number): string; + +//// [index.ts] +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +import { helperB } from 'package-b'; + +export function processData(text: string, num: number): string { + const resultA = helperA(text); + const resultB = helperB(num); + return `${resultA} | ${resultB}`; +} + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processData = processData; +// Workspace package that imports both third-party dependencies +const package_a_1 = require("package-a"); +const package_b_1 = require("package-b"); +function processData(text, num) { + const resultA = (0, package_a_1.helperA)(text); + const resultB = (0, package_b_1.helperB)(num); + return `${resultA} | ${resultB}`; +} diff --git a/testdata/baselines/reference/compiler/pnpSimpleTest.symbols b/testdata/baselines/reference/compiler/pnpSimpleTest.symbols new file mode 100644 index 0000000000..c2e6e68b83 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.symbols @@ -0,0 +1,39 @@ +//// [tests/cases/compiler/pnpSimpleTest.ts] //// + +=== /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts === +export declare function helperA(value: string): string; +>helperA : Symbol(helperA, Decl(index.d.ts, 0, 0)) +>value : Symbol(value, Decl(index.d.ts, 0, 32)) + +=== /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts === +export declare function helperB(value: number): string; +>helperB : Symbol(helperB, Decl(index.d.ts, 0, 0)) +>value : Symbol(value, Decl(index.d.ts, 0, 32)) + +=== /src/index.ts === +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +>helperA : Symbol(helperA, Decl(index.ts, 1, 8)) + +import { helperB } from 'package-b'; +>helperB : Symbol(helperB, Decl(index.ts, 2, 8)) + +export function processData(text: string, num: number): string { +>processData : Symbol(processData, Decl(index.ts, 2, 36)) +>text : Symbol(text, Decl(index.ts, 4, 28)) +>num : Symbol(num, Decl(index.ts, 4, 41)) + + const resultA = helperA(text); +>resultA : Symbol(resultA, Decl(index.ts, 5, 7)) +>helperA : Symbol(helperA, Decl(index.ts, 1, 8)) +>text : Symbol(text, Decl(index.ts, 4, 28)) + + const resultB = helperB(num); +>resultB : Symbol(resultB, Decl(index.ts, 6, 7)) +>helperB : Symbol(helperB, Decl(index.ts, 2, 8)) +>num : Symbol(num, Decl(index.ts, 4, 41)) + + return `${resultA} | ${resultB}`; +>resultA : Symbol(resultA, Decl(index.ts, 5, 7)) +>resultB : Symbol(resultB, Decl(index.ts, 6, 7)) +} diff --git a/testdata/baselines/reference/compiler/pnpSimpleTest.types b/testdata/baselines/reference/compiler/pnpSimpleTest.types new file mode 100644 index 0000000000..90ce12017b --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.types @@ -0,0 +1,42 @@ +//// [tests/cases/compiler/pnpSimpleTest.ts] //// + +=== /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts === +export declare function helperA(value: string): string; +>helperA : (value: string) => string +>value : string + +=== /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts === +export declare function helperB(value: number): string; +>helperB : (value: number) => string +>value : number + +=== /src/index.ts === +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +>helperA : (value: string) => string + +import { helperB } from 'package-b'; +>helperB : (value: number) => string + +export function processData(text: string, num: number): string { +>processData : (text: string, num: number) => string +>text : string +>num : number + + const resultA = helperA(text); +>resultA : string +>helperA(text) : string +>helperA : (value: string) => string +>text : string + + const resultB = helperB(num); +>resultB : string +>helperB(num) : string +>helperB : (value: number) => string +>num : number + + return `${resultA} | ${resultB}`; +>`${resultA} | ${resultB}` : string +>resultA : string +>resultB : string +} diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt new file mode 100644 index 0000000000..a3063b9048 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt @@ -0,0 +1,121 @@ +/src/index.ts(5,36): error TS2307: Cannot find module 'package-b' or its corresponding type declarations. + + +==== /.pnp.cjs (0 errors) ==== + module.exports = {}; + +==== /.pnp.data.json (0 errors) ==== + { + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] + } + +==== /package.json (0 errors) ==== + { + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } + } + +==== /packages/package-a/package.json (0 errors) ==== + { + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } + } + +==== /packages/package-a/index.ts (0 errors) ==== + import type { ConfigOptions } from 'package-b'; + + export interface HelperResult { + message: string; + config: ConfigOptions; + } + + export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; + } + +==== /packages/package-b/package.json (0 errors) ==== + { + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } + } + +==== /packages/package-b/index.ts (0 errors) ==== + export interface ConfigOptions { + enabled: boolean; + timeout: number; + } + + export function helperB(value: number): string { + return "Helper B: " + value; + } + +==== /src/index.ts (1 errors) ==== + // Test that the project can import package-a directly + // package-a's types depend on package-b's types (ConfigOptions) + import { helperA } from 'package-a'; + import type { HelperResult } from 'package-a'; + import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + ~~~~~~~~~~~ +!!! error TS2307: Cannot find module 'package-b' or its corresponding type declarations. + + export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); + } + + // Test that the project CANNOT import package-b directly even though package-a uses it + // This should cause an error since package-b is not in project's dependencies + export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; + } + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js new file mode 100644 index 0000000000..b55c670226 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js @@ -0,0 +1,153 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } +} + +//// [package.json] +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } +} + +//// [index.ts] +import type { ConfigOptions } from 'package-b'; + +export interface HelperResult { + message: string; + config: ConfigOptions; +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; +} + +//// [package.json] +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } +} + +//// [index.ts] +export interface ConfigOptions { + enabled: boolean; + timeout: number; +} + +export function helperB(value: number): string { + return "Helper B: " + value; +} + +//// [index.ts] +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +import type { HelperResult } from 'package-a'; +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + +export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; +} + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helperB = helperB; +function helperB(value) { + return "Helper B: " + value; +} +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helperA = helperA; +function helperA(value, config) { + return { + message: "Helper A: " + value, + config: config + }; +} +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useDirectDependency = useDirectDependency; +exports.attemptDirectImport = attemptDirectImport; +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +const package_a_1 = require("package-a"); +function useDirectDependency(text) { + const config = { enabled: true, timeout: 5000 }; + return (0, package_a_1.helperA)(text, config); +} +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +function attemptDirectImport() { + return { enabled: false, timeout: 1000 }; +} diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols new file mode 100644 index 0000000000..752518b2eb --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols @@ -0,0 +1,95 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +=== /packages/package-a/index.ts === +import type { ConfigOptions } from 'package-b'; +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) + +export interface HelperResult { +>HelperResult : Symbol(HelperResult, Decl(index.ts, 0, 47)) + + message: string; +>message : Symbol(HelperResult.message, Decl(index.ts, 2, 31)) + + config: ConfigOptions; +>config : Symbol(HelperResult.config, Decl(index.ts, 3, 18)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { +>helperA : Symbol(helperA, Decl(index.ts, 5, 1)) +>value : Symbol(value, Decl(index.ts, 7, 24)) +>config : Symbol(config, Decl(index.ts, 7, 38)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) +>HelperResult : Symbol(HelperResult, Decl(index.ts, 0, 47)) + + return { + message: "Helper A: " + value, +>message : Symbol(message, Decl(index.ts, 8, 10)) +>value : Symbol(value, Decl(index.ts, 7, 24)) + + config: config +>config : Symbol(config, Decl(index.ts, 9, 34)) +>config : Symbol(config, Decl(index.ts, 7, 38)) + + }; +} + +=== /packages/package-b/index.ts === +export interface ConfigOptions { +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 0)) + + enabled: boolean; +>enabled : Symbol(ConfigOptions.enabled, Decl(index.ts, 0, 32)) + + timeout: number; +>timeout : Symbol(ConfigOptions.timeout, Decl(index.ts, 1, 19)) +} + +export function helperB(value: number): string { +>helperB : Symbol(helperB, Decl(index.ts, 3, 1)) +>value : Symbol(value, Decl(index.ts, 5, 24)) + + return "Helper B: " + value; +>value : Symbol(value, Decl(index.ts, 5, 24)) +} + +=== /src/index.ts === +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +>helperA : Symbol(helperA, Decl(index.ts, 2, 8)) + +import type { HelperResult } from 'package-a'; +>HelperResult : Symbol(HelperResult, Decl(index.ts, 3, 13)) + +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) + +export function useDirectDependency(text: string): HelperResult { +>useDirectDependency : Symbol(useDirectDependency, Decl(index.ts, 4, 47)) +>text : Symbol(text, Decl(index.ts, 6, 36)) +>HelperResult : Symbol(HelperResult, Decl(index.ts, 3, 13)) + + const config: ConfigOptions = { enabled: true, timeout: 5000 }; +>config : Symbol(config, Decl(index.ts, 7, 7)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) +>enabled : Symbol(enabled, Decl(index.ts, 7, 33)) +>timeout : Symbol(timeout, Decl(index.ts, 7, 48)) + + return helperA(text, config); +>helperA : Symbol(helperA, Decl(index.ts, 2, 8)) +>text : Symbol(text, Decl(index.ts, 6, 36)) +>config : Symbol(config, Decl(index.ts, 7, 7)) +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { +>attemptDirectImport : Symbol(attemptDirectImport, Decl(index.ts, 9, 1)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) + + return { enabled: false, timeout: 1000 }; +>enabled : Symbol(enabled, Decl(index.ts, 14, 10)) +>timeout : Symbol(timeout, Decl(index.ts, 14, 26)) +} + diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types new file mode 100644 index 0000000000..67a56d6ef3 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types @@ -0,0 +1,98 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +=== /packages/package-a/index.ts === +import type { ConfigOptions } from 'package-b'; +>ConfigOptions : ConfigOptions + +export interface HelperResult { + message: string; +>message : string + + config: ConfigOptions; +>config : ConfigOptions +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { +>helperA : (value: string, config: ConfigOptions) => HelperResult +>value : string +>config : ConfigOptions + + return { +>{ message: "Helper A: " + value, config: config } : { message: string; config: ConfigOptions; } + + message: "Helper A: " + value, +>message : string +>"Helper A: " + value : string +>"Helper A: " : "Helper A: " +>value : string + + config: config +>config : ConfigOptions +>config : ConfigOptions + + }; +} + +=== /packages/package-b/index.ts === +export interface ConfigOptions { + enabled: boolean; +>enabled : boolean + + timeout: number; +>timeout : number +} + +export function helperB(value: number): string { +>helperB : (value: number) => string +>value : number + + return "Helper B: " + value; +>"Helper B: " + value : string +>"Helper B: " : "Helper B: " +>value : number +} + +=== /src/index.ts === +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +>helperA : (value: string, config: import("/packages/package-b/index").ConfigOptions) => HelperResult + +import type { HelperResult } from 'package-a'; +>HelperResult : HelperResult + +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency +>ConfigOptions : any + +export function useDirectDependency(text: string): HelperResult { +>useDirectDependency : (text: string) => HelperResult +>text : string + + const config: ConfigOptions = { enabled: true, timeout: 5000 }; +>config : ConfigOptions +>{ enabled: true, timeout: 5000 } : { enabled: boolean; timeout: number; } +>enabled : boolean +>true : true +>timeout : number +>5000 : 5000 + + return helperA(text, config); +>helperA(text, config) : HelperResult +>helperA : (value: string, config: import("/packages/package-b/index").ConfigOptions) => HelperResult +>text : string +>config : ConfigOptions +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { +>attemptDirectImport : () => ConfigOptions + + return { enabled: false, timeout: 1000 }; +>{ enabled: false, timeout: 1000 } : { enabled: boolean; timeout: number; } +>enabled : boolean +>false : false +>timeout : number +>1000 : 1000 +} + diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js new file mode 100644 index 0000000000..c1e9014271 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js @@ -0,0 +1,103 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["server-lib", "npm:2.0.0"], + ["@types/server-lib", "npm:2.0.0"] + ] + }] + ]], + ["server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/", + "packageDependencies": [] + }] + ]], + ["@types/server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/", + "packageDependencies": [ + ["@types/runtime", "npm:3.0.0"] + ] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "dependencies": { + "server-lib": "2.0.0" + }, + "devDependencies": { + "@types/server-lib": "2.0.0", + } +} + +//// [package.json] +{ + "name": "server-lib", + "version": "2.0.0" +} + +//// [package.json] +{ + "name": "@types/server-lib", + "version": "2.0.0", + "types": "index.d.ts" +} + +//// [index.d.ts] +export interface Request { + params: Record; + query: Record; +} + +export interface Response { + send(body: Record): void; + json(body: Record): void; +} + +export declare function createServer(): Record; + +//// [index.ts] +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +import { createServer } from 'server-lib'; + +export function handleRequest(req: Request, res: Response): void { + res.json({ data: 'Hello, world!' }); +} + +export const server = createServer(); + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.server = void 0; +exports.handleRequest = handleRequest; +const server_lib_1 = require("server-lib"); +function handleRequest(req, res) { + res.json({ data: 'Hello, world!' }); +} +exports.server = (0, server_lib_1.createServer)(); diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols new file mode 100644 index 0000000000..739cb2367e --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols @@ -0,0 +1,60 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +=== /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts === +export interface Request { +>Request : Symbol(Request, Decl(index.d.ts, 0, 0)) + + params: Record; +>params : Symbol(Request.params, Decl(index.d.ts, 0, 26)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + query: Record; +>query : Symbol(Request.query, Decl(index.d.ts, 1, 34)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +} + +export interface Response { +>Response : Symbol(Response, Decl(index.d.ts, 3, 1)) + + send(body: Record): void; +>send : Symbol(Response.send, Decl(index.d.ts, 5, 27)) +>body : Symbol(body, Decl(index.d.ts, 6, 7)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + json(body: Record): void; +>json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>body : Symbol(body, Decl(index.d.ts, 7, 7)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +} + +export declare function createServer(): Record; +>createServer : Symbol(createServer, Decl(index.d.ts, 8, 1)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + +=== /src/index.ts === +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +>Request : Symbol(Request, Decl(index.ts, 1, 13)) +>Response : Symbol(Response, Decl(index.ts, 1, 22)) + +import { createServer } from 'server-lib'; +>createServer : Symbol(createServer, Decl(index.ts, 2, 8)) + +export function handleRequest(req: Request, res: Response): void { +>handleRequest : Symbol(handleRequest, Decl(index.ts, 2, 42)) +>req : Symbol(req, Decl(index.ts, 4, 30)) +>Request : Symbol(Request, Decl(index.ts, 1, 13)) +>res : Symbol(res, Decl(index.ts, 4, 43)) +>Response : Symbol(Response, Decl(index.ts, 1, 22)) + + res.json({ data: 'Hello, world!' }); +>res.json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>res : Symbol(res, Decl(index.ts, 4, 43)) +>json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>data : Symbol(data, Decl(index.ts, 5, 12)) +} + +export const server = createServer(); +>server : Symbol(server, Decl(index.ts, 8, 12)) +>createServer : Symbol(createServer, Decl(index.ts, 2, 8)) + diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types new file mode 100644 index 0000000000..963f0b9c68 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types @@ -0,0 +1,53 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +=== /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts === +export interface Request { + params: Record; +>params : Record + + query: Record; +>query : Record +} + +export interface Response { + send(body: Record): void; +>send : (body: Record) => void +>body : Record + + json(body: Record): void; +>json : (body: Record) => void +>body : Record +} + +export declare function createServer(): Record; +>createServer : () => Record + +=== /src/index.ts === +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +>Request : Request +>Response : Response + +import { createServer } from 'server-lib'; +>createServer : () => Record + +export function handleRequest(req: Request, res: Response): void { +>handleRequest : (req: Request, res: Response) => void +>req : Request +>res : Response + + res.json({ data: 'Hello, world!' }); +>res.json({ data: 'Hello, world!' }) : void +>res.json : (body: Record) => void +>res : Response +>json : (body: Record) => void +>{ data: 'Hello, world!' } : { data: string; } +>data : string +>'Hello, world!' : "Hello, world!" +} + +export const server = createServer(); +>server : Record +>createServer() : Record +>createServer : () => Record + diff --git a/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts b/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts new file mode 100644 index 0000000000..a40f396867 --- /dev/null +++ b/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts @@ -0,0 +1,110 @@ +// @strict: true +// @declaration: true +// @currentDirectory: /src + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:*" + } +} + +// @filename: /tsconfig.json +{ + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} + +// @filename: /packages/package-a/package.json +{ + "name": "package-a", + "exports": { + "./other-subpath": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "dependencies": { + "package-b": "workspace:*" + } +} + +// @filename: /packages/package-a/index.d.ts +export interface BaseConfig { + timeout: number; + retries: number; +} + +export interface DataOptions { + format: "json" | "xml"; + encoding: string; +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; + options: DataOptions; +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; + +export declare function createServiceConfig(endpoint: string): ServiceConfig; + +// @filename: /packages/package-a/index.js +exports.initializeService = function(url) {}; + + +// @filename: /src/index.ts +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; + +export function initializeService(url: string): ServiceConfig { + return createServiceConfig(url); +} + +export const factory = createServiceConfig; + +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} diff --git a/testdata/tests/cases/compiler/pnpSimpleTest.ts b/testdata/tests/cases/compiler/pnpSimpleTest.ts new file mode 100644 index 0000000000..7c4b2603ee --- /dev/null +++ b/testdata/tests/cases/compiler/pnpSimpleTest.ts @@ -0,0 +1,97 @@ +// @strict: true + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "npm:1.0.0"], + ["package-b", "npm:2.0.0"] + ] + }] + ]], + ["package-a", [ + ["npm:1.0.0", { + "packageLocation": "./.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/", + "packageDependencies": [] + }] + ]], + ["package-b", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: package.json +{ + "name": "project", + "dependencies": { + "package-a": "npm:1.0.0", + "package-b": "npm:2.0.0" + } +} + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/package.json +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.js +exports.helperA = function(value) { + return "Helper A: " + value; +}; + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts +export declare function helperA(value: string): string; + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/package.json +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.js +exports.helperB = function(value) { + return "Helper B: " + value; +}; + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts +export declare function helperB(value: number): string; + +// @filename: /src/index.ts +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +import { helperB } from 'package-b'; + +export function processData(text: string, num: number): string { + const resultA = helperA(text); + const resultB = helperB(num); + return `${resultA} | ${resultB}`; +} \ No newline at end of file diff --git a/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts b/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts new file mode 100644 index 0000000000..41fdc6d77e --- /dev/null +++ b/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts @@ -0,0 +1,117 @@ +// @strict: true + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } +} + +// @filename: /packages/package-a/package.json +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } +} + +// @filename: /packages/package-a/index.ts +import type { ConfigOptions } from 'package-b'; + +export interface HelperResult { + message: string; + config: ConfigOptions; +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; +} + +// @filename: /packages/package-b/package.json +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } +} + +// @filename: /packages/package-b/index.ts +export interface ConfigOptions { + enabled: boolean; + timeout: number; +} + +export function helperB(value: number): string { + return "Helper B: " + value; +} + +// @filename: /src/index.ts +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +import type { HelperResult } from 'package-a'; +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + +export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; +} diff --git a/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts b/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts new file mode 100644 index 0000000000..8515344153 --- /dev/null +++ b/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts @@ -0,0 +1,91 @@ +// @strict: true + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["server-lib", "npm:2.0.0"], + ["@types/server-lib", "npm:2.0.0"] + ] + }] + ]], + ["server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/", + "packageDependencies": [] + }] + ]], + ["@types/server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/", + "packageDependencies": [ + ["@types/runtime", "npm:3.0.0"] + ] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "dependencies": { + "server-lib": "2.0.0" + }, + "devDependencies": { + "@types/server-lib": "2.0.0", + } +} + +// @filename: /.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/package.json +{ + "name": "server-lib", + "version": "2.0.0" +} + +// @filename: /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/package.json +{ + "name": "@types/server-lib", + "version": "2.0.0", + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts +export interface Request { + params: Record; + query: Record; +} + +export interface Response { + send(body: Record): void; + json(body: Record): void; +} + +export declare function createServer(): Record; + +// @filename: /src/index.ts +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +import { createServer } from 'server-lib'; + +export function handleRequest(req: Request, res: Response): void { + res.json({ data: 'Hello, world!' }); +} + +export const server = createServer();