Skip to content
9 changes: 1 addition & 8 deletions internal/core/compileroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"sync"

"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/pnp"
"github.com/microsoft/typescript-go/internal/tspath"
)

Expand Down Expand Up @@ -301,7 +300,7 @@ func (options *CompilerOptions) GetStrictOptionValue(value Tristate) bool {
return options.Strict == TSTrue
}

func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string, pnpApi *pnp.PnpApi) (result []string, fromConfig bool) {
func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) (result []string, fromConfig bool) {
if options.TypeRoots != nil {
return options.TypeRoots, true
}
Expand All @@ -318,12 +317,6 @@ func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string, p
}

nmTypes, nmFromConfig := options.GetNodeModulesTypeRoots(baseDir)

if pnpApi != nil {
typeRoots, fromConfig := pnpApi.AppendPnpTypeRoots(nmTypes, baseDir, nmFromConfig)
return typeRoots, fromConfig
}

return nmTypes, nmFromConfig
}

Expand Down
18 changes: 18 additions & 0 deletions internal/diagnostics/diagnostics_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions internal/diagnostics/extraDiagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,41 @@
"Project '{0}' is out of date because it has errors.": {
"category": "Message",
"code": 6423
},
"Your application tried to access '{0}'. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since '{0}' isn't otherwise declared in your dependencies, this makes the require call ambiguous and unsound.\n\nRequired package: {0}{1}\nRequired by: {2}": {
"category": "Error",
"code": 100003
},
"{0} tried to access '{1}'. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since '{1}' isn't otherwise declared in {0}'s dependencies, this makes the require call ambiguous and unsound.\n\nRequired package: {1}{2}\nRequired by: {3}": {
"category": "Error",
"code": 100004
},
"Your application tried to access '{0}', but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: {0}{1}\nRequired by: {2}": {
"category": "Error",
"code": 100005
},
"Your application tried to access '{0}' (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed.\n\nRequired package: {0}\nRequired by: {1}": {
"category": "Error",
"code": 100006
},
"{0} tried to access '{1}' (a peer dependency) but it isn't provided by its ancestors/your application; this makes the require call ambiguous and unsound.\n\nRequired package: {1}\nRequired by: {2}": {
"category": "Error",
"code": 100007
},
"no PnP manifest found": {
"category": "Error",
"code": 100008
},
"no package found for path '{0}'": {
"category": "Error",
"code": 100009
},
"Empty specifier: '{0}'": {
"category": "Error",
"code": 100010
},
"Invalid specifier: '{0}'": {
"category": "Error",
"code": 100011
}
}
44 changes: 39 additions & 5 deletions internal/module/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/packagejson"
"github.com/microsoft/typescript-go/internal/pnp"
"github.com/microsoft/typescript-go/internal/semver"
"github.com/microsoft/typescript-go/internal/tspath"
)
Expand Down Expand Up @@ -207,7 +208,8 @@ func (r *Resolver) ResolveTypeReferenceDirective(
compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference)
containingDirectory := tspath.GetDirectoryPath(containingFile)

typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory(), r.host.PnpApi())
typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory())
typeRoots = appendPnpTypeRoots(typeRoots, r.host.GetCurrentDirectory(), compilerOptions.ConfigFilePath, 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)
Expand Down Expand Up @@ -983,8 +985,13 @@ func (r *resolutionState) loadModuleFromPnpResolution(ext extensions, moduleName

if pnpApi != nil {
packageName, rest := ParsePackageName(moduleName)
// TODO: bubble up yarn resolution errors, instead of _
packageDirectory, _ := pnpApi.ResolveToUnqualified(packageName, issuer)
packageDirectory, err := pnpApi.ResolveToUnqualified(packageName, issuer)
if err != nil {
if r.tracer != nil {
r.tracer.write(err.Error())
}
return nil
}
if packageDirectory != "" {
candidate := tspath.NormalizePath(tspath.CombinePaths(packageDirectory, rest))
return r.loadModuleFromSpecificNodeModulesDirectoryImpl(ext, true /* nodeModulesDirectoryExists */, candidate, rest, packageDirectory)
Expand Down Expand Up @@ -1790,7 +1797,14 @@ func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packa
var peerDependencyPath string

if pnpApi != nil {
peerDependencyPath, _ = pnpApi.ResolveToUnqualified(name, packageDirectory)
var err error
peerDependencyPath, err = pnpApi.ResolveToUnqualified(name, packageDirectory)
if err != nil {
if r.tracer != nil {
r.tracer.write(err.Error())
}
continue
}
}

if peerDependencyPath == "" {
Expand Down Expand Up @@ -2040,7 +2054,8 @@ func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, host Resoluti
}

var result []string
typeRoots, _ := options.GetEffectiveTypeRoots(host.GetCurrentDirectory(), host.PnpApi())
typeRoots, _ := options.GetEffectiveTypeRoots(host.GetCurrentDirectory())
typeRoots = appendPnpTypeRoots(typeRoots, host.GetCurrentDirectory(), options.ConfigFilePath, host.PnpApi())
for _, root := range typeRoots {
if host.FS().DirectoryExists(root) {
for _, typeDirectivePath := range host.FS().GetAccessibleEntries(root).Directories {
Expand All @@ -2065,3 +2080,22 @@ func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, host Resoluti
}
return result
}

func appendPnpTypeRoots(typeRoots []string, currentDirectory string, configFilePath string, pnpApi *pnp.PnpApi) []string {
if pnpApi == nil {
return typeRoots
}

var baseDir string
if configFilePath != "" {
baseDir = tspath.GetDirectoryPath(configFilePath)
} else {
baseDir = currentDirectory
if baseDir == "" {
panic("cannot get effective type roots without a config file path or current directory")
}
}

typeRoots, _ = pnpApi.AppendPnpTypeRoots(typeRoots, baseDir, false)
return typeRoots
}
49 changes: 28 additions & 21 deletions internal/pnp/manifestparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,33 +81,40 @@ func parseManifestFromPath(fs PnpApiFS, manifestDir string) (*PnpManifestData, e
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")
dataString, err := extractPnpDataStringFromPath(fs, tspath.CombinePaths(manifestDir, ".pnp.cjs"))
if err != nil {
return nil, err
}
pnpDataString = dataString
}

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?")
}
return parseManifestFromData(pnpDataString, manifestDir)
}

start := matches.Index + matches.Length
var b strings.Builder
b.Grow(len(pnpScriptString))
for i := start; i < len(pnpScriptString); i++ {
if pnpScriptString[i] == '\'' {
break
}
func extractPnpDataStringFromPath(fs PnpApiFS, path string) (string, error) {
pnpScriptString, ok := fs.ReadFile(path)
if !ok {
return "", errors.New("failed to read file: " + path)
}
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 "", errors.New("we failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?")
}

if pnpScriptString[i] != '\\' {
b.WriteByte(pnpScriptString[i])
}
start := matches.Index + matches.Length
var b strings.Builder
b.Grow(len(pnpScriptString))
for i := start; i < len(pnpScriptString); i++ {
if pnpScriptString[i] == '\'' {
break
}
pnpDataString = b.String()
}

return parseManifestFromData(pnpDataString, manifestDir)
if pnpScriptString[i] != '\\' {
b.WriteByte(pnpScriptString[i])
}
}
return b.String(), nil
}

func parseManifestFromData(pnpDataString string, manifestDir string) (*PnpManifestData, error) {
Expand Down
71 changes: 52 additions & 19 deletions internal/pnp/pnpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ package pnp
*
* The full specification is available at https://yarnpkg.com/advanced/pnp-spec
*/

import (
"errors"
"fmt"
"slices"
"strings"

"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/tspath"
)

Expand All @@ -24,12 +25,30 @@ type PnpApi struct {
}

// 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 isNodeJSBuiltin(name string) bool {
return core.NodeCoreModules()[name]
}

func isDependencyTreeRoot(m *PnpManifestData, loc *Locator) bool {
return slices.Contains(m.dependencyTreeRoots, *loc)
}

func viaSuffix(specifier string, ident string) string {
if ident != specifier {
return ident + " (via \"" + specifier + "\")"
}
return ""
}

func findBrokenPeerDependencies(specifier string, parent *Locator) []Locator {
return []Locator{}
}

func (p *PnpApi) RefreshManifest() error {
var newData *PnpManifestData
var err error
Expand All @@ -56,13 +75,13 @@ func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (stri
ident, modulePath, err := p.ParseBareIdentifier(specifier)
if err != nil {
// Skipping resolution
return "", nil
return "", err
}

parentLocator, err := p.FindLocator(parentPath)
if err != nil || parentLocator == nil {
// Skipping resolution
return "", nil
return "", err
}

parentPkg := p.GetPackage(parentLocator)
Expand Down Expand Up @@ -96,20 +115,34 @@ func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (stri
}
}

// 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)
if isNodeJSBuiltin(specifier) {
if isDependencyTreeRoot(p.manifest, parentLocator) {
return "", errors.New(diagnostics.Your_application_tried_to_access_0_While_this_module_is_usually_interpreted_as_a_Node_builtin_your_resolver_is_running_inside_a_non_Node_resolution_context_where_such_builtins_are_ignored_Since_0_isn_t_otherwise_declared_in_your_dependencies_this_makes_the_require_call_ambiguous_and_unsound_Required_package_Colon_0_1_Required_by_Colon_2.Format(ident, ident, viaSuffix(specifier, ident), parentPath))
}
return "", errors.New(diagnostics.X_0_tried_to_access_1_While_this_module_is_usually_interpreted_as_a_Node_builtin_your_resolver_is_running_inside_a_non_Node_resolution_context_where_such_builtins_are_ignored_Since_1_isn_t_otherwise_declared_in_0_s_dependencies_this_makes_the_require_call_ambiguous_and_unsound_Required_package_Colon_1_2_Required_by_Colon_3.Format(parentLocator.Name, ident, ident, parentLocator.Name, ident, viaSuffix(specifier, 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)
if isDependencyTreeRoot(p.manifest, parentLocator) {
return "", errors.New(diagnostics.Your_application_tried_to_access_0_but_it_isn_t_declared_in_your_dependencies_this_makes_the_require_call_ambiguous_and_unsound_Required_package_Colon_0_1_Required_by_Colon_2.Format(ident, ident, viaSuffix(specifier, ident), parentPath))
}

brokenAncestors := findBrokenPeerDependencies(specifier, parentLocator)
allBrokenAreRoots := len(brokenAncestors) > 0
if allBrokenAreRoots {
for _, brokenAncestor := range brokenAncestors {
if !isDependencyTreeRoot(p.manifest, &brokenAncestor) {
allBrokenAreRoots = false
break
}
}
}

if len(brokenAncestors) > 0 && allBrokenAreRoots {
return "", errors.New(diagnostics.Your_application_tried_to_access_0_a_peer_dependency_this_isn_t_allowed_as_there_is_no_ancestor_to_satisfy_the_requirement_Use_a_devDependency_if_needed_Required_package_Colon_0_Required_by_Colon_1.Format(ident, ident, parentPath))
} else {
return "", errors.New(diagnostics.X_0_tried_to_access_1_a_peer_dependency_but_it_isn_t_provided_by_its_ancestors_Slashyour_application_this_makes_the_require_call_ambiguous_and_unsound_Required_package_Colon_1_Required_by_Colon_2.Format(parentLocator.Name, 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
Expand All @@ -132,7 +165,7 @@ func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) {
}

if tspath.IsDiskPathRoot(directoryPath) {
return nil, errors.New("no PnP manifest found")
return nil, errors.New(diagnostics.X_no_PnP_manifest_found.Format())
}

directoryPath = tspath.GetDirectoryPath(directoryPath)
Expand Down Expand Up @@ -191,7 +224,7 @@ func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) {
}

if bestLocator == nil {
return nil, fmt.Errorf("no package found for path %s", relativePath)
return nil, errors.New(diagnostics.X_no_package_found_for_path_0.Format(relativePath))
}

return bestLocator, nil
Expand Down Expand Up @@ -223,14 +256,14 @@ func (p *PnpApi) ResolveViaFallback(name string) *PackageDependency {

func (p *PnpApi) ParseBareIdentifier(specifier string) (ident string, modulePath string, err error) {
if len(specifier) == 0 {
return "", "", fmt.Errorf("Empty specifier: %s", specifier)
return "", "", errors.New(diagnostics.Empty_specifier_Colon_0.Format(specifier))
}

firstSlash := strings.Index(specifier, "/")

if specifier[0] == '@' {
if firstSlash == -1 {
return "", "", fmt.Errorf("Invalid specifier: %s", specifier)
return "", "", errors.New(diagnostics.Invalid_specifier_Colon_0.Format(specifier))
}

secondSlash := strings.Index(specifier[firstSlash+1:], "/")
Expand Down
Loading