Skip to content

Commit

Permalink
implement conditions for the "exports" field
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 9, 2021
1 parent d178ba0 commit 8b16b2d
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 1 deletion.
189 changes: 189 additions & 0 deletions internal/bundler/bundler_packagejson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1380,3 +1380,192 @@ Users/user/project/node_modules/pkg2/package.json: note: Importing the directory
`,
})
}

func TestPackageJsonExportsRequireOverImport(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
require('pkg')
`,
"/Users/user/project/node_modules/pkg/package.json": `
{
"exports": {
"import": "./import.js",
"require": "./require.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/import.js": `
console.log('FAILURE')
`,
"/Users/user/project/node_modules/pkg/require.js": `
console.log('SUCCESS')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
},
})
}

func TestPackageJsonExportsImportOverRequire(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg'
`,
"/Users/user/project/node_modules/pkg/package.json": `
{
"exports": {
"require": "./require.js",
"import": "./import.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/require.js": `
console.log('FAILURE')
`,
"/Users/user/project/node_modules/pkg/import.js": `
console.log('SUCCESS')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
},
})
}

func TestPackageJsonExportsDefaultOverImportAndRequire(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg'
`,
"/Users/user/project/node_modules/pkg/package.json": `
{
"exports": {
"default": "./default.js",
"import": "./import.js",
"require": "./require.js"
}
}
`,
"/Users/user/project/node_modules/pkg/require.js": `
console.log('FAILURE')
`,
"/Users/user/project/node_modules/pkg/import.js": `
console.log('FAILURE')
`,
"/Users/user/project/node_modules/pkg/default.js": `
console.log('SUCCESS')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
},
})
}

func TestPackageJsonExportsBrowser(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg'
`,
"/Users/user/project/node_modules/pkg/package.json": `
{
"exports": {
"node": "./node.js",
"browser": "./browser.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/node.js": `
console.log('FAILURE')
`,
"/Users/user/project/node_modules/pkg/browser.js": `
console.log('SUCCESS')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
Platform: config.PlatformBrowser,
},
})
}

func TestPackageJsonExportsNode(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg'
`,
"/Users/user/project/node_modules/pkg/package.json": `
{
"exports": {
"browser": "./browser.js",
"node": "./node.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/browser.js": `
console.log('FAILURE')
`,
"/Users/user/project/node_modules/pkg/node.js": `
console.log('SUCCESS')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
Platform: config.PlatformNode,
},
})
}

func TestPackageJsonExportsNeutral(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg'
`,
"/Users/user/project/node_modules/pkg/package.json": `
{
"exports": {
"node": "./node.js",
"browser": "./browser.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/node.js": `
console.log('FAILURE')
`,
"/Users/user/project/node_modules/pkg/browser.js": `
console.log('FAILURE')
`,
"/Users/user/project/node_modules/pkg/default.js": `
console.log('SUCCESS')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
Platform: config.PlatformNeutral,
},
})
}
41 changes: 41 additions & 0 deletions internal/bundler/snapshots/snapshots_packagejson.txt
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,47 @@ var require_main = __commonJS((exports, module) => {
// Users/user/project/src/entry.js
console.log(require_main());

================================================================================
TestPackageJsonExportsBrowser
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg/browser.js
console.log("SUCCESS");

================================================================================
TestPackageJsonExportsDefaultOverImportAndRequire
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg/default.js
console.log("SUCCESS");

================================================================================
TestPackageJsonExportsImportOverRequire
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg/import.js
console.log("SUCCESS");

================================================================================
TestPackageJsonExportsNeutral
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg/default.js
console.log("SUCCESS");

================================================================================
TestPackageJsonExportsNode
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg/node.js
console.log("SUCCESS");

================================================================================
TestPackageJsonExportsRequireOverImport
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg/require.js
var require_require = __commonJS(() => {
console.log("SUCCESS");
});

// Users/user/project/src/entry.js
require_require();

================================================================================
TestPackageJsonMain
---------- /Users/user/project/out.js ----------
Expand Down
35 changes: 34 additions & 1 deletion internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ type resolver struct {
caches *cache.CacheSet
options config.Options

// These are sets that represent various conditions for the "exports" field
// in package.json.
esmConditionsDefault map[string]bool
esmConditionsImport map[string]bool
esmConditionsRequire map[string]bool

// A special filtered import order for CSS "@import" imports.
//
// The "resolve extensions" setting determines the order of implicit
Expand Down Expand Up @@ -188,13 +194,31 @@ func NewResolver(fs fs.FS, log logger.Log, caches *cache.CacheSet, options confi
atImportExtensionOrder = append(atImportExtensionOrder, ext)
}

// Generate the condition sets for interpreting the "exports" field
esmConditionsDefault := map[string]bool{}
esmConditionsImport := map[string]bool{"import": true}
esmConditionsRequire := map[string]bool{"require": true}
switch options.Platform {
case config.PlatformBrowser:
esmConditionsDefault["browser"] = true
case config.PlatformNode:
esmConditionsDefault["node"] = true
}
for key := range esmConditionsDefault {
esmConditionsImport[key] = true
esmConditionsRequire[key] = true
}

return &resolver{
fs: fs,
log: log,
options: options,
caches: caches,
dirCache: make(map[string]*dirInfo),
atImportExtensionOrder: atImportExtensionOrder,
esmConditionsDefault: esmConditionsDefault,
esmConditionsImport: esmConditionsImport,
esmConditionsRequire: esmConditionsRequire,
}
}

Expand Down Expand Up @@ -1149,13 +1173,22 @@ func (r *resolver) loadNodeModules(path string, kind ast.ImportKind, dirInfo *di
absPkgPath := r.fs.Join(dirInfo.absPath, "node_modules", esmPackageName)
if pkgDirInfo := r.dirInfoCached(absPkgPath); pkgDirInfo != nil {
if pkgJSON := pkgDirInfo.packageJSON; pkgJSON != nil && pkgJSON.exportsMap != nil {
// The condition set is determined by the kind of import
conditions := r.esmConditionsDefault
switch kind {
case ast.ImportStmt, ast.ImportDynamic:
conditions = r.esmConditionsImport
case ast.ImportRequire, ast.ImportRequireResolve:
conditions = r.esmConditionsRequire
}

// Resolve against the path "/", then join it with the absolute
// directory path. This is done because ESM package resolution uses
// URLs while our path resolution uses file system paths. We don't
// want problems due to Windows paths, which are very unlike URL
// paths. We also want to avoid any "%" characters in the absolute
// directory path accidentally being interpreted as URL escapes.
resolvedPath, status, token := esmPackageExportsResolveWithPostConditions("/", esmPackageSubpath, pkgJSON.exportsMap.root, nil)
resolvedPath, status, token := esmPackageExportsResolveWithPostConditions("/", esmPackageSubpath, pkgJSON.exportsMap.root, conditions)
if status == peStatusOk && strings.HasPrefix(resolvedPath, "/") {
absResolvedPath := r.fs.Join(absPkgPath, resolvedPath[1:])
resolvedDirInfo := r.dirInfoCached(r.fs.Dir(absResolvedPath))
Expand Down

0 comments on commit 8b16b2d

Please sign in to comment.