diff --git a/doc/api/cli.md b/doc/api/cli.md
index dc695baed91ca5..61e833b878d899 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -148,6 +148,13 @@ the ability to import a directory that has an index file.
Please see [customizing esm specifier resolution][] for example usage.
+### `--experimental-exports`
+
+
+Enable experimental resolution using the `exports` field in `package.json`.
+
### `--experimental-modules`
- `--enable-fips`
- `--es-module-specifier-resolution`
+- `--experimental-exports`
- `--experimental-modules`
- `--experimental-policy`
- `--experimental-repl-await`
diff --git a/doc/api/esm.md b/doc/api/esm.md
index 74d818946e007c..14035cbf5ca243 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -216,6 +216,61 @@ a package would be accessible like `require('pkg')` and `import
module entry point and legacy users could be informed of the CommonJS entry
point path, e.g. `require('pkg/commonjs')`.
+## Package Exports
+
+By default, all subpaths from a package can be imported (`import 'pkg/x.js'`).
+Custom subpath aliasing and encapsulation can be provided through the
+`"exports"` field.
+
+
+```js
+// ./node_modules/es-module-package/package.json
+{
+ "exports": {
+ "./submodule": "./src/submodule.js"
+ }
+}
+```
+
+```js
+import submodule from 'es-module-package/submodule';
+// Loads ./node_modules/es-module-package/src/submodule.js
+```
+
+In addition to defining an alias, subpaths not defined by `"exports"` will
+throw when an attempt is made to import them:
+
+```js
+import submodule from 'es-module-package/private-module.js';
+// Throws - Package exports error
+```
+
+> Note: this is not a strong encapsulation as any private modules can still be
+> loaded by absolute paths.
+
+Folders can also be mapped with package exports as well:
+
+
+```js
+// ./node_modules/es-module-package/package.json
+{
+ "exports": {
+ "./features/": "./src/features/"
+ }
+}
+```
+
+```js
+import feature from 'es-module-package/features/x.js';
+// Loads ./node_modules/es-module-package/src/features/x.js
+```
+
+If a package has no exports, setting `"exports": false` can be used instead of
+`"exports": {}` to indicate the package does not intend for submodules to be
+exposed.
+This is just a convention that works because `false`, just like `{}`, has no
+iterable own properties.
+
## import
Specifiers
### Terminology
diff --git a/src/env.h b/src/env.h
index 2bcb07a6333b6f..d2066f54d81887 100644
--- a/src/env.h
+++ b/src/env.h
@@ -99,6 +99,8 @@ struct PackageConfig {
const HasMain has_main;
const std::string main;
const PackageType type;
+
+ v8::Global exports;
};
} // namespace loader
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index e104afb736c28d..f1c819874a3839 100644
--- a/src/module_wrap.cc
+++ b/src/module_wrap.cc
@@ -558,7 +558,7 @@ Maybe GetPackageConfig(Environment* env,
if (source.IsNothing()) {
auto entry = env->package_json_cache.emplace(path,
PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "",
- PackageType::None });
+ PackageType::None, Global() });
return Just(&entry.first->second);
}
@@ -578,7 +578,7 @@ Maybe GetPackageConfig(Environment* env,
!pkg_json_v->ToObject(context).ToLocal(&pkg_json)) {
env->package_json_cache.emplace(path,
PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "",
- PackageType::None });
+ PackageType::None, Global() });
std::string msg = "Invalid JSON in '" + path +
"' imported from " + base.ToFilePath();
node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str());
@@ -609,22 +609,22 @@ Maybe GetPackageConfig(Environment* env,
}
Local exports_v;
- if (pkg_json->Get(env->context(),
+ if (env->options()->experimental_exports &&
+ pkg_json->Get(env->context(),
env->exports_string()).ToLocal(&exports_v) &&
- (exports_v->IsObject() || exports_v->IsString() ||
- exports_v->IsBoolean())) {
+ !exports_v->IsNullOrUndefined()) {
Global exports;
exports.Reset(env->isolate(), exports_v);
auto entry = env->package_json_cache.emplace(path,
PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std,
- pkg_type });
+ pkg_type, std::move(exports) });
return Just(&entry.first->second);
}
auto entry = env->package_json_cache.emplace(path,
PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std,
- pkg_type });
+ pkg_type, Global() });
return Just(&entry.first->second);
}
@@ -800,6 +800,66 @@ Maybe PackageMainResolve(Environment* env,
return Nothing();
}
+Maybe PackageExportsResolve(Environment* env,
+ const URL& pjson_url,
+ const std::string& pkg_subpath,
+ const PackageConfig& pcfg,
+ const URL& base) {
+ CHECK(env->options()->experimental_exports);
+ Isolate* isolate = env->isolate();
+ Local context = env->context();
+ Local exports = pcfg.exports.Get(isolate);
+ if (exports->IsObject()) {
+ Local