Skip to content

esm: Implement package.json "esm": true flag for ES modules in ".js" files #18156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions lib/internal/loader/DefaultResolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ function resolve(specifier, parentURL) {
};
}

let url;
let url, esm;
try {
url = search(specifier, parentURL);
({ url, esm } = search(specifier, parentURL));
} catch (e) {
if (typeof e.message === 'string' &&
StringStartsWith(e.message, 'Cannot find module'))
Expand All @@ -76,7 +76,14 @@ function resolve(specifier, parentURL) {
}

const ext = extname(url.pathname);
return { url: `${url}`, format: extensionFormatMap[ext] || ext };
let format;
if (esm && ext === '.js') {
format = 'esm';
} else {
format = extensionFormatMap[ext];
}

return { url: `${url}`, format };
}

module.exports = resolve;
Expand Down
2 changes: 2 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class ModuleWrap;
V(env_pairs_string, "envPairs") \
V(errno_string, "errno") \
V(error_string, "error") \
V(esm_string, "esm") \
V(exiting_string, "_exiting") \
V(exit_code_string, "exitCode") \
V(exit_string, "exit") \
Expand Down Expand Up @@ -252,6 +253,7 @@ class ModuleWrap;
V(type_string, "type") \
V(uid_string, "uid") \
V(unknown_string, "<unknown>") \
V(url_string, "url") \
V(user_string, "user") \
V(username_string, "username") \
V(valid_from_string, "valid_from") \
Expand Down
186 changes: 124 additions & 62 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,9 @@ enum CheckFileOptions {
CLOSE_AFTER_CHECK
};

Maybe<uv_file> CheckFile(const URL& search,
Maybe<uv_file> CheckFile(const std::string& path,
CheckFileOptions opt = CLOSE_AFTER_CHECK) {
uv_fs_t fs_req;
std::string path = search.ToFilePath();
if (path.empty()) {
return Nothing<uv_file>();
}
Expand Down Expand Up @@ -383,40 +382,16 @@ Maybe<uv_file> CheckFile(const URL& search,
return Just(fd);
}

enum ResolveExtensionsOptions {
TRY_EXACT_NAME,
ONLY_VIA_EXTENSIONS
};

template<ResolveExtensionsOptions options>
Maybe<URL> ResolveExtensions(const URL& search) {
if (options == TRY_EXACT_NAME) {
Maybe<uv_file> check = CheckFile(search);
if (!check.IsNothing()) {
return Just(search);
}
}

for (const char* extension : EXTENSIONS) {
URL guess(search.path() + extension, &search);
Maybe<uv_file> check = CheckFile(guess);
if (!check.IsNothing()) {
return Just(guess);
}
PackageJson emptyPackage = { false, false, "", false };
std::unordered_map<std::string, PackageJson> pjson_cache_;
PackageJson GetPackageJson(Environment* env, const std::string path) {
auto existing = pjson_cache_.find(path);
if (existing != pjson_cache_.end()) {
return existing->second;
}

return Nothing<URL>();
}

inline Maybe<URL> ResolveIndex(const URL& search) {
return ResolveExtensions<ONLY_VIA_EXTENSIONS>(URL("index", search));
}

Maybe<URL> ResolveMain(Environment* env, const URL& search) {
URL pkg("package.json", &search);
Maybe<uv_file> check = CheckFile(pkg, LEAVE_OPEN_AFTER_CHECK);
Maybe<uv_file> check = CheckFile(path, LEAVE_OPEN_AFTER_CHECK);
if (check.IsNothing()) {
return Nothing<URL>();
return (pjson_cache_[path] = emptyPackage);
}

Isolate* isolate = env->isolate();
Expand All @@ -435,85 +410,156 @@ Maybe<URL> ResolveMain(Environment* env, const URL& search) {
pkg_src.c_str(),
v8::NewStringType::kNormal,
pkg_src.length()).ToLocal(&src)) {
return Nothing<URL>();
return (pjson_cache_[path] = emptyPackage);
}

Local<Value> pkg_json;
if (!JSON::Parse(context, src).ToLocal(&pkg_json) || !pkg_json->IsObject())
return Nothing<URL>();
return (pjson_cache_[path] = emptyPackage);
Local<Value> pkg_main;
if (!pkg_json.As<Object>()->Get(context, env->main_string())
.ToLocal(&pkg_main) || !pkg_main->IsString()) {
return Nothing<URL>();
bool has_main = false;
std::string main_std;
if (pkg_json.As<Object>()->Get(context, env->main_string())
.ToLocal(&pkg_main) && pkg_main->IsString()) {
has_main = true;
Utf8Value main_utf8(isolate, pkg_main.As<String>());
main_std = std::string(*main_utf8, main_utf8.length());
}

Local<Value> pkg_esm;
bool esm = false;
if (pkg_json.As<Object>()->Get(context, env->esm_string())
.ToLocal(&pkg_esm) && pkg_esm->IsBoolean()) {
esm = pkg_esm.As<v8::Boolean>()->Value();
}

PackageJson pjson = { true, has_main, main_std, esm };
pjson_cache_[path] = pjson;
return pjson;
}

ModuleResolution ResolveFormat(Environment* env, const URL& search) {
URL pjsonPath("package.json", &search);
PackageJson pjson;
do {
pjson = GetPackageJson(env, pjsonPath.ToFilePath());
if (pjson.exists) {
break;
}
URL lastPjsonPath = pjsonPath;
pjsonPath = URL("../package.json", pjsonPath);
if (pjsonPath.path() == lastPjsonPath.path()) {
break;
}
} while (true);
ModuleResolution resolution = { search, pjson.exists && pjson.esm };
return resolution;
}

enum ResolveExtensionsOptions {
TRY_EXACT_NAME,
ONLY_VIA_EXTENSIONS
};

template<ResolveExtensionsOptions options>
Maybe<ModuleResolution> ResolveExtensions(Environment* env, const URL& search) {
if (options == TRY_EXACT_NAME) {
Maybe<uv_file> check = CheckFile(search.ToFilePath());
if (!check.IsNothing()) {
return Just(ResolveFormat(env, search));
}
}
Utf8Value main_utf8(isolate, pkg_main.As<String>());
std::string main_std(*main_utf8, main_utf8.length());
if (!ShouldBeTreatedAsRelativeOrAbsolutePath(main_std)) {
main_std.insert(0, "./");

for (const char* extension : EXTENSIONS) {
URL guess(search.path() + extension, &search);
Maybe<uv_file> check = CheckFile(guess.ToFilePath());
if (!check.IsNothing()) {
return Just(ResolveFormat(env, guess));
}
}
return Resolve(env, main_std, search);

return Nothing<ModuleResolution>();
}

inline Maybe<ModuleResolution> ResolveIndex(Environment* env,
const URL& search) {
return ResolveExtensions<ONLY_VIA_EXTENSIONS>(env, URL("index", search));
}

Maybe<URL> ResolveModule(Environment* env,
Maybe<ModuleResolution> ResolveMain(Environment* env, const URL& search) {
URL pkg("package.json", &search);

PackageJson pjson = GetPackageJson(env, pkg.ToFilePath());
if (!pjson.exists || !pjson.has_main) {
return Nothing<ModuleResolution>();
}
if (!ShouldBeTreatedAsRelativeOrAbsolutePath(pjson.main)) {
return Resolve(env, "./" + pjson.main, search);
}
return Resolve(env, pjson.main, search);
}

Maybe<ModuleResolution> ResolveModule(Environment* env,
const std::string& specifier,
const URL& base) {
URL parent(".", base);
URL dir("");
do {
dir = parent;
Maybe<URL> check = Resolve(env, "./node_modules/" + specifier, dir, true);
Maybe<ModuleResolution> check =
Resolve(env, "./node_modules/" + specifier, dir, true);
if (!check.IsNothing()) {
const size_t limit = specifier.find('/');
const size_t spec_len =
limit == std::string::npos ? specifier.length() :
limit + 1;
std::string chroot =
dir.path() + "node_modules/" + specifier.substr(0, spec_len);
if (check.FromJust().path().substr(0, chroot.length()) != chroot) {
return Nothing<URL>();
if (check.FromJust().url.path().substr(0, chroot.length()) != chroot) {
return Nothing<ModuleResolution>();
}
return check;
} else {
// TODO(bmeck) PREVENT FALLTHROUGH
}
parent = URL("..", &dir);
} while (parent.path() != dir.path());
return Nothing<URL>();
return Nothing<ModuleResolution>();
}

Maybe<URL> ResolveDirectory(Environment* env,
Maybe<ModuleResolution> ResolveDirectory(Environment* env,
const URL& search,
bool read_pkg_json) {
if (read_pkg_json) {
Maybe<URL> main = ResolveMain(env, search);
Maybe<ModuleResolution> main = ResolveMain(env, search);
if (!main.IsNothing())
return main;
}
return ResolveIndex(search);
return ResolveIndex(env, search);
}

} // anonymous namespace


Maybe<URL> Resolve(Environment* env,
Maybe<ModuleResolution> Resolve(Environment* env,
const std::string& specifier,
const URL& base,
bool read_pkg_json) {
URL pure_url(specifier);
if (!(pure_url.flags() & URL_FLAGS_FAILED)) {
// just check existence, without altering
Maybe<uv_file> check = CheckFile(pure_url);
Maybe<uv_file> check = CheckFile(pure_url.ToFilePath());
if (check.IsNothing()) {
return Nothing<URL>();
return Nothing<ModuleResolution>();
}
return Just(pure_url);
return Just(ResolveFormat(env, pure_url));
}
if (specifier.length() == 0) {
return Nothing<URL>();
return Nothing<ModuleResolution>();
}
if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
URL resolved(specifier, base);
Maybe<URL> file = ResolveExtensions<TRY_EXACT_NAME>(resolved);
Maybe<ModuleResolution> file =
ResolveExtensions<TRY_EXACT_NAME>(env, resolved);
if (!file.IsNothing())
return file;
if (specifier.back() != '/') {
Expand Down Expand Up @@ -556,14 +602,30 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
return;
}

Maybe<URL> result = node::loader::Resolve(env, specifier_std, url, true);
if (result.IsNothing() || (result.FromJust().flags() & URL_FLAGS_FAILED)) {
Maybe<ModuleResolution> result =
node::loader::Resolve(env, specifier_std, url, true);
if (result.IsNothing() ||
(result.FromJust().url.flags() & URL_FLAGS_FAILED)) {
std::string msg = "Cannot find module " + specifier_std;
env->ThrowError(msg.c_str());
return;
}

args.GetReturnValue().Set(result.FromJust().ToObject(env));
Local<Object> resolved = Object::New(env->isolate());

resolved->DefineOwnProperty(
env->context(),
env->esm_string(),
v8::Boolean::New(env->isolate(), result.FromJust().esm),
v8::ReadOnly);

resolved->DefineOwnProperty(
env->context(),
env->url_string(),
result.FromJust().url.ToObject(env),
v8::ReadOnly);

args.GetReturnValue().Set(resolved);
}

static MaybeLocal<Promise> ImportModuleDynamically(
Expand Down
14 changes: 13 additions & 1 deletion src/module_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@
namespace node {
namespace loader {

v8::Maybe<url::URL> Resolve(Environment* env,
struct ModuleResolution {
url::URL url;
bool esm;
};

struct PackageJson {
bool exists;
bool has_main;
std::string main;
bool esm;
};

v8::Maybe<ModuleResolution> Resolve(Environment* env,
const std::string& specifier,
const url::URL& base,
bool read_pkg_json = false);
Expand Down
6 changes: 6 additions & 0 deletions test/es-module/test-pjson-cjs-esm-nested.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
import m from '../fixtures/es-modules/esm-cjs-nested/module';
import assert from 'assert';

assert.strictEqual(m, 'cjs');
6 changes: 6 additions & 0 deletions test/es-module/test-pjson-esm-flag-non-boolean.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
import m from '../fixtures/es-modules/esm-non-boolean/module';
import assert from 'assert';

assert.strictEqual(m, 'cjs');
6 changes: 6 additions & 0 deletions test/es-module/test-pjson-esm-flag-submodule.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
import m from '../fixtures/es-modules/esm/sub/dir/module';
import assert from 'assert';

assert.strictEqual(m, 'esm submodule');
6 changes: 6 additions & 0 deletions test/es-module/test-pjson-esm-flag.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Flags: --experimental-modules
/* eslint-disable required-modules */
import m from '../fixtures/es-modules/esm/module';
import assert from 'assert';

assert.strictEqual(m, 'esm');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'cjs';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions test/fixtures/es-modules/esm-cjs-nested/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './cjs-nested/module.js';
3 changes: 3 additions & 0 deletions test/fixtures/es-modules/esm-cjs-nested/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"esm": true
}
1 change: 1 addition & 0 deletions test/fixtures/es-modules/esm-non-boolean/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'cjs';
3 changes: 3 additions & 0 deletions test/fixtures/es-modules/esm-non-boolean/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"esm": 1
}
1 change: 1 addition & 0 deletions test/fixtures/es-modules/esm/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'esm';
3 changes: 3 additions & 0 deletions test/fixtures/es-modules/esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"esm": true
}
1 change: 1 addition & 0 deletions test/fixtures/es-modules/esm/sub/dir/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'esm submodule';