Skip to content

Commit

Permalink
support implicit extensions with "exports"
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 9, 2021
1 parent 2e89fcc commit 5436248
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 20 deletions.
85 changes: 83 additions & 2 deletions internal/bundler/bundler_packagejson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1346,7 +1346,7 @@ func TestPackageJsonExportsErrorModuleNotFound(t *testing.T) {
AbsOutputFile: "/Users/user/project/out.js",
},
expectedScanLog: `Users/user/project/src/entry.js: error: Could not resolve "pkg1" (mark it as external to exclude it from the bundle)
Users/user/project/node_modules/pkg1/package.json: note: The module "./foo.js" was not found
Users/user/project/node_modules/pkg1/package.json: note: The module "./foo.js" was not found on the file system
`,
})
}
Expand Down Expand Up @@ -1374,7 +1374,7 @@ func TestPackageJsonExportsErrorUnsupportedDirectoryImport(t *testing.T) {
AbsOutputFile: "/Users/user/project/out.js",
},
expectedScanLog: `Users/user/project/src/entry.js: error: Could not resolve "pkg1" (mark it as external to exclude it from the bundle)
Users/user/project/node_modules/pkg1/package.json: note: The module "./foo" was not found
Users/user/project/node_modules/pkg1/package.json: note: The module "./foo" was not found on the file system
Users/user/project/src/entry.js: error: Could not resolve "pkg2" (mark it as external to exclude it from the bundle)
Users/user/project/node_modules/pkg2/package.json: note: Importing the directory "./foo" is not supported
`,
Expand Down Expand Up @@ -1691,3 +1691,84 @@ func TestPackageJsonExportsCustomConditions(t *testing.T) {
},
})
}

func TestPackageJsonExportsNotExactMissingExtension(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg1/foo/bar'
`,
"/Users/user/project/node_modules/pkg1/package.json": `
{
"exports": {
"./foo/": "./dir/"
}
}
`,
"/Users/user/project/node_modules/pkg1/dir/bar.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 TestPackageJsonExportsNotExactMissingExtensionPattern(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg1/foo/bar'
`,
"/Users/user/project/node_modules/pkg1/package.json": `
{
"exports": {
"./foo/*": "./dir/*"
}
}
`,
"/Users/user/project/node_modules/pkg1/dir/bar.js": `
console.log('SUCCESS')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
},
expectedScanLog: `Users/user/project/src/entry.js: error: Could not resolve "pkg1/foo/bar" (mark it as external to exclude it from the bundle)
Users/user/project/node_modules/pkg1/package.json: note: The module "./dir/bar" was not found on the file system
`,
})
}

func TestPackageJsonExportsExactMissingExtension(t *testing.T) {
packagejson_suite.expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/entry.js": `
import 'pkg1/foo/bar'
`,
"/Users/user/project/node_modules/pkg1/package.json": `
{
"exports": {
"./foo/bar": "./dir/bar"
}
}
`,
"/Users/user/project/node_modules/pkg1/dir/bar.js": `
console.log('SUCCESS')
`,
},
entryPaths: []string{"/Users/user/project/src/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/Users/user/project/out.js",
},
expectedScanLog: `Users/user/project/src/entry.js: error: Could not resolve "pkg1/foo/bar" (mark it as external to exclude it from the bundle)
Users/user/project/node_modules/pkg1/package.json: note: The module "./dir/bar" was not found on the file system
`,
})
}
6 changes: 6 additions & 0 deletions internal/bundler/snapshots/snapshots_packagejson.txt
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,12 @@ TestPackageJsonExportsNode
// Users/user/project/node_modules/pkg/node.js
console.log("SUCCESS");

================================================================================
TestPackageJsonExportsNotExactMissingExtension
---------- /Users/user/project/out.js ----------
// Users/user/project/node_modules/pkg1/dir/bar.js
console.log("SUCCESS");

================================================================================
TestPackageJsonExportsOrderIndependent
---------- /Users/user/project/out.js ----------
Expand Down
18 changes: 12 additions & 6 deletions internal/resolver/package_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,8 @@ type peStatus uint8
const (
peStatusUndefined peStatus = iota
peStatusNull
peStatusOk
peStatusExact
peStatusInexact // This means we may need to try CommonJS-style extension suffixes

// Module specifier is an invalid URL, package name or package subpath specifier.
peStatusInvalidModuleSpecifier
Expand Down Expand Up @@ -465,7 +466,7 @@ func esmPackageExportsResolveWithPostConditions(
conditions map[string]bool,
) (string, peStatus, logger.Range) {
resolved, status, token := esmPackageExportsResolve(packageURL, subpath, exports, conditions)
if status != peStatusOk {
if status != peStatusExact && status != peStatusInexact {
return resolved, status, token
}

Expand All @@ -487,7 +488,7 @@ func esmPackageExportsResolveWithPostConditions(
}

// Set resolved to the real path of resolved.
return resolvedPath, peStatusOk, token
return resolvedPath, status, token
}

func esmPackageExportsResolve(
Expand Down Expand Up @@ -549,7 +550,12 @@ func esmPackageImportsExportsResolve(
if strings.HasPrefix(matchKey, expansion.key) {
target := expansion.value
subpath := matchKey[len(expansion.key):]
return esmPackageTargetResolve(packageURL, target, subpath, false, conditions)
result, status, token := esmPackageTargetResolve(packageURL, target, subpath, false, conditions)
if status == peStatusExact {
// Return the object { resolved, exact: false }.
status = peStatusInexact
}
return result, status, token
}
}

Expand Down Expand Up @@ -616,10 +622,10 @@ func esmPackageTargetResolve(

if pattern {
// Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath.
return strings.ReplaceAll(resolvedTarget, "*", subpath), peStatusOk, target.firstToken
return strings.ReplaceAll(resolvedTarget, "*", subpath), peStatusExact, target.firstToken
} else {
// Return the URL resolution of the concatenation of subpath and resolvedTarget.
return path.Join(resolvedTarget, subpath), peStatusOk, target.firstToken
return path.Join(resolvedTarget, subpath), peStatusExact, target.firstToken
}

case peObject:
Expand Down
37 changes: 25 additions & 12 deletions internal/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1192,19 +1192,32 @@ func (r *resolver) loadNodeModules(path string, kind ast.ImportKind, dirInfo *di
// 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, conditions)
if status == peStatusOk && strings.HasPrefix(resolvedPath, "/") {
if (status == peStatusExact || status == peStatusInexact) && strings.HasPrefix(resolvedPath, "/") {
absResolvedPath := r.fs.Join(absPkgPath, resolvedPath[1:])
resolvedDirInfo := r.dirInfoCached(r.fs.Dir(absResolvedPath))
if resolvedDirInfo == nil {
status = peStatusModuleNotFound
} else if entry, diffCase := resolvedDirInfo.entries.Get(r.fs.Base(absResolvedPath)); entry == nil {
status = peStatusModuleNotFound
} else if kind := entry.Kind(r.fs); kind == fs.DirEntry {
status = peStatusUnsupportedDirectoryImport
} else if kind != fs.FileEntry {

switch status {
case peStatusExact:
resolvedDirInfo := r.dirInfoCached(r.fs.Dir(absResolvedPath))
if resolvedDirInfo == nil {
status = peStatusModuleNotFound
} else if entry, diffCase := resolvedDirInfo.entries.Get(r.fs.Base(absResolvedPath)); entry == nil {
status = peStatusModuleNotFound
} else if kind := entry.Kind(r.fs); kind == fs.DirEntry {
status = peStatusUnsupportedDirectoryImport
} else if kind != fs.FileEntry {
status = peStatusModuleNotFound
} else {
return PathPair{Primary: logger.Path{Text: absResolvedPath, Namespace: "file"}}, true, diffCase, nil
}

case peStatusInexact:
// If this was resolved against an expansion key ending in a "/"
// instead of a "*", we need to try CommonJS-style implicit
// extension and/or directory detection.
if absolute, ok, diffCase := r.loadAsFileOrDirectory(absResolvedPath, kind); ok {
return absolute, true, diffCase, nil
}
status = peStatusModuleNotFound
} else {
return PathPair{Primary: logger.Path{Text: absResolvedPath, Namespace: "file"}}, true, diffCase, nil
}
}

Expand Down Expand Up @@ -1239,7 +1252,7 @@ func (r *resolver) loadNodeModules(path string, kind ast.ImportKind, dirInfo *di

case peStatusModuleNotFound:
notes = []logger.MsgData{logger.RangeData(&pkgJSON.source, token,
fmt.Sprintf("The module %q was not found", resolvedPath))}
fmt.Sprintf("The module %q was not found on the file system", resolvedPath))}

case peStatusUnsupportedDirectoryImport:
notes = []logger.MsgData{logger.RangeData(&pkgJSON.source, token,
Expand Down
20 changes: 20 additions & 0 deletions scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2887,6 +2887,26 @@
}
}`,
}),
test(['in.js', '--outfile=node.js', '--format=cjs'].concat(flags), {
'in.js': `const abc = require('pkg/dir/test'); if (abc !== 123) throw 'fail'`,
'package.json': `{ "type": "module" }`,
'node_modules/pkg/sub/test.js': `module.exports = 123`,
'node_modules/pkg/package.json': `{
"exports": {
"./dir/": "./sub/"
}
}`,
}),
test(['in.js', '--outfile=node.js', '--format=cjs'].concat(flags), {
'in.js': `const abc = require('pkg/dir/test'); if (abc !== 123) throw 'fail'`,
'package.json': `{ "type": "module" }`,
'node_modules/pkg/sub/test/index.js': `module.exports = 123`,
'node_modules/pkg/package.json': `{
"exports": {
"./dir/": "./sub/"
}
}`,
}),
test(['in.js', '--outfile=node.js', '--format=esm'].concat(flags), {
'in.js': `import abc from 'pkg/foo'; if (abc !== 123) throw 'fail'`,
'package.json': `{ "type": "module" }`,
Expand Down

0 comments on commit 5436248

Please sign in to comment.