Skip to content

Commit 5931cd5

Browse files
committed
esm: pjson "mode": "esm" flag indicating ".js" es modules
This allows ".js" files to be loaded as es modules whenever the parent package.json file contains "mode": "esm". Original proposal and discussion at nodejs/node-eps/pull/60
1 parent 39ef352 commit 5931cd5

File tree

17 files changed

+198
-71
lines changed

17 files changed

+198
-71
lines changed

lib/internal/loader/DefaultResolve.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ module.exports.search = search;
1818

1919
const realpathCache = new Map();
2020

21-
function search(target, base) {
21+
function search(target, base, checkPjsonMode) {
2222
if (base === undefined) {
2323
// We cannot search without a base.
2424
throw new errors.Error('ERR_MISSING_MODULE', target);
2525
}
2626
try {
27-
return moduleWrapResolve(target, base);
27+
return moduleWrapResolve(target, base, checkPjsonMode);
2828
} catch (e) {
2929
e.stack; // cause V8 to generate stack before rethrow
3030
let error = e;
@@ -65,10 +65,12 @@ function resolve(specifier, parentURL) {
6565
mainFormat = process.binding('config').mainFormat;
6666
}
6767

68-
let url;
68+
let url, esm;
6969
try {
70-
url = search(specifier, parentURL ||
71-
getURLFromFilePath(`${process.cwd()}/`).href);
70+
({ url, esm } =
71+
search(specifier,
72+
parentURL || getURLFromFilePath(`${process.cwd()}/`).href,
73+
mainFormat ? false : true));
7274
} catch (e) {
7375
if (typeof e.message === 'string' &&
7476
StringStartsWith(e.message, 'Cannot find module'))
@@ -93,9 +95,10 @@ function resolve(specifier, parentURL) {
9395
if (parentURL === undefined)
9496
return {
9597
url: `${url}`,
96-
format: mainFormat || extensionFormatMap[ext] || 'commonjs'
98+
format: mainFormat ||
99+
(esm ? 'esm' : extensionFormatMap[ext] || 'commonjs')
97100
};
98101

99102
// all other imports are extension-format-based
100-
return { url: `${url}`, format: extensionFormatMap[ext] };
103+
return { url: `${url}`, format: esm ? 'esm' : extensionFormatMap[ext] };
101104
}

src/env.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class ModuleWrap;
137137
V(env_pairs_string, "envPairs") \
138138
V(errno_string, "errno") \
139139
V(error_string, "error") \
140+
V(esm_string, "esm") \
140141
V(exiting_string, "_exiting") \
141142
V(exit_code_string, "exitCode") \
142143
V(exit_string, "exit") \
@@ -173,6 +174,7 @@ class ModuleWrap;
173174
V(max_buffer_string, "maxBuffer") \
174175
V(message_string, "message") \
175176
V(minttl_string, "minttl") \
177+
V(mode_string, "mode") \
176178
V(modulus_string, "modulus") \
177179
V(name_string, "name") \
178180
V(netmask_string, "netmask") \
@@ -252,6 +254,7 @@ class ModuleWrap;
252254
V(type_string, "type") \
253255
V(uid_string, "uid") \
254256
V(unknown_string, "<unknown>") \
257+
V(url_string, "url") \
255258
V(user_string, "user") \
256259
V(username_string, "username") \
257260
V(valid_from_string, "valid_from") \

src/module_wrap.cc

Lines changed: 138 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,9 @@ enum CheckFileOptions {
350350
CLOSE_AFTER_CHECK
351351
};
352352

353-
Maybe<uv_file> CheckFile(const URL& search,
353+
Maybe<uv_file> CheckFile(const std::string& path,
354354
CheckFileOptions opt = CLOSE_AFTER_CHECK) {
355355
uv_fs_t fs_req;
356-
std::string path = search.ToFilePath();
357356
if (path.empty()) {
358357
return Nothing<uv_file>();
359358
}
@@ -383,23 +382,103 @@ Maybe<uv_file> CheckFile(const URL& search,
383382
return Just(fd);
384383
}
385384

385+
PackageJson emptyPackage = { false, false, "", false };
386+
std::unordered_map<std::string, PackageJson> pjson_cache_;
387+
PackageJson GetPackageJson(Environment* env, const std::string path) {
388+
auto existing = pjson_cache_.find(path);
389+
if (existing != pjson_cache_.end()) {
390+
return existing->second;
391+
}
392+
Maybe<uv_file> check = CheckFile(path, LEAVE_OPEN_AFTER_CHECK);
393+
if (check.IsNothing()) {
394+
return (pjson_cache_[path] = emptyPackage);
395+
}
396+
397+
Isolate* isolate = env->isolate();
398+
Local<Context> context = isolate->GetCurrentContext();
399+
std::string pkg_src = ReadFile(check.FromJust());
400+
uv_fs_t fs_req;
401+
uv_fs_close(nullptr, &fs_req, check.FromJust(), nullptr);
402+
uv_fs_req_cleanup(&fs_req);
403+
404+
// It's not okay for the called of this method to not be able to tell
405+
// whether an exception is pending or not.
406+
TryCatch try_catch(isolate);
407+
408+
Local<String> src;
409+
if (!String::NewFromUtf8(isolate,
410+
pkg_src.c_str(),
411+
v8::NewStringType::kNormal,
412+
pkg_src.length()).ToLocal(&src)) {
413+
return (pjson_cache_[path] = emptyPackage);
414+
}
415+
416+
Local<Value> pkg_json;
417+
if (!JSON::Parse(context, src).ToLocal(&pkg_json) || !pkg_json->IsObject())
418+
return (pjson_cache_[path] = emptyPackage);
419+
Local<Value> pkg_main;
420+
bool has_main = false;
421+
std::string main_std;
422+
if (pkg_json.As<Object>()->Get(context, env->main_string())
423+
.ToLocal(&pkg_main) && pkg_main->IsString()) {
424+
has_main = true;
425+
Utf8Value main_utf8(isolate, pkg_main.As<String>());
426+
main_std = std::string(*main_utf8, main_utf8.length());
427+
}
428+
429+
bool esm = false;
430+
Local<Value> pkg_mode;
431+
std::string pkg_mode_std;
432+
if (pkg_json.As<Object>()->Get(context, env->mode_string())
433+
.ToLocal(&pkg_mode) && pkg_mode->IsString()) {
434+
Utf8Value pkg_mode_utf8(isolate, pkg_mode.As<String>());
435+
pkg_mode_std = std::string(*pkg_mode_utf8, pkg_mode_utf8.length());
436+
if (pkg_mode_std == "esm") {
437+
esm = true;
438+
}
439+
}
440+
441+
PackageJson pjson = { true, has_main, main_std, esm };
442+
pjson_cache_[path] = pjson;
443+
return pjson;
444+
}
445+
446+
bool CheckPjsonEsmMode(Environment* env, const URL& search) {
447+
URL pjsonPath("package.json", &search);
448+
PackageJson pjson;
449+
do {
450+
pjson = GetPackageJson(env, pjsonPath.ToFilePath());
451+
if (pjson.exists) {
452+
break;
453+
}
454+
URL lastPjsonPath = pjsonPath;
455+
pjsonPath = URL("../package.json", pjsonPath);
456+
if (pjsonPath.path() == lastPjsonPath.path()) {
457+
break;
458+
}
459+
} while (true);
460+
return pjson.exists && pjson.esm;
461+
}
462+
463+
386464
enum ResolveExtensionsOptions {
387465
TRY_EXACT_NAME,
388466
ONLY_VIA_EXTENSIONS
389467
};
390468

391469
template<ResolveExtensionsOptions options>
392-
Maybe<URL> ResolveExtensions(const URL& search) {
470+
Maybe<URL> ResolveExtensions(Environment* env, const URL& search) {
393471
if (options == TRY_EXACT_NAME) {
394-
Maybe<uv_file> check = CheckFile(search);
472+
std::string filePath = search.ToFilePath();
473+
Maybe<uv_file> check = CheckFile(filePath);
395474
if (!check.IsNothing()) {
396475
return Just(search);
397476
}
398477
}
399478

400479
for (const char* extension : EXTENSIONS) {
401480
URL guess(search.path() + extension, &search);
402-
Maybe<uv_file> check = CheckFile(guess);
481+
Maybe<uv_file> check = CheckFile(guess.ToFilePath());
403482
if (!check.IsNothing()) {
404483
return Just(guess);
405484
}
@@ -408,50 +487,21 @@ Maybe<URL> ResolveExtensions(const URL& search) {
408487
return Nothing<URL>();
409488
}
410489

411-
inline Maybe<URL> ResolveIndex(const URL& search) {
412-
return ResolveExtensions<ONLY_VIA_EXTENSIONS>(URL("index", search));
490+
inline Maybe<URL> ResolveIndex(Environment* env, const URL& search) {
491+
return ResolveExtensions<ONLY_VIA_EXTENSIONS>(env, URL("index", search));
413492
}
414493

415494
Maybe<URL> ResolveMain(Environment* env, const URL& search) {
416495
URL pkg("package.json", &search);
417-
Maybe<uv_file> check = CheckFile(pkg, LEAVE_OPEN_AFTER_CHECK);
418-
if (check.IsNothing()) {
419-
return Nothing<URL>();
420-
}
421-
422-
Isolate* isolate = env->isolate();
423-
Local<Context> context = isolate->GetCurrentContext();
424-
std::string pkg_src = ReadFile(check.FromJust());
425-
uv_fs_t fs_req;
426-
uv_fs_close(nullptr, &fs_req, check.FromJust(), nullptr);
427-
uv_fs_req_cleanup(&fs_req);
428-
429-
// It's not okay for the called of this method to not be able to tell
430-
// whether an exception is pending or not.
431-
TryCatch try_catch(isolate);
432-
433-
Local<String> src;
434-
if (!String::NewFromUtf8(isolate,
435-
pkg_src.c_str(),
436-
v8::NewStringType::kNormal,
437-
pkg_src.length()).ToLocal(&src)) {
438-
return Nothing<URL>();
439-
}
440496

441-
Local<Value> pkg_json;
442-
if (!JSON::Parse(context, src).ToLocal(&pkg_json) || !pkg_json->IsObject())
443-
return Nothing<URL>();
444-
Local<Value> pkg_main;
445-
if (!pkg_json.As<Object>()->Get(context, env->main_string())
446-
.ToLocal(&pkg_main) || !pkg_main->IsString()) {
497+
PackageJson pjson = GetPackageJson(env, pkg.ToFilePath());
498+
if (!pjson.exists || !pjson.has_main) {
447499
return Nothing<URL>();
448500
}
449-
Utf8Value main_utf8(isolate, pkg_main.As<String>());
450-
std::string main_std(*main_utf8, main_utf8.length());
451-
if (!ShouldBeTreatedAsRelativeOrAbsolutePath(main_std)) {
452-
main_std.insert(0, "./");
501+
if (!ShouldBeTreatedAsRelativeOrAbsolutePath(pjson.main)) {
502+
return Resolve(env, "./" + pjson.main, search);
453503
}
454-
return Resolve(env, main_std, search);
504+
return Resolve(env, pjson.main, search);
455505
}
456506

457507
Maybe<URL> ResolveModule(Environment* env,
@@ -461,7 +511,8 @@ Maybe<URL> ResolveModule(Environment* env,
461511
URL dir("");
462512
do {
463513
dir = parent;
464-
Maybe<URL> check = Resolve(env, "./node_modules/" + specifier, dir, true);
514+
Maybe<URL> check =
515+
Resolve(env, "./node_modules/" + specifier, dir);
465516
if (!check.IsNothing()) {
466517
const size_t limit = specifier.find('/');
467518
const size_t spec_len =
@@ -481,28 +532,22 @@ Maybe<URL> ResolveModule(Environment* env,
481532
return Nothing<URL>();
482533
}
483534

484-
Maybe<URL> ResolveDirectory(Environment* env,
485-
const URL& search,
486-
bool read_pkg_json) {
487-
if (read_pkg_json) {
488-
Maybe<URL> main = ResolveMain(env, search);
489-
if (!main.IsNothing())
490-
return main;
491-
}
492-
return ResolveIndex(search);
535+
Maybe<URL> ResolveDirectory(Environment* env, const URL& search) {
536+
Maybe<URL> main = ResolveMain(env, search);
537+
if (!main.IsNothing())
538+
return main;
539+
return ResolveIndex(env, search);
493540
}
494541

495542
} // anonymous namespace
496543

497-
498544
Maybe<URL> Resolve(Environment* env,
499545
const std::string& specifier,
500-
const URL& base,
501-
bool read_pkg_json) {
546+
const URL& base) {
502547
URL pure_url(specifier);
503548
if (!(pure_url.flags() & URL_FLAGS_FAILED)) {
504549
// just check existence, without altering
505-
Maybe<uv_file> check = CheckFile(pure_url);
550+
Maybe<uv_file> check = CheckFile(pure_url.ToFilePath());
506551
if (check.IsNothing()) {
507552
return Nothing<URL>();
508553
}
@@ -513,13 +558,14 @@ Maybe<URL> Resolve(Environment* env,
513558
}
514559
if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
515560
URL resolved(specifier, base);
516-
Maybe<URL> file = ResolveExtensions<TRY_EXACT_NAME>(resolved);
561+
Maybe<URL> file =
562+
ResolveExtensions<TRY_EXACT_NAME>(env, resolved);
517563
if (!file.IsNothing())
518564
return file;
519565
if (specifier.back() != '/') {
520566
resolved = URL(specifier + "/", base);
521567
}
522-
return ResolveDirectory(env, resolved, read_pkg_json);
568+
return ResolveDirectory(env, resolved);
523569
} else {
524570
return ResolveModule(env, specifier, base);
525571
}
@@ -532,8 +578,9 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
532578
env->ThrowError("resolve() must not be called as a constructor");
533579
return;
534580
}
535-
if (args.Length() != 2) {
536-
env->ThrowError("resolve must have exactly 2 arguments (string, string)");
581+
if (args.Length() != 3) {
582+
env->ThrowError(
583+
"resolve must have exactly 3 arguments (string, string, boolean)");
537584
return;
538585
}
539586

@@ -556,14 +603,43 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
556603
return;
557604
}
558605

559-
Maybe<URL> result = node::loader::Resolve(env, specifier_std, url, true);
560-
if (result.IsNothing() || (result.FromJust().flags() & URL_FLAGS_FAILED)) {
606+
if (!args[2]->IsBoolean()) {
607+
env->ThrowError("third argument is not a boolean");
608+
return;
609+
}
610+
bool check_pjson_mode = args[2]->ToBoolean().As<v8::Boolean>()->Value();
611+
612+
Maybe<URL> result = node::loader::Resolve(env, specifier_std, url);
613+
if (result.IsNothing() ||
614+
(result.FromJust().flags() & URL_FLAGS_FAILED)) {
561615
std::string msg = "Cannot find module " + specifier_std;
562616
env->ThrowError(msg.c_str());
563617
return;
564618
}
565619

566-
args.GetReturnValue().Set(result.FromJust().ToObject(env));
620+
bool esm = false;
621+
if (check_pjson_mode) {
622+
std::string path = result.FromJust().ToFilePath();
623+
if (path.substr(path.length() - 3, 3) == ".js") {
624+
esm = CheckPjsonEsmMode(env, result.FromJust());
625+
}
626+
}
627+
628+
Local<Object> resolved = Object::New(env->isolate());
629+
630+
resolved->DefineOwnProperty(
631+
env->context(),
632+
env->esm_string(),
633+
v8::Boolean::New(env->isolate(), esm),
634+
v8::ReadOnly);
635+
636+
resolved->DefineOwnProperty(
637+
env->context(),
638+
env->url_string(),
639+
result.FromJust().ToObject(env),
640+
v8::ReadOnly);
641+
642+
args.GetReturnValue().Set(resolved);
567643
}
568644

569645
static MaybeLocal<Promise> ImportModuleDynamically(

src/module_wrap.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@
1212
namespace node {
1313
namespace loader {
1414

15+
struct PackageJson {
16+
bool exists;
17+
bool has_main;
18+
std::string main;
19+
bool esm;
20+
};
21+
1522
v8::Maybe<url::URL> Resolve(Environment* env,
1623
const std::string& specifier,
17-
const url::URL& base,
18-
bool read_pkg_json = false);
24+
const url::URL& base);
1925

2026
class ModuleWrap : public BaseObject {
2127
public:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Flags: --experimental-modules
2+
/* eslint-disable required-modules */
3+
import m from '../fixtures/es-modules/esm-cjs-nested/module';
4+
import assert from 'assert';
5+
6+
assert.strictEqual(m, 'cjs');

0 commit comments

Comments
 (0)