Skip to content

Commit 32261fc

Browse files
avivkelleraduh95
authored andcommitted
module: support loading entrypoint as url
Co-Authored-By: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: #54933 Refs: #49975 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: LiviaMedeiros <livia@cirno.name> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent e99d4a4 commit 32261fc

File tree

8 files changed

+147
-8
lines changed

8 files changed

+147
-8
lines changed

doc/api/cli.md

+24
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,28 @@ when `Error.stack` is accessed. If you access `Error.stack` frequently
795795
in your application, take into account the performance implications
796796
of `--enable-source-maps`.
797797

798+
### `--entry-url`
799+
800+
<!-- YAML
801+
added:
802+
- REPLACEME
803+
-->
804+
805+
> Stability: 1 - Experimental
806+
807+
When present, Node.js will interpret the entry point as a URL, rather than a
808+
path.
809+
810+
Follows [ECMAScript module][] resolution rules.
811+
812+
Any query parameter or hash in the URL will be accessible via [`import.meta.url`][].
813+
814+
```bash
815+
node --entry-url 'file:///path/to/file.js?queryparams=work#and-hashes-too'
816+
node --entry-url --experimental-strip-types 'file.ts?query#hash'
817+
node --entry-url 'data:text/javascript,console.log("Hello")'
818+
```
819+
798820
### `--env-file=config`
799821

800822
> Stability: 1.1 - Active development
@@ -3024,6 +3046,7 @@ one is included in the list below.
30243046
* `--enable-fips`
30253047
* `--enable-network-family-autoselection`
30263048
* `--enable-source-maps`
3049+
* `--entry-url`
30273050
* `--experimental-abortcontroller`
30283051
* `--experimental-async-context-frame`
30293052
* `--experimental-default-type`
@@ -3623,6 +3646,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
36233646
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
36243647
[`dns.setDefaultResultOrder()`]: dns.md#dnssetdefaultresultorderorder
36253648
[`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options
3649+
[`import.meta.url`]: esm.md#importmetaurl
36263650
[`import` specifier]: esm.md#import-specifiers
36273651
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: net.md#netgetdefaultautoselectfamilyattempttimeout
36283652
[`node:sqlite`]: sqlite.md

doc/node.1

+3
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ Requires Node.js to be built with
160160
.It Fl -enable-source-maps
161161
Enable Source Map V3 support for stack traces.
162162
.
163+
.It Fl -entry-url
164+
Interpret the entry point as a URL.
165+
.
163166
.It Fl -experimental-default-type Ns = Ns Ar type
164167
Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified;
165168
.js or extensionless files with no sibling or parent package.json;

lib/internal/main/run_main_module.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@ const {
99
markBootstrapComplete,
1010
} = require('internal/process/pre_execution');
1111
const { getOptionValue } = require('internal/options');
12+
const { emitExperimentalWarning } = require('internal/util');
1213

13-
const mainEntry = prepareMainThreadExecution(true);
14+
const isEntryURL = getOptionValue('--entry-url');
15+
const mainEntry = prepareMainThreadExecution(!isEntryURL);
1416

1517
markBootstrapComplete();
1618

1719
// Necessary to reset RegExp statics before user code runs.
1820
RegExpPrototypeExec(/^/, '');
1921

22+
if (isEntryURL) {
23+
emitExperimentalWarning('--entry-url');
24+
}
25+
2026
if (getOptionValue('--experimental-default-type') === 'module') {
2127
require('internal/modules/run_main').executeUserEntryPoint(mainEntry);
2228
} else {

lib/internal/modules/run_main.js

+10-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const {
88
const { getNearestParentPackageJSONType } = internalBinding('modules');
99
const { getOptionValue } = require('internal/options');
1010
const path = require('path');
11-
const { pathToFileURL } = require('internal/url');
11+
const { pathToFileURL, URL } = require('internal/url');
1212
const { kEmptyObject, getCWDURL } = require('internal/util');
1313
const {
1414
hasUncaughtExceptionCaptureCallback,
@@ -154,9 +154,14 @@ function runEntryPointWithESMLoader(callback) {
154154
* @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js`
155155
*/
156156
function executeUserEntryPoint(main = process.argv[1]) {
157-
const resolvedMain = resolveMainPath(main);
158-
const useESMLoader = shouldUseESMLoader(resolvedMain);
159-
let mainURL;
157+
let useESMLoader;
158+
let resolvedMain;
159+
if (getOptionValue('--entry-url')) {
160+
useESMLoader = true;
161+
} else {
162+
resolvedMain = resolveMainPath(main);
163+
useESMLoader = shouldUseESMLoader(resolvedMain);
164+
}
160165
// Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first
161166
// try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM.
162167
if (!useESMLoader) {
@@ -165,9 +170,7 @@ function executeUserEntryPoint(main = process.argv[1]) {
165170
wrapModuleLoad(main, null, true);
166171
} else {
167172
const mainPath = resolvedMain || main;
168-
if (mainURL === undefined) {
169-
mainURL = pathToFileURL(mainPath).href;
170-
}
173+
const mainURL = getOptionValue('--entry-url') ? new URL(mainPath, getCWDURL()) : pathToFileURL(mainPath);
171174

172175
runEntryPointWithESMLoader((cascadedLoader) => {
173176
// Note that if the graph contains unsettled TLA, this may never resolve

src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
406406
"Source Map V3 support for stack traces",
407407
&EnvironmentOptions::enable_source_maps,
408408
kAllowedInEnvvar);
409+
AddOption("--entry-url",
410+
"Treat the entrypoint as a URL",
411+
&EnvironmentOptions::entry_is_url,
412+
kAllowedInEnvvar);
409413
AddOption("--experimental-abortcontroller", "", NoOp{}, kAllowedInEnvvar);
410414
AddOption("--experimental-eventsource",
411415
"experimental EventSource API",

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ class EnvironmentOptions : public Options {
133133
bool experimental_import_meta_resolve = false;
134134
std::string input_type; // Value of --input-type
135135
std::string type; // Value of --experimental-default-type
136+
bool entry_is_url = false;
136137
bool experimental_permission = false;
137138
std::vector<std::string> allow_fs_read;
138139
std::vector<std::string> allow_fs_write;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { spawnPromisified } from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import assert from 'node:assert';
4+
import { execPath } from 'node:process';
5+
import { describe, it } from 'node:test';
6+
7+
// Helper function to assert the spawned process
8+
async function assertSpawnedProcess(args, options = {}, expected = {}) {
9+
const { code, signal, stderr, stdout } = await spawnPromisified(execPath, args, options);
10+
11+
if (expected.stderr) {
12+
assert.match(stderr, expected.stderr);
13+
}
14+
15+
if (expected.stdout) {
16+
assert.match(stdout, expected.stdout);
17+
}
18+
19+
assert.strictEqual(code, expected.code ?? 0);
20+
assert.strictEqual(signal, expected.signal ?? null);
21+
}
22+
23+
// Common expectation for experimental feature warning in stderr
24+
const experimentalFeatureWarning = { stderr: /--entry-url is an experimental feature/ };
25+
26+
describe('--entry-url', { concurrency: true }, () => {
27+
it('should reject loading a path that contains %', async () => {
28+
await assertSpawnedProcess(
29+
['--entry-url', './test-esm-double-encoding-native%20.mjs'],
30+
{ cwd: fixtures.fileURL('es-modules') },
31+
{
32+
code: 1,
33+
stderr: /ERR_MODULE_NOT_FOUND/,
34+
}
35+
);
36+
});
37+
38+
it('should support loading properly encoded Unix path', async () => {
39+
await assertSpawnedProcess(
40+
['--entry-url', fixtures.fileURL('es-modules/test-esm-double-encoding-native%20.mjs').pathname],
41+
{},
42+
experimentalFeatureWarning
43+
);
44+
});
45+
46+
it('should support loading absolute URLs', async () => {
47+
await assertSpawnedProcess(
48+
['--entry-url', fixtures.fileURL('printA.js')],
49+
{},
50+
{
51+
...experimentalFeatureWarning,
52+
stdout: /^A\r?\n$/,
53+
}
54+
);
55+
});
56+
57+
it('should support loading relative URLs', async () => {
58+
await assertSpawnedProcess(
59+
['--entry-url', 'es-modules/print-entrypoint.mjs?key=value#hash'],
60+
{ cwd: fixtures.fileURL('./') },
61+
{
62+
...experimentalFeatureWarning,
63+
stdout: /print-entrypoint\.mjs\?key=value#hash\r?\n$/,
64+
}
65+
);
66+
});
67+
68+
it('should support loading `data:` URLs', async () => {
69+
await assertSpawnedProcess(
70+
['--entry-url', 'data:text/javascript,console.log(import.meta.url)'],
71+
{},
72+
{
73+
...experimentalFeatureWarning,
74+
stdout: /^data:text\/javascript,console\.log\(import\.meta\.url\)\r?\n$/,
75+
}
76+
);
77+
});
78+
79+
it('should support loading TypeScript URLs', async () => {
80+
const typescriptUrls = [
81+
'typescript/cts/test-require-ts-file.cts',
82+
'typescript/mts/test-import-ts-file.mts',
83+
];
84+
85+
for (const url of typescriptUrls) {
86+
await assertSpawnedProcess(
87+
['--entry-url', '--experimental-strip-types', fixtures.fileURL(url)],
88+
{},
89+
{
90+
...experimentalFeatureWarning,
91+
stdout: /Hello, TypeScript!/,
92+
}
93+
);
94+
}
95+
});
96+
97+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log(import.meta.url);

0 commit comments

Comments
 (0)