Skip to content

Commit 8b0ff27

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

File tree

17 files changed

+188
-66
lines changed

17 files changed

+188
-66
lines changed

lib/internal/loader/DefaultResolve.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ function resolve(specifier, parentURL) {
5555
};
5656
}
5757

58-
let url;
58+
let url, esm;
5959
try {
60-
url = search(specifier, parentURL);
60+
({ url, esm } = search(specifier, parentURL));
6161
} catch (e) {
6262
if (typeof e.message === 'string' &&
6363
StringStartsWith(e.message, 'Cannot find module'))
@@ -76,7 +76,14 @@ function resolve(specifier, parentURL) {
7676
}
7777

7878
const ext = extname(url.pathname);
79-
return { url: `${url}`, format: extensionFormatMap[ext] || ext };
79+
let format;
80+
if (esm && ext === '.js') {
81+
format = 'esm';
82+
} else {
83+
format = extensionFormatMap[ext];
84+
}
85+
86+
return { url: `${url}`, format };
8087
}
8188

8289
module.exports = resolve;

src/env.h

Lines changed: 2 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") \
@@ -252,6 +253,7 @@ class ModuleWrap;
252253
V(type_string, "type") \
253254
V(uid_string, "uid") \
254255
V(unknown_string, "<unknown>") \
256+
V(url_string, "url") \
255257
V(user_string, "user") \
256258
V(username_string, "username") \
257259
V(valid_from_string, "valid_from") \

src/module_wrap.cc

Lines changed: 124 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,40 +382,16 @@ Maybe<uv_file> CheckFile(const URL& search,
383382
return Just(fd);
384383
}
385384

386-
enum ResolveExtensionsOptions {
387-
TRY_EXACT_NAME,
388-
ONLY_VIA_EXTENSIONS
389-
};
390-
391-
template<ResolveExtensionsOptions options>
392-
Maybe<URL> ResolveExtensions(const URL& search) {
393-
if (options == TRY_EXACT_NAME) {
394-
Maybe<uv_file> check = CheckFile(search);
395-
if (!check.IsNothing()) {
396-
return Just(search);
397-
}
398-
}
399-
400-
for (const char* extension : EXTENSIONS) {
401-
URL guess(search.path() + extension, &search);
402-
Maybe<uv_file> check = CheckFile(guess);
403-
if (!check.IsNothing()) {
404-
return Just(guess);
405-
}
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;
406391
}
407-
408-
return Nothing<URL>();
409-
}
410-
411-
inline Maybe<URL> ResolveIndex(const URL& search) {
412-
return ResolveExtensions<ONLY_VIA_EXTENSIONS>(URL("index", search));
413-
}
414-
415-
Maybe<URL> ResolveMain(Environment* env, const URL& search) {
416-
URL pkg("package.json", &search);
417-
Maybe<uv_file> check = CheckFile(pkg, LEAVE_OPEN_AFTER_CHECK);
392+
Maybe<uv_file> check = CheckFile(path, LEAVE_OPEN_AFTER_CHECK);
418393
if (check.IsNothing()) {
419-
return Nothing<URL>();
394+
return (pjson_cache_[path] = emptyPackage);
420395
}
421396

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

441416
Local<Value> pkg_json;
442417
if (!JSON::Parse(context, src).ToLocal(&pkg_json) || !pkg_json->IsObject())
443-
return Nothing<URL>();
418+
return (pjson_cache_[path] = emptyPackage);
444419
Local<Value> pkg_main;
445-
if (!pkg_json.As<Object>()->Get(context, env->main_string())
446-
.ToLocal(&pkg_main) || !pkg_main->IsString()) {
447-
return Nothing<URL>();
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+
Local<Value> pkg_esm;
430+
bool esm = false;
431+
if (pkg_json.As<Object>()->Get(context, env->esm_string())
432+
.ToLocal(&pkg_esm) && pkg_esm->IsBoolean()) {
433+
esm = pkg_esm.As<v8::Boolean>()->Value();
434+
}
435+
436+
PackageJson pjson = { true, has_main, main_std, esm };
437+
pjson_cache_[path] = pjson;
438+
return pjson;
439+
}
440+
441+
ModuleResolution ResolveFormat(Environment* env, const URL& search) {
442+
URL pjsonPath("package.json", &search);
443+
PackageJson pjson;
444+
do {
445+
pjson = GetPackageJson(env, pjsonPath.ToFilePath());
446+
if (pjson.exists) {
447+
break;
448+
}
449+
URL lastPjsonPath = pjsonPath;
450+
pjsonPath = URL("../package.json", pjsonPath);
451+
if (pjsonPath.path() == lastPjsonPath.path()) {
452+
break;
453+
}
454+
} while (true);
455+
ModuleResolution resolution = { search, pjson.exists && pjson.esm };
456+
return resolution;
457+
}
458+
459+
enum ResolveExtensionsOptions {
460+
TRY_EXACT_NAME,
461+
ONLY_VIA_EXTENSIONS
462+
};
463+
464+
template<ResolveExtensionsOptions options>
465+
Maybe<ModuleResolution> ResolveExtensions(Environment* env, const URL& search) {
466+
if (options == TRY_EXACT_NAME) {
467+
Maybe<uv_file> check = CheckFile(search.ToFilePath());
468+
if (!check.IsNothing()) {
469+
return Just(ResolveFormat(env, search));
470+
}
448471
}
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, "./");
472+
473+
for (const char* extension : EXTENSIONS) {
474+
URL guess(search.path() + extension, &search);
475+
Maybe<uv_file> check = CheckFile(guess.ToFilePath());
476+
if (!check.IsNothing()) {
477+
return Just(ResolveFormat(env, guess));
478+
}
453479
}
454-
return Resolve(env, main_std, search);
480+
481+
return Nothing<ModuleResolution>();
482+
}
483+
484+
inline Maybe<ModuleResolution> ResolveIndex(Environment* env,
485+
const URL& search) {
486+
return ResolveExtensions<ONLY_VIA_EXTENSIONS>(env, URL("index", search));
455487
}
456488

457-
Maybe<URL> ResolveModule(Environment* env,
489+
Maybe<ModuleResolution> ResolveMain(Environment* env, const URL& search) {
490+
URL pkg("package.json", &search);
491+
492+
PackageJson pjson = GetPackageJson(env, pkg.ToFilePath());
493+
if (!pjson.exists || !pjson.has_main) {
494+
return Nothing<ModuleResolution>();
495+
}
496+
if (!ShouldBeTreatedAsRelativeOrAbsolutePath(pjson.main)) {
497+
return Resolve(env, "./" + pjson.main, search);
498+
}
499+
return Resolve(env, pjson.main, search);
500+
}
501+
502+
Maybe<ModuleResolution> ResolveModule(Environment* env,
458503
const std::string& specifier,
459504
const URL& base) {
460505
URL parent(".", base);
461506
URL dir("");
462507
do {
463508
dir = parent;
464-
Maybe<URL> check = Resolve(env, "./node_modules/" + specifier, dir, true);
509+
Maybe<ModuleResolution> check =
510+
Resolve(env, "./node_modules/" + specifier, dir, true);
465511
if (!check.IsNothing()) {
466512
const size_t limit = specifier.find('/');
467513
const size_t spec_len =
468514
limit == std::string::npos ? specifier.length() :
469515
limit + 1;
470516
std::string chroot =
471517
dir.path() + "node_modules/" + specifier.substr(0, spec_len);
472-
if (check.FromJust().path().substr(0, chroot.length()) != chroot) {
473-
return Nothing<URL>();
518+
if (check.FromJust().url.path().substr(0, chroot.length()) != chroot) {
519+
return Nothing<ModuleResolution>();
474520
}
475521
return check;
476522
} else {
477523
// TODO(bmeck) PREVENT FALLTHROUGH
478524
}
479525
parent = URL("..", &dir);
480526
} while (parent.path() != dir.path());
481-
return Nothing<URL>();
527+
return Nothing<ModuleResolution>();
482528
}
483529

484-
Maybe<URL> ResolveDirectory(Environment* env,
530+
Maybe<ModuleResolution> ResolveDirectory(Environment* env,
485531
const URL& search,
486532
bool read_pkg_json) {
487533
if (read_pkg_json) {
488-
Maybe<URL> main = ResolveMain(env, search);
534+
Maybe<ModuleResolution> main = ResolveMain(env, search);
489535
if (!main.IsNothing())
490536
return main;
491537
}
492-
return ResolveIndex(search);
538+
return ResolveIndex(env, search);
493539
}
494540

495541
} // anonymous namespace
496542

497-
498-
Maybe<URL> Resolve(Environment* env,
543+
Maybe<ModuleResolution> Resolve(Environment* env,
499544
const std::string& specifier,
500545
const URL& base,
501546
bool read_pkg_json) {
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()) {
507-
return Nothing<URL>();
552+
return Nothing<ModuleResolution>();
508553
}
509-
return Just(pure_url);
554+
return Just(ResolveFormat(env, pure_url));
510555
}
511556
if (specifier.length() == 0) {
512-
return Nothing<URL>();
557+
return Nothing<ModuleResolution>();
513558
}
514559
if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
515560
URL resolved(specifier, base);
516-
Maybe<URL> file = ResolveExtensions<TRY_EXACT_NAME>(resolved);
561+
Maybe<ModuleResolution> file =
562+
ResolveExtensions<TRY_EXACT_NAME>(env, resolved);
517563
if (!file.IsNothing())
518564
return file;
519565
if (specifier.back() != '/') {
@@ -556,14 +602,30 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
556602
return;
557603
}
558604

559-
Maybe<URL> result = node::loader::Resolve(env, specifier_std, url, true);
560-
if (result.IsNothing() || (result.FromJust().flags() & URL_FLAGS_FAILED)) {
605+
Maybe<ModuleResolution> result =
606+
node::loader::Resolve(env, specifier_std, url, true);
607+
if (result.IsNothing() ||
608+
(result.FromJust().url.flags() & URL_FLAGS_FAILED)) {
561609
std::string msg = "Cannot find module " + specifier_std;
562610
env->ThrowError(msg.c_str());
563611
return;
564612
}
565613

566-
args.GetReturnValue().Set(result.FromJust().ToObject(env));
614+
Local<Object> resolved = Object::New(env->isolate());
615+
616+
resolved->DefineOwnProperty(
617+
env->context(),
618+
env->esm_string(),
619+
v8::Boolean::New(env->isolate(), result.FromJust().esm),
620+
v8::ReadOnly);
621+
622+
resolved->DefineOwnProperty(
623+
env->context(),
624+
env->url_string(),
625+
result.FromJust().url.ToObject(env),
626+
v8::ReadOnly);
627+
628+
args.GetReturnValue().Set(resolved);
567629
}
568630

569631
static MaybeLocal<Promise> ImportModuleDynamically(

src/module_wrap.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,19 @@
1212
namespace node {
1313
namespace loader {
1414

15-
v8::Maybe<url::URL> Resolve(Environment* env,
15+
struct ModuleResolution {
16+
url::URL url;
17+
bool esm;
18+
};
19+
20+
struct PackageJson {
21+
bool exists;
22+
bool has_main;
23+
std::string main;
24+
bool esm;
25+
};
26+
27+
v8::Maybe<ModuleResolution> Resolve(Environment* env,
1628
const std::string& specifier,
1729
const url::URL& base,
1830
bool read_pkg_json = false);
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');
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-non-boolean/module';
4+
import assert from 'assert';
5+
6+
assert.strictEqual(m, 'cjs');
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/sub/dir/module';
4+
import assert from 'assert';
5+
6+
assert.strictEqual(m, 'esm submodule');
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/module';
4+
import assert from 'assert';
5+
6+
assert.strictEqual(m, 'esm');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = 'cjs';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)