gowebbuild/vendor/github.com/evanw/esbuild/internal/resolver/package_json.go

1226 lines
38 KiB
Go

package resolver
import (
"fmt"
"net/url"
"path"
"regexp"
"sort"
"strings"
"github.com/evanw/esbuild/internal/config"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_lexer"
"github.com/evanw/esbuild/internal/js_parser"
"github.com/evanw/esbuild/internal/logger"
)
type packageJSON struct {
source logger.Source
mainFields map[string]mainField
moduleType js_ast.ModuleType
// Present if the "browser" field is present. This field is intended to be
// used by bundlers and lets you redirect the paths of certain 3rd-party
// modules that don't work in the browser to other modules that shim that
// functionality. That way you don't have to rewrite the code for those 3rd-
// party modules. For example, you might remap the native "util" node module
// to something like https://www.npmjs.com/package/util so it works in the
// browser.
//
// This field contains the original mapping object in "package.json". Mapping
// to a nil path indicates that the module is disabled. As far as I can
// tell, the official spec is an abandoned GitHub repo hosted by a user account:
// https://github.com/defunctzombie/package-browser-field-spec. The npm docs
// say almost nothing: https://docs.npmjs.com/files/package.json.
//
// Note that the non-package "browser" map has to be checked twice to match
// Webpack's behavior: once before resolution and once after resolution. It
// leads to some unintuitive failure cases that we must emulate around missing
// file extensions:
//
// * Given the mapping "./no-ext": "./no-ext-browser.js" the query "./no-ext"
// should match but the query "./no-ext.js" should NOT match.
//
// * Given the mapping "./ext.js": "./ext-browser.js" the query "./ext.js"
// should match and the query "./ext" should ALSO match.
//
browserMap map[string]*string
// If this is non-nil, each entry in this map is the absolute path of a file
// with side effects. Any entry not in this map should be considered to have
// no side effects, which means import statements for these files can be
// removed if none of the imports are used. This is a convention from Webpack:
// https://webpack.js.org/guides/tree-shaking/.
//
// Note that if a file is included, all statements that can't be proven to be
// free of side effects must be included. This convention does not say
// anything about whether any statements within the file have side effects or
// not.
sideEffectsMap map[string]bool
sideEffectsRegexps []*regexp.Regexp
sideEffectsData *SideEffectsData
// This represents the "imports" field in this package.json file.
importsMap *pjMap
// This represents the "exports" field in this package.json file.
exportsMap *pjMap
}
type mainField struct {
keyLoc logger.Loc
relPath string
}
type browserPathKind uint8
const (
absolutePathKind browserPathKind = iota
packagePathKind
)
func (r resolverQuery) checkBrowserMap(resolveDirInfo *dirInfo, inputPath string, kind browserPathKind) (remapped *string, ok bool) {
// This only applies if the current platform is "browser"
if r.options.Platform != config.PlatformBrowser {
return nil, false
}
// There must be an enclosing directory with a "package.json" file with a "browser" map
if resolveDirInfo.enclosingBrowserScope == nil {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("No \"browser\" map found in directory %q", resolveDirInfo.absPath))
}
return nil, false
}
packageJSON := resolveDirInfo.enclosingBrowserScope.packageJSON
browserMap := packageJSON.browserMap
checkPath := func(pathToCheck string) bool {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Checking for %q in the \"browser\" map in %q",
pathToCheck, packageJSON.source.KeyPath.Text))
}
// Check for equality
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Checking for %q", pathToCheck))
}
remapped, ok = browserMap[pathToCheck]
if ok {
inputPath = pathToCheck
return true
}
// If that failed, try adding implicit extensions
for _, ext := range r.options.ExtensionOrder {
extPath := pathToCheck + ext
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Checking for %q", extPath))
}
remapped, ok = browserMap[extPath]
if ok {
inputPath = extPath
return true
}
}
// If that failed, try assuming this is a directory and looking for an "index" file
indexPath := path.Join(pathToCheck, "index")
if IsPackagePath(indexPath) && !IsPackagePath(pathToCheck) {
indexPath = "./" + indexPath
}
// Check for equality
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Checking for %q", indexPath))
}
remapped, ok = browserMap[indexPath]
if ok {
inputPath = indexPath
return true
}
// If that failed, try adding implicit extensions
for _, ext := range r.options.ExtensionOrder {
extPath := indexPath + ext
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf(" Checking for %q", extPath))
}
remapped, ok = browserMap[extPath]
if ok {
inputPath = extPath
return true
}
}
return false
}
// Turn absolute paths into paths relative to the "browser" map location
if kind == absolutePathKind {
relPath, ok := r.fs.Rel(resolveDirInfo.enclosingBrowserScope.absPath, inputPath)
if !ok {
return nil, false
}
inputPath = strings.ReplaceAll(relPath, "\\", "/")
}
if inputPath == "." {
// No bundler supports remapping ".", so we don't either
return nil, false
}
// First try the import path as a package path
if !checkPath(inputPath) && IsPackagePath(inputPath) {
// If a package path didn't work, try the import path as a relative path
switch kind {
case absolutePathKind:
checkPath("./" + inputPath)
case packagePathKind:
// Browserify allows a browser map entry of "./pkg" to override a package
// path of "require('pkg')". This is weird, and arguably a bug. But we
// replicate this bug for compatibility. However, Browserify only allows
// this within the same package. It does not allow such an entry in a
// parent package to override this in a child package. So this behavior
// is disallowed if there is a "node_modules" folder in between the child
// package and the parent package.
isInSamePackage := true
for info := resolveDirInfo; info != nil && info != resolveDirInfo.enclosingBrowserScope; info = info.parent {
if info.isNodeModules {
isInSamePackage = false
break
}
}
if isInSamePackage {
checkPath("./" + inputPath)
}
}
}
if r.debugLogs != nil {
if ok {
if remapped == nil {
r.debugLogs.addNote(fmt.Sprintf("Found %q marked as disabled", inputPath))
} else {
r.debugLogs.addNote(fmt.Sprintf("Found %q mapping to %q", inputPath, *remapped))
}
} else {
r.debugLogs.addNote(fmt.Sprintf("Failed to find %q", inputPath))
}
}
return
}
func (r resolverQuery) parsePackageJSON(inputPath string) *packageJSON {
packageJSONPath := r.fs.Join(inputPath, "package.json")
contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, packageJSONPath)
if r.debugLogs != nil && originalError != nil {
r.debugLogs.addNote(fmt.Sprintf("Failed to read file %q: %s", packageJSONPath, originalError.Error()))
}
if err != nil {
r.log.Add(logger.Error, nil, logger.Range{},
fmt.Sprintf("Cannot read file %q: %s",
r.PrettyPath(logger.Path{Text: packageJSONPath, Namespace: "file"}), err.Error()))
return nil
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The file %q exists", packageJSONPath))
}
keyPath := logger.Path{Text: packageJSONPath, Namespace: "file"}
jsonSource := logger.Source{
KeyPath: keyPath,
PrettyPath: r.PrettyPath(keyPath),
Contents: contents,
}
tracker := logger.MakeLineColumnTracker(&jsonSource)
json, ok := r.caches.JSONCache.Parse(r.log, jsonSource, js_parser.JSONOptions{})
if !ok {
return nil
}
packageJSON := &packageJSON{
source: jsonSource,
mainFields: make(map[string]mainField),
}
// Read the "type" field
if typeJSON, _, ok := getProperty(json, "type"); ok {
if typeValue, ok := getString(typeJSON); ok {
switch typeValue {
case "commonjs":
packageJSON.moduleType = js_ast.ModuleCommonJS
case "module":
packageJSON.moduleType = js_ast.ModuleESM
default:
r.log.AddWithNotes(logger.Warning, &tracker, jsonSource.RangeOfString(typeJSON.Loc),
fmt.Sprintf("%q is not a valid value for the \"type\" field", typeValue),
[]logger.MsgData{{Text: "The \"type\" field must be set to either \"commonjs\" or \"module\"."}},
)
}
} else {
r.log.Add(logger.Warning, &tracker, logger.Range{Loc: typeJSON.Loc},
"The value for \"type\" must be a string")
}
}
// Read the "main" fields
mainFields := r.options.MainFields
if mainFields == nil {
mainFields = defaultMainFields[r.options.Platform]
}
for _, field := range mainFields {
if mainJSON, mainRange, ok := getProperty(json, field); ok {
if main, ok := getString(mainJSON); ok && main != "" {
packageJSON.mainFields[field] = mainField{mainRange, main}
}
}
}
for _, field := range mainFieldsForFailure {
if _, ok := packageJSON.mainFields[field]; !ok {
if mainJSON, mainRange, ok := getProperty(json, field); ok {
if main, ok := getString(mainJSON); ok && main != "" {
packageJSON.mainFields[field] = mainField{mainRange, main}
}
}
}
}
// Read the "browser" property, but only when targeting the browser
if browserJSON, _, ok := getProperty(json, "browser"); ok && r.options.Platform == config.PlatformBrowser {
// We both want the ability to have the option of CJS vs. ESM and the
// option of having node vs. browser. The way to do this is to use the
// object literal form of the "browser" field like this:
//
// "main": "dist/index.node.cjs.js",
// "module": "dist/index.node.esm.js",
// "browser": {
// "./dist/index.node.cjs.js": "./dist/index.browser.cjs.js",
// "./dist/index.node.esm.js": "./dist/index.browser.esm.js"
// },
//
if browser, ok := browserJSON.Data.(*js_ast.EObject); ok {
// The value is an object
browserMap := make(map[string]*string)
// Remap all files in the browser field
for _, prop := range browser.Properties {
if key, ok := getString(prop.Key); ok && prop.ValueOrNil.Data != nil {
if value, ok := getString(prop.ValueOrNil); ok {
// If this is a string, it's a replacement package
browserMap[key] = &value
} else if value, ok := getBool(prop.ValueOrNil); ok {
// If this is false, it means the package is disabled
if !value {
browserMap[key] = nil
}
} else {
r.log.Add(logger.Warning, &tracker, logger.Range{Loc: prop.ValueOrNil.Loc},
"Each \"browser\" mapping must be a string or a boolean")
}
}
}
packageJSON.browserMap = browserMap
}
}
// Read the "sideEffects" property
if sideEffectsJSON, sideEffectsLoc, ok := getProperty(json, "sideEffects"); ok {
switch data := sideEffectsJSON.Data.(type) {
case *js_ast.EBoolean:
if !data.Value {
// Make an empty map for "sideEffects: false", which indicates all
// files in this module can be considered to not have side effects.
packageJSON.sideEffectsMap = make(map[string]bool)
packageJSON.sideEffectsData = &SideEffectsData{
IsSideEffectsArrayInJSON: false,
Source: &jsonSource,
Range: jsonSource.RangeOfString(sideEffectsLoc),
}
}
case *js_ast.EArray:
// The "sideEffects: []" format means all files in this module but not in
// the array can be considered to not have side effects.
packageJSON.sideEffectsMap = make(map[string]bool)
packageJSON.sideEffectsData = &SideEffectsData{
IsSideEffectsArrayInJSON: true,
Source: &jsonSource,
Range: jsonSource.RangeOfString(sideEffectsLoc),
}
for _, itemJSON := range data.Items {
item, ok := itemJSON.Data.(*js_ast.EString)
if !ok || item.Value == nil {
r.log.Add(logger.Warning, &tracker, logger.Range{Loc: itemJSON.Loc},
"Expected string in array for \"sideEffects\"")
continue
}
// Reference: https://github.com/webpack/webpack/blob/ed175cd22f89eb9fecd0a70572a3fd0be028e77c/lib/optimize/SideEffectsFlagPlugin.js
pattern := js_lexer.UTF16ToString(item.Value)
if !strings.ContainsRune(pattern, '/') {
pattern = "**/" + pattern
}
absPattern := r.fs.Join(inputPath, pattern)
re, hadWildcard := globstarToEscapedRegexp(absPattern)
// Wildcard patterns require more expensive matching
if hadWildcard {
packageJSON.sideEffectsRegexps = append(packageJSON.sideEffectsRegexps, regexp.MustCompile(re))
continue
}
// Normal strings can be matched with a map lookup
packageJSON.sideEffectsMap[absPattern] = true
}
default:
r.log.Add(logger.Warning, &tracker, logger.Range{Loc: sideEffectsJSON.Loc},
"The value for \"sideEffects\" must be a boolean or an array")
}
}
// Read the "imports" map
if importsJSON, _, ok := getProperty(json, "imports"); ok {
if importsMap := parseImportsExportsMap(jsonSource, r.log, importsJSON); importsMap != nil {
if importsMap.root.kind != pjObject {
r.log.Add(logger.Warning, &tracker, importsMap.root.firstToken,
"The value for \"imports\" must be an object")
}
packageJSON.importsMap = importsMap
}
}
// Read the "exports" map
if exportsJSON, _, ok := getProperty(json, "exports"); ok {
if exportsMap := parseImportsExportsMap(jsonSource, r.log, exportsJSON); exportsMap != nil {
packageJSON.exportsMap = exportsMap
}
}
return packageJSON
}
// Reference: https://github.com/fitzgen/glob-to-regexp/blob/2abf65a834259c6504ed3b80e85f893f8cd99127/index.js
func globstarToEscapedRegexp(glob string) (string, bool) {
sb := strings.Builder{}
sb.WriteByte('^')
hadWildcard := false
n := len(glob)
for i := 0; i < n; i++ {
c := glob[i]
switch c {
case '\\', '^', '$', '.', '+', '|', '(', ')', '[', ']', '{', '}':
sb.WriteByte('\\')
sb.WriteByte(c)
case '?':
sb.WriteByte('.')
hadWildcard = true
case '*':
// Move over all consecutive "*"'s.
// Also store the previous and next characters
prevChar := -1
if i > 0 {
prevChar = int(glob[i-1])
}
starCount := 1
for i+1 < n && glob[i+1] == '*' {
starCount++
i++
}
nextChar := -1
if i+1 < n {
nextChar = int(glob[i+1])
}
// Determine if this is a globstar segment
isGlobstar := starCount > 1 && // multiple "*"'s
(prevChar == '/' || prevChar == -1) && // from the start of the segment
(nextChar == '/' || nextChar == -1) // to the end of the segment
if isGlobstar {
// It's a globstar, so match zero or more path segments
sb.WriteString("(?:[^/]*(?:/|$))*")
i++ // Move over the "/"
} else {
// It's not a globstar, so only match one path segment
sb.WriteString("[^/]*")
}
hadWildcard = true
default:
sb.WriteByte(c)
}
}
sb.WriteByte('$')
return sb.String(), hadWildcard
}
// Reference: https://nodejs.org/api/esm.html#esm_resolver_algorithm_specification
type pjMap struct {
root pjEntry
}
type pjKind uint8
const (
pjNull pjKind = iota
pjString
pjArray
pjObject
pjInvalid
)
type pjEntry struct {
strData string
arrData []pjEntry
mapData []pjMapEntry // Can't be a "map" because order matters
expansionKeys expansionKeysArray
firstToken logger.Range
kind pjKind
}
type pjMapEntry struct {
key string
keyRange logger.Range
value pjEntry
}
// This type is just so we can use Go's native sort function
type expansionKeysArray []pjMapEntry
func (a expansionKeysArray) Len() int { return len(a) }
func (a expansionKeysArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
func (a expansionKeysArray) Less(i int, j int) bool {
return len(a[i].key) > len(a[j].key)
}
func (entry pjEntry) valueForKey(key string) (pjEntry, bool) {
for _, item := range entry.mapData {
if item.key == key {
return item.value, true
}
}
return pjEntry{}, false
}
func parseImportsExportsMap(source logger.Source, log logger.Log, json js_ast.Expr) *pjMap {
var visit func(expr js_ast.Expr) pjEntry
tracker := logger.MakeLineColumnTracker(&source)
visit = func(expr js_ast.Expr) pjEntry {
var firstToken logger.Range
switch e := expr.Data.(type) {
case *js_ast.ENull:
return pjEntry{
kind: pjNull,
firstToken: js_lexer.RangeOfIdentifier(source, expr.Loc),
}
case *js_ast.EString:
return pjEntry{
kind: pjString,
firstToken: source.RangeOfString(expr.Loc),
strData: js_lexer.UTF16ToString(e.Value),
}
case *js_ast.EArray:
arrData := make([]pjEntry, len(e.Items))
for i, item := range e.Items {
arrData[i] = visit(item)
}
return pjEntry{
kind: pjArray,
firstToken: logger.Range{Loc: expr.Loc, Len: 1},
arrData: arrData,
}
case *js_ast.EObject:
mapData := make([]pjMapEntry, len(e.Properties))
expansionKeys := make(expansionKeysArray, 0, len(e.Properties))
firstToken := logger.Range{Loc: expr.Loc, Len: 1}
isConditionalSugar := false
for i, property := range e.Properties {
keyStr, _ := property.Key.Data.(*js_ast.EString)
key := js_lexer.UTF16ToString(keyStr.Value)
keyRange := source.RangeOfString(property.Key.Loc)
// If exports is an Object with both a key starting with "." and a key
// not starting with ".", throw an Invalid Package Configuration error.
curIsConditionalSugar := !strings.HasPrefix(key, ".")
if i == 0 {
isConditionalSugar = curIsConditionalSugar
} else if isConditionalSugar != curIsConditionalSugar {
prevEntry := mapData[i-1]
log.AddWithNotes(logger.Warning, &tracker, keyRange,
"This object cannot contain keys that both start with \".\" and don't start with \".\"",
[]logger.MsgData{tracker.MsgData(prevEntry.keyRange,
fmt.Sprintf("The key %q is incompatible with the previous key %q:", key, prevEntry.key))})
return pjEntry{
kind: pjInvalid,
firstToken: firstToken,
}
}
entry := pjMapEntry{
key: key,
keyRange: keyRange,
value: visit(property.ValueOrNil),
}
if strings.HasSuffix(key, "/") || strings.HasSuffix(key, "*") {
expansionKeys = append(expansionKeys, entry)
}
mapData[i] = entry
}
// Let expansionKeys be the list of keys of matchObj ending in "/" or "*",
// sorted by length descending.
sort.Stable(expansionKeys)
return pjEntry{
kind: pjObject,
firstToken: firstToken,
mapData: mapData,
expansionKeys: expansionKeys,
}
case *js_ast.EBoolean:
firstToken = js_lexer.RangeOfIdentifier(source, expr.Loc)
case *js_ast.ENumber:
firstToken = source.RangeOfNumber(expr.Loc)
default:
firstToken.Loc = expr.Loc
}
log.Add(logger.Warning, &tracker, firstToken,
"This value must be a string, an object, an array, or null")
return pjEntry{
kind: pjInvalid,
firstToken: firstToken,
}
}
root := visit(json)
if root.kind == pjNull {
return nil
}
return &pjMap{root: root}
}
func (entry pjEntry) keysStartWithDot() bool {
return len(entry.mapData) > 0 && strings.HasPrefix(entry.mapData[0].key, ".")
}
type pjStatus uint8
const (
pjStatusUndefined pjStatus = iota
pjStatusUndefinedNoConditionsMatch // A more friendly error message for when no conditions are matched
pjStatusNull
pjStatusExact
pjStatusInexact // This means we may need to try CommonJS-style extension suffixes
pjStatusPackageResolve // Need to re-run package resolution on the result
// Module specifier is an invalid URL, package name or package subpath specifier.
pjStatusInvalidModuleSpecifier
// package.json configuration is invalid or contains an invalid configuration.
pjStatusInvalidPackageConfiguration
// Package exports or imports define a target module for the package that is an invalid type or string target.
pjStatusInvalidPackageTarget
// Package exports do not define or permit a target subpath in the package for the given module.
pjStatusPackagePathNotExported
// Package imports do not define the specifiespecifier
pjStatusPackageImportNotDefined
// The package or module requested does not exist.
pjStatusModuleNotFound
// The resolved path corresponds to a directory, which is not a supported target for module imports.
pjStatusUnsupportedDirectoryImport
)
func (status pjStatus) isUndefined() bool {
return status == pjStatusUndefined || status == pjStatusUndefinedNoConditionsMatch
}
type pjDebug struct {
// This is the range of the token to use for error messages
token logger.Range
// If the status is "pjStatusUndefinedNoConditionsMatch", this is the set of
// conditions that didn't match. This information is used for error messages.
unmatchedConditions []string
}
func (r resolverQuery) esmHandlePostConditions(
resolved string,
status pjStatus,
debug pjDebug,
) (string, pjStatus, pjDebug) {
if status != pjStatusExact && status != pjStatusInexact {
return resolved, status, debug
}
// If resolved contains any percent encodings of "/" or "\" ("%2f" and "%5C"
// respectively), then throw an Invalid Module Specifier error.
resolvedPath, err := url.PathUnescape(resolved)
if err != nil {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The path %q contains invalid URL escapes: %s", resolved, err.Error()))
}
return resolved, pjStatusInvalidModuleSpecifier, debug
}
var found string
if strings.Contains(resolved, "%2f") {
found = "%2f"
} else if strings.Contains(resolved, "%2F") {
found = "%2F"
} else if strings.Contains(resolved, "%5c") {
found = "%5c"
} else if strings.Contains(resolved, "%5C") {
found = "%5C"
}
if found != "" {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The path %q is not allowed to contain %q", resolved, found))
}
return resolved, pjStatusInvalidModuleSpecifier, debug
}
// If the file at resolved is a directory, then throw an Unsupported Directory
// Import error.
if strings.HasSuffix(resolvedPath, "/") || strings.HasSuffix(resolvedPath, "\\") {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The path %q is not allowed to end with a slash", resolved))
}
return resolved, pjStatusUnsupportedDirectoryImport, debug
}
// Set resolved to the real path of resolved.
return resolvedPath, status, debug
}
func (r resolverQuery) esmPackageImportsResolve(
specifier string,
imports pjEntry,
conditions map[string]bool,
) (string, pjStatus, pjDebug) {
// ALGORITHM DEVIATION: Provide a friendly error message if "imports" is not an object
if imports.kind != pjObject {
return "", pjStatusInvalidPackageConfiguration, pjDebug{token: imports.firstToken}
}
resolved, status, debug := r.esmPackageImportsExportsResolve(specifier, imports, "/", true, conditions)
if status != pjStatusNull && status != pjStatusUndefined {
return resolved, status, debug
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The package import %q is not defined", specifier))
}
return specifier, pjStatusPackageImportNotDefined, pjDebug{token: imports.firstToken}
}
func (r resolverQuery) esmPackageExportsResolve(
packageURL string,
subpath string,
exports pjEntry,
conditions map[string]bool,
) (string, pjStatus, pjDebug) {
if exports.kind == pjInvalid {
if r.debugLogs != nil {
r.debugLogs.addNote("Invalid package configuration")
}
return "", pjStatusInvalidPackageConfiguration, pjDebug{token: exports.firstToken}
}
if subpath == "." {
mainExport := pjEntry{kind: pjNull}
if exports.kind == pjString || exports.kind == pjArray || (exports.kind == pjObject && !exports.keysStartWithDot()) {
mainExport = exports
} else if exports.kind == pjObject {
if dot, ok := exports.valueForKey("."); ok {
if r.debugLogs != nil {
r.debugLogs.addNote("Using the entry for \".\"")
}
mainExport = dot
}
}
if mainExport.kind != pjNull {
resolved, status, debug := r.esmPackageTargetResolve(packageURL, mainExport, "", false, false, conditions)
if status != pjStatusNull && status != pjStatusUndefined {
return resolved, status, debug
}
}
} else if exports.kind == pjObject && exports.keysStartWithDot() {
resolved, status, debug := r.esmPackageImportsExportsResolve(subpath, exports, packageURL, false, conditions)
if status != pjStatusNull && status != pjStatusUndefined {
return resolved, status, debug
}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The path %q is not exported", subpath))
}
return "", pjStatusPackagePathNotExported, pjDebug{token: exports.firstToken}
}
func (r resolverQuery) esmPackageImportsExportsResolve(
matchKey string,
matchObj pjEntry,
packageURL string,
isImports bool,
conditions map[string]bool,
) (string, pjStatus, pjDebug) {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Checking object path map for %q", matchKey))
}
if !strings.HasSuffix(matchKey, "*") {
if target, ok := matchObj.valueForKey(matchKey); ok {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Found exact match for %q", matchKey))
}
return r.esmPackageTargetResolve(packageURL, target, "", false, isImports, conditions)
}
}
for _, expansion := range matchObj.expansionKeys {
// If expansionKey ends in "*" and matchKey starts with but is not equal to
// the substring of expansionKey excluding the last "*" character
if strings.HasSuffix(expansion.key, "*") {
if substr := expansion.key[:len(expansion.key)-1]; strings.HasPrefix(matchKey, substr) && matchKey != substr {
target := expansion.value
subpath := matchKey[len(expansion.key)-1:]
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath))
}
return r.esmPackageTargetResolve(packageURL, target, subpath, true, isImports, conditions)
}
}
if strings.HasPrefix(matchKey, expansion.key) {
target := expansion.value
subpath := matchKey[len(expansion.key):]
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath))
}
result, status, debug := r.esmPackageTargetResolve(packageURL, target, subpath, false, isImports, conditions)
if status == pjStatusExact {
// Return the object { resolved, exact: false }.
status = pjStatusInexact
}
return result, status, debug
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The key %q did not match", expansion.key))
}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("No keys matched %q", matchKey))
}
return "", pjStatusNull, pjDebug{token: matchObj.firstToken}
}
// If path split on "/" or "\" contains any ".", ".." or "node_modules"
// segments after the first segment, throw an Invalid Package Target error.
func findInvalidSegment(path string) string {
slash := strings.IndexAny(path, "/\\")
if slash == -1 {
return ""
}
path = path[slash+1:]
for path != "" {
slash := strings.IndexAny(path, "/\\")
segment := path
if slash != -1 {
segment = path[:slash]
path = path[slash+1:]
} else {
path = ""
}
if segment == "." || segment == ".." || segment == "node_modules" {
return segment
}
}
return ""
}
func (r resolverQuery) esmPackageTargetResolve(
packageURL string,
target pjEntry,
subpath string,
pattern bool,
internal bool,
conditions map[string]bool,
) (string, pjStatus, pjDebug) {
switch target.kind {
case pjString:
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Checking path %q against target %q", subpath, target.strData))
r.debugLogs.increaseIndent()
defer r.debugLogs.decreaseIndent()
}
// If pattern is false, subpath has non-zero length and target
// does not end with "/", throw an Invalid Module Specifier error.
if !pattern && subpath != "" && !strings.HasSuffix(target.strData, "/") {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The target %q is invalid because it doesn't end \"/\"", target.strData))
}
return target.strData, pjStatusInvalidModuleSpecifier, pjDebug{token: target.firstToken}
}
// If target does not start with "./", then...
if !strings.HasPrefix(target.strData, "./") {
if internal && !strings.HasPrefix(target.strData, "../") && !strings.HasPrefix(target.strData, "/") {
if pattern {
result := strings.ReplaceAll(target.strData, "*", subpath)
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Substituted %q for \"*\" in %q to get %q", subpath, target.strData, result))
}
return result, pjStatusPackageResolve, pjDebug{token: target.firstToken}
}
result := target.strData + subpath
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Joined %q to %q to get %q", target.strData, subpath, result))
}
return result, pjStatusPackageResolve, pjDebug{token: target.firstToken}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The target %q is invalid because it doesn't start with \"./\"", target.strData))
}
return target.strData, pjStatusInvalidPackageTarget, pjDebug{token: target.firstToken}
}
// If target split on "/" or "\" contains any ".", ".." or "node_modules"
// segments after the first segment, throw an Invalid Package Target error.
if invalidSegment := findInvalidSegment(target.strData); invalidSegment != "" {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The target %q is invalid because it contains invalid segment %q", target.strData, invalidSegment))
}
return target.strData, pjStatusInvalidPackageTarget, pjDebug{token: target.firstToken}
}
// Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
resolvedTarget := path.Join(packageURL, target.strData)
// If subpath split on "/" or "\" contains any ".", ".." or "node_modules"
// segments, throw an Invalid Module Specifier error.
if invalidSegment := findInvalidSegment(subpath); invalidSegment != "" {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The path %q is invalid because it contains invalid segment %q", subpath, invalidSegment))
}
return subpath, pjStatusInvalidModuleSpecifier, pjDebug{token: target.firstToken}
}
if pattern {
// Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath.
result := strings.ReplaceAll(resolvedTarget, "*", subpath)
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Substituted %q for \"*\" in %q to get %q", subpath, "."+resolvedTarget, "."+result))
}
return result, pjStatusExact, pjDebug{token: target.firstToken}
} else {
// Return the URL resolution of the concatenation of subpath and resolvedTarget.
result := path.Join(resolvedTarget, subpath)
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Joined %q to %q to get %q", subpath, "."+resolvedTarget, "."+result))
}
return result, pjStatusExact, pjDebug{token: target.firstToken}
}
case pjObject:
if r.debugLogs != nil {
keys := make([]string, 0, len(conditions))
for key := range conditions {
keys = append(keys, fmt.Sprintf("%q", key))
}
sort.Strings(keys)
r.debugLogs.addNote(fmt.Sprintf("Checking condition map for one of [%s]", strings.Join(keys, ", ")))
r.debugLogs.increaseIndent()
defer r.debugLogs.decreaseIndent()
}
var didFindMapEntry bool
var lastMapEntry pjMapEntry
for _, p := range target.mapData {
if p.key == "default" || conditions[p.key] {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The key %q applies", p.key))
}
resolved, status, debug := r.esmPackageTargetResolve(packageURL, p.value, subpath, pattern, internal, conditions)
if status.isUndefined() {
didFindMapEntry = true
lastMapEntry = p
continue
}
return resolved, status, debug
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The key %q does not apply", p.key))
}
}
if r.debugLogs != nil {
r.debugLogs.addNote("No keys in the map were applicable")
}
// ALGORITHM DEVIATION: Provide a friendly error message if no conditions matched
if len(target.mapData) > 0 && !target.keysStartWithDot() {
if didFindMapEntry && lastMapEntry.value.kind == pjObject &&
len(lastMapEntry.value.mapData) > 0 && !lastMapEntry.value.keysStartWithDot() {
// If a top-level condition did match but no sub-condition matched,
// complain about the sub-condition instead of the top-level condition.
// This leads to a less confusing error message. For example:
//
// "exports": {
// "node": {
// "require": "./dist/bwip-js-node.js"
// }
// },
//
// We want the warning to say this:
//
// note: None of the conditions provided ("require") match any of the
// currently active conditions ("default", "import", "node")
// 14 | "node": {
// | ^
//
// We don't want the warning to say this:
//
// note: None of the conditions provided ("browser", "electron", "node")
// match any of the currently active conditions ("default", "import", "node")
// 7 | "exports": {
// | ^
//
// More information: https://github.com/evanw/esbuild/issues/1484
target = lastMapEntry.value
}
keys := make([]string, len(target.mapData))
for i, p := range target.mapData {
keys[i] = p.key
}
return "", pjStatusUndefinedNoConditionsMatch, pjDebug{
token: target.firstToken,
unmatchedConditions: keys,
}
}
return "", pjStatusUndefined, pjDebug{token: target.firstToken}
case pjArray:
if len(target.arrData) == 0 {
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The path %q is set to an empty array", subpath))
}
return "", pjStatusNull, pjDebug{token: target.firstToken}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Checking for %q in an array", subpath))
r.debugLogs.increaseIndent()
defer r.debugLogs.decreaseIndent()
}
lastException := pjStatusUndefined
lastDebug := pjDebug{token: target.firstToken}
for _, targetValue := range target.arrData {
// Let resolved be the result, continuing the loop on any Invalid Package Target error.
resolved, status, debug := r.esmPackageTargetResolve(packageURL, targetValue, subpath, pattern, internal, conditions)
if status == pjStatusInvalidPackageTarget || status == pjStatusNull {
lastException = status
lastDebug = debug
continue
}
if status.isUndefined() {
continue
}
return resolved, status, debug
}
// Return or throw the last fallback resolution null return or error.
return "", lastException, lastDebug
case pjNull:
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("The path %q is set to null", subpath))
}
return "", pjStatusNull, pjDebug{token: target.firstToken}
}
if r.debugLogs != nil {
r.debugLogs.addNote(fmt.Sprintf("Invalid package target for path %q", subpath))
}
return "", pjStatusInvalidPackageTarget, pjDebug{token: target.firstToken}
}
func esmParsePackageName(packageSpecifier string) (packageName string, packageSubpath string, ok bool) {
if packageSpecifier == "" {
return
}
slash := strings.IndexByte(packageSpecifier, '/')
if !strings.HasPrefix(packageSpecifier, "@") {
if slash == -1 {
slash = len(packageSpecifier)
}
packageName = packageSpecifier[:slash]
} else {
if slash == -1 {
return
}
slash2 := strings.IndexByte(packageSpecifier[slash+1:], '/')
if slash2 == -1 {
slash2 = len(packageSpecifier[slash+1:])
}
packageName = packageSpecifier[:slash+1+slash2]
}
if strings.HasPrefix(packageName, ".") || strings.ContainsAny(packageName, "\\%") {
return
}
packageSubpath = "." + packageSpecifier[len(packageName):]
ok = true
return
}
func (r resolverQuery) esmPackageExportsReverseResolve(
query string,
root pjEntry,
conditions map[string]bool,
) (bool, string, logger.Range) {
if root.kind == pjObject && root.keysStartWithDot() {
if ok, subpath, token := r.esmPackageImportsExportsReverseResolve(query, root, conditions); ok {
return true, subpath, token
}
}
return false, "", logger.Range{}
}
func (r resolverQuery) esmPackageImportsExportsReverseResolve(
query string,
matchObj pjEntry,
conditions map[string]bool,
) (bool, string, logger.Range) {
if !strings.HasSuffix(query, "*") {
for _, entry := range matchObj.mapData {
if ok, subpath, token := r.esmPackageTargetReverseResolve(query, entry.key, entry.value, esmReverseExact, conditions); ok {
return true, subpath, token
}
}
}
for _, expansion := range matchObj.expansionKeys {
if strings.HasSuffix(expansion.key, "*") {
if ok, subpath, token := r.esmPackageTargetReverseResolve(query, expansion.key, expansion.value, esmReversePattern, conditions); ok {
return true, subpath, token
}
}
if ok, subpath, token := r.esmPackageTargetReverseResolve(query, expansion.key, expansion.value, esmReversePrefix, conditions); ok {
return true, subpath, token
}
}
return false, "", logger.Range{}
}
type esmReverseKind uint8
const (
esmReverseExact esmReverseKind = iota
esmReversePattern
esmReversePrefix
)
func (r resolverQuery) esmPackageTargetReverseResolve(
query string,
key string,
target pjEntry,
kind esmReverseKind,
conditions map[string]bool,
) (bool, string, logger.Range) {
switch target.kind {
case pjString:
switch kind {
case esmReverseExact:
if query == target.strData {
return true, key, target.firstToken
}
case esmReversePrefix:
if strings.HasPrefix(query, target.strData) {
return true, key + query[len(target.strData):], target.firstToken
}
case esmReversePattern:
star := strings.IndexByte(target.strData, '*')
keyWithoutTrailingStar := strings.TrimSuffix(key, "*")
// Handle the case of no "*"
if star == -1 {
if query == target.strData {
return true, keyWithoutTrailingStar, target.firstToken
}
break
}
// Only support tracing through a single "*"
prefix := target.strData[0:star]
suffix := target.strData[star+1:]
if !strings.ContainsRune(suffix, '*') && strings.HasPrefix(query, prefix) {
if afterPrefix := query[len(prefix):]; strings.HasSuffix(afterPrefix, suffix) {
starData := afterPrefix[:len(afterPrefix)-len(suffix)]
return true, keyWithoutTrailingStar + starData, target.firstToken
}
}
break
}
case pjObject:
for _, p := range target.mapData {
if p.key == "default" || conditions[p.key] {
if ok, subpath, token := r.esmPackageTargetReverseResolve(query, key, p.value, kind, conditions); ok {
return true, subpath, token
}
}
}
case pjArray:
for _, targetValue := range target.arrData {
if ok, subpath, token := r.esmPackageTargetReverseResolve(query, key, targetValue, kind, conditions); ok {
return true, subpath, token
}
}
}
return false, "", logger.Range{}
}