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

module: support loading entrypoint as url #54933

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,28 @@ when `Error.stack` is accessed. If you access `Error.stack` frequently
in your application, take into account the performance implications
of `--enable-source-maps`.

### `--entry-url`

<!-- YAML
added:
- REPLACEME
-->

> Stability: 1 - Experimental

When present, Node.js will interpret the entry point as a URL, rather than a
path.
RedYetiDev marked this conversation as resolved.
Show resolved Hide resolved

Follows [ECMAScript module][] resolution rules.

Any query parameter or hash in the URL will be accessible via [`import.meta.url`][].

```bash
node --entry-url 'file:///path/to/file.js?queryparams=work#and-hashes-too'
node --entry-url --experimental-strip-types 'file.ts?query#hash'
node --entry-url 'data:text/javascript,console.log("Hello")'
```

### `--env-file=config`

> Stability: 1.1 - Active development
Expand Down Expand Up @@ -2981,6 +3003,7 @@ one is included in the list below.
* `--enable-fips`
* `--enable-network-family-autoselection`
* `--enable-source-maps`
* `--entry-url`
* `--experimental-abortcontroller`
* `--experimental-async-context-frame`
* `--experimental-default-type`
Expand Down Expand Up @@ -3571,6 +3594,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
[`dns.setDefaultResultOrder()`]: dns.md#dnssetdefaultresultorderorder
[`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options
[`import.meta.url`]: esm.md#importmetaurl
[`import` specifier]: esm.md#import-specifiers
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: net.md#netgetdefaultautoselectfamilyattempttimeout
[`node:sqlite`]: sqlite.md
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ Requires Node.js to be built with
.It Fl -enable-source-maps
Enable Source Map V3 support for stack traces.
.
.It Fl -entry-url
Interpret the entry point as a URL.
.
.It Fl -experimental-default-type Ns = Ns Ar type
Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified;
.js or extensionless files with no sibling or parent package.json;
Expand Down
8 changes: 7 additions & 1 deletion lib/internal/main/run_main_module.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ const {
markBootstrapComplete,
} = require('internal/process/pre_execution');
const { getOptionValue } = require('internal/options');
const { emitExperimentalWarning } = require('internal/util');

const mainEntry = prepareMainThreadExecution(true);
const isEntryURL = getOptionValue('--entry-url');
const mainEntry = prepareMainThreadExecution(!isEntryURL);

markBootstrapComplete();

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

if (isEntryURL) {
emitExperimentalWarning('--entry-url');
}

if (getOptionValue('--experimental-default-type') === 'module') {
require('internal/modules/run_main').executeUserEntryPoint(mainEntry);
} else {
Expand Down
17 changes: 10 additions & 7 deletions lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {
const { getNearestParentPackageJSONType } = internalBinding('modules');
const { getOptionValue } = require('internal/options');
const path = require('path');
const { pathToFileURL } = require('internal/url');
const { pathToFileURL, URL } = require('internal/url');
const { kEmptyObject, getCWDURL } = require('internal/util');
const {
hasUncaughtExceptionCaptureCallback,
Expand Down Expand Up @@ -155,9 +155,14 @@ function runEntryPointWithESMLoader(callback) {
* @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js`
*/
function executeUserEntryPoint(main = process.argv[1]) {
const resolvedMain = resolveMainPath(main);
const useESMLoader = shouldUseESMLoader(resolvedMain);
let mainURL;
let useESMLoader;
let resolvedMain;
if (getOptionValue('--entry-url')) {
useESMLoader = true;
} else {
resolvedMain = resolveMainPath(main);
useESMLoader = shouldUseESMLoader(resolvedMain);
}
// Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first
// try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM.
if (!useESMLoader) {
Expand All @@ -166,9 +171,7 @@ function executeUserEntryPoint(main = process.argv[1]) {
wrapModuleLoad(main, null, true);
} else {
const mainPath = resolvedMain || main;
if (mainURL === undefined) {
mainURL = pathToFileURL(mainPath).href;
}
const mainURL = getOptionValue('--entry-url') ? new URL(mainPath, getCWDURL()) : pathToFileURL(mainPath);
LiviaMedeiros marked this conversation as resolved.
Show resolved Hide resolved

runEntryPointWithESMLoader((cascadedLoader) => {
// Note that if the graph contains unsettled TLA, this may never resolve
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"Source Map V3 support for stack traces",
&EnvironmentOptions::enable_source_maps,
kAllowedInEnvvar);
AddOption("--entry-url",
"Treat the entrypoint as a URL",
&EnvironmentOptions::entry_is_url,
kAllowedInEnvvar);
AddOption("--experimental-abortcontroller", "", NoOp{}, kAllowedInEnvvar);
AddOption("--experimental-eventsource",
"experimental EventSource API",
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class EnvironmentOptions : public Options {
bool experimental_import_meta_resolve = false;
std::string input_type; // Value of --input-type
std::string type; // Value of --experimental-default-type
bool entry_is_url = false;
bool experimental_permission = false;
std::vector<std::string> allow_fs_read;
std::vector<std::string> allow_fs_write;
Expand Down
97 changes: 97 additions & 0 deletions test/es-module/test-esm-loader-entry-url.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';

// Helper function to assert the spawned process
async function assertSpawnedProcess(args, options = {}, expected = {}) {
const { code, signal, stderr, stdout } = await spawnPromisified(execPath, args, options);

if (expected.stderr) {
assert.match(stderr, expected.stderr);
}

if (expected.stdout) {
assert.match(stdout, expected.stdout);
}

assert.strictEqual(code, expected.code ?? 0);
assert.strictEqual(signal, expected.signal ?? null);
}

// Common expectation for experimental feature warning in stderr
const experimentalFeatureWarning = { stderr: /--entry-url is an experimental feature/ };

describe('--entry-url', { concurrency: true }, () => {
it('should reject loading a path that contains %', async () => {
await assertSpawnedProcess(
['--entry-url', './test-esm-double-encoding-native%20.mjs'],
{ cwd: fixtures.fileURL('es-modules') },
{
code: 1,
stderr: /ERR_MODULE_NOT_FOUND/,
}
);
});

it('should support loading properly encoded Unix path', async () => {
await assertSpawnedProcess(
['--entry-url', fixtures.fileURL('es-modules/test-esm-double-encoding-native%20.mjs').pathname],
{},
experimentalFeatureWarning
);
});

it('should support loading absolute URLs', async () => {
await assertSpawnedProcess(
['--entry-url', fixtures.fileURL('printA.js')],
{},
{
...experimentalFeatureWarning,
stdout: /^A\r?\n$/,
}
);
});

it('should support loading relative URLs', async () => {
await assertSpawnedProcess(
['--entry-url', 'es-modules/print-entrypoint.mjs?key=value#hash'],
{ cwd: fixtures.fileURL('./') },
{
...experimentalFeatureWarning,
stdout: /print-entrypoint\.mjs\?key=value#hash\r?\n$/,
}
);
});

it('should support loading `data:` URLs', async () => {
await assertSpawnedProcess(
['--entry-url', 'data:text/javascript,console.log(import.meta.url)'],
{},
{
...experimentalFeatureWarning,
stdout: /^data:text\/javascript,console\.log\(import\.meta\.url\)\r?\n$/,
}
);
});

it('should support loading TypeScript URLs', async () => {
const typescriptUrls = [
'typescript/cts/test-require-ts-file.cts',
'typescript/mts/test-import-ts-file.mts',
];

for (const url of typescriptUrls) {
await assertSpawnedProcess(
['--entry-url', '--experimental-strip-types', fixtures.fileURL(url)],
{},
{
...experimentalFeatureWarning,
stdout: /Hello, TypeScript!/,
}
);
}
});

});
1 change: 1 addition & 0 deletions test/fixtures/es-modules/print-entrypoint.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(import.meta.url);
Loading