From 5b00566c5ec49ad7539a413e7e17521aa08759eb Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Sat, 6 Mar 2021 23:35:46 -0800 Subject: [PATCH] move package.json code to separate file --- internal/resolver/package_json.go | 255 ++++++++++++++++++++++++++++++ internal/resolver/resolver.go | 243 ---------------------------- 2 files changed, 255 insertions(+), 243 deletions(-) create mode 100644 internal/resolver/package_json.go diff --git a/internal/resolver/package_json.go b/internal/resolver/package_json.go new file mode 100644 index 00000000000..8bd681d453a --- /dev/null +++ b/internal/resolver/package_json.go @@ -0,0 +1,255 @@ +package resolver + +import ( + "fmt" + "regexp" + "strings" + "syscall" + + "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 { + absMainFields map[string]string + + // 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 a mapping of absolute paths to absolute paths. Mapping + // to an empty path indicates that the module is disabled. As far as I can + // tell, the official spec is a 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. + // + browserNonPackageMap map[string]*string + browserPackageMap 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 + ignoreIfUnusedData *IgnoreIfUnusedData +} + +func (r *resolver) parsePackageJSON(path string) *packageJSON { + packageJSONPath := r.fs.Join(path, "package.json") + contents, err := r.caches.FSCache.ReadFile(r.fs, packageJSONPath) + if err != nil { + r.log.AddError(nil, logger.Loc{}, + fmt.Sprintf("Cannot read file %q: %s", + r.PrettyPath(logger.Path{Text: packageJSONPath, Namespace: "file"}), err.Error())) + return nil + } + + keyPath := logger.Path{Text: packageJSONPath, Namespace: "file"} + jsonSource := logger.Source{ + KeyPath: keyPath, + PrettyPath: r.PrettyPath(keyPath), + Contents: contents, + } + + json, ok := r.caches.JSONCache.Parse(r.log, jsonSource, js_parser.JSONOptions{}) + if !ok { + return nil + } + + toAbsPath := func(pathText string, pathRange logger.Range) *string { + // Is it a file? + if absolute, ok, _ := r.loadAsFile(pathText, r.options.ExtensionOrder); ok { + return &absolute + } + + // Is it a directory? + if mainEntries, err := r.fs.ReadDirectory(pathText); err == nil { + // Look for an "index" file with known extensions + if absolute, ok, _ := r.loadAsIndex(pathText, mainEntries); ok { + return &absolute + } + } else if err != syscall.ENOENT { + r.log.AddRangeError(&jsonSource, pathRange, + fmt.Sprintf("Cannot read directory %q: %s", + r.PrettyPath(logger.Path{Text: pathText, Namespace: "file"}), err.Error())) + } + return nil + } + + packageJSON := &packageJSON{} + + // Read the "main" fields + mainFields := r.options.MainFields + if mainFields == nil { + mainFields = defaultMainFields[r.options.Platform] + } + for _, field := range mainFields { + if mainJSON, _, ok := getProperty(json, field); ok { + if main, ok := getString(mainJSON); ok { + if packageJSON.absMainFields == nil { + packageJSON.absMainFields = make(map[string]string) + } + if absPath := toAbsPath(r.fs.Join(path, main), jsonSource.RangeOfString(mainJSON.Loc)); absPath != nil { + packageJSON.absMainFields[field] = *absPath + } + } + } + } + + // 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 + browserPackageMap := make(map[string]*string) + browserNonPackageMap := make(map[string]*string) + + // Remap all files in the browser field + for _, prop := range browser.Properties { + if key, ok := getString(prop.Key); ok && prop.Value != nil { + isPackagePath := IsPackagePath(key) + + // Make this an absolute path if it's not a package + if !isPackagePath { + key = r.fs.Join(path, key) + } + + if value, ok := getString(*prop.Value); ok { + // If this is a string, it's a replacement package + if isPackagePath { + browserPackageMap[key] = &value + } else { + browserNonPackageMap[key] = &value + } + } else if value, ok := getBool(*prop.Value); ok && !value { + // If this is false, it means the package is disabled + if isPackagePath { + browserPackageMap[key] = nil + } else { + browserNonPackageMap[key] = nil + } + } + } + } + + packageJSON.browserPackageMap = browserPackageMap + packageJSON.browserNonPackageMap = browserNonPackageMap + } + } + + // 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.ignoreIfUnusedData = &IgnoreIfUnusedData{ + 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.ignoreIfUnusedData = &IgnoreIfUnusedData{ + 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.AddWarning(&jsonSource, itemJSON.Loc, + "Expected string in array for \"sideEffects\"") + continue + } + + absPattern := r.fs.Join(path, js_lexer.UTF16ToString(item.Value)) + re, hadWildcard := globToEscapedRegexp(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.AddWarning(&jsonSource, sideEffectsJSON.Loc, + "The value for \"sideEffects\" must be a boolean or an array") + } + } + + return packageJSON +} + +func globToEscapedRegexp(glob string) (string, bool) { + sb := strings.Builder{} + sb.WriteByte('^') + hadWildcard := false + + for _, c := range glob { + switch c { + case '\\', '^', '$', '.', '+', '|', '(', ')', '[', ']', '{', '}': + sb.WriteByte('\\') + sb.WriteRune(c) + + case '*': + sb.WriteString(".*") + hadWildcard = true + + case '?': + sb.WriteByte('.') + hadWildcard = true + + default: + sb.WriteRune(c) + } + } + + sb.WriteByte('$') + return sb.String(), hadWildcard +} diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 5edd4461125..41a70596c5d 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -3,7 +3,6 @@ package resolver import ( "errors" "fmt" - "regexp" "strings" "sync" "syscall" @@ -14,7 +13,6 @@ import ( "github.com/evanw/esbuild/internal/fs" "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" ) @@ -603,52 +601,6 @@ func (r *resolver) PrettyPath(path logger.Path) string { //////////////////////////////////////////////////////////////////////////////// -type packageJSON struct { - absMainFields map[string]string - - // 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 a mapping of absolute paths to absolute paths. Mapping - // to an empty path indicates that the module is disabled. As far as I can - // tell, the official spec is a 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. - // - browserNonPackageMap map[string]*string - browserPackageMap 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 - ignoreIfUnusedData *IgnoreIfUnusedData -} - type dirInfo struct { // These objects are immutable, so we can just point to the parent directory // and avoid having to lock the cache again @@ -912,201 +864,6 @@ func (r *resolver) dirInfoUncached(path string) *dirInfo { return info } -func (r *resolver) parsePackageJSON(path string) *packageJSON { - packageJsonPath := r.fs.Join(path, "package.json") - contents, err := r.caches.FSCache.ReadFile(r.fs, packageJsonPath) - if err != nil { - r.log.AddError(nil, logger.Loc{}, - fmt.Sprintf("Cannot read file %q: %s", - r.PrettyPath(logger.Path{Text: packageJsonPath, Namespace: "file"}), err.Error())) - return nil - } - - keyPath := logger.Path{Text: packageJsonPath, Namespace: "file"} - jsonSource := logger.Source{ - KeyPath: keyPath, - PrettyPath: r.PrettyPath(keyPath), - Contents: contents, - } - - json, ok := r.caches.JSONCache.Parse(r.log, jsonSource, js_parser.JSONOptions{}) - if !ok { - return nil - } - - toAbsPath := func(pathText string, pathRange logger.Range) *string { - // Is it a file? - if absolute, ok, _ := r.loadAsFile(pathText, r.options.ExtensionOrder); ok { - return &absolute - } - - // Is it a directory? - if mainEntries, err := r.fs.ReadDirectory(pathText); err == nil { - // Look for an "index" file with known extensions - if absolute, ok, _ := r.loadAsIndex(pathText, mainEntries); ok { - return &absolute - } - } else if err != syscall.ENOENT { - r.log.AddRangeError(&jsonSource, pathRange, - fmt.Sprintf("Cannot read directory %q: %s", - r.PrettyPath(logger.Path{Text: pathText, Namespace: "file"}), err.Error())) - } - return nil - } - - packageJSON := &packageJSON{} - - // Read the "main" fields - mainFields := r.options.MainFields - if mainFields == nil { - mainFields = defaultMainFields[r.options.Platform] - } - for _, field := range mainFields { - if mainJson, _, ok := getProperty(json, field); ok { - if main, ok := getString(mainJson); ok { - if packageJSON.absMainFields == nil { - packageJSON.absMainFields = make(map[string]string) - } - if absPath := toAbsPath(r.fs.Join(path, main), jsonSource.RangeOfString(mainJson.Loc)); absPath != nil { - packageJSON.absMainFields[field] = *absPath - } - } - } - } - - // 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 - browserPackageMap := make(map[string]*string) - browserNonPackageMap := make(map[string]*string) - - // Remap all files in the browser field - for _, prop := range browser.Properties { - if key, ok := getString(prop.Key); ok && prop.Value != nil { - isPackagePath := IsPackagePath(key) - - // Make this an absolute path if it's not a package - if !isPackagePath { - key = r.fs.Join(path, key) - } - - if value, ok := getString(*prop.Value); ok { - // If this is a string, it's a replacement package - if isPackagePath { - browserPackageMap[key] = &value - } else { - browserNonPackageMap[key] = &value - } - } else if value, ok := getBool(*prop.Value); ok && !value { - // If this is false, it means the package is disabled - if isPackagePath { - browserPackageMap[key] = nil - } else { - browserNonPackageMap[key] = nil - } - } - } - } - - packageJSON.browserPackageMap = browserPackageMap - packageJSON.browserNonPackageMap = browserNonPackageMap - } - } - - // 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.ignoreIfUnusedData = &IgnoreIfUnusedData{ - 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.ignoreIfUnusedData = &IgnoreIfUnusedData{ - 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.AddWarning(&jsonSource, itemJson.Loc, - "Expected string in array for \"sideEffects\"") - continue - } - - absPattern := r.fs.Join(path, js_lexer.UTF16ToString(item.Value)) - re, hadWildcard := globToEscapedRegexp(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.AddWarning(&jsonSource, sideEffectsJson.Loc, - "The value for \"sideEffects\" must be a boolean or an array") - } - } - - return packageJSON -} - -func globToEscapedRegexp(glob string) (string, bool) { - sb := strings.Builder{} - sb.WriteByte('^') - hadWildcard := false - - for _, c := range glob { - switch c { - case '\\', '^', '$', '.', '+', '|', '(', ')', '[', ']', '{', '}': - sb.WriteByte('\\') - sb.WriteRune(c) - - case '*': - sb.WriteString(".*") - hadWildcard = true - - case '?': - sb.WriteByte('.') - hadWildcard = true - - default: - sb.WriteRune(c) - } - } - - sb.WriteByte('$') - return sb.String(), hadWildcard -} - func (r *resolver) loadAsFile(path string, extensionOrder []string) (string, bool, *fs.DifferentCase) { // Read the directory entries once to minimize locking dirPath := r.fs.Dir(path)