-
-
Notifications
You must be signed in to change notification settings - Fork 19
esm resolver spec and implementation refinements #12
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,10 +63,6 @@ ESM must have the `.mjs` extension. | |
|
||
You must provide a file extension when using the `import` keyword. | ||
|
||
### No importing directories | ||
|
||
There is no support for importing directories. | ||
|
||
### No NODE_PATH | ||
|
||
`NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks | ||
|
@@ -146,6 +142,52 @@ fs.readFileSync = () => Buffer.from('Hello, ESM'); | |
fs.readFileSync === readFileSync; | ||
``` | ||
|
||
## Resolution Algorithm | ||
|
||
### Features | ||
|
||
The resolver has the following properties: | ||
|
||
* FileURL-based resolution as is used by ES modules | ||
* Support for builtin module loading | ||
* Relative and absolute URL resolution | ||
* No default extensions | ||
* No folder mains | ||
* Bare specifier package resolution lookup through node_modules | ||
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. I'm not sure if we should take on the historical burden of Should we maybe split this into scoped resolution and an example for how the underlying data for scoped resolution might be calculated from a directory tree? 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. I'm confused, how is it hard to predict? That directory layout isn't something that can be trivially nor any time soon gotten away from. |
||
|
||
### Resolver Algorithm | ||
|
||
The algorithm to resolve an ES module specifier is provided through | ||
_ESM_RESOLVE_: | ||
|
||
**ESM_RESOLVE**(_specifier_, _parentURL_) | ||
> 1. Let _resolvedURL_ be _undefined_. | ||
> 1. If _specifier_ is a valid URL then, | ||
> 1. Set _resolvedURL_ to the result of parsing and reserializing | ||
> _specifier_ as a URL. | ||
> 1. Otherwise, if _specifier_ starts with _"/"_, _"./"_ or _"../"_ then, | ||
> 1. Set _resolvedURL_ to the URL resolution of _specifier_ relative to | ||
> _parentURL_. | ||
> 1. Otherwise, | ||
> 1. Note: _specifier_ is now a bare specifier. | ||
> 1. Set _resolvedURL_ the result of | ||
> **PACKAGE_RESOLVE**(_specifier_, _parentURL_). | ||
> 1. If the file at _resolvedURL_ does not exist then, | ||
> 1. Throw a _Module Not Found_ error. | ||
> 1. Return _resolvedURL_. | ||
giltayar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
**PACKAGE_RESOLVE**(_packageSpecifier_, _parentURL_) | ||
> 1. Assert: _packageSpecifier_ is a bare specifier. | ||
> 1. If _packageSpecifier_ is a Node.js builtin module then, | ||
> 1. Return the string _"node:"_ concatenated with _packageSpecifier_. | ||
> 1. While _parentURL_ contains a non-empty _pathname_, | ||
guybedford marked this conversation as resolved.
Show resolved
Hide resolved
|
||
> 1. Set _parentURL_ to the parent folder URL of _parentURL_. | ||
> 1. Let _packageURL_ be the URL resolution of the string concatenation of | ||
> _parentURL_, _"/node_modules/"_ and _"packageSpecifier"_. | ||
> 1. If the file at _packageURL_ exists then, | ||
> 1. Return _packageURL_. | ||
> 1. Throw a _Module Not Found_ error. | ||
|
||
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md | ||
[`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename | ||
[ESM Minimal Kernel]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -466,104 +466,37 @@ std::string ReadFile(uv_file file) { | |
return contents; | ||
} | ||
|
||
enum CheckFileOptions { | ||
LEAVE_OPEN_AFTER_CHECK, | ||
CLOSE_AFTER_CHECK | ||
enum DescriptorType { | ||
NONE, | ||
FILE, | ||
DIRECTORY | ||
}; | ||
|
||
Maybe<uv_file> CheckFile(const std::string& path, | ||
CheckFileOptions opt = CLOSE_AFTER_CHECK) { | ||
DescriptorType CheckDescriptor(const std::string& path) { | ||
uv_fs_t fs_req; | ||
if (path.empty()) { | ||
return Nothing<uv_file>(); | ||
} | ||
|
||
uv_file fd = uv_fs_open(nullptr, &fs_req, path.c_str(), O_RDONLY, 0, nullptr); | ||
uv_fs_req_cleanup(&fs_req); | ||
|
||
if (fd < 0) { | ||
return Nothing<uv_file>(); | ||
} | ||
|
||
uv_fs_fstat(nullptr, &fs_req, fd, nullptr); | ||
uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; | ||
uv_fs_req_cleanup(&fs_req); | ||
|
||
if (is_directory) { | ||
CHECK_EQ(0, uv_fs_close(nullptr, &fs_req, fd, nullptr)); | ||
uv_fs_req_cleanup(&fs_req); | ||
return Nothing<uv_file>(); | ||
} | ||
|
||
if (opt == CLOSE_AFTER_CHECK) { | ||
CHECK_EQ(0, uv_fs_close(nullptr, &fs_req, fd, nullptr)); | ||
int rc = uv_fs_stat(nullptr, &fs_req, path.c_str(), nullptr); | ||
if (rc == 0) { | ||
uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; | ||
uv_fs_req_cleanup(&fs_req); | ||
return is_directory ? DIRECTORY : FILE; | ||
} | ||
|
||
return Just(fd); | ||
} | ||
|
||
enum ResolveExtensionsOptions { | ||
TRY_EXACT_NAME, | ||
ONLY_VIA_EXTENSIONS | ||
}; | ||
|
||
inline bool ResolvesToFile(const URL& search) { | ||
std::string filePath = search.ToFilePath(); | ||
Maybe<uv_file> check = CheckFile(filePath); | ||
return !check.IsNothing(); | ||
} | ||
|
||
template <ResolveExtensionsOptions options> | ||
Maybe<URL> ResolveExtensions(const URL& search) { | ||
if (options == TRY_EXACT_NAME) { | ||
std::string filePath = search.ToFilePath(); | ||
Maybe<uv_file> check = CheckFile(filePath); | ||
if (!check.IsNothing()) { | ||
return Just(search); | ||
} | ||
} | ||
|
||
for (const char* extension : EXTENSIONS) { | ||
URL guess(search.path() + extension, &search); | ||
Maybe<uv_file> check = CheckFile(guess.ToFilePath()); | ||
if (!check.IsNothing()) { | ||
return Just(guess); | ||
} | ||
} | ||
|
||
return Nothing<URL>(); | ||
} | ||
|
||
inline Maybe<URL> ResolveIndex(const URL& search) { | ||
return ResolveExtensions<ONLY_VIA_EXTENSIONS>(URL("index", search)); | ||
uv_fs_req_cleanup(&fs_req); | ||
return NONE; | ||
} | ||
|
||
Maybe<URL> ResolveModule(Environment* env, | ||
const std::string& specifier, | ||
const URL& base) { | ||
Maybe<URL> PackageResolve(Environment* env, | ||
const std::string& specifier, | ||
const URL& base) { | ||
URL parent(".", base); | ||
URL dir(""); | ||
std::string last_path; | ||
do { | ||
dir = parent; | ||
Maybe<URL> check = | ||
Resolve(env, "./node_modules/" + specifier, dir); | ||
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>(); | ||
} | ||
return check; | ||
} else { | ||
// TODO(bmeck) PREVENT FALLTHROUGH | ||
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. Note this TODO is fixed :) |
||
} | ||
parent = URL("..", &dir); | ||
} while (parent.path() != dir.path()); | ||
URL pkg_url("./node_modules/" + specifier, &parent); | ||
DescriptorType check = CheckDescriptor(pkg_url.ToFilePath()); | ||
if (check == FILE) return Just(pkg_url); | ||
last_path = parent.path(); | ||
parent = URL("..", &parent); | ||
// cross-platform root check | ||
} while (parent.path() != last_path); | ||
return Nothing<URL>(); | ||
} | ||
|
||
|
@@ -572,26 +505,27 @@ Maybe<URL> ResolveModule(Environment* env, | |
Maybe<URL> Resolve(Environment* env, | ||
const std::string& specifier, | ||
const URL& base) { | ||
URL pure_url(specifier); | ||
if (!(pure_url.flags() & URL_FLAGS_FAILED)) { | ||
// just check existence, without altering | ||
Maybe<uv_file> check = CheckFile(pure_url.ToFilePath()); | ||
if (check.IsNothing()) { | ||
return Nothing<URL>(); | ||
// Order swapped from spec for minor perf gain. | ||
// Ok since relative URLs cannot parse as URLs. | ||
URL resolved; | ||
if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { | ||
resolved = URL(specifier, base); | ||
} else { | ||
URL pure_url(specifier); | ||
if (!(pure_url.flags() & URL_FLAGS_FAILED)) { | ||
resolved = pure_url; | ||
} else { | ||
return PackageResolve(env, specifier, base); | ||
} | ||
return Just(pure_url); | ||
} | ||
if (specifier.length() == 0) { | ||
DescriptorType check = CheckDescriptor(resolved.ToFilePath()); | ||
if (check != FILE) { | ||
std::string msg = "Cannot find module '" + resolved.ToFilePath() + | ||
"' imported from " + base.ToFilePath(); | ||
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); | ||
return Nothing<URL>(); | ||
} | ||
if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { | ||
URL resolved(specifier, base); | ||
if (ResolvesToFile(resolved)) | ||
return Just(resolved); | ||
return Nothing<URL>(); | ||
} else { | ||
return ResolveModule(env, specifier, base); | ||
} | ||
return Just(resolved); | ||
} | ||
|
||
void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) { | ||
|
@@ -613,10 +547,18 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) { | |
env, "second argument is not a URL string"); | ||
} | ||
|
||
TryCatch try_catch(env->isolate()); | ||
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()); | ||
if (try_catch.HasCaught()) { | ||
try_catch.ReThrow(); | ||
return; | ||
} else if (result.IsNothing() || | ||
(result.FromJust().flags() & URL_FLAGS_FAILED)) { | ||
std::string msg = "Cannot find module '" + specifier_std + | ||
"' imported from " + url.ToFilePath(); | ||
node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); | ||
try_catch.ReThrow(); | ||
return; | ||
} | ||
|
||
args.GetReturnValue().Set(result.FromJust().ToObject(env)); | ||
|
Uh oh!
There was an error while loading. Please reload this page.