Skip to content
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: add import.meta.dirname and import.meta.filename #48740

Merged
merged 12 commits into from
Oct 31, 2023
3 changes: 3 additions & 0 deletions benchmark/fixtures/esm-dir-file.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import assert from 'assert';
assert.ok(import.meta.dirname);
assert.ok(import.meta.filename);
5 changes: 5 additions & 0 deletions benchmark/fixtures/load-esm-dir-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(async function () {
for (let i = 0; i < 1000; i += 1) {
await import(`./esm-dir-file.mjs?i=${i}`);
}
}());
1 change: 1 addition & 0 deletions benchmark/misc/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const bench = common.createBenchmark(main, {
script: [
'benchmark/fixtures/require-builtins',
'test/fixtures/semicolon',
'benchmark/fixtures/load-esm-dir-file',
],
mode: ['process', 'worker'],
count: [30],
Expand Down
36 changes: 35 additions & 1 deletion doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,36 @@ modules it can be used to load ES modules.
The `import.meta` meta property is an `Object` that contains the following
properties.

### `import.meta.dirname`

<!-- YAML
added: REPLACEME
-->

jsumners marked this conversation as resolved.
Show resolved Hide resolved
> Stability: 1.2 - Release candidate
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved

* {string} The directory name of the current module. This is the same as the
[`path.dirname()`][] of the [`import.meta.filename`][].

> **Caveat** only local modules support this property. Modules not using the
> `file:` protocol will not provide it.
jsumners marked this conversation as resolved.
Show resolved Hide resolved

### `import.meta.filename`
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved

<!-- YAML
added: REPLACEME
-->

jsumners marked this conversation as resolved.
Show resolved Hide resolved
> Stability: 1.2 - Release candidate
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved

* {string} The full absolute path and filename of the current module, with
* symlinks resolved.
* This is the same as the [`url.fileURLToPath()`][] of the
* [`import.meta.url`][].

> **Caveat** only local modules support this property. Modules not using the
> `file:` protocol will not provide it.

### `import.meta.url`

* {string} The absolute `file:` URL of the module.
Expand Down Expand Up @@ -503,7 +533,7 @@ If needed, a `require` function can be constructed within an ES module using
These CommonJS variables are not available in ES modules.

`__filename` and `__dirname` use cases can be replicated via
[`import.meta.url`][].
[`import.meta.filename`][] and [`import.meta.dirname`][].

#### No Addon Loading

Expand Down Expand Up @@ -1065,13 +1095,17 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
[`import()`]: #import-expressions
[`import.meta.dirname`]: #importmetadirname
[`import.meta.filename`]: #importmetafilename
[`import.meta.resolve`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve
[`import.meta.url`]: #importmetaurl
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
[`module.createRequire()`]: module.md#modulecreaterequirefilename
[`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
[`path.dirname()`]: path.md#pathdirnamepath
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
[`url.fileURLToPath()`]: url.md#urlfileurltopathurl
[cjs-module-lexer]: https://github.com/nodejs/cjs-module-lexer/tree/1.2.2
[commonjs-extension-resolution-loader]: https://github.com/nodejs/loaders-test/tree/main/commonjs-extension-resolution-loader
[custom https loader]: module.md#import-from-https
Expand Down
28 changes: 27 additions & 1 deletion lib/internal/modules/esm/initialize_import_meta.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use strict';

const { StringPrototypeStartsWith } = primordials;
const { getOptionValue } = require('internal/options');
const { fileURLToPath } = require('internal/url');
const { dirname } = require('path');
const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve');

/**
Expand Down Expand Up @@ -45,12 +48,16 @@ function createImportMetaResolve(defaultParentURL, loader, allowParentURL) {
* @param {object} meta
* @param {{url: string}} context
* @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader
* @returns {{url: string, resolve?: Function}}
* @returns {{dirname?: string, filename?: string, url: string, resolve?: Function}}
*/
function initializeImportMeta(meta, context, loader) {
const { url } = context;

// Alphabetical
const moduleMeta = getModuleMetaPathProperties(url);
meta.dirname = moduleMeta.dirname;
meta.filename = moduleMeta.filename;
aduh95 marked this conversation as resolved.
Show resolved Hide resolved

if (!loader || loader.allowImportMetaResolve) {
meta.resolve = createImportMetaResolve(url, loader, experimentalImportMetaResolve);
}
Expand All @@ -60,6 +67,25 @@ function initializeImportMeta(meta, context, loader) {
return meta;
}

/**
* Produce path-based `dirname` and `filename` properties for modules loaded from the filesystem.
* @param {string} url
* @returns {{dirname?: string, filename?: string}}
*/
function getModuleMetaPathProperties(url) {
if (StringPrototypeStartsWith(url, 'file://') === false) {
// These only make sense for locally loaded modules,
// i.e. network modules are not supported.
return { dirname: undefined, filename: undefined };
}

const filePath = fileURLToPath(url);
return {
dirname: dirname(filePath),
filename: filePath,
};
}

module.exports = {
initializeImportMeta,
};
12 changes: 11 additions & 1 deletion test/es-module/test-esm-import-meta.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from 'assert';

assert.strictEqual(Object.getPrototypeOf(import.meta), null);

const keys = ['resolve', 'url'];
const keys = ['dirname', 'filename', 'resolve', 'url'];
assert.deepStrictEqual(Reflect.ownKeys(import.meta), keys);

const descriptors = Object.getOwnPropertyDescriptors(import.meta);
Expand All @@ -18,3 +18,13 @@ for (const descriptor of Object.values(descriptors)) {

const urlReg = /^file:\/\/\/.*\/test\/es-module\/test-esm-import-meta\.mjs$/;
assert(import.meta.url.match(urlReg));

const dirReg = /^\/.*\/test\/es-module$/;
assert.match(import.meta.dirname, dirReg);

const fileReg = /^\/.*\/test\/es-module\/test-esm-import-meta\.mjs$/;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, we can make this regular expression more portable and versatile across different platforms and URL schemes, something like this:

Suggested change
const fileReg = /^\/.*\/test\/es-module\/test-esm-import-meta\.mjs$/;
const fileReg = /^(\/|\\|file:\/\/).*\/test\/es-module\/test-esm-import-meta\.mjs$/;

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the issue with the current, simpler, expression?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current is very specific to Unix-like file paths. It assumes that the file path starts with a single forward slash (/) followed by directories, which might not hold true for Windows paths or file URLs. I assume that code needs to run on multiple platforms, so.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By definition this can’t be a file: URL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately, the string being evaluated here comes from

function getPathFromURLWin32(url) {

I'd like to see some unit tests for that function to understand what it outputs, but I don't get any results for such tests:

node on  esm-dir-file [$] 
❯ rg getPathFromURLWin32
lib/internal/url.js
1342:function getPathFromURLWin32(url) {
1402:  return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this PR passes CI, then it will be proven to work in Windows, and that’s good enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the example provided in #48740 (comment), I have updated the regular expressions to match Unix and Windows paths. Please take a look:

603f050 (#48740)

assert.match(import.meta.filename, fileReg);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need any separate tests for Windows paths? What about file: URLs that are loading network paths, like file://server/folder/file.js?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need any separate tests for Windows paths?

Is this done for the CJS implementation? What would we actually be testing? That path.dirname works with Windows paths?

What about file: URLs that are loading network paths, like file://server/folder/file.js?

That looks like a mounted file system to me.


// Verify that `data:` imports do not behave like `file:` imports.
import dataDirname from 'data:text/javascript,export default import.meta.dirname';
assert.strictEqual(dataDirname, undefined);
aduh95 marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions test/js-native-api/test_cannot_run_js/entry_point.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include <node_api.h>

EXTERN_C_START
napi_value Init(napi_env env, napi_value exports);
EXTERN_C_END

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)