diff --git a/doc/api/esm.md b/doc/api/esm.md
index c4edc52357..05d01f7390 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 intent 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 3e3d41790e..e68f120bf7 100644
--- a/src/env.h
+++ b/src/env.h
@@ -92,6 +92,7 @@ struct PackageConfig {
enum class Exists { Yes, No };
enum class IsValid { Yes, No };
enum class HasMain { Yes, No };
+ enum class HasExports { Yes, No };
enum PackageType : uint32_t { None = 0, CommonJS, Module };
const Exists exists;
@@ -99,6 +100,9 @@ struct PackageConfig {
const HasMain has_main;
const std::string main;
const PackageType type;
+
+ const HasExports has_exports;
+ const v8::CopyablePersistentTraits::CopyablePersistent exports;
};
} // namespace loader
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index e104afb736..037d691f3a 100644
--- a/src/module_wrap.cc
+++ b/src/module_wrap.cc
@@ -26,7 +26,6 @@ using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
-using v8::Global;
using v8::HandleScope;
using v8::Integer;
using v8::IntegrityLevel;
@@ -39,6 +38,7 @@ using v8::Module;
using v8::Nothing;
using v8::Number;
using v8::Object;
+using v8::Persistent;
using v8::PrimitiveArray;
using v8::Promise;
using v8::ScriptCompiler;
@@ -537,6 +537,7 @@ using Exists = PackageConfig::Exists;
using IsValid = PackageConfig::IsValid;
using HasMain = PackageConfig::HasMain;
using PackageType = PackageConfig::PackageType;
+using HasExports = PackageConfig::HasExports;
Maybe GetPackageConfig(Environment* env,
const std::string& path,
@@ -558,7 +559,8 @@ 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, HasExports::No,
+ Persistent() });
return Just(&entry.first->second);
}
@@ -578,7 +580,8 @@ 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, HasExports::No,
+ Persistent() });
std::string msg = "Invalid JSON in '" + path +
"' imported from " + base.ToFilePath();
node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str());
@@ -609,22 +612,22 @@ Maybe GetPackageConfig(Environment* env,
}
Local exports_v;
+ Persistent exports;
if (pkg_json->Get(env->context(),
env->exports_string()).ToLocal(&exports_v) &&
- (exports_v->IsObject() || exports_v->IsString() ||
- exports_v->IsBoolean())) {
- Global exports;
+ !exports_v->IsNullOrUndefined()) {
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, HasExports::Yes, 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, HasExports::No,
+ Persistent() });
return Just(&entry.first->second);
}
@@ -646,7 +649,8 @@ Maybe GetPackageScopeConfig(Environment* env,
if (pjson_url.path() == last_pjson_url.path()) {
auto entry = env->package_json_cache.emplace(pjson_url.ToFilePath(),
PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "",
- PackageType::None });
+ PackageType::None, HasExports::No,
+ Persistent() });
const PackageConfig* pcfg = &entry.first->second;
return Just(pcfg);
}
@@ -800,6 +804,65 @@ 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) {
+ Isolate* isolate = env->isolate();
+ Local context = env->context();
+ Local exports = pcfg.exports.Get(isolate);
+ if (exports->IsObject()) {
+ Local