Repository navigation
esm: Implement esm mode flag #18392
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
esm: Implement esm mode flag #18392
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
This implements a package.json "mode": "esm" module boundary. When loading a module from a package with this esm mode, ".js" files are loaded as es modules. In addition package.json caching is implemented in the module lookup process, and package.json boundaries are only checked when necessary.
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -502,7 +502,7 @@ const PackageConfig& GetPackageConfig(Environment* env, | |
| Maybe<uv_file> check = CheckFile(path, LEAVE_OPEN_AFTER_CHECK); | ||
| if (check.IsNothing()) { | ||
| auto entry = env->package_json_cache.emplace(path, | ||
| PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "" }); | ||
| PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "", NONE }); | ||
| return entry.first->second; | ||
| } | ||
|
|
||
|
|
@@ -520,7 +520,7 @@ const PackageConfig& GetPackageConfig(Environment* env, | |
| v8::NewStringType::kNormal, | ||
| pkg_src.length()).ToLocal(&src)) { | ||
| auto entry = env->package_json_cache.emplace(path, | ||
| PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "" }); | ||
| PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "", NONE }); | ||
| return entry.first->second; | ||
| } | ||
|
|
||
|
|
@@ -530,7 +530,7 @@ const PackageConfig& GetPackageConfig(Environment* env, | |
| if (!JSON::Parse(env->context(), src).ToLocal(&pkg_json_v) || | ||
| !pkg_json_v->ToObject(env->context()).ToLocal(&pkg_json)) { | ||
| auto entry = env->package_json_cache.emplace(path, | ||
| PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "" }); | ||
| PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "", NONE }); | ||
| return entry.first->second; | ||
| } | ||
|
|
||
|
|
@@ -543,11 +543,51 @@ const PackageConfig& GetPackageConfig(Environment* env, | |
| main_std.assign(std::string(*main_utf8, main_utf8.length())); | ||
| } | ||
|
|
||
| Local<Value> pkg_mode_v; | ||
| PackageMode pkg_mode = CJS; | ||
| std::string pkg_mode_std; | ||
| if (pkg_json->Get(env->context(), env->mode_string()).ToLocal(&pkg_mode_v)) { | ||
| Utf8Value pkg_mode_utf8(isolate, pkg_mode_v); | ||
| pkg_mode_std.assign(std::string(*pkg_mode_utf8, pkg_mode_utf8.length())); | ||
| if (pkg_mode_std == "esm") { | ||
| pkg_mode = ESM; | ||
| } | ||
| } | ||
|
|
||
| auto entry = env->package_json_cache.emplace(path, | ||
| PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std }); | ||
| PackageConfig { Exists::Yes, IsValid::Yes, has_main, | ||
| main_std, pkg_mode }); | ||
| return entry.first->second; | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this C++ json reader/parser is faster than JS would it be possible in a follow up PR to just use it for reading of package.json's or at least cached the parsed data here to avoid a subsequent load/parse?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes the goal is to unify on a single cache with further work. |
||
| PackageMode GetPackageMode(Environment* env, const URL& search) { | ||
| URL pjsonPath("package.json", &search); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style: |
||
| while (true) { | ||
| const PackageConfig& pkg_json = | ||
| GetPackageConfig(env, pjsonPath.ToFilePath()); | ||
| if (pkg_json.exists == Exists::Yes) { | ||
| return pkg_json.mode; | ||
| } | ||
| URL lastPjsonPath = pjsonPath; | ||
| pjsonPath = URL("../package.json", pjsonPath); | ||
| if (pjsonPath.path() == lastPjsonPath.path()) { | ||
| return CJS; | ||
| } | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What guarantees that this loop terminates? It's not evident to me. A comment or a check is probably in order. |
||
| } | ||
|
|
||
| void SetPackageMode(Environment* env, const URL& search, | ||
| PackageMode pkg_mode) { | ||
| std::string pjsonPathStr = URL("package.json", &search).ToFilePath(); | ||
| const PackageConfig& pkg_json = GetPackageConfig(env, pjsonPathStr); | ||
| if (pkg_json.mode != pkg_mode) { | ||
| env->package_json_cache.erase(env->package_json_cache.find(pjsonPathStr)); | ||
| env->package_json_cache.emplace(pjsonPathStr, | ||
| PackageConfig { pkg_json.exists, pkg_json.is_valid, | ||
| pkg_json.has_main, pkg_json.main, pkg_mode }); | ||
| } | ||
| } | ||
|
|
||
| enum ResolveExtensionsOptions { | ||
| TRY_EXACT_NAME, | ||
| ONLY_VIA_EXTENSIONS | ||
|
|
@@ -670,8 +710,8 @@ Maybe<URL> Resolve(Environment* env, | |
| void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) { | ||
| Environment* env = Environment::GetCurrent(args); | ||
|
|
||
| // module.resolve(specifier, url) | ||
| CHECK_EQ(args.Length(), 2); | ||
| // module.resolve(specifier, url, setPackageEsmMode) | ||
| CHECK_EQ(args.Length(), 3); | ||
|
|
||
| CHECK(args[0]->IsString()); | ||
| Utf8Value specifier_utf8(env->isolate(), args[0]); | ||
|
|
@@ -686,13 +726,49 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) { | |
| env, "second argument is not a URL string"); | ||
| } | ||
|
|
||
| if (!args[2]->IsBoolean()) { | ||
| env->ThrowError("third argument is not a boolean"); | ||
| return; | ||
| } | ||
|
|
||
| Maybe<URL> result = node::loader::Resolve(env, specifier_std, url); | ||
| if (result.IsNothing() || (result.FromJust().flags() & URL_FLAGS_FAILED)) { | ||
| std::string msg = "Cannot find module " + specifier_std; | ||
| return node::THROW_ERR_MISSING_MODULE(env, msg.c_str()); | ||
| } | ||
|
|
||
| args.GetReturnValue().Set(result.FromJust().ToObject(env)); | ||
| bool esmPackage = false; | ||
| bool set_package_esm_mode = args[2]->ToBoolean().As<v8::Boolean>()->Value(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (set_package_esm_mode) { | ||
| esmPackage = true; | ||
| SetPackageMode(env, result.FromJust(), ESM); | ||
| } else { | ||
| std::string filePath = result.FromJust().ToFilePath(); | ||
| // check the package esm mode for ambiguous extensions | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style: can you capitalize and punctuate the comment? |
||
| if (filePath.substr(filePath.length() - 4, 4) != ".mjs" && | ||
| filePath.substr(filePath.length() - 5, 5) != ".json" && | ||
| filePath.substr(filePath.length() - 5, 5) != ".node") { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsafe when .length() is < than the value subtracted from it. |
||
| if (GetPackageMode(env, result.FromJust()) == ESM) { | ||
| esmPackage = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Local<Object> resolved = Object::New(env->isolate()); | ||
|
|
||
| (void)resolved->DefineOwnProperty( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't cast to void, call |
||
| env->context(), | ||
| env->esm_string(), | ||
| v8::Boolean::New(env->isolate(), esmPackage), | ||
| v8::ReadOnly); | ||
|
|
||
| (void)resolved->DefineOwnProperty( | ||
| env->context(), | ||
| env->url_string(), | ||
| result.FromJust().ToObject(env), | ||
| v8::ReadOnly); | ||
|
|
||
| args.GetReturnValue().Set(resolved); | ||
| } | ||
|
|
||
| static MaybeLocal<Promise> ImportModuleDynamically( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // Flags: --experimental-modules | ||
| import '../common'; | ||
| import assert from 'assert'; | ||
| import { x } from '../fixtures/es-modules/node_lookups/module.mjs'; | ||
|
|
||
| assert.deepStrictEqual(x, 42); |
| 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'); |
| 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-invalid/module'; | ||
| import assert from 'assert'; | ||
|
|
||
| assert.strictEqual(m, 'cjs'); |
| 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'); |
| 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 @@ | ||
| {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from './cjs-nested/module.js'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "mode": "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,3 @@ | ||
| { | ||
| "mode": 1 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export default 'esm'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "mode": "esm" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export default 'esm submodule'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default as x } from 'cjs'; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simpler: