SandpointsGitHook/vendor/github.com/evanw/esbuild/internal/resolver/yarnpnp.go

667 lines
23 KiB
Go

package resolver
// This file implements the Yarn PnP specification: https://yarnpkg.com/advanced/pnp-spec/
import (
"fmt"
"regexp"
"strings"
"syscall"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_parser"
"github.com/evanw/esbuild/internal/logger"
)
type pnpData struct {
// Keys are the package idents, values are sets of references. Combining the
// ident with each individual reference yields the set of affected locators.
fallbackExclusionList map[string]map[string]bool
// A map of locators that all packages are allowed to access, regardless
// whether they list them in their dependencies or not.
fallbackPool map[string]pnpIdentAndReference
// A nullable regexp. If set, all project-relative importer paths should be
// matched against it. If the match succeeds, the resolution should follow
// the classic Node.js resolution algorithm rather than the Plug'n'Play one.
// Note that unlike other paths in the manifest, the one checked against this
// regexp won't begin by `./`.
ignorePatternData *regexp.Regexp
invalidIgnorePatternData string
// This is the main part of the PnP data file. This table contains the list
// of all packages, first keyed by package ident then by package reference.
// One entry will have `null` in both fields and represents the absolute
// top-level package.
packageRegistryData map[string]map[string]pnpPackage
packageLocatorsByLocations map[string]pnpPackageLocatorByLocation
// If true, should a dependency resolution fail for an importer that isn't
// explicitly listed in `fallbackExclusionList`, the runtime must first check
// whether the resolution would succeed for any of the packages in
// `fallbackPool`; if it would, transparently return this resolution. Note
// that all dependencies from the top-level package are implicitly part of
// the fallback pool, even if not listed here.
enableTopLevelFallback bool
tracker logger.LineColumnTracker
absPath string
absDirPath string
}
// This is called both a "locator" and a "dependency target" in the specification.
// When it's used as a dependency target, it can only be in one of three states:
//
// 1. A reference, to link with the dependency name
// In this case ident is "".
//
// 2. An aliased package
// In this case neither ident nor reference are "".
//
// 3. A missing peer dependency
// In this case ident and reference are "".
type pnpIdentAndReference struct {
ident string // Empty if null
reference string // Empty if null
span logger.Range
}
type pnpPackage struct {
packageDependencies map[string]pnpIdentAndReference
packageLocation string
packageDependenciesRange logger.Range
discardFromLookup bool
}
type pnpPackageLocatorByLocation struct {
locator pnpIdentAndReference
discardFromLookup bool
}
func parseBareIdentifier(specifier string) (ident string, modulePath string, ok bool) {
slash := strings.IndexByte(specifier, '/')
// If specifier starts with "@", then
if strings.HasPrefix(specifier, "@") {
// If specifier doesn't contain a "/" separator, then
if slash == -1 {
// Throw an error
return
}
// Otherwise,
// Set ident to the substring of specifier until the second "/" separator or the end of string, whatever happens first
if slash2 := strings.IndexByte(specifier[slash+1:], '/'); slash2 != -1 {
ident = specifier[:slash+1+slash2]
} else {
ident = specifier
}
} else {
// Otherwise,
// Set ident to the substring of specifier until the first "/" separator or the end of string, whatever happens first
if slash != -1 {
ident = specifier[:slash]
} else {
ident = specifier
}
}
// Set modulePath to the substring of specifier starting from ident.length
modulePath = specifier[len(ident):]
// Return {ident, modulePath}
ok = true
return
}
type pnpStatus uint8
const (
pnpErrorGeneric pnpStatus = iota
pnpErrorDependencyNotFound
pnpErrorUnfulfilledPeerDependency
pnpSuccess
pnpSkipped
)
func (status pnpStatus) isError() bool {
return status < pnpSuccess
}
type pnpResult struct {
status pnpStatus
pkgDirPath string
pkgIdent string
pkgSubpath string
// This is for error messages
errorIdent string
errorRange logger.Range
}
// Note: If this returns successfully then the node module resolution algorithm
// (i.e. NM_RESOLVE in the Yarn PnP specification) is always run afterward
func (r resolverQuery) resolveToUnqualified(specifier string, parentURL string, manifest *pnpData) pnpResult {
// Let resolved be undefined
// Let manifest be FIND_PNP_MANIFEST(parentURL)
// (this is already done by the time we get here)
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Using Yarn PnP manifest from %q", manifest.absPath))
r.debugLogs.addNote(fmt.Sprintf(" Resolving %q in %q", specifier, parentURL))
}
// Let ident and modulePath be the result of PARSE_BARE_IDENTIFIER(specifier)
ident, modulePath, ok := parseBareIdentifier(specifier)
if !ok {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Failed to parse specifier %q into a bare identifier", specifier))
}
return pnpResult{status: pnpErrorGeneric}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Parsed bare identifier %q and module path %q", ident, modulePath))
}
// Let parentLocator be FIND_LOCATOR(manifest, parentURL)
parentLocator, ok := r.findLocator(manifest, parentURL)
// If parentLocator is null, then
// Set resolved to NM_RESOLVE(specifier, parentURL) and return it
if !ok {
return pnpResult{status: pnpSkipped}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Found parent locator: [%s, %s]", quoteOrNullIfEmpty(parentLocator.ident), quoteOrNullIfEmpty(parentLocator.reference)))
}
// Let parentPkg be GET_PACKAGE(manifest, parentLocator)
parentPkg, ok := r.getPackage(manifest, parentLocator.ident, parentLocator.reference)
if !ok {
// We aren't supposed to get here according to the Yarn PnP specification
return pnpResult{status: pnpErrorGeneric}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Found parent package at %q", parentPkg.packageLocation))
}
// Let referenceOrAlias be the entry from parentPkg.packageDependencies referenced by ident
referenceOrAlias, ok := parentPkg.packageDependencies[ident]
// If referenceOrAlias is null or undefined, then
if !ok || referenceOrAlias.reference == "" {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Failed to find %q in \"packageDependencies\" of parent package", ident))
}
// If manifest.enableTopLevelFallback is true, then
if manifest.enableTopLevelFallback {
if r.debugLogs != nil {
r.debugLogs.addNote(" Searching for a fallback because \"enableTopLevelFallback\" is true")
}
// If parentLocator isn't in manifest.fallbackExclusionList, then
if set, _ := manifest.fallbackExclusionList[parentLocator.ident]; !set[parentLocator.reference] {
// Let fallback be RESOLVE_VIA_FALLBACK(manifest, ident)
fallback, _ := r.resolveViaFallback(manifest, ident)
// If fallback is neither null nor undefined
if fallback.reference != "" {
// Set referenceOrAlias to fallback
referenceOrAlias = fallback
ok = true
}
} else if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Stopping because [%s, %s] is in \"fallbackExclusionList\"",
quoteOrNullIfEmpty(parentLocator.ident), quoteOrNullIfEmpty(parentLocator.reference)))
}
}
}
// If referenceOrAlias is still undefined, then
if !ok {
// Throw a resolution error
return pnpResult{
status: pnpErrorDependencyNotFound,
errorIdent: ident,
errorRange: parentPkg.packageDependenciesRange,
}
}
// If referenceOrAlias is still null, then
if referenceOrAlias.reference == "" {
// Note: It means that parentPkg has an unfulfilled peer dependency on ident
// Throw a resolution error
return pnpResult{
status: pnpErrorUnfulfilledPeerDependency,
errorIdent: ident,
errorRange: referenceOrAlias.span,
}
}
if r.debugLogs != nil {
var referenceOrAliasStr string
if referenceOrAlias.ident != "" {
referenceOrAliasStr = fmt.Sprintf("[%q, %q]", referenceOrAlias.ident, referenceOrAlias.reference)
} else {
referenceOrAliasStr = quoteOrNullIfEmpty(referenceOrAlias.reference)
}
r.debugLogs.addNote(fmt.Sprintf(" Found dependency locator: [%s, %s]", quoteOrNullIfEmpty(ident), referenceOrAliasStr))
}
// Otherwise, if referenceOrAlias is an array, then
var dependencyPkg pnpPackage
if referenceOrAlias.ident != "" {
// Let alias be referenceOrAlias
alias := referenceOrAlias
// Let dependencyPkg be GET_PACKAGE(manifest, alias)
dependencyPkg, ok = r.getPackage(manifest, alias.ident, alias.reference)
if !ok {
// We aren't supposed to get here according to the Yarn PnP specification
return pnpResult{status: pnpErrorGeneric}
}
} else {
// Otherwise,
// Let dependencyPkg be GET_PACKAGE(manifest, {ident, reference})
dependencyPkg, ok = r.getPackage(manifest, ident, referenceOrAlias.reference)
if !ok {
// We aren't supposed to get here according to the Yarn PnP specification
return pnpResult{status: pnpErrorGeneric}
}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Found package %q at %q", ident, dependencyPkg.packageLocation))
}
// Return path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)
pkgDirPath := r.fs.Join(manifest.absDirPath, dependencyPkg.packageLocation)
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Resolved %q via Yarn PnP to %q with subpath %q", specifier, pkgDirPath, modulePath))
}
return pnpResult{
status: pnpSuccess,
pkgDirPath: pkgDirPath,
pkgIdent: ident,
pkgSubpath: modulePath,
}
}
func (r resolverQuery) findLocator(manifest *pnpData, moduleUrl string) (pnpIdentAndReference, bool) {
// Let relativeUrl be the relative path between manifest and moduleUrl
relativeUrl, ok := r.fs.Rel(manifest.absDirPath, moduleUrl)
if !ok {
return pnpIdentAndReference{}, false
} else {
// Relative URLs on Windows will use \ instead of /, which will break
// everything we do below. Use normal slashes to keep things working.
relativeUrl = strings.ReplaceAll(relativeUrl, "\\", "/")
}
// The relative path must not start with ./; trim it if needed
if strings.HasPrefix(relativeUrl, "./") {
relativeUrl = relativeUrl[2:]
}
// If relativeUrl matches manifest.ignorePatternData, then
if manifest.ignorePatternData != nil && manifest.ignorePatternData.MatchString(relativeUrl) {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Ignoring %q because it matches \"ignorePatternData\"", relativeUrl))
}
// Return null
return pnpIdentAndReference{}, false
}
// Note: Make sure relativeUrl always starts with a ./ or ../
if !strings.HasSuffix(relativeUrl, "/") {
relativeUrl += "/"
}
if !strings.HasPrefix(relativeUrl, "./") && !strings.HasPrefix(relativeUrl, "../") {
relativeUrl = "./" + relativeUrl
}
// This is the inner loop from Yarn's PnP resolver implementation. This is
// different from the specification, which contains a hypothetical slow
// algorithm instead. The algorithm from the specification can sometimes
// produce different results from the one used by the implementation, so
// we follow the implementation.
for {
entry, ok := manifest.packageLocatorsByLocations[relativeUrl]
if !ok || entry.discardFromLookup {
// Remove the last path component and try again
relativeUrl = relativeUrl[:strings.LastIndexByte(relativeUrl[:len(relativeUrl)-1], '/')+1]
if relativeUrl == "" {
break
}
continue
}
return entry.locator, true
}
return pnpIdentAndReference{}, false
}
func (r resolverQuery) resolveViaFallback(manifest *pnpData, ident string) (pnpIdentAndReference, bool) {
// Let topLevelPkg be GET_PACKAGE(manifest, {null, null})
topLevelPkg, ok := r.getPackage(manifest, "", "")
if !ok {
// We aren't supposed to get here according to the Yarn PnP specification
return pnpIdentAndReference{}, false
}
// Let referenceOrAlias be the entry from topLevelPkg.packageDependencies referenced by ident
referenceOrAlias, ok := topLevelPkg.packageDependencies[ident]
// If referenceOrAlias is defined, then
if ok {
// Return it immediately
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Found fallback for %q in \"packageDependencies\" of top-level package: [%s, %s]", ident,
quoteOrNullIfEmpty(referenceOrAlias.ident), quoteOrNullIfEmpty(referenceOrAlias.reference)))
}
return referenceOrAlias, true
}
// Otherwise,
// Let referenceOrAlias be the entry from manifest.fallbackPool referenced by ident
referenceOrAlias, ok = manifest.fallbackPool[ident]
// Return it immediatly, whether it's defined or not
if r.debugLogs != nil {
if ok {
r.debugLogs.addNote(fmt.Sprintf(" Found fallback for %q in \"fallbackPool\": [%s, %s]", ident,
quoteOrNullIfEmpty(referenceOrAlias.ident), quoteOrNullIfEmpty(referenceOrAlias.reference)))
} else {
r.debugLogs.addNote(fmt.Sprintf(" Failed to find fallback for %q in \"fallbackPool\"", ident))
}
}
return referenceOrAlias, ok
}
func (r resolverQuery) getPackage(manifest *pnpData, ident string, reference string) (pnpPackage, bool) {
if inner, ok := manifest.packageRegistryData[ident]; ok {
if pkg, ok := inner[reference]; ok {
return pkg, true
}
}
if r.debugLogs != nil {
// We aren't supposed to get here according to the Yarn PnP specification:
// "Note: pkg cannot be undefined here; all packages referenced in any of the
// Plug'n'Play data tables MUST have a corresponding entry inside packageRegistryData."
r.debugLogs.addNote(fmt.Sprintf(" Yarn PnP invariant violation: GET_PACKAGE failed to find a package: [%s, %s]",
quoteOrNullIfEmpty(ident), quoteOrNullIfEmpty(reference)))
}
return pnpPackage{}, false
}
func quoteOrNullIfEmpty(str string) string {
if str != "" {
return fmt.Sprintf("%q", str)
}
return "null"
}
func compileYarnPnPData(absPath string, absDirPath string, json js_ast.Expr, source logger.Source) *pnpData {
data := pnpData{
absPath: absPath,
absDirPath: absDirPath,
tracker: logger.MakeLineColumnTracker(&source),
}
if value, _, ok := getProperty(json, "enableTopLevelFallback"); ok {
if enableTopLevelFallback, ok := getBool(value); ok {
data.enableTopLevelFallback = enableTopLevelFallback
}
}
if value, _, ok := getProperty(json, "fallbackExclusionList"); ok {
if array, ok := value.Data.(*js_ast.EArray); ok {
data.fallbackExclusionList = make(map[string]map[string]bool, len(array.Items))
for _, item := range array.Items {
if tuple, ok := item.Data.(*js_ast.EArray); ok && len(tuple.Items) == 2 {
if ident, ok := getStringOrNull(tuple.Items[0]); ok {
if array2, ok := tuple.Items[1].Data.(*js_ast.EArray); ok {
references := make(map[string]bool, len(array2.Items))
for _, item2 := range array2.Items {
if reference, ok := getString(item2); ok {
references[reference] = true
}
}
data.fallbackExclusionList[ident] = references
}
}
}
}
}
}
if value, _, ok := getProperty(json, "fallbackPool"); ok {
if array, ok := value.Data.(*js_ast.EArray); ok {
data.fallbackPool = make(map[string]pnpIdentAndReference, len(array.Items))
for _, item := range array.Items {
if array2, ok := item.Data.(*js_ast.EArray); ok && len(array2.Items) == 2 {
if ident, ok := getString(array2.Items[0]); ok {
if dependencyTarget, ok := getDependencyTarget(array2.Items[1]); ok {
data.fallbackPool[ident] = dependencyTarget
}
}
}
}
}
}
if value, _, ok := getProperty(json, "ignorePatternData"); ok {
if ignorePatternData, ok := getString(value); ok {
// The Go regular expression engine doesn't support some of the features
// that JavaScript regular expressions support, including "(?!" negative
// lookaheads which Yarn uses. This is deliberate on Go's part. See this:
// https://github.com/golang/go/issues/18868.
//
// Yarn uses this feature to exclude the "." and ".." path segments in
// the middle of a relative path. However, we shouldn't ever generate
// such path segments in the first place. So as a hack, we just remove
// the specific character sequences used by Yarn for this so that the
// regular expression is more likely to be able to be compiled.
ignorePatternData = strings.ReplaceAll(ignorePatternData, `(?!\.)`, "")
ignorePatternData = strings.ReplaceAll(ignorePatternData, `(?!(?:^|\/)\.)`, "")
ignorePatternData = strings.ReplaceAll(ignorePatternData, `(?!\.{1,2}(?:\/|$))`, "")
ignorePatternData = strings.ReplaceAll(ignorePatternData, `(?!(?:^|\/)\.{1,2}(?:\/|$))`, "")
if reg, err := regexp.Compile(ignorePatternData); err == nil {
data.ignorePatternData = reg
} else {
data.invalidIgnorePatternData = ignorePatternData
}
}
}
if value, _, ok := getProperty(json, "packageRegistryData"); ok {
if array, ok := value.Data.(*js_ast.EArray); ok {
data.packageRegistryData = make(map[string]map[string]pnpPackage, len(array.Items))
data.packageLocatorsByLocations = make(map[string]pnpPackageLocatorByLocation)
for _, item := range array.Items {
if tuple, ok := item.Data.(*js_ast.EArray); ok && len(tuple.Items) == 2 {
if packageIdent, ok := getStringOrNull(tuple.Items[0]); ok {
if array2, ok := tuple.Items[1].Data.(*js_ast.EArray); ok {
references := make(map[string]pnpPackage, len(array2.Items))
data.packageRegistryData[packageIdent] = references
for _, item2 := range array2.Items {
if tuple2, ok := item2.Data.(*js_ast.EArray); ok && len(tuple2.Items) == 2 {
if packageReference, ok := getStringOrNull(tuple2.Items[0]); ok {
pkg := tuple2.Items[1]
if packageLocation, _, ok := getProperty(pkg, "packageLocation"); ok {
if packageDependencies, _, ok := getProperty(pkg, "packageDependencies"); ok {
if packageLocation, ok := getString(packageLocation); ok {
if array3, ok := packageDependencies.Data.(*js_ast.EArray); ok {
deps := make(map[string]pnpIdentAndReference, len(array3.Items))
discardFromLookup := false
for _, dep := range array3.Items {
if array4, ok := dep.Data.(*js_ast.EArray); ok && len(array4.Items) == 2 {
if ident, ok := getString(array4.Items[0]); ok {
if dependencyTarget, ok := getDependencyTarget(array4.Items[1]); ok {
deps[ident] = dependencyTarget
}
}
}
}
if value, _, ok := getProperty(pkg, "discardFromLookup"); ok {
if value, ok := getBool(value); ok {
discardFromLookup = value
}
}
references[packageReference] = pnpPackage{
packageLocation: packageLocation,
packageDependencies: deps,
packageDependenciesRange: logger.Range{
Loc: packageDependencies.Loc,
Len: array3.CloseBracketLoc.Start + 1 - packageDependencies.Loc.Start,
},
discardFromLookup: discardFromLookup,
}
// This is what Yarn's PnP implementation does (specifically in
// "hydrateRuntimeState"), so we replicate that behavior here:
if entry, ok := data.packageLocatorsByLocations[packageLocation]; !ok {
data.packageLocatorsByLocations[packageLocation] = pnpPackageLocatorByLocation{
locator: pnpIdentAndReference{ident: packageIdent, reference: packageReference},
discardFromLookup: discardFromLookup,
}
} else {
entry.discardFromLookup = entry.discardFromLookup && discardFromLookup
if !discardFromLookup {
entry.locator = pnpIdentAndReference{ident: packageIdent, reference: packageReference}
}
data.packageLocatorsByLocations[packageLocation] = entry
}
}
}
}
}
}
}
}
}
}
}
}
}
}
return &data
}
func getStringOrNull(json js_ast.Expr) (string, bool) {
switch value := json.Data.(type) {
case *js_ast.EString:
return helpers.UTF16ToString(value.Value), true
case *js_ast.ENull:
return "", true
}
return "", false
}
func getDependencyTarget(json js_ast.Expr) (pnpIdentAndReference, bool) {
switch d := json.Data.(type) {
case *js_ast.ENull:
return pnpIdentAndReference{span: logger.Range{Loc: json.Loc, Len: 4}}, true
case *js_ast.EString:
return pnpIdentAndReference{reference: helpers.UTF16ToString(d.Value), span: logger.Range{Loc: json.Loc}}, true
case *js_ast.EArray:
if len(d.Items) == 2 {
if name, ok := getString(d.Items[0]); ok {
if reference, ok := getString(d.Items[1]); ok {
return pnpIdentAndReference{
ident: name,
reference: reference,
span: logger.Range{Loc: json.Loc, Len: d.CloseBracketLoc.Start + 1 - json.Loc.Start},
}, true
}
}
}
}
return pnpIdentAndReference{}, false
}
type pnpDataMode uint8
const (
pnpIgnoreErrorsAboutMissingFiles pnpDataMode = iota
pnpReportErrorsAboutMissingFiles
)
func (r resolverQuery) extractYarnPnPDataFromJSON(pnpDataPath string, mode pnpDataMode) (result js_ast.Expr, source logger.Source) {
contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, pnpDataPath)
if r.debugLogs != nil && originalError != nil {
r.debugLogs.addNote(fmt.Sprintf("Failed to read file %q: %s", pnpDataPath, originalError.Error()))
}
if err != nil {
if mode == pnpReportErrorsAboutMissingFiles || err != syscall.ENOENT {
r.log.AddError(nil, logger.Range{},
fmt.Sprintf("Cannot read file %q: %s",
PrettyPath(r.fs, logger.Path{Text: pnpDataPath, Namespace: "file"}), err.Error()))
}
return
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The file %q exists", pnpDataPath))
}
keyPath := logger.Path{Text: pnpDataPath, Namespace: "file"}
source = logger.Source{
KeyPath: keyPath,
PrettyPath: PrettyPath(r.fs, keyPath),
Contents: contents,
}
result, _ = r.caches.JSONCache.Parse(r.log, source, js_parser.JSONOptions{})
return
}
func (r resolverQuery) tryToExtractYarnPnPDataFromJS(pnpDataPath string, mode pnpDataMode) (result js_ast.Expr, source logger.Source) {
contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, pnpDataPath)
if r.debugLogs != nil && originalError != nil {
r.debugLogs.addNote(fmt.Sprintf("Failed to read file %q: %s", pnpDataPath, originalError.Error()))
}
if err != nil {
if mode == pnpReportErrorsAboutMissingFiles || err != syscall.ENOENT {
r.log.AddError(nil, logger.Range{},
fmt.Sprintf("Cannot read file %q: %s",
PrettyPath(r.fs, logger.Path{Text: pnpDataPath, Namespace: "file"}), err.Error()))
}
return
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The file %q exists", pnpDataPath))
}
keyPath := logger.Path{Text: pnpDataPath, Namespace: "file"}
source = logger.Source{
KeyPath: keyPath,
PrettyPath: PrettyPath(r.fs, keyPath),
Contents: contents,
}
ast, _ := r.caches.JSCache.Parse(r.log, source, js_parser.OptionsForYarnPnP())
if r.debugLogs != nil && ast.ManifestForYarnPnP.Data != nil {
r.debugLogs.addNote(fmt.Sprintf(" Extracted JSON data from %q", pnpDataPath))
}
return ast.ManifestForYarnPnP, source
}