diff --git a/.eslintignore b/.eslintignore index 153ac6e24f731e..268232d911bbb5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ test/fixtures test/message/esm_display_syntax_error.mjs tools/icu tools/lint-md/lint-md.mjs +tools/github_reporter benchmark/tmp benchmark/fixtures doc/**/*.js diff --git a/.eslintrc.js b/.eslintrc.js index 5a63c79371c984..47b218c55e9171 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -111,6 +111,14 @@ module.exports = { }, ] }, }, + { + files: [ + 'lib/internal/test_runner/**/*.js', + ], + rules: { + 'node-core/set-proto-to-null-in-object': 'error', + }, + }, ], rules: { // ESLint built-in rules diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fe46bf4deadeb7..7e72cfbd77e972 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,9 @@ -Enable experimental `import.meta.resolve()` support. +Enable experimental `import.meta.resolve()` parent URL support, which allows +passing a second `parentURL` argument for contextual resolution. + +Previously gated the entire `import.meta.resolve` feature. ### `--experimental-loader=module` @@ -971,6 +980,41 @@ surface on other platforms, but the performance impact may be severe. This flag is inherited from V8 and is subject to change upstream. It may disappear in a non-semver-major release. +### `--env-file=config` + +> Stability: 1.1 - Active development + + + +Loads environment variables from a file relative to the current directory, +making them available to applications on `process.env`. The [environment +variables which configure Node.js][environment_variables], such as `NODE_OPTIONS`, +are parsed and applied. If the same variable is defined in the environment and +in the file, the value from the environment takes precedence. + +The format of the file should be one line per key-value pair of environment +variable name and value separated by `=`: + +```text +PORT=3000 +``` + +Any text after a `#` is treated as a comment: + +```text +# This is a comment +PORT=3000 # This is also a comment +``` + +Values can start and end with the following quotes: `\`, `"` or `'`. +They are omitted from the values. + +```text +USERNAME="nodejs" # will result in `nodejs` as the value. +``` + ### `--max-http-header-size=size` -> Stability: 1 - Experimental - -This feature is only available with the `--experimental-import-meta-resolve` -command flag enabled. +> Stability: 1.2 - Release candidate -* `specifier` {string} The module specifier to resolve relative to `parent`. -* `parent` {string|URL} The absolute parent module URL to resolve from. If none - is specified, the value of `import.meta.url` is used as the default. -* Returns: {string} +* `specifier` {string} The module specifier to resolve relative to the + current module. +* Returns: {string} The absolute (`file:`) URL string for the resolved module. -Provides a module-relative resolution function scoped to each module, returning -the URL string. In alignment with browser behavior, this now returns -synchronously. - -> **Caveat** This can result in synchronous file-system operations, which -> can impact performance similarly to `require.resolve`. +[`import.meta.resolve`][] is a module-relative resolution function scoped to +each module, returning the URL string. ```js const dependencyAsset = import.meta.resolve('component-lib/asset.css'); +// file:///app/node_modules/component-lib/asset.css ``` -`import.meta.resolve` also accepts a second argument which is the parent module -from which to resolve: +All features of the Node.js module resolution are supported. Dependency +resolutions are subject to the permitted exports resolutions within the package. ```js import.meta.resolve('./dep', import.meta.url); +// file:///app/dep ``` +> **Caveat** This can result in synchronous file-system operations, which +> can impact performance similarly to `require.resolve`. + +Previously, Node.js implemented an asynchronous resolver which also permitted +a second contextual argument. The implementation has since been updated to be +synchronous, with the second contextual `parent` argument still accessible +behind the `--experimental-import-meta-resolve` flag: + +* `parent` {string|URL} An optional absolute parent module URL to resolve from. + ## Interoperability with CommonJS ### `import` statements @@ -501,8 +510,8 @@ They can instead be loaded with [`module.createRequire()`][] or Relative resolution can be handled via `new URL('./local', import.meta.url)`. -For a complete `require.resolve` replacement, there is a flagged experimental -[`import.meta.resolve`][] API. +For a complete `require.resolve` replacement, there is the +[import.meta.resolve][] API. Alternatively `module.createRequire()` can be used. @@ -685,6 +694,9 @@ of Node.js applications. + +> The loaders API is being redesigned. This hook may disappear or its +> signature may change. Do not rely on the API described below. + +* `data` {any} The data from `register(loader, import.meta.url, { data })`. +* Returns: {any} The data to be returned to the caller of `register`. + +The `initialize` hook provides a way to define a custom function that runs +in the loader's thread when the loader is initialized. Initialization happens +when the loader is registered via [`register`][] or registered via the +`--experimental-loader` command line option. + +This hook can send and receive data from a [`register`][] invocation, including +ports and other transferrable objects. The return value of `initialize` must be +either: + +* `undefined`, +* something that can be posted as a message between threads (e.g. the input to + [`port.postMessage`][]), +* a `Promise` resolving to one of the aforementioned values. + +Loader code: + +```js +// In the below example this file is referenced as +// '/path-to-my-loader.js' + +export async function initialize({ number, port }) { + port.postMessage(`increment: ${number + 1}`); + return 'ok'; +} +``` + +Caller code: + +```js +import assert from 'node:assert'; +import { register } from 'node:module'; +import { MessageChannel } from 'node:worker_threads'; + +// This example showcases how a message channel can be used to +// communicate between the main (application) thread and the loader +// running on the loaders thread, by sending `port2` to the loader. +const { port1, port2 } = new MessageChannel(); + +port1.on('message', (msg) => { + assert.strictEqual(msg, 'increment: 2'); +}); + +const result = register('/path-to-my-loader.js', { + parentURL: import.meta.url, + data: { number: 1, port: port2 }, + transferList: [port2], +}); + +assert.strictEqual(result, 'ok'); +``` + #### `resolve(specifier, context, nextResolve)` -> The loaders API is being redesigned. This hook may disappear or its -> signature may change. Do not rely on the API described below. +> This hook will be removed in a future version. Use [`initialize`][] instead. +> When a loader has an `initialize` export, `globalPreload` will be ignored. > In a previous version of this API, this hook was named > `getGlobalPreloadCode`. @@ -1225,6 +1326,17 @@ console.log('some module!'); If you run `node --experimental-loader ./import-map-loader.js main.js` the output will be `some module!`. +### Register loaders programmatically + + + +In addition to using the `--experimental-loader` option in the CLI, +loaders can also be registered programmatically. You can find +detailed information about this process in the documentation page +for [`module.register()`][]. + ## Resolution and loading algorithm ### Features @@ -1595,20 +1707,25 @@ 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.resolve`]: #importmetaresolvespecifier-parent +[`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 +[`initialize`]: #initialize [`module.createRequire()`]: module.md#modulecreaterequirefilename +[`module.register()`]: module.md#moduleregister [`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports [`package.json`]: packages.md#nodejs-packagejson-field-definitions +[`port.postMessage`]: worker_threads.md#portpostmessagevalue-transferlist [`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref [`port.unref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portunref [`process.dlopen`]: process.md#processdlopenmodule-filename-flags +[`register`]: module.md#moduleregister [`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String [`util.TextDecoder`]: util.md#class-utiltextdecoder [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]: #https-loader +[import.meta.resolve]: #importmetaresolvespecifier [load hook]: #loadurl-context-nextload [percent-encoded]: url.md#percent-encoding-in-urls [special scheme]: https://url.spec.whatwg.org/#special-scheme diff --git a/doc/api/events.md b/doc/api/events.md index dc28fe83c99a21..d1043bc1207d47 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -1799,7 +1799,7 @@ const emitter = new EventEmitter(); setMaxListeners(5, target, emitter); ``` -## `events.addAbortListener(signal, resource)` +## `events.addAbortListener(signal, listener)` -* `prefix` {string} +* `prefix` {string|Buffer|URL} * `options` {string|Object} * `encoding` {string} **Default:** `'utf8'` * Returns: {Promise} Fulfills with a string containing the file system path @@ -3244,6 +3247,9 @@ See the POSIX mkdir(2) documentation for more details. -* `prefix` {string} +* `prefix` {string|Buffer|URL} * `options` {string|Object} * `encoding` {string} **Default:** `'utf8'` * `callback` {Function} @@ -5550,6 +5556,9 @@ See the POSIX mkdir(2) documentation for more details. -* `prefix` {string} +* `prefix` {string|Buffer|URL} * `options` {string|Object} * `encoding` {string} **Default:** `'utf8'` * Returns: {string} diff --git a/doc/api/http.md b/doc/api/http.md index a0437f329bb148..95708332139aaf 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -187,7 +187,14 @@ of these values set to their respective defaults. To configure any of them, a custom [`http.Agent`][] instance must be created. -```js +```mjs +import { Agent, request } from 'node:http'; +const keepAliveAgent = new Agent({ keepAlive: true }); +options.agent = keepAliveAgent; +request(options, onResponseCallback); +``` + +```cjs const http = require('node:http'); const keepAliveAgent = new http.Agent({ keepAlive: true }); options.agent = keepAliveAgent; @@ -474,7 +481,62 @@ type other than {net.Socket}. A client and server pair demonstrating how to listen for the `'connect'` event: -```js +```mjs +import { createServer, request } from 'node:http'; +import { connect } from 'node:net'; +import { URL } from 'node:url'; + +// Create an HTTP tunneling proxy +const proxy = createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('okay'); +}); +proxy.on('connect', (req, clientSocket, head) => { + // Connect to an origin server + const { port, hostname } = new URL(`http://${req.url}`); + const serverSocket = connect(port || 80, hostname, () => { + clientSocket.write('HTTP/1.1 200 Connection Established\r\n' + + 'Proxy-agent: Node.js-Proxy\r\n' + + '\r\n'); + serverSocket.write(head); + serverSocket.pipe(clientSocket); + clientSocket.pipe(serverSocket); + }); +}); + +// Now that proxy is running +proxy.listen(1337, '127.0.0.1', () => { + + // Make a request to a tunneling proxy + const options = { + port: 1337, + host: '127.0.0.1', + method: 'CONNECT', + path: 'www.google.com:80', + }; + + const req = request(options); + req.end(); + + req.on('connect', (res, socket, head) => { + console.log('got connected!'); + + // Make a request over an HTTP tunnel + socket.write('GET / HTTP/1.1\r\n' + + 'Host: www.google.com:80\r\n' + + 'Connection: close\r\n' + + '\r\n'); + socket.on('data', (chunk) => { + console.log(chunk.toString()); + }); + socket.on('end', () => { + proxy.close(); + }); + }); +}); +``` + +```cjs const http = require('node:http'); const net = require('node:net'); const { URL } = require('node:url'); @@ -570,7 +632,25 @@ Upgrade). The listeners of this event will receive an object containing the HTTP version, status code, status message, key-value headers object, and array with the raw header names followed by their respective values. -```js +```mjs +import { request } from 'node:http'; + +const options = { + host: '127.0.0.1', + port: 8080, + path: '/length_request', +}; + +// Make a request +const req = request(options); +req.end(); + +req.on('information', (info) => { + console.log(`Got information prior to main response: ${info.statusCode}`); +}); +``` + +```cjs const http = require('node:http'); const options = { @@ -648,7 +728,49 @@ type other than {net.Socket}. A client server pair demonstrating how to listen for the `'upgrade'` event. -```js +```mjs +import http from 'node:http'; +import process from 'node:process'; + +// Create an HTTP server +const server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('okay'); +}); +server.on('upgrade', (req, socket, head) => { + socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' + + 'Upgrade: WebSocket\r\n' + + 'Connection: Upgrade\r\n' + + '\r\n'); + + socket.pipe(socket); // echo back +}); + +// Now that server is running +server.listen(1337, '127.0.0.1', () => { + + // make a request + const options = { + port: 1337, + host: '127.0.0.1', + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + }, + }; + + const req = http.request(options); + req.end(); + + req.on('upgrade', (res, socket, upgradeHead) => { + console.log('got upgraded!'); + socket.end(); + process.exit(0); + }); +}); +``` + +```cjs const http = require('node:http'); // Create an HTTP server @@ -1019,7 +1141,28 @@ When sending request through a keep-alive enabled agent, the underlying socket might be reused. But if server closes connection at unfortunate time, client may run into a 'ECONNRESET' error. -```js +```mjs +import http from 'node:http'; + +// Server has a 5 seconds keep-alive timeout by default +http + .createServer((req, res) => { + res.write('hello\n'); + res.end(); + }) + .listen(3000); + +setInterval(() => { + // Adapting a keep-alive agent + http.get('http://localhost:3000', { agent }, (res) => { + res.on('data', (data) => { + // Do nothing + }); + }); +}, 5000); // Sending request on 5s interval so it's easy to hit idle timeout +``` + +```cjs const http = require('node:http'); // Server has a 5 seconds keep-alive timeout by default @@ -1043,7 +1186,27 @@ setInterval(() => { By marking a request whether it reused socket or not, we can do automatic error retry base on it. -```js +```mjs +import http from 'node:http'; +const agent = new http.Agent({ keepAlive: true }); + +function retriableRequest() { + const req = http + .get('http://localhost:3000', { agent }, (res) => { + // ... + }) + .on('error', (err) => { + // Check if retry is needed + if (req.reusedSocket && err.code === 'ECONNRESET') { + retriableRequest(); + } + }); +} + +retriableRequest(); +``` + +```cjs const http = require('node:http'); const agent = new http.Agent({ keepAlive: true }); @@ -1153,7 +1316,22 @@ Reference to the underlying socket. Usually users will not want to access this property. In particular, the socket will not emit `'readable'` events because of how the protocol parser attaches to the socket. -```js +```mjs +import http from 'node:http'; +const options = { + host: 'www.google.com', +}; +const req = http.get(options); +req.end(); +req.once('response', (res) => { + const ip = req.socket.localAddress; + const port = req.socket.localPort; + console.log(`Your IP address is ${ip} and your source port is ${port}.`); + // Consume response object +}); +``` + +```cjs const http = require('node:http'); const options = { host: 'www.google.com', @@ -1326,7 +1504,19 @@ immediately destroyed. `socket` is the [`net.Socket`][] object that the error originated from. -```js +```mjs +import http from 'node:http'; + +const server = http.createServer((req, res) => { + res.end(); +}); +server.on('clientError', (err, socket) => { + socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); +}); +server.listen(8000); +``` + +```cjs const http = require('node:http'); const server = http.createServer((req, res) => { @@ -2034,7 +2224,16 @@ this property. In particular, the socket will not emit `'readable'` events because of how the protocol parser attaches to the socket. After `response.end()`, the property is nulled. -```js +```mjs +import http from 'node:http'; +const server = http.createServer((req, res) => { + const ip = res.socket.remoteAddress; + const port = res.socket.remotePort; + res.end(`Your IP address is ${ip} and your source port is ${port}.`); +}).listen(3000); +``` + +```cjs const http = require('node:http'); const server = http.createServer((req, res) => { const ip = res.socket.remoteAddress; @@ -3255,9 +3454,10 @@ changes: * `IncomingMessage` {http.IncomingMessage} Specifies the `IncomingMessage` class to be used. Useful for extending the original `IncomingMessage`. **Default:** `IncomingMessage`. - * `joinDuplicateHeaders` {boolean} It joins the field line values of multiple - headers in a request with `, ` instead of discarding the duplicates. - See [`message.headers`][] for more information. + * `joinDuplicateHeaders` {boolean} If set to `true`, this option allows + joining the field line values of multiple headers in a request with + a comma (`, `) instead of discarding the duplicates. + For more information, refer to [`message.headers`][]. **Default:** `false`. * `keepAlive` {boolean} If set to `true`, it enables keep-alive functionality on the socket immediately after a new incoming connection is received, @@ -3282,9 +3482,10 @@ changes: the entire request from the client. See [`server.requestTimeout`][] for more information. **Default:** `300000`. - * `requireHostHeader` {boolean} It forces the server to respond with - a 400 (Bad Request) status code to any HTTP/1.1 request message - that lacks a Host header (as mandated by the specification). + * `requireHostHeader` {boolean} If set to `true`, it forces the server to + respond with a 400 (Bad Request) status code to any HTTP/1.1 + request message that lacks a Host header + (as mandated by the specification). **Default:** `true`. * `ServerResponse` {http.ServerResponse} Specifies the `ServerResponse` class to be used. Useful for extending the original `ServerResponse`. **Default:** @@ -3302,6 +3503,20 @@ Returns a new instance of [`http.Server`][]. The `requestListener` is a function which is automatically added to the [`'request'`][] event. +```mjs +import http from 'node:http'; + +// Create a local server to receive data from +const server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + data: 'Hello World!', + })); +}); + +server.listen(8000); +``` + ```cjs const http = require('node:http'); @@ -3316,6 +3531,23 @@ const server = http.createServer((req, res) => { server.listen(8000); ``` +```mjs +import http from 'node:http'; + +// Create a local server to receive data from +const server = http.createServer(); + +// Listen to the request event +server.on('request', (request, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + data: 'Hello World!', + })); +}); + +server.listen(8000); +``` + ```cjs const http = require('node:http'); @@ -3565,7 +3797,47 @@ the [`'response'`][] event. class. The `ClientRequest` instance is a writable stream. If one needs to upload a file with a POST request, then write to the `ClientRequest` object. -```js +```mjs +import http from 'node:http'; +import { Buffer } from 'node:buffer'; + +const postData = JSON.stringify({ + 'msg': 'Hello World!', +}); + +const options = { + hostname: 'www.google.com', + port: 80, + path: '/upload', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, +}; + +const req = http.request(options, (res) => { + console.log(`STATUS: ${res.statusCode}`); + console.log(`HEADERS: ${JSON.stringify(res.headers)}`); + res.setEncoding('utf8'); + res.on('data', (chunk) => { + console.log(`BODY: ${chunk}`); + }); + res.on('end', () => { + console.log('No more data in response.'); + }); +}); + +req.on('error', (e) => { + console.error(`problem with request: ${e.message}`); +}); + +// Write data to request body +req.write(postData); +req.end(); +``` + +```cjs const http = require('node:http'); const postData = JSON.stringify({ @@ -3773,7 +4045,19 @@ Examples: Example: -```js +```mjs +import { validateHeaderName } from 'node:http'; + +try { + validateHeaderName(''); +} catch (err) { + console.error(err instanceof TypeError); // --> true + console.error(err.code); // --> 'ERR_INVALID_HTTP_TOKEN' + console.error(err.message); // --> 'Header name must be a valid HTTP token [""]' +} +``` + +```cjs const { validateHeaderName } = require('node:http'); try { @@ -3807,7 +4091,27 @@ or response. The HTTP module will automatically validate such headers. Examples: -```js +```mjs +import { validateHeaderValue } from 'node:http'; + +try { + validateHeaderValue('x-my-header', undefined); +} catch (err) { + console.error(err instanceof TypeError); // --> true + console.error(err.code === 'ERR_HTTP_INVALID_HEADER_VALUE'); // --> true + console.error(err.message); // --> 'Invalid value "undefined" for header "x-my-header"' +} + +try { + validateHeaderValue('x-my-header', 'oʊmɪɡə'); +} catch (err) { + console.error(err instanceof TypeError); // --> true + console.error(err.code === 'ERR_INVALID_CHAR'); // --> true + console.error(err.message); // --> 'Invalid character in header content ["x-my-header"]' +} +``` + +```cjs const { validateHeaderValue } = require('node:http'); try { diff --git a/doc/api/inspector.md b/doc/api/inspector.md index 34fad2a0ca1805..7d91377fc0040e 100644 --- a/doc/api/inspector.md +++ b/doc/api/inspector.md @@ -419,12 +419,20 @@ console. ### `inspector.open([port[, host[, wait]]])` + + * `port` {number} Port to listen on for inspector connections. Optional. **Default:** what was specified on the CLI. * `host` {string} Host to listen on for inspector connections. Optional. **Default:** what was specified on the CLI. * `wait` {boolean} Block until a client has connected. Optional. **Default:** `false`. +* Returns: {Disposable} that calls [`inspector.close()`][]. Activate inspector on host and port. Equivalent to `node --inspect=[[host:]port]`, but can be done programmatically after node has @@ -472,5 +480,6 @@ An exception will be thrown if there is no active inspector. [Chrome DevTools Protocol Viewer]: https://chromedevtools.github.io/devtools-protocol/v8/ [Heap Profiler]: https://chromedevtools.github.io/devtools-protocol/v8/HeapProfiler [`'Debugger.paused'`]: https://chromedevtools.github.io/devtools-protocol/v8/Debugger#event-paused +[`inspector.close()`]: #inspectorclose [`session.connect()`]: #sessionconnect [security warning]: cli.md#warning-binding-inspector-to-a-public-ipport-combination-is-insecure diff --git a/doc/api/module.md b/doc/api/module.md index d77b6e29cc7a17..5531aedda0b5ee 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -80,6 +80,123 @@ isBuiltin('fs'); // true isBuiltin('wss'); // false ``` +### `module.register()` + + + +In addition to using the `--experimental-loader` option in the CLI, +loaders can be registered programmatically using the +`module.register()` method. + +```mjs +import { register } from 'node:module'; + +register('http-to-https', import.meta.url); + +// Because this is a dynamic `import()`, the `http-to-https` hooks will run +// before importing `./my-app.mjs`. +await import('./my-app.mjs'); +``` + +In the example above, we are registering the `http-to-https` loader, +but it will only be available for subsequently imported modules—in +this case, `my-app.mjs`. If the `await import('./my-app.mjs')` had +instead been a static `import './my-app.mjs'`, _the app would already +have been loaded_ before the `http-to-https` hooks were +registered. This is part of the design of ES modules, where static +imports are evaluated from the leaves of the tree first back to the +trunk. There can be static imports _within_ `my-app.mjs`, which +will not be evaluated until `my-app.mjs` is when it's dynamically +imported. + +The `--experimental-loader` flag of the CLI can be used together +with the `register` function; the loaders registered with the +function will follow the same evaluation chain of loaders registered +within the CLI: + +```console +node \ + --experimental-loader unpkg \ + --experimental-loader http-to-https \ + --experimental-loader cache-buster \ + entrypoint.mjs +``` + +```mjs +// entrypoint.mjs +import { URL } from 'node:url'; +import { register } from 'node:module'; + +const loaderURL = new URL('./my-programmatically-loader.mjs', import.meta.url); + +register(loaderURL); +await import('./my-app.mjs'); +``` + +The `my-programmatic-loader.mjs` can leverage `unpkg`, +`http-to-https`, and `cache-buster` loaders. + +It's also possible to use `register` more than once: + +```mjs +// entrypoint.mjs +import { URL } from 'node:url'; +import { register } from 'node:module'; + +register(new URL('./first-loader.mjs', import.meta.url)); +register('./second-loader.mjs', import.meta.url); +await import('./my-app.mjs'); +``` + +Both loaders (`first-loader.mjs` and `second-loader.mjs`) can use +all the resources provided by the loaders registered in the CLI. But +remember that they will only be available in the next imported +module (`my-app.mjs`). The evaluation order of the hooks when +importing `my-app.mjs` and consecutive modules in the example above +will be: + +```console +resolve: second-loader.mjs +resolve: first-loader.mjs +resolve: cache-buster +resolve: http-to-https +resolve: unpkg +load: second-loader.mjs +load: first-loader.mjs +load: cache-buster +load: http-to-https +load: unpkg +globalPreload: second-loader.mjs +globalPreload: first-loader.mjs +globalPreload: cache-buster +globalPreload: http-to-https +globalPreload: unpkg +``` + +This function can also be used to pass data to the loader's [`initialize`][] +hook; the data passed to the hook may include transferrable objects like ports. + +```mjs +import { register } from 'node:module'; +import { MessageChannel } from 'node:worker_threads'; + +// This example showcases how a message channel can be used to +// communicate to the loader, by sending `port2` to the loader. +const { port1, port2 } = new MessageChannel(); + +port1.on('message', (msg) => { + console.log(msg); +}); + +register('./my-programmatic-loader.mjs', { + parentURL: import.meta.url, + data: { number: 1, port: port2 }, + transferList: [port2], +}); +``` + ### `module.syncBuiltinESMExports()` - - - - - - - - - - - - - - - - + + - - - - + + - - - - + + -
123
v6.xv6.14.2*
v8.xv8.6.0**v8.10.0*v8.11.2Node-API versionSupported In
v9.xv9.0.0*v9.3.0*v9.11.0*9v18.17.0+, 20.3.0+, 21.0.0 and all later versions
≥ v10.xall releasesall releasesall releases8v12.22.0+, v14.17.0+, v15.12.0+, 16.0.0 and all later versions
- - - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - - - - - - - - - + + + - - - - - - + + - - - - - - + +
456787v10.23.0+, v12.19.0+, v14.12.0+, 15.0.0 and all later versions
v10.xv10.16.0v10.17.0v10.20.0v10.23.06v10.20.0+, v12.17.0+, 14.0.0 and all later versions
v11.xv11.8.05v10.17.0+, v12.11.0+, 13.0.0 and all later versions
v12.xv12.0.0v12.11.0v12.17.0v12.19.0v12.22.04v10.16.0+, v11.8.0+, 12.0.0 and all later versions
v13.xv13.0.0v13.0.0
v14.xv14.0.0v14.0.0v14.0.0v14.12.0v14.17.0
3v6.14.2*, 8.11.2+, v9.11.0+*, 10.0.0 and all later versions
v15.xv15.0.0v15.0.0v15.0.0v15.0.0v15.12.02v8.10.0+*, v9.3.0+*, 10.0.0 and all later versions
v16.xv16.0.0v16.0.0v16.0.0v16.0.0v16.0.01v8.6.0+**, v9.0.0+*, 10.0.0 and all later versions
diff --git a/doc/api/policy.md b/doc/api/policy.md index cf2ff88b7ffdaf..c3a974a2d81b2b 100644 --- a/doc/api/policy.md +++ b/doc/api/policy.md @@ -6,6 +6,6 @@ > Stability: 1 - Experimental -The former Policies documentation is now at [Permissions documentation][] +The former Policies documentation is now at [Permissions documentation][]. [Permissions documentation]: permissions.md#policies diff --git a/doc/api/process.md b/doc/api/process.md index de56638e70d27e..5b08ce8a4b8367 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -1666,7 +1666,9 @@ each [`Worker`][] thread has its own copy of `process.env`, based on its parent thread's `process.env`, or whatever was specified as the `env` option to the [`Worker`][] constructor. Changes to `process.env` will not be visible across [`Worker`][] threads, and only the main thread can make changes that -are visible to the operating system or to native add-ons. +are visible to the operating system or to native add-ons. On Windows, a copy of +`process.env` on a [`Worker`][] instance operates in a case-sensitive manner +unlike the main thread. ## `process.execArgv` diff --git a/doc/api/readline.md b/doc/api/readline.md index 034a285073ce6f..bf0951fdd1b55c 100644 --- a/doc/api/readline.md +++ b/doc/api/readline.md @@ -1247,8 +1247,8 @@ const { createInterface } = require('node:readline'); Meta+Y - Cycle among previously deleted lines - Only available when the last keystroke is Ctrl+Y + Cycle among previously deleted texts + Only available when the last keystroke is Ctrl+Y or Meta+Y Ctrl+A diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 2db06fdd7239e1..12c9e34a9805f3 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -6,6 +6,13 @@ added: - v19.7.0 - v18.16.0 +changes: + - version: v20.6.0 + pr-url: https://github.com/nodejs/node/pull/46824 + description: Added support for "useSnapshot". + - version: v20.6.0 + pr-url: https://github.com/nodejs/node/pull/48191 + description: Added support for "useCodeCache". --> > Stability: 1 - Experimental: This feature is being designed and will change. @@ -169,7 +176,9 @@ The configuration currently reads the following top-level fields: { "main": "/path/to/bundled/script.js", "output": "/path/to/write/the/generated/blob.blob", - "disableExperimentalSEAWarning": true // Default: false + "disableExperimentalSEAWarning": true, // Default: false + "useSnapshot": false, // Default: false + "useCodeCache": true // Default: false } ``` @@ -177,6 +186,49 @@ If the paths are not absolute, Node.js will use the path relative to the current working directory. The version of the Node.js binary used to produce the blob must be the same as the one to which the blob will be injected. +### Startup snapshot support + +The `useSnapshot` field can be used to enable startup snapshot support. In this +case the `main` script would not be when the final executable is launched. +Instead, it would be run when the single executable application preparation +blob is generated on the building machine. The generated preparation blob would +then include a snapshot capturing the states initialized by the `main` script. +The final executable with the preparation blob injected would deserialize +the snapshot at run time. + +When `useSnapshot` is true, the main script must invoke the +[`v8.startupSnapshot.setDeserializeMainFunction()`][] API to configure code +that needs to be run when the final executable is launched by the users. + +The typical pattern for an application to use snapshot in a single executable +application is: + +1. At build time, on the building machine, the main script is run to + initialize the heap to a state that's ready to take user input. The script + should also configure a main function with + [`v8.startupSnapshot.setDeserializeMainFunction()`][]. This function will be + compiled and serialized into the snapshot, but not invoked at build time. +2. At run time, the main function will be run on top of the deserialized heap + on the user machine to process user input and generate output. + +The general constraints of the startup snapshot scripts also apply to the main +script when it's used to build snapshot for the single executable application, +and the main script can use the [`v8.startupSnapshot` API][] to adapt to +these constraints. See +[documentation about startup snapshot support in Node.js][]. + +### V8 code cache support + +When `useCodeCache` is set to `true` in the configuration, during the generation +of the single executable preparation blob, Node.js will compile the `main` +script to generate the V8 code cache. The generated code cache would be part of +the preparation blob and get injected into the final executable. When the single +executable application is launched, instead of compiling the `main` script from +scratch, Node.js would use the code cache to speed up the compilation, then +execute the script, which would improve the startup performance. + +**Note:** `import()` does not work when `useCodeCache` is `true`. + ## Notes ### `require(id)` in the injected module is not file based @@ -249,6 +301,9 @@ to help us document them. [`process.execPath`]: process.md#processexecpath [`require()`]: modules.md#requireid [`require.main`]: modules.md#accessing-the-main-module +[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data +[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api +[documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot [fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses [postject]: https://github.com/nodejs/postject [signtool]: https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool diff --git a/doc/api/test.md b/doc/api/test.md index a6b8ab26a50051..375cd41c949e8b 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -687,6 +687,15 @@ const customReporter = new Transform({ writableObjectMode: true, transform(event, encoding, callback) { switch (event.type) { + case 'test:dequeue': + callback(null, `test ${event.data.name} dequeued`); + break; + case 'test:enqueue': + callback(null, `test ${event.data.name} enqueued`); + break; + case 'test:watch:drained': + callback(null, 'test watch queue drained'); + break; case 'test:start': callback(null, `test ${event.data.name} started`); break; @@ -700,6 +709,8 @@ const customReporter = new Transform({ callback(null, 'test plan'); break; case 'test:diagnostic': + case 'test:stderr': + case 'test:stdout': callback(null, event.data.message); break; case 'test:coverage': { @@ -721,6 +732,15 @@ const customReporter = new Transform({ writableObjectMode: true, transform(event, encoding, callback) { switch (event.type) { + case 'test:dequeue': + callback(null, `test ${event.data.name} dequeued`); + break; + case 'test:enqueue': + callback(null, `test ${event.data.name} enqueued`); + break; + case 'test:watch:drained': + callback(null, 'test watch queue drained'); + break; case 'test:start': callback(null, `test ${event.data.name} started`); break; @@ -734,6 +754,8 @@ const customReporter = new Transform({ callback(null, 'test plan'); break; case 'test:diagnostic': + case 'test:stderr': + case 'test:stdout': callback(null, event.data.message); break; case 'test:coverage': { @@ -754,6 +776,15 @@ Example of a custom reporter using a generator function: export default async function * customReporter(source) { for await (const event of source) { switch (event.type) { + case 'test:dequeue': + yield `test ${event.data.name} dequeued`; + break; + case 'test:enqueue': + yield `test ${event.data.name} enqueued`; + break; + case 'test:watch:drained': + yield 'test watch queue drained'; + break; case 'test:start': yield `test ${event.data.name} started\n`; break; @@ -767,6 +798,8 @@ export default async function * customReporter(source) { yield 'test plan'; break; case 'test:diagnostic': + case 'test:stderr': + case 'test:stdout': yield `${event.data.message}\n`; break; case 'test:coverage': { @@ -783,6 +816,15 @@ export default async function * customReporter(source) { module.exports = async function * customReporter(source) { for await (const event of source) { switch (event.type) { + case 'test:dequeue': + yield `test ${event.data.name} dequeued`; + break; + case 'test:enqueue': + yield `test ${event.data.name} enqueued`; + break; + case 'test:watch:drained': + yield 'test watch queue drained'; + break; case 'test:start': yield `test ${event.data.name} started\n`; break; @@ -796,6 +838,8 @@ module.exports = async function * customReporter(source) { yield 'test plan\n'; break; case 'test:diagnostic': + case 'test:stderr': + case 'test:stdout': yield `${event.data.message}\n`; break; case 'test:coverage': { @@ -1951,6 +1995,13 @@ clocks or actual timers outside of the mocking environment. added: - v18.9.0 - v16.19.0 +changes: + - version: + - v20.0.0 + - v19.9.0 + - v18.17.0 + pr-url: https://github.com/nodejs/node/pull/47094 + description: added type to test:pass and test:fail events for when the test is a suite. --> * Extends {ReadableStream} @@ -2029,8 +2080,11 @@ Emitted when a test is enqueued for execution. * `data` {Object} * `details` {Object} Additional execution metadata. - * `duration` {number} The duration of the test in milliseconds. - * `error` {Error} The error thrown by the test. + * `duration_ms` {number} The duration of the test in milliseconds. + * `error` {Error} An error wrapping the error thrown by the test. + * `cause` {Error} The actual error thrown by the test. + * `type` {string|undefined} The type of the test, used to denote whether + this is a suite. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `name` {string} The test name. @@ -2045,7 +2099,9 @@ Emitted when a test fails. * `data` {Object} * `details` {Object} Additional execution metadata. - * `duration` {number} The duration of the test in milliseconds. + * `duration_ms` {number} The duration of the test in milliseconds. + * `type` {string|undefined} The type of the test, used to denote whether + this is a suite. * `file` {string|undefined} The path of the test file, `undefined` if test was run through the REPL. * `name` {string} The test name. diff --git a/doc/api/webstreams.md b/doc/api/webstreams.md index a413beb7174fd4..ed8cddd2fdbfdd 100644 --- a/doc/api/webstreams.md +++ b/doc/api/webstreams.md @@ -248,6 +248,7 @@ const transformedStream = stream.pipeThrough(transform); for await (const chunk of transformedStream) console.log(chunk); + // Prints: A ``` ```cjs @@ -273,6 +274,7 @@ const transformedStream = stream.pipeThrough(transform); (async () => { for await (const chunk of transformedStream) console.log(chunk); + // Prints: A })(); ``` @@ -387,6 +389,49 @@ port1.onmessage = ({ data }) => { port2.postMessage(stream, [stream]); ``` +### `ReadableStream.from(iterable)` + + + +* `iterable` {Iterable} Object implementing the `Symbol.asyncIterator` or + `Symbol.iterator` iterable protocol. + +A utility method that creates a new {ReadableStream} from an iterable. + +```mjs +import { ReadableStream } from 'node:stream/web'; + +async function* asyncIterableGenerator() { + yield 'a'; + yield 'b'; + yield 'c'; +} + +const stream = ReadableStream.from(asyncIterableGenerator()); + +for await (const chunk of stream) + console.log(chunk); // Prints: 'a', 'b', 'c' +``` + +```cjs +const { ReadableStream } = require('node:stream/web'); + +async function* asyncIterableGenerator() { + yield 'a'; + yield 'b'; + yield 'c'; +} + +(async () => { + const stream = ReadableStream.from(asyncIterableGenerator()); + + for await (const chunk of stream) + console.log(chunk); // Prints: 'a', 'b', 'c' +})(); +``` + ### Class: `ReadableStreamDefaultReader` -* type: A promise that is fulfilled with `undefined` when the - writer is ready to be used. +* Type: {Promise} Fulfilled with `undefined` when the writer is ready + to be used. #### `writableStreamDefaultWriter.releaseLock()` @@ -1477,6 +1522,7 @@ const dataArray = encoder.encode('hello world from consumers!'); const readable = Readable.from(dataArray); const data = await arrayBuffer(readable); console.log(`from readable: ${data.byteLength}`); +// Prints: from readable: 76 ``` ```cjs @@ -1489,6 +1535,7 @@ const dataArray = encoder.encode('hello world from consumers!'); const readable = Readable.from(dataArray); arrayBuffer(readable).then((data) => { console.log(`from readable: ${data.byteLength}`); + // Prints: from readable: 76 }); ``` @@ -1510,6 +1557,7 @@ const dataBlob = new Blob(['hello world from consumers!']); const readable = dataBlob.stream(); const data = await blob(readable); console.log(`from readable: ${data.size}`); +// Prints: from readable: 27 ``` ```cjs @@ -1520,6 +1568,7 @@ const dataBlob = new Blob(['hello world from consumers!']); const readable = dataBlob.stream(); blob(readable).then((data) => { console.log(`from readable: ${data.size}`); + // Prints: from readable: 27 }); ``` @@ -1543,6 +1592,7 @@ const dataBuffer = Buffer.from('hello world from consumers!'); const readable = Readable.from(dataBuffer); const data = await buffer(readable); console.log(`from readable: ${data.length}`); +// Prints: from readable: 27 ``` ```cjs @@ -1555,6 +1605,7 @@ const dataBuffer = Buffer.from('hello world from consumers!'); const readable = Readable.from(dataBuffer); buffer(readable).then((data) => { console.log(`from readable: ${data.length}`); + // Prints: from readable: 27 }); ``` @@ -1584,6 +1635,7 @@ const items = Array.from( const readable = Readable.from(JSON.stringify(items)); const data = await json(readable); console.log(`from readable: ${data.length}`); +// Prints: from readable: 100 ``` ```cjs @@ -1602,6 +1654,7 @@ const items = Array.from( const readable = Readable.from(JSON.stringify(items)); json(readable).then((data) => { console.log(`from readable: ${data.length}`); + // Prints: from readable: 100 }); ``` @@ -1622,6 +1675,7 @@ import { Readable } from 'node:stream'; const readable = Readable.from('Hello world from consumers!'); const data = await text(readable); console.log(`from readable: ${data.length}`); +// Prints: from readable: 27 ``` ```cjs @@ -1631,6 +1685,7 @@ const { Readable } = require('node:stream'); const readable = Readable.from('Hello world from consumers!'); text(readable).then((data) => { console.log(`from readable: ${data.length}`); + // Prints: from readable: 27 }); ``` diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 2c2d70c4f79280..532396e61ba7a8 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -850,7 +850,8 @@ Notable differences inside a Worker environment are: unless otherwise specified. Changes to one copy are not visible in other threads, and are not visible to native add-ons (unless [`worker.SHARE_ENV`][] is passed as the `env` option to the - [`Worker`][] constructor). + [`Worker`][] constructor). On Windows, unlike the main thread, a copy of the + environment variables operates in a case-sensitive manner. * [`process.title`][] cannot be modified. * Signals are not delivered through [`process.on('...')`][Signals events]. * Execution may stop at any point as a result of [`worker.terminate()`][] diff --git a/doc/changelogs/CHANGELOG_V20.md b/doc/changelogs/CHANGELOG_V20.md index 103c0adffd7304..a30029285dfa6b 100644 --- a/doc/changelogs/CHANGELOG_V20.md +++ b/doc/changelogs/CHANGELOG_V20.md @@ -8,6 +8,7 @@ +20.6.0
20.5.1
20.5.0
20.4.0
@@ -42,6 +43,231 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + + +## 2023-09-04, Version 20.6.0 (Current), @juanarbol prepared by @UlisesGascon + +### Notable changes + +#### built-in `.env` file support + +Starting from Node.js v20.6.0, Node.js supports `.env` files for configuring environment variables. + +Your configuration file should follow the INI file format, with each line containing a key-value pair for an environment variable. +To initialize your Node.js application with predefined configurations, use the following CLI command: `node --env-file=config.env index.js`. + +For example, you can access the following environment variable using `process.env.PASSWORD` when your application is initialized: + +```text +PASSWORD=nodejs +``` + +In addition to environment variables, this change allows you to define your `NODE_OPTIONS` directly in the `.env` file, eliminating the need to include it in your `package.json`. + +This feature was contributed by Yagiz Nizipli in [#48890](https://github.com/nodejs/node/pull/48890). + +#### `import.meta.resolve` unflagged + +In ES modules, [`import.meta.resolve(specifier)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve) can be used to get an absolute URL string to which `specifier` resolves, similar to `require.resolve` in CommonJS. This aligns Node.js with browsers and other server-side runtimes. + +This feature was contributed by Guy Bedford in + +#### New `node:module` API `register` for module customization hooks; new `initialize` hook + +There is a new API `register` available on `node:module` to specify a file that exports module customization hooks, and pass data to the hooks, and establish communication channels with them. The “define the file with the hooks” part was previously handled by a flag `--experimental-loader`, but when the hooks moved into a dedicated thread in 20.0.0 there was a need to provide a way to communicate between the main (application) thread and the hooks thread. This can now be done by calling `register` from the main thread and passing data, including `MessageChannel` instances. + +We encourage users to migrate to an approach that uses [`--import`](https://nodejs.org/api/cli.html#--importmodule) with `register`, such as: + +```bash +node --import ./file-that-calls-register.js ./app.js +``` + +Using `--import` ensures that the customization hooks are registered before any application code runs, even the entry point. + +This feature was contributed by Izaak Schroeder in and + +#### Module customization `load` hook can now support CommonJS + +Authors of module customization hooks can how handle both ES module and CommonJS sources in the `load` hook. This works for CommonJS modules referenced via either `import` or `require`, so long as [the main entry point of the application is handled by the ES module loader](https://nodejs.org/api/cli.html#program-entry-point) (such as because the entry point is an ES module file, or if the `--import` flag is passed). This should simplify the customization of the Node.js module loading process, as package authors can customize more of Node.js without relying on deprecated APIs such as `require.extensions`. + +This feature was contributed by Antoine du Hamel in + +#### Node.js C++ addons now have experimental support for cppgc (Oilpan), a C++ garbage collection library in V8. + +Now when Node.js starts up, it makes sure that there is a `v8::CppHeap` attached to the V8 isolate. This enables users to allocate in the `v8::CppHeap` using `` headers from V8, which are now also included into the Node.js headers available to addons. Note that since Node.js only bundles the cppgc library coming from V8, [the ABI stability](https://nodejs.org/en/docs/guides/abi-stability#abi-stability-in-nodejs) of cppgc is currently not guaranteed in semver-minor and -patch updates, but we do not expect the ABI to break often, as it has been stable and battle-tested in Chromium for years. We may consider including cppgc into the ABI stability guarantees when it gets enough adoption internally and externally. + +To help addon authors create JavaScript-to-C++ references of which V8's garbage collector can be aware, a helper function [`node::SetCppgcReference(isolate, js_object, cppgc_object)`](https://github.com/nodejs/node/blob/v20.6.0/test/addons/cppgc-object/binding.cc) has been added to `node.h`. V8 may provide a native alternative in the future, which could then replace this Node.js-specific helper. In the mean time, users can use this API to avoid having to hard-code the layout of JavaScript wrapper objects. An example of how to create garbage-collected C++ objects in the unified heap and wrap it in a JavaScript object can be found in the [Node.js addon tests](https://github.com/nodejs/node/blob/v20.6.0/test/addons/cppgc-object/binding.cc). + +The existing `node::ObjectWrap` helper would continue to work, while cppgc-based object management serves as an alternative with some advantages mentioned in [the V8 blog post about Oilpan](https://v8.dev/blog/oilpan-library). + +This feature was contributed by Daryl Haresign and Joyee Cheung in and . + +#### Other notable changes + +* \[[`d6862b085c`](https://github.com/nodejs/node/commit/d6862b085c)] - **deps**: V8: cherry-pick 93275031284c (Joyee Cheung) [#48660](https://github.com/nodejs/node/pull/48660) +* \[[`00fc8bb8b3`](https://github.com/nodejs/node/commit/00fc8bb8b3)] - **doc**: add rluvaton to collaborators (Raz Luvaton) [#49215](https://github.com/nodejs/node/pull/49215) +* \[[`d649339abd`](https://github.com/nodejs/node/commit/d649339abd)] - **doc**: add new TSC members (Michael Dawson) [#48841](https://github.com/nodejs/node/pull/48841) +* \[[`67f9896247`](https://github.com/nodejs/node/commit/67f9896247)] - **(SEMVER-MINOR)** **inspector**: open add `SymbolDispose` (Chemi Atlow) [#48765](https://github.com/nodejs/node/pull/48765) +* \[[`5aef593db3`](https://github.com/nodejs/node/commit/5aef593db3)] - **module**: implement `register` utility (João Lenon) [#46826](https://github.com/nodejs/node/pull/46826) + +### Commits + +* \[[`771abcb5da`](https://github.com/nodejs/node/commit/771abcb5da)] - **benchmark**: add benchmarks for the test\_runner (Raz Luvaton) [#48931](https://github.com/nodejs/node/pull/48931) +* \[[`6b27bb0dab`](https://github.com/nodejs/node/commit/6b27bb0dab)] - **benchmark**: add pm startup benchmark (Rafael Gonzaga) [#48905](https://github.com/nodejs/node/pull/48905) +* \[[`1f35c0ca55`](https://github.com/nodejs/node/commit/1f35c0ca55)] - **child\_process**: harden against prototype pollution (Livia Medeiros) [#48726](https://github.com/nodejs/node/pull/48726) +* \[[`d6862b085c`](https://github.com/nodejs/node/commit/d6862b085c)] - **deps**: V8: cherry-pick 93275031284c (Joyee Cheung) [#48660](https://github.com/nodejs/node/pull/48660) +* \[[`f71e383948`](https://github.com/nodejs/node/commit/f71e383948)] - **deps**: update simdutf to 3.2.17 (Node.js GitHub Bot) [#49019](https://github.com/nodejs/node/pull/49019) +* \[[`e14f0456ae`](https://github.com/nodejs/node/commit/e14f0456ae)] - **deps**: update googletest to 7e33b6a (Node.js GitHub Bot) [#49034](https://github.com/nodejs/node/pull/49034) +* \[[`bfaa0fb500`](https://github.com/nodejs/node/commit/bfaa0fb500)] - **deps**: update zlib to 1.2.13.1-motley-526382e (Node.js GitHub Bot) [#49033](https://github.com/nodejs/node/pull/49033) +* \[[`b79c652c85`](https://github.com/nodejs/node/commit/b79c652c85)] - **deps**: update undici to 5.23.0 (Node.js GitHub Bot) [#49021](https://github.com/nodejs/node/pull/49021) +* \[[`6ead86145c`](https://github.com/nodejs/node/commit/6ead86145c)] - **deps**: update googletest to c875c4e (Node.js GitHub Bot) [#48964](https://github.com/nodejs/node/pull/48964) +* \[[`4b0e50501e`](https://github.com/nodejs/node/commit/4b0e50501e)] - **deps**: update ada to 2.6.0 (Node.js GitHub Bot) [#48896](https://github.com/nodejs/node/pull/48896) +* \[[`d960ee0ba3`](https://github.com/nodejs/node/commit/d960ee0ba3)] - **deps**: upgrade npm to 9.8.1 (npm team) [#48838](https://github.com/nodejs/node/pull/48838) +* \[[`d92b0139ca`](https://github.com/nodejs/node/commit/d92b0139ca)] - **deps**: update zlib to 1.2.13.1-motley-61dc0bd (Node.js GitHub Bot) [#48788](https://github.com/nodejs/node/pull/48788) +* \[[`2a7835c376`](https://github.com/nodejs/node/commit/2a7835c376)] - **deps**: V8: cherry-pick 9f4b7699f68e (Joyee Cheung) [#48830](https://github.com/nodejs/node/pull/48830) +* \[[`c8e17829ac`](https://github.com/nodejs/node/commit/c8e17829ac)] - **deps**: V8: cherry-pick c1a54d5ffcd1 (Joyee Cheung) [#48830](https://github.com/nodejs/node/pull/48830) +* \[[`318e075b6f`](https://github.com/nodejs/node/commit/318e075b6f)] - **deps**: update googletest to cc36671 (Node.js GitHub Bot) [#48789](https://github.com/nodejs/node/pull/48789) +* \[[`114e088267`](https://github.com/nodejs/node/commit/114e088267)] - **diagnostics\_channel**: fix last subscriber removal (Gabriel Schulhof) [#48933](https://github.com/nodejs/node/pull/48933) +* \[[`00fc8bb8b3`](https://github.com/nodejs/node/commit/00fc8bb8b3)] - **doc**: add rluvaton to collaborators (Raz Luvaton) [#49215](https://github.com/nodejs/node/pull/49215) +* \[[`21949c45b6`](https://github.com/nodejs/node/commit/21949c45b6)] - **doc**: add print results for examples in `WebStreams` (Jungku Lee) [#49143](https://github.com/nodejs/node/pull/49143) +* \[[`032107a6fe`](https://github.com/nodejs/node/commit/032107a6fe)] - **doc**: fix `Type` notation in webstreams (Deokjin Kim) [#49121](https://github.com/nodejs/node/pull/49121) +* \[[`91d41e7c5a`](https://github.com/nodejs/node/commit/91d41e7c5a)] - **doc**: fix name of the flag in `initialize()` docs (Antoine du Hamel) [#49158](https://github.com/nodejs/node/pull/49158) +* \[[`aa4caf810e`](https://github.com/nodejs/node/commit/aa4caf810e)] - **doc**: make the NODE\_VERSION\_IS\_RELEASE revert clear (Rafael Gonzaga) [#49114](https://github.com/nodejs/node/pull/49114) +* \[[`f888a1dbe3`](https://github.com/nodejs/node/commit/f888a1dbe3)] - **doc**: update process.binding deprecation text (Tobias Nießen) [#49086](https://github.com/nodejs/node/pull/49086) +* \[[`89fa3faf92`](https://github.com/nodejs/node/commit/89fa3faf92)] - **doc**: update with latest security release (Rafael Gonzaga) [#49085](https://github.com/nodejs/node/pull/49085) +* \[[`3d36e7a941`](https://github.com/nodejs/node/commit/3d36e7a941)] - **doc**: add description for `--port` flag of `node inspect` (Michael Bianco) [#48785](https://github.com/nodejs/node/pull/48785) +* \[[`e9d9ca12a3`](https://github.com/nodejs/node/commit/e9d9ca12a3)] - **doc**: add missing period (Rich Trott) [#49094](https://github.com/nodejs/node/pull/49094) +* \[[`7e7b554de0`](https://github.com/nodejs/node/commit/7e7b554de0)] - **doc**: add ESM examples in http.md (btea) [#47763](https://github.com/nodejs/node/pull/47763) +* \[[`48f8ccfd54`](https://github.com/nodejs/node/commit/48f8ccfd54)] - **doc**: detailed description of keystrokes Ctrl-Y and Meta-Y (Ray) [#43529](https://github.com/nodejs/node/pull/43529) +* \[[`195885c8f8`](https://github.com/nodejs/node/commit/195885c8f8)] - **doc**: add "type" to test runner event details (Phil Nash) [#49014](https://github.com/nodejs/node/pull/49014) +* \[[`6ce25f8415`](https://github.com/nodejs/node/commit/6ce25f8415)] - **doc**: reserve 118 for Electron 27 (David Sanders) [#49023](https://github.com/nodejs/node/pull/49023) +* \[[`9c26c0f296`](https://github.com/nodejs/node/commit/9c26c0f296)] - **doc**: clarify use of process.env in worker threads on Windows (Daeyeon Jeong) [#49008](https://github.com/nodejs/node/pull/49008) +* \[[`7186e02aa0`](https://github.com/nodejs/node/commit/7186e02aa0)] - **doc**: remove v14 mention (Rafael Gonzaga) [#49005](https://github.com/nodejs/node/pull/49005) +* \[[`9641ac6c65`](https://github.com/nodejs/node/commit/9641ac6c65)] - **doc**: drop github actions check in sec release process (Rafael Gonzaga) [#48978](https://github.com/nodejs/node/pull/48978) +* \[[`f3d62abb19`](https://github.com/nodejs/node/commit/f3d62abb19)] - **doc**: improved joinDuplicateHeaders definition (Matteo Bianchi) [#48859](https://github.com/nodejs/node/pull/48859) +* \[[`0db104a08b`](https://github.com/nodejs/node/commit/0db104a08b)] - **doc**: fix second parameter name of `events.addAbortListener` (Deokjin Kim) [#48922](https://github.com/nodejs/node/pull/48922) +* \[[`5173c559b7`](https://github.com/nodejs/node/commit/5173c559b7)] - **doc**: add new reporter events to custom reporter examples (Chemi Atlow) [#48903](https://github.com/nodejs/node/pull/48903) +* \[[`660da785e6`](https://github.com/nodejs/node/commit/660da785e6)] - **doc**: run license-builder (github-actions\[bot]) [#48898](https://github.com/nodejs/node/pull/48898) +* \[[`092f9fe92a`](https://github.com/nodejs/node/commit/092f9fe92a)] - **doc**: change duration to duration\_ms on test documentation (Ardi\_Nugraha) [#48892](https://github.com/nodejs/node/pull/48892) +* \[[`5e4730858d`](https://github.com/nodejs/node/commit/5e4730858d)] - **doc**: improve requireHostHeader (Guido Penta) [#48860](https://github.com/nodejs/node/pull/48860) +* \[[`045e3c549a`](https://github.com/nodejs/node/commit/045e3c549a)] - **doc**: add ver of 18.x where Node-api 9 is supported (Michael Dawson) [#48876](https://github.com/nodejs/node/pull/48876) +* \[[`c20d35df34`](https://github.com/nodejs/node/commit/c20d35df34)] - **doc**: include experimental features assessment (Rafael Gonzaga) [#48824](https://github.com/nodejs/node/pull/48824) +* \[[`d649339abd`](https://github.com/nodejs/node/commit/d649339abd)] - **doc**: add new TSC members (Michael Dawson) [#48841](https://github.com/nodejs/node/pull/48841) +* \[[`aeac327f2b`](https://github.com/nodejs/node/commit/aeac327f2b)] - **doc**: refactor node-api support matrix (Michael Dawson) [#48774](https://github.com/nodejs/node/pull/48774) +* \[[`388c7d9232`](https://github.com/nodejs/node/commit/388c7d9232)] - **doc**: declare `path` on example of `async_hooks.executionAsyncId()` (Deokjin Kim) [#48556](https://github.com/nodejs/node/pull/48556) +* \[[`fe20528c8e`](https://github.com/nodejs/node/commit/fe20528c8e)] - **doc**: remove the . in the end to reduce confusing (Jason) [#48719](https://github.com/nodejs/node/pull/48719) +* \[[`e69c8e173f`](https://github.com/nodejs/node/commit/e69c8e173f)] - **doc**: nodejs-social over nodejs/tweet (Rafael Gonzaga) [#48769](https://github.com/nodejs/node/pull/48769) +* \[[`ea547849fd`](https://github.com/nodejs/node/commit/ea547849fd)] - **doc**: expand on squashing and rebasing to land a PR (Chengzhong Wu) [#48751](https://github.com/nodejs/node/pull/48751) +* \[[`31442b96a5`](https://github.com/nodejs/node/commit/31442b96a5)] - **esm**: fix `globalPreload` warning (Antoine du Hamel) [#49069](https://github.com/nodejs/node/pull/49069) +* \[[`eb1215878b`](https://github.com/nodejs/node/commit/eb1215878b)] - **esm**: unflag import.meta.resolve (Guy Bedford) [#49028](https://github.com/nodejs/node/pull/49028) +* \[[`57b24a34e6`](https://github.com/nodejs/node/commit/57b24a34e6)] - **esm**: import.meta.resolve exact module not found errors should return (Guy Bedford) [#49038](https://github.com/nodejs/node/pull/49038) +* \[[`f23b2a3066`](https://github.com/nodejs/node/commit/f23b2a3066)] - **esm**: protect `ERR_UNSUPPORTED_DIR_IMPORT` against prototype pollution (Antoine du Hamel) [#49060](https://github.com/nodejs/node/pull/49060) +* \[[`386e826a56`](https://github.com/nodejs/node/commit/386e826a56)] - **esm**: add `initialize` hook, integrate with `register` (Izaak Schroeder) [#48842](https://github.com/nodejs/node/pull/48842) +* \[[`74a2e1e0ab`](https://github.com/nodejs/node/commit/74a2e1e0ab)] - **esm**: fix typo `parentUrl` -> `parentURL` (Antoine du Hamel) [#48999](https://github.com/nodejs/node/pull/48999) +* \[[`0a4f7c669a`](https://github.com/nodejs/node/commit/0a4f7c669a)] - **esm**: unflag `Module.register` and allow nested loader `import()` (Izaak Schroeder) [#48559](https://github.com/nodejs/node/pull/48559) +* \[[`a5597470ce`](https://github.com/nodejs/node/commit/a5597470ce)] - **esm**: add back `globalPreload` tests and fix failing ones (Antoine du Hamel) [#48779](https://github.com/nodejs/node/pull/48779) +* \[[`d568600b42`](https://github.com/nodejs/node/commit/d568600b42)] - **events**: remove weak listener for event target (Raz Luvaton) [#48952](https://github.com/nodejs/node/pull/48952) +* \[[`3d942d9842`](https://github.com/nodejs/node/commit/3d942d9842)] - **fs**: fix readdir recursive sync & callback (Ethan Arrowood) [#48698](https://github.com/nodejs/node/pull/48698) +* \[[`c14ff69d69`](https://github.com/nodejs/node/commit/c14ff69d69)] - **fs**: mention `URL` in NUL character error message (LiviaMedeiros) [#48828](https://github.com/nodejs/node/pull/48828) +* \[[`d634d781d7`](https://github.com/nodejs/node/commit/d634d781d7)] - **fs**: make `mkdtemp` accept buffers and URL (LiviaMedeiros) [#48828](https://github.com/nodejs/node/pull/48828) +* \[[`4515a285a4`](https://github.com/nodejs/node/commit/4515a285a4)] - **fs**: remove redundant `nullCheck` (Livia Medeiros) [#48826](https://github.com/nodejs/node/pull/48826) +* \[[`742597b14a`](https://github.com/nodejs/node/commit/742597b14a)] - **http**: start connections checking interval on listen (Paolo Insogna) [#48611](https://github.com/nodejs/node/pull/48611) +* \[[`67f9896247`](https://github.com/nodejs/node/commit/67f9896247)] - **(SEMVER-MINOR)** **inspector**: open add `SymbolDispose` (Chemi Atlow) [#48765](https://github.com/nodejs/node/pull/48765) +* \[[`b66a3c1c96`](https://github.com/nodejs/node/commit/b66a3c1c96)] - **lib**: fix MIME overmatch in data URLs (André Alves) [#49104](https://github.com/nodejs/node/pull/49104) +* \[[`dca8678a22`](https://github.com/nodejs/node/commit/dca8678a22)] - **lib**: fix to add resolve() before return at Blob.stream()'s source.pull() (bellbind) [#48935](https://github.com/nodejs/node/pull/48935) +* \[[`420b85c00f`](https://github.com/nodejs/node/commit/420b85c00f)] - **lib**: remove invalid parameter to toASCII (Yagiz Nizipli) [#48878](https://github.com/nodejs/node/pull/48878) +* \[[`a12ce11b09`](https://github.com/nodejs/node/commit/a12ce11b09)] - **lib,permission**: drop repl autocomplete when pm enabled (Rafael Gonzaga) [#48920](https://github.com/nodejs/node/pull/48920) +* \[[`458eaf5e75`](https://github.com/nodejs/node/commit/458eaf5e75)] - **meta**: bump github/codeql-action from 2.20.1 to 2.21.2 (dependabot\[bot]) [#48986](https://github.com/nodejs/node/pull/48986) +* \[[`4f88cb10e0`](https://github.com/nodejs/node/commit/4f88cb10e0)] - **meta**: bump step-security/harden-runner from 2.4.1 to 2.5.0 (dependabot\[bot]) [#48985](https://github.com/nodejs/node/pull/48985) +* \[[`22fc2a6ec6`](https://github.com/nodejs/node/commit/22fc2a6ec6)] - **meta**: bump actions/setup-node from 3.6.0 to 3.7.0 (dependabot\[bot]) [#48984](https://github.com/nodejs/node/pull/48984) +* \[[`40103adabd`](https://github.com/nodejs/node/commit/40103adabd)] - **meta**: bump actions/setup-python from 4.6.1 to 4.7.0 (dependabot\[bot]) [#48983](https://github.com/nodejs/node/pull/48983) +* \[[`84c0c6848c`](https://github.com/nodejs/node/commit/84c0c6848c)] - **meta**: add mailmap entry for atlowChemi (Chemi Atlow) [#48810](https://github.com/nodejs/node/pull/48810) +* \[[`1a6e9450b8`](https://github.com/nodejs/node/commit/1a6e9450b8)] - **module**: make CJS load from ESM loader (Antoine du Hamel) [#47999](https://github.com/nodejs/node/pull/47999) +* \[[`a5322c4b4a`](https://github.com/nodejs/node/commit/a5322c4b4a)] - **module**: ensure successful import returns the same result (Antoine du Hamel) [#46662](https://github.com/nodejs/node/pull/46662) +* \[[`5aef593db3`](https://github.com/nodejs/node/commit/5aef593db3)] - **module**: implement `register` utility (João Lenon) [#46826](https://github.com/nodejs/node/pull/46826) +* \[[`015c4f788d`](https://github.com/nodejs/node/commit/015c4f788d)] - **node-api**: avoid macro redefinition (Tobias Nießen) [#48879](https://github.com/nodejs/node/pull/48879) +* \[[`53ee98566b`](https://github.com/nodejs/node/commit/53ee98566b)] - **permission**: move PrintTree into unnamed namespace (Tobias Nießen) [#48874](https://github.com/nodejs/node/pull/48874) +* \[[`30ea480135`](https://github.com/nodejs/node/commit/30ea480135)] - **permission**: fix data types in PrintTree (Tobias Nießen) [#48770](https://github.com/nodejs/node/pull/48770) +* \[[`8380800375`](https://github.com/nodejs/node/commit/8380800375)] - **readline**: add paste bracket mode (Jakub Jankiewicz) [#47150](https://github.com/nodejs/node/pull/47150) +* \[[`bc009d0c10`](https://github.com/nodejs/node/commit/bc009d0c10)] - **sea**: add support for V8 bytecode-only caching (Darshan Sen) [#48191](https://github.com/nodejs/node/pull/48191) +* \[[`f2f4ce9e29`](https://github.com/nodejs/node/commit/f2f4ce9e29)] - **src**: use effective cppgc wrapper id to deduce non-cppgc id (Joyee Cheung) [#48660](https://github.com/nodejs/node/pull/48660) +* \[[`bf7ff369f6`](https://github.com/nodejs/node/commit/bf7ff369f6)] - **src**: add built-in `.env` file support (Yagiz Nizipli) [#48890](https://github.com/nodejs/node/pull/48890) +* \[[`8d6948f8e2`](https://github.com/nodejs/node/commit/8d6948f8e2)] - **src**: remove duplicated code in `GenerateSingleExecutableBlob()` (Jungku Lee) [#49119](https://github.com/nodejs/node/pull/49119) +* \[[`b030004cee`](https://github.com/nodejs/node/commit/b030004cee)] - **src**: refactor vector writing in snapshot builder (Joyee Cheung) [#48851](https://github.com/nodejs/node/pull/48851) +* \[[`497df8288d`](https://github.com/nodejs/node/commit/497df8288d)] - **src**: add ability to overload fast api functions (Yagiz Nizipli) [#48993](https://github.com/nodejs/node/pull/48993) +* \[[`e5b0dfa359`](https://github.com/nodejs/node/commit/e5b0dfa359)] - **src**: remove redundant code for uv\_handle\_type (Jungku Lee) [#49061](https://github.com/nodejs/node/pull/49061) +* \[[`f126b9e3d1`](https://github.com/nodejs/node/commit/f126b9e3d1)] - **src**: modernize use-equals-default (Jason) [#48735](https://github.com/nodejs/node/pull/48735) +* \[[`db4370fc3e`](https://github.com/nodejs/node/commit/db4370fc3e)] - **src**: avoid string copy in BuiltinLoader::GetBuiltinIds (Yagiz Nizipli) [#48721](https://github.com/nodejs/node/pull/48721) +* \[[`9d13503c4e`](https://github.com/nodejs/node/commit/9d13503c4e)] - **src**: fix callback\_queue.h missing header (Jason) [#48733](https://github.com/nodejs/node/pull/48733) +* \[[`6c389df3aa`](https://github.com/nodejs/node/commit/6c389df3aa)] - **src**: cast v8::Object::GetInternalField() return value to v8::Value (Joyee Cheung) [#48943](https://github.com/nodejs/node/pull/48943) +* \[[`7b9adff0be`](https://github.com/nodejs/node/commit/7b9adff0be)] - **src**: do not pass user input to format string (Antoine du Hamel) [#48973](https://github.com/nodejs/node/pull/48973) +* \[[`e0fdb7b092`](https://github.com/nodejs/node/commit/e0fdb7b092)] - **src**: remove ContextEmbedderIndex::kBindingDataStoreIndex (Joyee Cheung) [#48836](https://github.com/nodejs/node/pull/48836) +* \[[`578c3d1e14`](https://github.com/nodejs/node/commit/578c3d1e14)] - **src**: use ARES\_SUCCESS instead of 0 (Hyunjin Kim) [#48834](https://github.com/nodejs/node/pull/48834) +* \[[`ed23426aac`](https://github.com/nodejs/node/commit/ed23426aac)] - **src**: save the performance milestone time origin in the AliasedArray (Joyee Cheung) [#48708](https://github.com/nodejs/node/pull/48708) +* \[[`5dec186663`](https://github.com/nodejs/node/commit/5dec186663)] - **src**: support snapshot in single executable applications (Joyee Cheung) [#46824](https://github.com/nodejs/node/pull/46824) +* \[[`d759d4f631`](https://github.com/nodejs/node/commit/d759d4f631)] - **src**: remove unnecessary temporary creation (Jason) [#48734](https://github.com/nodejs/node/pull/48734) +* \[[`409cc692db`](https://github.com/nodejs/node/commit/409cc692db)] - **src**: fix nullptr access on realm (Jan Olaf Krems) [#48802](https://github.com/nodejs/node/pull/48802) +* \[[`07d0fd61b1`](https://github.com/nodejs/node/commit/07d0fd61b1)] - **src**: remove OnScopeLeaveImpl's move assignment overload (Jason) [#48732](https://github.com/nodejs/node/pull/48732) +* \[[`41cc3efa23`](https://github.com/nodejs/node/commit/41cc3efa23)] - **src**: use string\_view for utf-8 string creation (Yagiz Nizipli) [#48722](https://github.com/nodejs/node/pull/48722) +* \[[`62a46d9335`](https://github.com/nodejs/node/commit/62a46d9335)] - **src,permission**: restrict by default when pm enabled (Rafael Gonzaga) [#48907](https://github.com/nodejs/node/pull/48907) +* \[[`099159ce04`](https://github.com/nodejs/node/commit/099159ce04)] - **src,tools**: initialize cppgc (Daryl Haresign) [#48660](https://github.com/nodejs/node/pull/48660) +* \[[`600c08d197`](https://github.com/nodejs/node/commit/600c08d197)] - **stream**: improve WebStreams performance (Raz Luvaton) [#49089](https://github.com/nodejs/node/pull/49089) +* \[[`609b25fa99`](https://github.com/nodejs/node/commit/609b25fa99)] - **stream**: implement ReadableStream.from (Debadree Chatterjee) [#48395](https://github.com/nodejs/node/pull/48395) +* \[[`750cca2738`](https://github.com/nodejs/node/commit/750cca2738)] - **test**: use `tmpdir.resolve()` (Livia Medeiros) [#49128](https://github.com/nodejs/node/pull/49128) +* \[[`6595367649`](https://github.com/nodejs/node/commit/6595367649)] - **test**: use `tmpdir.resolve()` (Livia Medeiros) [#49127](https://github.com/nodejs/node/pull/49127) +* \[[`661b055e75`](https://github.com/nodejs/node/commit/661b055e75)] - **test**: use `tmpdir.resolve()` in fs tests (Livia Medeiros) [#49126](https://github.com/nodejs/node/pull/49126) +* \[[`b3c56d206f`](https://github.com/nodejs/node/commit/b3c56d206f)] - **test**: use `tmpdir.resolve()` in fs tests (Livia Medeiros) [#49125](https://github.com/nodejs/node/pull/49125) +* \[[`3ddb155d16`](https://github.com/nodejs/node/commit/3ddb155d16)] - **test**: fix assertion message in test\_async.c (Tobias Nießen) [#49146](https://github.com/nodejs/node/pull/49146) +* \[[`1d17c1032d`](https://github.com/nodejs/node/commit/1d17c1032d)] - **test**: refactor `test-esm-loader-hooks` for easier debugging (Antoine du Hamel) [#49131](https://github.com/nodejs/node/pull/49131) +* \[[`13bd7a0293`](https://github.com/nodejs/node/commit/13bd7a0293)] - **test**: add `tmpdir.resolve()` (Livia Medeiros) [#49079](https://github.com/nodejs/node/pull/49079) +* \[[`89b1bce56d`](https://github.com/nodejs/node/commit/89b1bce56d)] - **test**: document `fixtures.fileURL()` (Livia Medeiros) [#49083](https://github.com/nodejs/node/pull/49083) +* \[[`2fcb855c76`](https://github.com/nodejs/node/commit/2fcb855c76)] - **test**: reduce flakiness of `test-esm-loader-hooks` (Antoine du Hamel) [#49105](https://github.com/nodejs/node/pull/49105) +* \[[`7816e040df`](https://github.com/nodejs/node/commit/7816e040df)] - **test**: stabilize the inspector-open-dispose test (Chemi Atlow) [#49000](https://github.com/nodejs/node/pull/49000) +* \[[`e70e9747e4`](https://github.com/nodejs/node/commit/e70e9747e4)] - **test**: print instruction for creating missing snapshot in assertSnapshot (Raz Luvaton) [#48914](https://github.com/nodejs/node/pull/48914) +* \[[`669ac03520`](https://github.com/nodejs/node/commit/669ac03520)] - **test**: add `tmpdir.fileURL()` (Livia Medeiros) [#49040](https://github.com/nodejs/node/pull/49040) +* \[[`b945d7be35`](https://github.com/nodejs/node/commit/b945d7be35)] - **test**: use `spawn` and `spawnPromisified` instead of `exec` (Antoine du Hamel) [#48991](https://github.com/nodejs/node/pull/48991) +* \[[`b3a7427583`](https://github.com/nodejs/node/commit/b3a7427583)] - **test**: refactor `test-node-output-errors` (Antoine du Hamel) [#48992](https://github.com/nodejs/node/pull/48992) +* \[[`6c3e5c4d69`](https://github.com/nodejs/node/commit/6c3e5c4d69)] - **test**: use `fixtures.fileURL` when appropriate (Antoine du Hamel) [#48990](https://github.com/nodejs/node/pull/48990) +* \[[`9138b78bcb`](https://github.com/nodejs/node/commit/9138b78bcb)] - **test**: validate error code rather than message (Antoine du Hamel) [#48972](https://github.com/nodejs/node/pull/48972) +* \[[`b4ca4a6f80`](https://github.com/nodejs/node/commit/b4ca4a6f80)] - **test**: fix snapshot tests when cwd contains spaces or backslashes (Antoine du Hamel) [#48959](https://github.com/nodejs/node/pull/48959) +* \[[`d4398d458c`](https://github.com/nodejs/node/commit/d4398d458c)] - **test**: order `common.mjs` in ASCII order (Antoine du Hamel) [#48960](https://github.com/nodejs/node/pull/48960) +* \[[`b5991f5250`](https://github.com/nodejs/node/commit/b5991f5250)] - **test**: fix some assumptions in tests (Antoine du Hamel) [#48958](https://github.com/nodejs/node/pull/48958) +* \[[`62e23f83f9`](https://github.com/nodejs/node/commit/62e23f83f9)] - **test**: improve internal/worker/io.js coverage (Yoshiki Kurihara) [#42387](https://github.com/nodejs/node/pull/42387) +* \[[`314bd6095c`](https://github.com/nodejs/node/commit/314bd6095c)] - **test**: fix `es-module/test-esm-initialization` (Antoine du Hamel) [#48880](https://github.com/nodejs/node/pull/48880) +* \[[`3680a66df4`](https://github.com/nodejs/node/commit/3680a66df4)] - **test**: validate host with commas on url.parse (Yagiz Nizipli) [#48878](https://github.com/nodejs/node/pull/48878) +* \[[`24c3742372`](https://github.com/nodejs/node/commit/24c3742372)] - **test**: delete test-net-bytes-per-incoming-chunk-overhead (Michaël Zasso) [#48811](https://github.com/nodejs/node/pull/48811) +* \[[`e01cce50f5`](https://github.com/nodejs/node/commit/e01cce50f5)] - **test**: skip experimental test with pointer compression (Colin Ihrig) [#48738](https://github.com/nodejs/node/pull/48738) +* \[[`d5e93b1074`](https://github.com/nodejs/node/commit/d5e93b1074)] - **test**: fix flaky test-string-decode.js on x86 (Stefan Stojanovic) [#48750](https://github.com/nodejs/node/pull/48750) +* \[[`9136667d7d`](https://github.com/nodejs/node/commit/9136667d7d)] - **test\_runner**: dont set exit code on todo tests (Moshe Atlow) [#48929](https://github.com/nodejs/node/pull/48929) +* \[[`52c94908c0`](https://github.com/nodejs/node/commit/52c94908c0)] - **test\_runner**: fix todo and only in spec reporter (Moshe Atlow) [#48929](https://github.com/nodejs/node/pull/48929) +* \[[`5ccfb8d515`](https://github.com/nodejs/node/commit/5ccfb8d515)] - **test\_runner**: unwrap error message in TAP reporter (Colin Ihrig) [#48942](https://github.com/nodejs/node/pull/48942) +* \[[`fa19b0ed05`](https://github.com/nodejs/node/commit/fa19b0ed05)] - **test\_runner**: add `__proto__` null (Raz Luvaton) [#48663](https://github.com/nodejs/node/pull/48663) +* \[[`65d23940bf`](https://github.com/nodejs/node/commit/65d23940bf)] - **test\_runner**: fix async callback in describe not awaited (Raz Luvaton) [#48856](https://github.com/nodejs/node/pull/48856) +* \[[`4bd5e55b43`](https://github.com/nodejs/node/commit/4bd5e55b43)] - **test\_runner**: fix test\_runner `test:fail` event type (Ethan Arrowood) [#48854](https://github.com/nodejs/node/pull/48854) +* \[[`41058beed8`](https://github.com/nodejs/node/commit/41058beed8)] - **test\_runner**: call abort on test finish (Raz Luvaton) [#48827](https://github.com/nodejs/node/pull/48827) +* \[[`821b11a59f`](https://github.com/nodejs/node/commit/821b11a59f)] - **tls**: fix bugs of double TLS (rogertyang) [#48969](https://github.com/nodejs/node/pull/48969) +* \[[`4439327e73`](https://github.com/nodejs/node/commit/4439327e73)] - **tools**: update lint-md-dependencies (Node.js GitHub Bot) [#49122](https://github.com/nodejs/node/pull/49122) +* \[[`21dc844309`](https://github.com/nodejs/node/commit/21dc844309)] - **tools**: use spec reporter in actions (Moshe Atlow) [#49129](https://github.com/nodejs/node/pull/49129) +* \[[`3471758696`](https://github.com/nodejs/node/commit/3471758696)] - **tools**: use @reporters/github when running in github actions (Moshe Atlow) [#49129](https://github.com/nodejs/node/pull/49129) +* \[[`95a6e7661e`](https://github.com/nodejs/node/commit/95a6e7661e)] - **tools**: add @reporters/github to tools (Moshe Atlow) [#49129](https://github.com/nodejs/node/pull/49129) +* \[[`995cbf93eb`](https://github.com/nodejs/node/commit/995cbf93eb)] - **tools**: update eslint to 8.47.0 (Node.js GitHub Bot) [#49124](https://github.com/nodejs/node/pull/49124) +* \[[`ed065bc56e`](https://github.com/nodejs/node/commit/ed065bc56e)] - **tools**: update lint-md-dependencies to rollup\@3.27.2 (Node.js GitHub Bot) [#49035](https://github.com/nodejs/node/pull/49035) +* \[[`a5f37178ad`](https://github.com/nodejs/node/commit/a5f37178ad)] - **tools**: limit the number of auto start CIs (Antoine du Hamel) [#49067](https://github.com/nodejs/node/pull/49067) +* \[[`c1bd680f89`](https://github.com/nodejs/node/commit/c1bd680f89)] - **tools**: update eslint to 8.46.0 (Node.js GitHub Bot) [#48966](https://github.com/nodejs/node/pull/48966) +* \[[`e09a6b4821`](https://github.com/nodejs/node/commit/e09a6b4821)] - **tools**: update lint-md-dependencies to rollup\@3.27.0 (Node.js GitHub Bot) [#48965](https://github.com/nodejs/node/pull/48965) +* \[[`0cd2393bd9`](https://github.com/nodejs/node/commit/0cd2393bd9)] - **tools**: update lint-md-dependencies to rollup\@3.26.3 (Node.js GitHub Bot) [#48888](https://github.com/nodejs/node/pull/48888) +* \[[`41929a2906`](https://github.com/nodejs/node/commit/41929a2906)] - **tools**: update lint-md-dependencies to @rollup/plugin-commonjs\@25.0.3 (Node.js GitHub Bot) [#48791](https://github.com/nodejs/node/pull/48791) +* \[[`1761bdfbd9`](https://github.com/nodejs/node/commit/1761bdfbd9)] - **tools**: update eslint to 8.45.0 (Node.js GitHub Bot) [#48793](https://github.com/nodejs/node/pull/48793) +* \[[`b82f05cc4b`](https://github.com/nodejs/node/commit/b82f05cc4b)] - **typings**: update JSDoc for `cwd` in `child_process` (LiviaMedeiros) [#49029](https://github.com/nodejs/node/pull/49029) +* \[[`be7b511255`](https://github.com/nodejs/node/commit/be7b511255)] - **typings**: sync JSDoc with the actual implementation (Hyunjin Kim) [#48853](https://github.com/nodejs/node/pull/48853) +* \[[`45c860035d`](https://github.com/nodejs/node/commit/45c860035d)] - **url**: overload `canParse` V8 fast api method (Yagiz Nizipli) [#48993](https://github.com/nodejs/node/pull/48993) +* \[[`60d614157b`](https://github.com/nodejs/node/commit/60d614157b)] - **url**: fix `isURL` detection by checking `path` (Zhuo Zhang) [#48928](https://github.com/nodejs/node/pull/48928) +* \[[`b12c3b5240`](https://github.com/nodejs/node/commit/b12c3b5240)] - **url**: ensure getter access do not mutate observable symbols (Antoine du Hamel) [#48897](https://github.com/nodejs/node/pull/48897) +* \[[`30fb7b7535`](https://github.com/nodejs/node/commit/30fb7b7535)] - **url**: reduce `pathToFileURL` cpp calls (Yagiz Nizipli) [#48709](https://github.com/nodejs/node/pull/48709) +* \[[`c3dbd0c1e4`](https://github.com/nodejs/node/commit/c3dbd0c1e4)] - **util**: use `primordials.ArrayPrototypeIndexOf` instead of mutable method (DaisyDogs07) [#48586](https://github.com/nodejs/node/pull/48586) +* \[[`b79b2927ca`](https://github.com/nodejs/node/commit/b79b2927ca)] - **watch**: decrease debounce rate (Moshe Atlow) [#48926](https://github.com/nodejs/node/pull/48926) +* \[[`a12996298e`](https://github.com/nodejs/node/commit/a12996298e)] - **watch**: use debounce instead of throttle (Moshe Atlow) [#48926](https://github.com/nodejs/node/pull/48926) + ## 2023-08-09, Version 20.5.1 (Current), @RafaelGSS diff --git a/doc/contributing/collaborator-guide.md b/doc/contributing/collaborator-guide.md index 0ad07f73144a78..a4218acc7d4fd0 100644 --- a/doc/contributing/collaborator-guide.md +++ b/doc/contributing/collaborator-guide.md @@ -540,6 +540,12 @@ For pull requests from first-time contributors, be [welcoming](#welcoming-first-time-contributors). Also, verify that their git settings are to their liking. +If a pull request contains more than one commit, it can be landed either by +squashing into one commit or by rebasing all the commits, or a mix of the two. +Generally, a collaborator should land a pull request by squashing. If a pull +request has more than one self-contained subsystem commits, a collaborator +may land it as several commits. + All commits should be self-contained, meaning every commit should pass all tests. This makes it much easier when bisecting to find a breaking change. diff --git a/doc/contributing/maintaining/maintaining-dependencies.md b/doc/contributing/maintaining/maintaining-dependencies.md index 831d28cf9dbe03..aa93b638cce060 100644 --- a/doc/contributing/maintaining/maintaining-dependencies.md +++ b/doc/contributing/maintaining/maintaining-dependencies.md @@ -9,13 +9,13 @@ All dependencies are located within the `deps` directory. This a list of all the dependencies: * [acorn 8.10.0][] -* [ada 2.5.0][] +* [ada 2.6.0][] * [base64 0.5.0][] * [brotli 1.0.9][] * [c-ares 1.19.0][] * [cjs-module-lexer 1.2.2][] * [corepack][] -* [googletest ec4fed9][] +* [googletest 7e33b6a][] * [histogram 0.11.8][] * [icu-small 73.2][] * [libuv 1.46.0][] @@ -27,11 +27,11 @@ This a list of all the dependencies: * [npm 9.6.7][] * [openssl 3.0.8][] * [postject 1.0.0-alpha.6][] -* [simdutf 3.2.14][] -* [undici 5.22.1][] +* [simdutf 3.2.17][] +* [undici 5.23.0][] * [uvwasi 0.0.16][] * [V8 11.3.244.8][] -* [zlib 1.2.13.1-motley-f81f385][] +* [zlib 1.2.13.1-motley-526382e][] Any code which meets one or more of these conditions should be managed as a dependency: @@ -150,7 +150,7 @@ The [acorn](https://github.com/acornjs/acorn) dependency is a JavaScript parser. [acorn-walk](https://github.com/acornjs/acorn/tree/master/acorn-walk) is an abstract syntax tree walker for the ESTree format. -### ada 2.5.0 +### ada 2.6.0 The [ada](https://github.com/ada-url/ada) dependency is a fast and spec-compliant URL parser written in C++. @@ -189,7 +189,7 @@ In practical terms, Corepack will let you use Yarn and pnpm without having to install them - just like what currently happens with npm, which is shipped by Node.js by default. -### googletest ec4fed9 +### googletest 7e33b6a The [googletest](https://github.com/google/googletest) dependency is Google’s C++ testing and mocking framework. @@ -286,12 +286,12 @@ See [maintaining-openssl][] for more informations. The [postject](https://github.com/nodejs/postject) dependency is used for the [Single Executable strategic initiative](https://github.com/nodejs/single-executable). -### simdutf 3.2.14 +### simdutf 3.2.17 The [simdutf](https://github.com/simdutf/simdutf) dependency is a C++ library for fast UTF-8 decoding and encoding. -### undici 5.22.1 +### undici 5.23.0 The [undici](https://github.com/nodejs/undici) dependency is an HTTP/1.1 client, written from scratch for Node.js.. @@ -311,7 +311,7 @@ See [maintaining-web-assembly][] for more informations. high-performance JavaScript and WebAssembly engine, written in C++. See [maintaining-V8][] for more informations. -### zlib 1.2.13.1-motley-f81f385 +### zlib 1.2.13.1-motley-526382e The [zlib](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/zlib) dependency lossless data-compression library, @@ -319,14 +319,14 @@ it comes from the Chromium team's zlib fork which incorporated performance improvements not currently available in standard zlib. [acorn 8.10.0]: #acorn-8100 -[ada 2.5.0]: #ada-250 +[ada 2.6.0]: #ada-260 [base64 0.5.0]: #base64-050 [brotli 1.0.9]: #brotli-109 [c-ares 1.19.0]: #c-ares-1190 [cjs-module-lexer 1.2.2]: #cjs-module-lexer-122 [corepack]: #corepack [dependency-update-action]: ../../../.github/workflows/tools.yml -[googletest ec4fed9]: #googletest-ec4fed9 +[googletest 7e33b6a]: #googletest-7e33b6a [histogram 0.11.8]: #histogram-0118 [icu-small 73.2]: #icu-small-732 [libuv 1.46.0]: #libuv-1460 @@ -344,9 +344,9 @@ performance improvements not currently available in standard zlib. [npm 9.6.7]: #npm-967 [openssl 3.0.8]: #openssl-308 [postject 1.0.0-alpha.6]: #postject-100-alpha6 -[simdutf 3.2.14]: #simdutf-3214 -[undici 5.22.1]: #undici-5221 +[simdutf 3.2.17]: #simdutf-3217 +[undici 5.23.0]: #undici-5230 [update-openssl-action]: ../../../.github/workflows/update-openssl.yml [uvwasi 0.0.16]: #uvwasi-0016 [v8 11.3.244.8]: #v8-1132448 -[zlib 1.2.13.1-motley-f81f385]: #zlib-12131-motley-f81f385 +[zlib 1.2.13.1-motley-526382e]: #zlib-12131-motley-526382e diff --git a/doc/contributing/maintaining/maintaining-openssl.md b/doc/contributing/maintaining/maintaining-openssl.md index b6de7ae340ef01..2814634dd76a03 100644 --- a/doc/contributing/maintaining/maintaining-openssl.md +++ b/doc/contributing/maintaining/maintaining-openssl.md @@ -13,8 +13,6 @@ currently need to generate four PRs as follows: of this guide. * a PR for 16.x following the instructions in the v16.x-staging version of this guide. -* a PR for 14.x following the instructions in the [v14.x-staging version][] - of this guide. ## Use of the quictls/openssl fork @@ -158,4 +156,3 @@ regenerated and committed by: Finally, build Node.js and run the tests. [update-openssl-action]: ../../../.github/workflows/update-openssl.yml -[v14.x-staging version]: https://github.com/nodejs/node/blob/v14.x-staging/doc/guides/maintaining-openssl.md diff --git a/doc/contributing/releases.md b/doc/contributing/releases.md index 00fd536c2a0a50..27907bdaeb7492 100644 --- a/doc/contributing/releases.md +++ b/doc/contributing/releases.md @@ -813,7 +813,9 @@ git commit --amend Even if there are no conflicts, ensure that you revert all the changes that were -made to `src/node_version.h`. +made to `src/node_version.h`. `NODE_VERSION_IS_RELEASE` must be `0`. + +Edit `src/node_version.h`, revert `NODE_VERSION_IS_RELEASE` back to `0`, and `git commit --amend` If there are conflicts in `doc` due to updated `REPLACEME` placeholders (that happens when a change previously landed on another release diff --git a/doc/contributing/security-release-process.md b/doc/contributing/security-release-process.md index 1f54aae33f6cd6..557ff8b7a9ec2b 100644 --- a/doc/contributing/security-release-process.md +++ b/doc/contributing/security-release-process.md @@ -29,7 +29,7 @@ The current security stewards are documented in the main Node.js | NodeSource | Juan | 2022-Nov-04 | | RH and IBM | Michael | 2023-Feb-16 | | NearForm | Rafael | 2023-Jun-20 | -| NearForm | Rafael | | +| NearForm | Rafael | 2023-Aug-09 | | Datadog | Bryan | | | IBM | Joe | | | Platformatic | Matteo | | @@ -70,8 +70,6 @@ The current security stewards are documented in the main Node.js ## Announcement (one week in advance of the planned release) -* [ ] Verify that GitHub Actions are working as normal: . - * [ ] Check that all vulnerabilities are ready for release integration: * PRs against all affected release lines or cherry-pick clean * Approved @@ -117,7 +115,7 @@ The google groups UI does not support adding a CC, until we figure out a better way, forward the email you receive to `oss-security@lists.openwall.com` as a CC. -* [ ] Create a new issue in [nodejs/tweet][] +* [ ] Send a message to `#nodejs-social` in OpenJS Foundation slack ```text Security release pre-alert: diff --git a/lib/_http_server.js b/lib/_http_server.js index 0242e7a089dd6f..c62ea17599512f 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -500,14 +500,16 @@ function storeHTTPOptions(options) { } } -function setupConnectionsTracking(server) { +function setupConnectionsTracking() { // Start connection handling - server[kConnections] = new ConnectionsList(); + if (!this[kConnections]) { + this[kConnections] = new ConnectionsList(); + } // This checker is started without checking whether any headersTimeout or requestTimeout is non zero // otherwise it would not be started if such timeouts are modified after createServer. - server[kConnectionsCheckingInterval] = - setInterval(checkConnections.bind(server), server.connectionsCheckingInterval).unref(); + this[kConnectionsCheckingInterval] = + setInterval(checkConnections.bind(this), this.connectionsCheckingInterval).unref(); } function httpServerPreClose(server) { @@ -545,11 +547,12 @@ function Server(options, requestListener) { this.httpAllowHalfOpen = false; this.on('connection', connectionListener); + this.on('listening', setupConnectionsTracking); this.timeout = 0; this.maxHeadersCount = null; this.maxRequestsPerSocket = 0; - setupConnectionsTracking(this); + this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders); } ObjectSetPrototypeOf(Server.prototype, net.Server.prototype); @@ -565,6 +568,10 @@ Server.prototype[SymbolAsyncDispose] = async function() { }; Server.prototype.closeAllConnections = function() { + if (!this[kConnections]) { + return; + } + const connections = this[kConnections].all(); for (let i = 0, l = connections.length; i < l; i++) { @@ -573,6 +580,10 @@ Server.prototype.closeAllConnections = function() { }; Server.prototype.closeIdleConnections = function() { + if (!this[kConnections]) { + return; + } + const connections = this[kConnections].idle(); for (let i = 0, l = connections.length; i < l; i++) { diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index 1975e823ee588e..909f36dd00fe15 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -545,17 +545,28 @@ function TLSSocket(socket, opts) { this[kPendingSession] = null; let wrap; - if ((socket instanceof net.Socket && socket._handle) || !socket) { - // 1. connected socket - // 2. no socket, one will be created with net.Socket().connect - wrap = socket; + let handle; + let wrapHasActiveWriteFromPrevOwner; + + if (socket) { + if (socket instanceof net.Socket && socket._handle) { + // 1. connected socket + wrap = socket; + } else { + // 2. socket has no handle so it is js not c++ + // 3. unconnected sockets are wrapped + // TLS expects to interact from C++ with a net.Socket that has a C++ stream + // handle, but a JS stream doesn't have one. Wrap it up to make it look like + // a socket. + wrap = new JSStreamSocket(socket); + } + + handle = wrap._handle; + wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0; } else { - // 3. socket has no handle so it is js not c++ - // 4. unconnected sockets are wrapped - // TLS expects to interact from C++ with a net.Socket that has a C++ stream - // handle, but a JS stream doesn't have one. Wrap it up to make it look like - // a socket. - wrap = new JSStreamSocket(socket); + // 4. no socket, one will be created with net.Socket().connect + wrap = null; + wrapHasActiveWriteFromPrevOwner = false; } // Just a documented property to make secure sockets @@ -563,7 +574,7 @@ function TLSSocket(socket, opts) { this.encrypted = true; ReflectApply(net.Socket, this, [{ - handle: this._wrapHandle(wrap), + handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner), allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen, pauseOnCreate: tlsOptions.pauseOnConnect, manualStart: true, @@ -582,6 +593,21 @@ function TLSSocket(socket, opts) { if (enableTrace && this._handle) this._handle.enableTrace(); + if (wrapHasActiveWriteFromPrevOwner) { + // `wrap` is a streams.Writable in JS. This empty write will be queued + // and hence finish after all existing writes, which is the timing + // we want to start to send any tls data to `wrap`. + wrap.write('', (err) => { + if (err) { + debug('error got before writing any tls data to the underlying stream'); + this.destroy(err); + return; + } + + this._handle.writesIssuedByPrevListenerDone(); + }); + } + // Read on next tick so the caller has a chance to setup listeners process.nextTick(initRead, this, socket); } @@ -642,11 +668,14 @@ TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() { this[kDisableRenegotiation] = true; }; -TLSSocket.prototype._wrapHandle = function(wrap, handle) { - if (!handle && wrap) { - handle = wrap._handle; - } - +/** + * + * @param {null|net.Socket} wrap + * @param {null|object} handle + * @param {boolean} wrapHasActiveWriteFromPrevOwner + * @returns {object} + */ +TLSSocket.prototype._wrapHandle = function(wrap, handle, wrapHasActiveWriteFromPrevOwner) { const options = this._tlsOptions; if (!handle) { handle = options.pipe ? @@ -663,7 +692,10 @@ TLSSocket.prototype._wrapHandle = function(wrap, handle) { if (!(context.context instanceof NativeSecureContext)) { throw new ERR_TLS_INVALID_CONTEXT('context'); } - const res = tls_wrap.wrap(handle, context.context, !!options.isServer); + + const res = tls_wrap.wrap(handle, context.context, + !!options.isServer, + wrapHasActiveWriteFromPrevOwner); res._parent = handle; // C++ "wrap" object: TCPWrap, JSStream, ... res._parentWrap = wrap; // JS object: net.Socket, JSStreamSocket, ... res._secureContext = context; @@ -680,7 +712,7 @@ TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) { const originalServername = this.ssl ? this._handle.getServername() : null; const originalSession = this.ssl ? this._handle.getSession() : null; - this.handle = this._wrapHandle(null, handle); + this.handle = this._wrapHandle(null, handle, false); this.ssl = this._handle; net.Socket.prototype[kReinitializeHandle].call(this, this.handle); diff --git a/lib/child_process.js b/lib/child_process.js index c32756437833b6..449013906e93e5 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -103,7 +103,7 @@ let addAbortListener; * @param {string|URL} modulePath * @param {string[]} [args] * @param {{ - * cwd?: string; + * cwd?: string | URL; * detached?: boolean; * env?: Record; * execPath?: string; @@ -138,7 +138,7 @@ function fork(modulePath, args = [], options) { if (options != null) { validateObject(options, 'options'); } - options = { ...options, shell: false }; + options = { __proto__: null, ...options, shell: false }; options.execPath = options.execPath || process.execPath; validateArgumentNullCheck(options.execPath, 'options.execPath'); @@ -196,7 +196,7 @@ function normalizeExecArgs(command, options, callback) { } // Make a shallow copy so we don't clobber the user's options object. - options = { ...options }; + options = { __proto__: null, ...options }; options.shell = typeof options.shell === 'string' ? options.shell : true; return { @@ -305,7 +305,7 @@ function normalizeExecFileArgs(file, args, options, callback) { * @param {string} file * @param {string[]} [args] * @param {{ - * cwd?: string; + * cwd?: string | URL; * env?: Record; * encoding?: string; * timeout?: number; @@ -329,6 +329,7 @@ function execFile(file, args, options, callback) { ({ file, args, options, callback } = normalizeExecFileArgs(file, args, options, callback)); options = { + __proto__: null, encoding: 'utf8', timeout: 0, maxBuffer: MAX_BUFFER, @@ -703,6 +704,7 @@ function normalizeSpawnArguments(file, args, options) { return { // Make a shallow copy so we don't clobber the user's options object. + __proto__: null, ...options, args, cwd, @@ -731,7 +733,7 @@ function abortChildProcess(child, killSignal, reason) { * @param {string} file * @param {string[]} [args] * @param {{ - * cwd?: string; + * cwd?: string | URL; * env?: Record; * argv0?: string; * stdio?: Array | string; @@ -801,7 +803,7 @@ function spawn(file, args, options) { * @param {string} file * @param {string[]} [args] * @param {{ - * cwd?: string; + * cwd?: string | URL; * input?: string | Buffer | TypedArray | DataView; * argv0?: string; * stdio?: string | Array; @@ -828,6 +830,7 @@ function spawn(file, args, options) { */ function spawnSync(file, args, options) { options = { + __proto__: null, maxBuffer: MAX_BUFFER, ...normalizeSpawnArguments(file, args, options), }; @@ -894,7 +897,7 @@ function checkExecSyncError(ret, args, cmd) { * @param {string} file * @param {string[]} [args] * @param {{ - * cwd?: string; + * cwd?: string | URL; * input?: string | Buffer | TypedArray | DataView; * stdio?: string | Array; * env?: Record; @@ -932,7 +935,7 @@ function execFileSync(file, args, options) { * Spawns a shell executing the given `command` synchronously. * @param {string} command * @param {{ - * cwd?: string; + * cwd?: string | URL; * input?: string | Buffer | TypedArray | DataView; * stdio?: string | Array; * env?: Record; diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index b399c1e2f87ad3..dae0e930a395e9 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -136,7 +136,7 @@ class ActiveChannel { } publish(data) { - for (let i = 0; i < this._subscribers.length; i++) { + for (let i = 0; i < (this._subscribers?.length || 0); i++) { try { const onMessage = this._subscribers[i]; onMessage(data, this.name); diff --git a/lib/fs.js b/lib/fs.js index ddef598126ae16..616ce0aacf8c3b 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -105,7 +105,6 @@ const { getValidatedPath, getValidMode, handleErrorFromBinding, - nullCheck, possiblyTransformPath, preprocessSymlinkDestination, Stats, @@ -1420,14 +1419,12 @@ function mkdirSync(path, options) { /** * An iterative algorithm for reading the entire contents of the `basePath` directory. * This function does not validate `basePath` as a directory. It is passed directly to - * `binding.readdir` after a `nullCheck`. + * `binding.readdir`. * @param {string} basePath * @param {{ encoding: string, withFileTypes: boolean }} options * @returns {string[] | Dirent[]} */ function readdirSyncRecursive(basePath, options) { - nullCheck(basePath, 'path', true); - const withFileTypes = Boolean(options.withFileTypes); const encoding = options.encoding; @@ -1446,14 +1443,21 @@ function readdirSyncRecursive(basePath, options) { ); handleErrorFromBinding(ctx); - for (let i = 0; i < readdirResult.length; i++) { - if (withFileTypes) { + if (withFileTypes) { + // Calling `readdir` with `withFileTypes=true`, the result is an array of arrays. + // The first array is the names, and the second array is the types. + // They are guaranteed to be the same length; hence, setting `length` to the length + // of the first array within the result. + const length = readdirResult[0].length; + for (let i = 0; i < length; i++) { const dirent = getDirent(path, readdirResult[0][i], readdirResult[1][i]); ArrayPrototypePush(readdirResults, dirent); if (dirent.isDirectory()) { ArrayPrototypePush(pathsQueue, pathModule.join(dirent.path, dirent.name)); } - } else { + } + } else { + for (let i = 0; i < readdirResult.length; i++) { const resultPath = pathModule.join(path, readdirResult[i]); const relativeResultPath = pathModule.relative(basePath, resultPath); const stat = binding.internalModuleStat(resultPath); @@ -2902,7 +2906,7 @@ realpath.native = (path, options, callback) => { /** * Creates a unique temporary directory. - * @param {string} prefix + * @param {string | Buffer | URL} prefix * @param {string | { encoding?: string; }} [options] * @param {( * err?: Error, @@ -2914,29 +2918,40 @@ function mkdtemp(prefix, options, callback) { callback = makeCallback(typeof options === 'function' ? options : callback); options = getOptions(options); - validateString(prefix, 'prefix'); - nullCheck(prefix, 'prefix'); prefix = getValidatedPath(prefix, 'prefix'); warnOnNonPortableTemplate(prefix); + + let path; + if (typeof prefix === 'string') { + path = `${prefix}XXXXXX`; + } else { + path = Buffer.concat([prefix, Buffer.from('XXXXXX')]); + } + const req = new FSReqCallback(); req.oncomplete = callback; - binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, req); + binding.mkdtemp(path, options.encoding, req); } /** * Synchronously creates a unique temporary directory. - * @param {string} prefix + * @param {string | Buffer | URL} prefix * @param {string | { encoding?: string; }} [options] * @returns {string} */ function mkdtempSync(prefix, options) { options = getOptions(options); - validateString(prefix, 'prefix'); - nullCheck(prefix, 'prefix'); prefix = getValidatedPath(prefix, 'prefix'); warnOnNonPortableTemplate(prefix); - const path = `${prefix}XXXXXX`; + + let path; + if (typeof prefix === 'string') { + path = `${prefix}XXXXXX`; + } else { + path = Buffer.concat([prefix, Buffer.from('XXXXXX')]); + } + const ctx = { path }; const result = binding.mkdtemp(path, options.encoding, undefined, ctx); diff --git a/lib/https.js b/lib/https.js index d8b42c85493f7e..70ffa73ff1996b 100644 --- a/lib/https.js +++ b/lib/https.js @@ -96,8 +96,9 @@ function Server(opts, requestListener) { this.timeout = 0; this.maxHeadersCount = null; - setupConnectionsTracking(this); + this.on('listening', setupConnectionsTracking); } + ObjectSetPrototypeOf(Server.prototype, tls.Server.prototype); ObjectSetPrototypeOf(Server, tls.Server); diff --git a/lib/inspector.js b/lib/inspector.js index 567d825c4f6a72..70796c83fcffe3 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -5,6 +5,7 @@ const { JSONStringify, SafeMap, Symbol, + SymbolDispose, } = primordials; const { @@ -181,6 +182,8 @@ function inspectorOpen(port, host, wait) { open(port, host); if (wait) waitForDebugger(); + + return { __proto__: null, [SymbolDispose]() { _debugEnd(); } }; } /** diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 0d0e9906dbdb31..167c0521b4573d 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -361,6 +361,11 @@ class Blob { queueMicrotask(() => { if (c.desiredSize <= 0) { // A manual backpressure check. + if (this.pendingPulls.length !== 0) { + // A case of waiting pull finished (= not yet canceled) + const pending = this.pendingPulls.shift(); + pending.resolve(); + } return; } readNext(); diff --git a/lib/internal/debugger/inspect.js b/lib/internal/debugger/inspect.js index 24e5c0bd401f01..5e93699f8ba078 100644 --- a/lib/internal/debugger/inspect.js +++ b/lib/internal/debugger/inspect.js @@ -338,7 +338,7 @@ function startInspect(argv = ArrayPrototypeSlice(process.argv, 2), process.stderr.write(`Usage: ${invokedAs} script.js\n` + ` ${invokedAs} :\n` + - ` ${invokedAs} --port=\n` + + ` ${invokedAs} --port= Use 0 for random port assignment\n` + ` ${invokedAs} -p \n`); process.exit(kInvalidCommandLineArgument); } diff --git a/lib/internal/errors.js b/lib/internal/errors.js index ac2e62147e41d1..bed02da76a3bab 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1453,8 +1453,12 @@ E('ERR_MISSING_ARGS', return `${msg} must be specified`; }, TypeError); E('ERR_MISSING_OPTION', '%s is required', TypeError); -E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => { - return `Cannot find ${type} '${path}' imported from ${base}`; +E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) { + if (exactUrl) { + lazyInternalUtil().setOwnProperty(this, 'url', exactUrl); + } + return `Cannot find ${ + exactUrl ? 'module' : 'package'} '${path}' imported from ${base}`; }, Error); E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); @@ -1696,8 +1700,11 @@ E('ERR_UNKNOWN_FILE_EXTENSION', (ext, path, suggestion) => { E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s for URL %s', RangeError); E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); -E('ERR_UNSUPPORTED_DIR_IMPORT', "Directory import '%s' is not supported " + -'resolving ES modules imported from %s', Error); +E('ERR_UNSUPPORTED_DIR_IMPORT', function(path, base, exactUrl) { + lazyInternalUtil().setOwnProperty(this, 'url', exactUrl); + return `Directory import '${path}' is not supported ` + + `resolving ES modules imported from ${base}`; +}, Error); E('ERR_UNSUPPORTED_ESM_URL_SCHEME', (url, supported) => { let msg = `Only URLs with a scheme in: ${formatList(supported)} are supported by the default ESM loader`; if (isWindows && url.protocol.length === 2) { diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js index 2da787d6388603..177f2a939d2c45 100644 --- a/lib/internal/event_target.js +++ b/lib/internal/event_target.js @@ -62,6 +62,7 @@ const kWeakHandler = Symbol('kWeak'); const kResistStopPropagation = Symbol('kResistStopPropagation'); const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch'); +const kRemoveWeakListenerHelper = Symbol('nodejs.internal.removeWeakListenerHelper'); const kCreateEvent = Symbol('kCreateEvent'); const kNewListener = Symbol('kNewListener'); const kRemoveListener = Symbol('kRemoveListener'); @@ -410,7 +411,7 @@ let weakListenersState = null; let objectToWeakListenerMap = null; function weakListeners() { weakListenersState ??= new SafeFinalizationRegistry( - (listener) => listener.remove(), + ({ eventTarget, listener, eventType }) => eventTarget.deref()?.[kRemoveWeakListenerHelper](eventType, listener), ); objectToWeakListenerMap ??= new SafeWeakMap(); return { registry: weakListenersState, map: objectToWeakListenerMap }; @@ -432,7 +433,7 @@ const kFlagResistStopPropagation = 1 << 6; // the linked list makes dispatching faster, even if adding/removing is // slower. class Listener { - constructor(previous, listener, once, capture, passive, + constructor(eventTarget, eventType, previous, listener, once, capture, passive, isNodeStyleListener, weak, resistStopPropagation) { this.next = undefined; if (previous !== undefined) @@ -459,7 +460,13 @@ class Listener { if (this.weak) { this.callback = new SafeWeakRef(listener); - weakListeners().registry.register(listener, this, this); + weakListeners().registry.register(listener, { + __proto__: null, + // Weak ref so the listener won't hold the eventTarget alive + eventTarget: new SafeWeakRef(eventTarget), + listener: this, + eventType, + }, this); // Make the retainer retain the listener in a WeakMap weakListeners().map.set(weak, listener); this.listener = this.callback; @@ -625,7 +632,7 @@ class EventTarget { if (root === undefined) { root = { size: 1, next: undefined, resistStopPropagation: Boolean(resistStopPropagation) }; // This is the first handler in our linked list. - new Listener(root, listener, once, capture, passive, + new Listener(this, type, root, listener, once, capture, passive, isNodeStyleListener, weak, resistStopPropagation); this[kNewListener]( root.size, @@ -652,7 +659,7 @@ class EventTarget { return; } - new Listener(previous, listener, once, capture, passive, + new Listener(this, type, previous, listener, once, capture, passive, isNodeStyleListener, weak, resistStopPropagation); root.size++; root.resistStopPropagation ||= Boolean(resistStopPropagation); @@ -695,6 +702,28 @@ class EventTarget { } } + [kRemoveWeakListenerHelper](type, listener) { + const root = this[kEvents].get(type); + if (root === undefined || root.next === undefined) + return; + + const capture = listener.capture === true; + + let handler = root.next; + while (handler !== undefined) { + if (handler === listener) { + handler.remove(); + root.size--; + if (root.size === 0) + this[kEvents].delete(type); + // Undefined is passed as the listener as the listener was GCed + this[kRemoveListener](root.size, type, undefined, capture); + break; + } + handler = handler.next; + } + } + /** * @param {Event} event */ diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 1a5618b6955bb8..af7eb0d44b8421 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -59,7 +59,6 @@ const { getStatsFromBinding, getValidatedPath, getValidMode, - nullCheck, preprocessSymlinkDestination, stringToFlags, stringToSymlinkType, @@ -254,6 +253,9 @@ class FileHandle extends EventEmitterMixin(JSTransferable) { /** * @typedef {import('../webstreams/readablestream').ReadableStream * } ReadableStream + * @param {{ + * type?: string; + * }} [options] * @returns {ReadableStream} */ readableWebStream(options = kEmptyObject) { @@ -993,10 +995,17 @@ async function realpath(path, options) { async function mkdtemp(prefix, options) { options = getOptions(options); - validateString(prefix, 'prefix'); - nullCheck(prefix); + prefix = getValidatedPath(prefix, 'prefix'); warnOnNonPortableTemplate(prefix); - return binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, kUsePromises); + + let path; + if (typeof prefix === 'string') { + path = `${prefix}XXXXXX`; + } else { + path = Buffer.concat([prefix, Buffer.from('XXXXXX')]); + } + + return binding.mkdtemp(path, options.encoding, kUsePromises); } async function writeFile(path, data, options) { diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index a3f5f0df8ca85f..6e6c7ee58cf5d1 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -20,6 +20,7 @@ const { StringPrototypeEndsWith, StringPrototypeIncludes, Symbol, + TypedArrayPrototypeAt, TypedArrayPrototypeIncludes, } = primordials; @@ -375,7 +376,7 @@ const nullCheck = hideStackFrames((path, propName, throwError = true) => { const err = new ERR_INVALID_ARG_VALUE( propName, path, - 'must be a string or Uint8Array without null bytes', + 'must be a string, Uint8Array, or URL without null bytes', ); if (throwError) { throw err; @@ -751,7 +752,9 @@ let nonPortableTemplateWarn = true; function warnOnNonPortableTemplate(template) { // Template strings passed to the mkdtemp() family of functions should not // end with 'X' because they are handled inconsistently across platforms. - if (nonPortableTemplateWarn && StringPrototypeEndsWith(template, 'X')) { + if (nonPortableTemplateWarn && + ((typeof template === 'string' && StringPrototypeEndsWith(template, 'X')) || + (typeof template !== 'string' && TypedArrayPrototypeAt(template, -1) === 0x58))) { process.emitWarning('mkdtemp() templates ending with X are not portable. ' + 'For details see: https://nodejs.org/api/fs.html'); nonPortableTemplateWarn = false; diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index ec6a2d51af5450..dc59a2ce4f7709 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -24,7 +24,7 @@ markBootstrapComplete(); const source = getOptionValue('--eval'); const print = getOptionValue('--print'); -const loadESM = getOptionValue('--import').length > 0; +const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0; if (getOptionValue('--input-type') === 'module') evalModule(source, print); else { diff --git a/lib/internal/main/mksnapshot.js b/lib/internal/main/mksnapshot.js index 2207d9253d7ec4..52d859d491a93f 100644 --- a/lib/internal/main/mksnapshot.js +++ b/lib/internal/main/mksnapshot.js @@ -16,6 +16,10 @@ const { anonymousMainPath, } = internalBinding('mksnapshot'); +const { isExperimentalSeaWarningNeeded } = internalBinding('sea'); + +const { emitExperimentalWarning } = require('internal/util'); + const { getOptionValue, } = require('internal/options'); @@ -126,6 +130,7 @@ function requireForUserSnapshot(id) { return require(normalizedId); } + function main() { prepareMainThreadExecution(true, false); initializeCallbacks(); @@ -167,6 +172,10 @@ function main() { const serializeMainArgs = [process, requireForUserSnapshot, minimalRunCjs]; + if (isExperimentalSeaWarningNeeded()) { + emitExperimentalWarning('Single executable application'); + } + if (getOptionValue('--inspect-brk')) { internalBinding('inspector').callAndPauseOnStart( runEmbedderEntryPoint, undefined, ...serializeMainArgs); diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index 064731d77bede1..4974f4ce0d338b 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -58,6 +58,8 @@ if (shardOption) { } run({ concurrency, inspectPort, watch: getOptionValue('--watch'), setup: setupTestReporters, shard }) -.once('test:fail', () => { - process.exitCode = kGenericUserError; +.on('test:fail', (data) => { + if (data.todo === undefined || data.todo === false) { + process.exitCode = kGenericUserError; + } }); diff --git a/lib/internal/main/watch_mode.js b/lib/internal/main/watch_mode.js index 5d0d29cc2ff9da..4fae6363226310 100644 --- a/lib/internal/main/watch_mode.js +++ b/lib/internal/main/watch_mode.js @@ -44,7 +44,7 @@ const args = ArrayPrototypeFilter(process.execArgv, (arg, i, arr) => arg !== '--watch' && arg !== '--watch-preserve-output'); ArrayPrototypePushApply(args, kCommand); -const watcher = new FilesWatcher({ throttle: 500, mode: kShouldFilterModules ? 'filter' : 'all' }); +const watcher = new FilesWatcher({ debounce: 200, mode: kShouldFilterModules ? 'filter' : 'all' }); ArrayPrototypeForEach(kWatchedPaths, (p) => watcher.watchPath(p)); let graceTimer; diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 6e847d0a5c6033..19a7d7e671f5ab 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1123,7 +1123,7 @@ Module.prototype.require = function(id) { let resolvedArgv; let hasPausedEntry = false; let Script; -function wrapSafe(filename, content, cjsModuleInstance) { +function wrapSafe(filename, content, cjsModuleInstance, codeCache) { if (patched) { const wrapper = Module.wrap(content); if (Script === undefined) { @@ -1158,6 +1158,7 @@ function wrapSafe(filename, content, cjsModuleInstance) { '__dirname', ], { filename, + cachedData: codeCache, importModuleDynamically(specifier, _, importAssertions) { const cascadedLoader = getCascadedLoader(); return cascadedLoader.import(specifier, normalizeReferrerURL(filename), @@ -1165,6 +1166,13 @@ function wrapSafe(filename, content, cjsModuleInstance) { }, }); + // The code cache is used for SEAs only. + if (codeCache && + result.cachedDataRejected !== false && + internalBinding('sea').isSea()) { + process.emitWarning('Code cache data rejected.'); + } + // Cache the source map for the module if present. if (result.sourceMapURL) { maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL); diff --git a/lib/internal/modules/esm/fetch_module.js b/lib/internal/modules/esm/fetch_module.js index 74d2d2599dbd45..ca5c9c83c316de 100644 --- a/lib/internal/modules/esm/fetch_module.js +++ b/lib/internal/modules/esm/fetch_module.js @@ -144,7 +144,7 @@ function fetchWithRedirects(parsed) { return entry; } if (res.statusCode === 404) { - const err = new ERR_MODULE_NOT_FOUND(parsed.href, null); + const err = new ERR_MODULE_NOT_FOUND(parsed.href, null, parsed); err.message = `Cannot find module '${parsed.href}', HTTP 404`; throw err; } diff --git a/lib/internal/modules/esm/formats.js b/lib/internal/modules/esm/formats.js index 63742914597c46..c4181b50f10451 100644 --- a/lib/internal/modules/esm/formats.js +++ b/lib/internal/modules/esm/formats.js @@ -26,7 +26,7 @@ if (experimentalWasmModules) { function mimeToFormat(mime) { if ( RegExpPrototypeExec( - /\s*(text|application)\/javascript\s*(;\s*charset=utf-?8\s*)?/i, + /^\s*(text|application)\/javascript\s*(;\s*charset=utf-?8\s*)?$/i, mime, ) !== null ) return 'module'; diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index 9bbf75ce745b60..b7e1afac31060d 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -2,6 +2,7 @@ const { ArrayPrototypePush, + ArrayPrototypePushApply, FunctionPrototypeCall, Int32Array, ObjectAssign, @@ -10,6 +11,7 @@ const { Promise, SafeSet, StringPrototypeSlice, + StringPrototypeStartsWith, StringPrototypeToUpperCase, globalThis, } = primordials; @@ -30,6 +32,8 @@ const { ERR_INVALID_RETURN_PROPERTY_VALUE, ERR_INVALID_RETURN_VALUE, ERR_LOADER_CHAIN_INCOMPLETE, + ERR_METHOD_NOT_IMPLEMENTED, + ERR_UNKNOWN_BUILTIN_MODULE, ERR_WORKER_UNSERIALIZABLE_ERROR, } = require('internal/errors').codes; const { exitCodes: { kUnfinishedTopLevelAwait } } = internalBinding('errors'); @@ -44,6 +48,10 @@ const { validateObject, validateString, } = require('internal/validators'); +const { + emitExperimentalWarning, + kEmptyObject, +} = require('internal/util'); const { defaultResolve, @@ -61,7 +69,7 @@ const { let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { debug = fn; }); - +let importMetaInitializer; /** * @typedef {object} ExportedHooks @@ -78,7 +86,6 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { // [2] `validate...()`s throw the wrong error - class Hooks { #chains = { /** @@ -117,28 +124,57 @@ class Hooks { // Cache URLs we've already validated to avoid repeated validation #validatedUrls = new SafeSet(); + allowImportMetaResolve = false; + + /** + * Import and register custom/user-defined module loader hook(s). + * @param {string} urlOrSpecifier + * @param {string} parentURL + * @param {any} [data] Arbitrary data to be passed from the custom + * loader (user-land) to the worker. + */ + async register(urlOrSpecifier, parentURL, data) { + const moduleLoader = require('internal/process/esm_loader').esmLoader; + const keyedExports = await moduleLoader.import( + urlOrSpecifier, + parentURL, + kEmptyObject, + ); + return this.addCustomLoader(urlOrSpecifier, keyedExports, data); + } + /** * Collect custom/user-defined module loader hook(s). * After all hooks have been collected, the global preload hook(s) must be initialized. * @param {string} url Custom loader specifier * @param {Record} exports + * @param {any} [data] Arbitrary data to be passed from the custom loader (user-land) + * to the worker. + * @returns {any} The result of the loader's `initialize` hook, if provided. */ - addCustomLoader(url, exports) { + addCustomLoader(url, exports, data) { const { globalPreload, + initialize, resolve, load, } = pluckHooks(exports); - if (globalPreload) { - ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url }); + if (globalPreload && !initialize) { + emitExperimentalWarning( + '`globalPreload` is planned for removal in favor of `initialize`. `globalPreload`', + ); + ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url }); } if (resolve) { - ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url }); + const next = this.#chains.resolve[this.#chains.resolve.length - 1]; + ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next }); } if (load) { - ArrayPrototypePush(this.#chains.load, { fn: load, url }); + const next = this.#chains.load[this.#chains.load.length - 1]; + ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next }); } + return initialize?.(data); } /** @@ -214,7 +250,6 @@ class Hooks { chainFinished: null, context, hookErrIdentifier: '', - hookIndex: chain.length - 1, hookName: 'resolve', shortCircuited: false, }; @@ -237,7 +272,7 @@ class Hooks { } }; - const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput }); + const nextResolve = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput }); const resolution = await nextResolve(originalSpecifier, context); const { hookErrIdentifier } = meta; // Retrieve the value after all settled @@ -314,6 +349,10 @@ class Hooks { }; } + resolveSync(_originalSpecifier, _parentURL, _importAssertions) { + throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()'); + } + /** * Provide source that is understood by one of Node's translators. * @@ -330,7 +369,6 @@ class Hooks { chainFinished: null, context, hookErrIdentifier: '', - hookIndex: chain.length - 1, hookName: 'load', shortCircuited: false, }; @@ -372,7 +410,7 @@ class Hooks { } }; - const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput }); + const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput }); const loaded = await nextLoad(url, context); const { hookErrIdentifier } = meta; // Retrieve the value after all settled @@ -447,6 +485,16 @@ class Hooks { source, }; } + + forceLoadHooks() { + // No-op + } + + importMetaInitialize(meta, context, loader) { + importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta; + meta = importMetaInitializer(meta, context, loader); + return meta; + } } ObjectSetPrototypeOf(Hooks.prototype, null); @@ -502,14 +550,14 @@ class HooksProxy { this.#worker.on('exit', process.exit); } - #waitForWorker() { + waitForWorker() { if (!this.#isReady) { const { kIsOnline } = require('internal/worker'); if (!this.#worker[kIsOnline]) { debug('wait for signal from worker'); AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0); const response = this.#worker.receiveMessageSync(); - if (response.message.status === 'exit') { return; } + if (response == null || response.message.status === 'exit') { return; } const { preloadScripts } = this.#unwrapMessage(response); this.#executePreloadScripts(preloadScripts); } @@ -518,15 +566,30 @@ class HooksProxy { } } - async makeAsyncRequest(method, ...args) { - this.#waitForWorker(); + /** + * Invoke a remote method asynchronously. + * @param {string} method Method to invoke + * @param {any[]} [transferList] Objects in `args` to be transferred + * @param {any[]} args Arguments to pass to `method` + * @returns {Promise} + */ + async makeAsyncRequest(method, transferList, ...args) { + this.waitForWorker(); MessageChannel ??= require('internal/worker/io').MessageChannel; const asyncCommChannel = new MessageChannel(); // Pass work to the worker. - debug('post async message to worker', { method, args }); - this.#worker.postMessage({ method, args, port: asyncCommChannel.port2 }, [asyncCommChannel.port2]); + debug('post async message to worker', { method, args, transferList }); + const finalTransferList = [asyncCommChannel.port2]; + if (transferList) { + ArrayPrototypePushApply(finalTransferList, transferList); + } + this.#worker.postMessage({ + __proto__: null, + method, args, + port: asyncCommChannel.port2, + }, finalTransferList); if (this.#numberOfPendingAsyncResponses++ === 0) { // On the next lines, the main thread will await a response from the worker thread that might @@ -558,12 +621,19 @@ class HooksProxy { return body; } - makeSyncRequest(method, ...args) { - this.#waitForWorker(); + /** + * Invoke a remote method synchronously. + * @param {string} method Method to invoke + * @param {any[]} [transferList] Objects in `args` to be transferred + * @param {any[]} args Arguments to pass to `method` + * @returns {any} + */ + makeSyncRequest(method, transferList, ...args) { + this.waitForWorker(); // Pass work to the worker. - debug('post sync message to worker', { method, args }); - this.#worker.postMessage({ method, args }); + debug('post sync message to worker', { method, args, transferList }); + this.#worker.postMessage({ __proto__: null, method, args }, transferList); let response; do { @@ -601,35 +671,66 @@ class HooksProxy { } } + #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; + + importMetaInitialize(meta, context, loader) { + this.#importMetaInitializer(meta, context, loader); + } + #executePreloadScripts(preloadScripts) { for (let i = 0; i < preloadScripts.length; i++) { const { code, port } = preloadScripts[i]; const { compileFunction } = require('vm'); const preloadInit = compileFunction( code, - ['getBuiltin', 'port'], + ['getBuiltin', 'port', 'setImportMetaCallback'], { filename: '', }, ); + let finished = false; + let replacedImportMetaInitializer = false; + let next = this.#importMetaInitializer; const { BuiltinModule } = require('internal/bootstrap/realm'); // Calls the compiled preload source text gotten from the hook // Since the parameters are named we use positional parameters // see compileFunction above to cross reference the names - FunctionPrototypeCall( - preloadInit, - globalThis, - // Param getBuiltin - (builtinName) => { - if (BuiltinModule.canBeRequiredByUsers(builtinName) && - BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { - return require(builtinName); - } - throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); - }, - // Param port - port, - ); + try { + FunctionPrototypeCall( + preloadInit, + globalThis, + // Param getBuiltin + (builtinName) => { + if (StringPrototypeStartsWith(builtinName, 'node:')) { + builtinName = StringPrototypeSlice(builtinName, 5); + } else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { + throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); + } + if (BuiltinModule.canBeRequiredByUsers(builtinName)) { + return require(builtinName); + } + throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); + }, + // Param port + port, + // setImportMetaCallback + (fn) => { + if (finished || typeof fn !== 'function') { + throw new ERR_INVALID_ARG_TYPE('fn', fn); + } + replacedImportMetaInitializer = true; + const parent = next; + next = (meta, context) => { + return fn(meta, context, parent); + }; + }, + ); + } finally { + finished = true; + if (replacedImportMetaInitializer) { + this.#importMetaInitializer = next; + } + } } } } @@ -642,6 +743,7 @@ ObjectSetPrototypeOf(HooksProxy.prototype, null); */ function pluckHooks({ globalPreload, + initialize, resolve, load, }) { @@ -657,6 +759,10 @@ function pluckHooks({ acceptedHooks.load = load; } + if (initialize) { + acceptedHooks.initialize = initialize; + } + return acceptedHooks; } @@ -665,15 +771,14 @@ function pluckHooks({ * A utility function to iterate through a hook chain, track advancement in the * chain, and generate and supply the `next` argument to the custom * hook. - * @param {KeyedHook[]} chain The whole hook chain. + * @param {Hook} current The (currently) first hook in the chain (this shifts + * on every call). * @param {object} meta Properties that change as the current hook advances * along the chain. * @param {boolean} meta.chainFinished Whether the end of the chain has been * reached AND invoked. * @param {string} meta.hookErrIdentifier A user-facing identifier to help * pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'". - * @param {number} meta.hookIndex A non-negative integer tracking the current - * position in the hook chain. * @param {string} meta.hookName The kind of hook the chain is (ex 'resolve') * @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit. * @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function @@ -681,13 +786,14 @@ function pluckHooks({ * validation within MUST throw. * @returns {function next(...hookArgs)} The next hook in the chain. */ -function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { +function nextHookFactory(current, meta, { validateArgs, validateOutput }) { // First, prepare the current const { hookName } = meta; const { fn: hook, url: hookFilePath, - } = chain[meta.hookIndex]; + next, + } = current; // ex 'nextResolve' const nextHookName = `next${ @@ -695,16 +801,9 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { StringPrototypeSlice(hookName, 1) }`; - // When hookIndex is 0, it's reached the default, which does not call next() - // so feed it a noop that blows up if called, so the problem is obvious. - const generatedHookIndex = meta.hookIndex; let nextNextHook; - if (meta.hookIndex > 0) { - // Now, prepare the next: decrement the pointer so the next call to the - // factory generates the next link in the chain. - meta.hookIndex--; - - nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput }); + if (next) { + nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput }); } else { // eslint-disable-next-line func-name-matching nextNextHook = function chainAdvancedTooFar() { @@ -721,17 +820,16 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context); - const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`; + const outputErrIdentifier = `${hookFilePath} '${hookName}' hook's ${nextHookName}()`; // Set when next is actually called, not just generated. - if (generatedHookIndex === 0) { meta.chainFinished = true; } + if (!next) { meta.chainFinished = true; } if (context) { // `context` has already been validated, so no fancy check needed. ObjectAssign(meta.context, context); } const output = await hook(arg0, meta.context, nextNextHook); - validateOutput(outputErrIdentifier, output); if (output?.shortCircuit === true) { meta.shortCircuited = true; } diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index c548f71bef837a..f55f60a5b7647a 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -5,25 +5,38 @@ const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta /** * Generate a function to be used as import.meta.resolve for a particular module. - * @param {string} defaultParentUrl The default base to use for resolution + * @param {string} defaultParentURL The default base to use for resolution * @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader - * @returns {(specifier: string, parentUrl?: string) => string} Function to assign to import.meta.resolve + * @param {bool} allowParentURL Whether to permit parentURL second argument for contextual resolution + * @returns {(specifier: string) => string} Function to assign to import.meta.resolve */ -function createImportMetaResolve(defaultParentUrl, loader) { - return function resolve(specifier, parentUrl = defaultParentUrl) { +function createImportMetaResolve(defaultParentURL, loader, allowParentURL) { + /** + * @param {string} specifier + * @param {URL['href']} [parentURL] When `--experimental-import-meta-resolve` is specified, a + * second argument can be provided. + */ + return function resolve(specifier, parentURL = defaultParentURL) { let url; + if (!allowParentURL) { + parentURL = defaultParentURL; + } + try { - ({ url } = loader.resolve(specifier, parentUrl)); + ({ url } = loader.resolveSync(specifier, parentURL)); + return url; } catch (error) { - if (error?.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { - ({ url } = error); - } else { - throw error; + switch (error?.code) { + case 'ERR_UNSUPPORTED_DIR_IMPORT': + case 'ERR_MODULE_NOT_FOUND': + ({ url } = error); + if (url) { + return url; + } } + throw error; } - - return url; }; } @@ -38,8 +51,8 @@ function initializeImportMeta(meta, context, loader) { const { url } = context; // Alphabetical - if (experimentalImportMetaResolve && loader.loaderType !== 'internal') { - meta.resolve = createImportMetaResolve(url, loader); + if (!loader || loader.allowImportMetaResolve) { + meta.resolve = createImportMetaResolve(url, loader, experimentalImportMetaResolve); } meta.url = url; diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js index fbd86e2881c0f0..1998ed1dab67fb 100644 --- a/lib/internal/modules/esm/load.js +++ b/lib/internal/modules/esm/load.js @@ -10,6 +10,7 @@ const { kEmptyObject } = require('internal/util'); const { defaultGetFormat } = require('internal/modules/esm/get_format'); const { validateAssertions } = require('internal/modules/esm/assert'); const { getOptionValue } = require('internal/options'); +const { readFileSync } = require('fs'); // Do not eagerly grab .manifest, it may be in TDZ const policy = getOptionValue('--experimental-policy') ? @@ -69,12 +70,35 @@ async function getSource(url, context) { return { __proto__: null, responseURL, source }; } +function getSourceSync(url, context) { + const parsed = new URL(url); + const responseURL = url; + let source; + if (parsed.protocol === 'file:') { + source = readFileSync(parsed); + } else if (parsed.protocol === 'data:') { + const match = RegExpPrototypeExec(DATA_URL_PATTERN, parsed.pathname); + if (!match) { + throw new ERR_INVALID_URL(url); + } + const { 1: base64, 2: body } = match; + source = BufferFrom(decodeURIComponent(body), base64 ? 'base64' : 'utf8'); + } else { + const supportedSchemes = ['file', 'data']; + throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, supportedSchemes); + } + if (policy?.manifest) { + policy.manifest.assertIntegrity(parsed, source); + } + return { __proto__: null, responseURL, source }; +} + /** * Node.js default load hook. * @param {string} url - * @param {object} context - * @returns {object} + * @param {LoadContext} context + * @returns {LoadReturn} */ async function defaultLoad(url, context = kEmptyObject) { let responseURL = url; @@ -108,6 +132,51 @@ async function defaultLoad(url, context = kEmptyObject) { source, }; } +/** + * @typedef LoadContext + * @property {string} [format] A hint (possibly returned from `resolve`) + * @property {string | Buffer | ArrayBuffer} [source] source + * @property {Record} [importAssertions] import attributes + */ + +/** + * @typedef LoadReturn + * @property {string} format format + * @property {URL['href']} responseURL The module's fully resolved URL + * @property {Buffer} source source + */ + +/** + * @param {URL['href']} url + * @param {LoadContext} [context] + * @returns {LoadReturn} + */ +function defaultLoadSync(url, context = kEmptyObject) { + let responseURL = url; + const { importAssertions } = context; + let { + format, + source, + } = context; + + format ??= defaultGetFormat(new URL(url), context); + + validateAssertions(url, format, importAssertions); + + if (format === 'builtin') { + source = null; + } else if (source == null) { + ({ responseURL, source } = getSourceSync(url, context)); + } + + return { + __proto__: null, + format, + responseURL, + source, + }; +} + /** * throws an error if the protocol is not one of the protocols @@ -160,5 +229,6 @@ function throwUnknownModuleFormat(url, format) { module.exports = { defaultLoad, + defaultLoadSync, throwUnknownModuleFormat, }; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 0651f775eeafe0..5305c1eb8f37d9 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -6,11 +6,11 @@ require('internal/modules/cjs/loader'); const { FunctionPrototypeCall, ObjectSetPrototypeOf, - PromisePrototypeThen, SafeWeakMap, } = primordials; const { + ERR_REQUIRE_ESM, ERR_UNKNOWN_MODULE_FORMAT, } = require('internal/errors').codes; const { getOptionValue } = require('internal/options'); @@ -19,11 +19,16 @@ const { emitExperimentalWarning } = require('internal/util'); const { getDefaultConditions, } = require('internal/modules/esm/utils'); -let defaultResolve, defaultLoad, importMetaInitializer; +let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer; -function newModuleMap() { - const ModuleMap = require('internal/modules/esm/module_map'); - return new ModuleMap(); +function newResolveCache() { + const { ResolveCache } = require('internal/modules/esm/module_map'); + return new ResolveCache(); +} + +function newLoadCache() { + const { LoadCache } = require('internal/modules/esm/module_map'); + return new LoadCache(); } function getTranslators() { @@ -51,12 +56,12 @@ let hooksProxy; * @typedef {ArrayBuffer|TypedArray|string} ModuleSource */ - /** - * This class covers the default case of an module loader instance where no custom user loaders are used. - * The below CustomizedModuleLoader class extends this one to support custom user loader hooks. + * This class covers the base machinery of module loading. To add custom + * behavior you can pass a customizations object and this object will be + * used to do the loading/resolving/registration process. */ -class DefaultModuleLoader { +class ModuleLoader { /** * The conditions for resolving packages if `--conditions` is not used. */ @@ -72,10 +77,15 @@ class DefaultModuleLoader { */ evalIndex = 0; + /** + * Registry of resolved specifiers + */ + #resolveCache = newResolveCache(); + /** * Registry of loaded modules, akin to `require.cache` */ - moduleMap = newModuleMap(); + loadCache = newLoadCache(); /** * Methods which translate input code or other information into ES modules @@ -83,15 +93,85 @@ class DefaultModuleLoader { translators = getTranslators(); /** - * Type of loader. - * @type {'default' | 'internal'} + * Truthy to allow the use of `import.meta.resolve`. This is needed + * currently because the `Hooks` class does not have `resolveSync` + * implemented and `import.meta.resolve` requires it. */ - loaderType = 'default'; + allowImportMetaResolve; - constructor() { + /** + * Customizations to pass requests to. + * + * Note that this value _MUST_ be set with `setCustomizations` + * because it needs to copy `customizations.allowImportMetaResolve` + * to this property and failure to do so will cause undefined + * behavior when invoking `import.meta.resolve`. + * @see {ModuleLoader.setCustomizations} + */ + #customizations; + + constructor(customizations) { if (getOptionValue('--experimental-network-imports')) { emitExperimentalWarning('Network Imports'); } + this.setCustomizations(customizations); + } + + /** + * Change the currently activate customizations for this module + * loader to be the provided `customizations`. + * + * If present, this class customizes its core functionality to the + * `customizations` object, including registration, loading, and resolving. + * There are some responsibilities that this class _always_ takes + * care of, like validating outputs, so that the customizations object + * does not have to do so. + * + * The customizations object has the shape: + * + * ```ts + * interface LoadResult { + * format: ModuleFormat; + * source: ModuleSource; + * } + * + * interface ResolveResult { + * format: string; + * url: URL['href']; + * } + * + * interface Customizations { + * allowImportMetaResolve: boolean; + * load(url: string, context: object): Promise + * resolve( + * originalSpecifier: + * string, parentURL: string, + * importAssertions: Record + * ): Promise + * resolveSync( + * originalSpecifier: + * string, parentURL: string, + * importAssertions: Record + * ) ResolveResult; + * register(specifier: string, parentURL: string): any; + * forceLoadHooks(): void; + * } + * ``` + * + * Note that this class _also_ implements the `Customizations` + * interface, as does `CustomizedModuleLoader` and `Hooks`. + * + * Calling this function alters how modules are loaded and should be + * invoked with care. + * @param {object} customizations + */ + setCustomizations(customizations) { + this.#customizations = customizations; + if (customizations) { + this.allowImportMetaResolve = customizations.allowImportMetaResolve; + } else { + this.allowImportMetaResolve = true; + } } async eval( @@ -114,7 +194,7 @@ class DefaultModuleLoader { const ModuleJob = require('internal/modules/esm/module_job'); const job = new ModuleJob( this, url, undefined, evalInstance, false, false); - this.moduleMap.set(url, undefined, job); + this.loadCache.set(url, undefined, job); const { module } = await job.run(); return { @@ -134,26 +214,30 @@ class DefaultModuleLoader { * point. * @param {Record} importAssertions Validations for the * module import. - * @returns {ModuleJob} The (possibly pending) module job + * @returns {Promise} The (possibly pending) module job */ - getModuleJob(specifier, parentURL, importAssertions) { - const resolveResult = this.resolve(specifier, parentURL, importAssertions); + async getModuleJob(specifier, parentURL, importAssertions) { + const resolveResult = await this.resolve(specifier, parentURL, importAssertions); return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); } - getJobFromResolveResult(resolveResult, parentURL, importAssertions) { + getModuleJobSync(specifier, parentURL, importAssertions) { + const resolveResult = this.resolveSync(specifier, parentURL, importAssertions); + return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions, true); + } + + getJobFromResolveResult(resolveResult, parentURL, importAssertions, sync) { const { url, format } = resolveResult; const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions; - - let job = this.moduleMap.get(url, resolvedImportAssertions.type); + let job = this.loadCache.get(url, resolvedImportAssertions.type); // CommonJS will set functions for lazy job evaluation. if (typeof job === 'function') { - this.moduleMap.set(url, undefined, job = job()); + this.loadCache.set(url, undefined, job = job()); } if (job === undefined) { - job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format); + job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format, sync); } return job; @@ -170,17 +254,8 @@ class DefaultModuleLoader { * `resolve` hook * @returns {Promise} The (possibly pending) module job */ - #createModuleJob(url, importAssertions, parentURL, format) { - const moduleProvider = async (url, isMain) => { - const { - format: finalFormat, - responseURL, - source, - } = await this.load(url, { - format, - importAssertions, - }); - + #createModuleJob(url, importAssertions, parentURL, format, sync) { + const callTranslator = ({ format: finalFormat, responseURL, source }, isMain) => { const translator = getTranslators().get(finalFormat); if (!translator) { @@ -189,6 +264,10 @@ class DefaultModuleLoader { return FunctionPrototypeCall(translator, this, responseURL, source, isMain); }; + const context = { format, importAssertions }; + const moduleProvider = sync ? + (url, isMain) => callTranslator(this.loadSync(url, context), isMain) : + async (url, isMain) => callTranslator(await this.load(url, context), isMain); const inspectBrk = ( parentURL === undefined && @@ -207,9 +286,10 @@ class DefaultModuleLoader { moduleProvider, parentURL === undefined, inspectBrk, + sync, ); - this.moduleMap.set(url, importAssertions.type, job); + this.loadCache.set(url, importAssertions.type, job); return job; } @@ -224,11 +304,25 @@ class DefaultModuleLoader { * @returns {Promise} */ async import(specifier, parentURL, importAssertions) { - const moduleJob = this.getModuleJob(specifier, parentURL, importAssertions); + const moduleJob = await this.getModuleJob(specifier, parentURL, importAssertions); const { module } = await moduleJob.run(); return module.getNamespace(); } + /** + * @see {@link CustomizedModuleLoader.register} + */ + register(specifier, parentURL, data, transferList) { + if (!this.#customizations) { + // `CustomizedModuleLoader` is defined at the bottom of this file and + // available well before this line is ever invoked. This is here in + // order to preserve the git diff instead of moving the class. + // eslint-disable-next-line no-use-before-define + this.setCustomizations(new CustomizedModuleLoader()); + } + return this.#customizations.register(specifier, parentURL, data, transferList); + } + /** * Resolve the location of the module. * @param {string} originalSpecifier The specified URL path of the module to @@ -239,6 +333,36 @@ class DefaultModuleLoader { * @returns {{ format: string, url: URL['href'] }} */ resolve(originalSpecifier, parentURL, importAssertions) { + if (this.#customizations) { + return this.#customizations.resolve(originalSpecifier, parentURL, importAssertions); + } + const requestKey = this.#resolveCache.serializeKey(originalSpecifier, importAssertions); + const cachedResult = this.#resolveCache.get(requestKey, parentURL); + if (cachedResult != null) { + return cachedResult; + } + const result = this.defaultResolve(originalSpecifier, parentURL, importAssertions); + this.#resolveCache.set(requestKey, parentURL, result); + return result; + } + + /** + * Just like `resolve` except synchronous. This is here specifically to support + * `import.meta.resolve` which must happen synchronously. + */ + resolveSync(originalSpecifier, parentURL, importAssertions) { + if (this.#customizations) { + return this.#customizations.resolveSync(originalSpecifier, parentURL, importAssertions); + } + return this.defaultResolve(originalSpecifier, parentURL, importAssertions); + } + + /** + * Our `defaultResolve` is synchronous and can be used in both + * `resolve` and `resolveSync`. This function is here just to avoid + * repeating the same code block twice in those functions. + */ + defaultResolve(originalSpecifier, parentURL, importAssertions) { defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve; const context = { @@ -259,12 +383,31 @@ class DefaultModuleLoader { */ async load(url, context) { defaultLoad ??= require('internal/modules/esm/load').defaultLoad; - - const result = await defaultLoad(url, context); + const result = this.#customizations ? + await this.#customizations.load(url, context) : + await defaultLoad(url, context); this.validateLoadResult(url, result?.format); return result; } + loadSync(url, context) { + defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync; + + let result = this.#customizations ? + this.#customizations.loadSync(url, context) : + defaultLoadSync(url, context); + let format = result?.format; + if (format === 'module') { + throw new ERR_REQUIRE_ESM(url, true); + } + if (format === 'commonjs') { + format = 'require-commonjs'; + result = { __proto__: result, format }; + } + this.validateLoadResult(url, format); + return result; + } + validateLoadResult(url, format) { if (format == null) { require('internal/modules/esm/load').throwUnknownModuleFormat(url, format); @@ -272,27 +415,47 @@ class DefaultModuleLoader { } importMetaInitialize(meta, context) { + if (this.#customizations) { + return this.#customizations.importMetaInitialize(meta, context, this); + } importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta; meta = importMetaInitializer(meta, context, this); return meta; } + + /** + * No-op when no hooks have been supplied. + */ + forceLoadHooks() { + this.#customizations?.forceLoadHooks(); + } } -ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null); +ObjectSetPrototypeOf(ModuleLoader.prototype, null); + +class CustomizedModuleLoader { + allowImportMetaResolve = true; -class CustomizedModuleLoader extends DefaultModuleLoader { /** * Instantiate a module loader that uses user-provided custom loader hooks. */ constructor() { - super(); + getHooksProxy(); + } - if (hooksProxy) { - // The worker proxy is shared across all instances, so don't recreate it if it already exists. - return; - } - const { HooksProxy } = require('internal/modules/esm/hooks'); - hooksProxy = new HooksProxy(); // The user's custom hooks are loaded within the worker as part of its startup. + /** + * Register some loader specifier. + * @param {string} originalSpecifier The specified URL path of the loader to + * be registered. + * @param {string} parentURL The parent URL from where the loader will be + * registered if using it package name as specifier + * @param {any} [data] Arbitrary data to be passed from the custom loader + * (user-land) to the worker. + * @param {any[]} [transferList] Objects in `data` that are changing ownership + * @returns {{ format: string, url: URL['href'] }} + */ + register(originalSpecifier, parentURL, data, transferList) { + return hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data); } /** @@ -305,28 +468,12 @@ class CustomizedModuleLoader extends DefaultModuleLoader { * @returns {{ format: string, url: URL['href'] }} */ resolve(originalSpecifier, parentURL, importAssertions) { - return hooksProxy.makeSyncRequest('resolve', originalSpecifier, parentURL, importAssertions); - } - - async #getModuleJob(specifier, parentURL, importAssertions) { - const resolveResult = await hooksProxy.makeAsyncRequest('resolve', specifier, parentURL, importAssertions); - - return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); + return hooksProxy.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAssertions); } - getModuleJob(specifier, parentURL, importAssertions) { - const jobPromise = this.#getModuleJob(specifier, parentURL, importAssertions); - return { - run() { - return PromisePrototypeThen(jobPromise, (job) => job.run()); - }, - get modulePromise() { - return PromisePrototypeThen(jobPromise, (job) => job.modulePromise); - }, - get linked() { - return PromisePrototypeThen(jobPromise, (job) => job.linked); - }, - }; + resolveSync(originalSpecifier, parentURL, importAssertions) { + // This happens only as a result of `import.meta.resolve` calls, which must be sync per spec. + return hooksProxy.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAssertions); } /** @@ -335,14 +482,21 @@ class CustomizedModuleLoader extends DefaultModuleLoader { * @param {object} [context] Metadata about the module * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>} */ - async load(url, context) { - const result = await hooksProxy.makeAsyncRequest('load', url, context); - this.validateLoadResult(url, result?.format); + load(url, context) { + return hooksProxy.makeAsyncRequest('load', undefined, url, context); + } + loadSync(url, context) { + return hooksProxy.makeSyncRequest('load', undefined, url, context); + } - return result; + importMetaInitialize(meta, context, loader) { + hooksProxy.importMetaInitialize(meta, context, loader); } -} + forceLoadHooks() { + hooksProxy.waitForWorker(); + } +} let emittedExperimentalWarning = false; /** @@ -350,9 +504,10 @@ let emittedExperimentalWarning = false; * only one used for loading the main module and everything in its dependency graph, though separate instances of this * class might be instantiated as part of bootstrap for other purposes. * @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them. - * @returns {DefaultModuleLoader | CustomizedModuleLoader} + * @returns {ModuleLoader} */ function createModuleLoader(useCustomLoadersIfPresent = true) { + let customizations = null; if (useCustomLoadersIfPresent && // Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader; // doing so would cause an infinite loop. @@ -363,14 +518,72 @@ function createModuleLoader(useCustomLoadersIfPresent = true) { emitExperimentalWarning('Custom ESM Loaders'); emittedExperimentalWarning = true; } - return new CustomizedModuleLoader(); + customizations = new CustomizedModuleLoader(); } } - return new DefaultModuleLoader(); + return new ModuleLoader(customizations); +} + + +/** + * Get the HooksProxy instance. If it is not defined, then create a new one. + * @returns {HooksProxy} + */ +function getHooksProxy() { + if (!hooksProxy) { + const { HooksProxy } = require('internal/modules/esm/hooks'); + hooksProxy = new HooksProxy(); + } + + return hooksProxy; +} + +/** + * Register a single loader programmatically. + * @param {string} specifier + * @param {string} [parentURL] Base to use when resolving `specifier`; optional if + * `specifier` is absolute. Same as `options.parentUrl`, just inline + * @param {object} [options] Additional options to apply, described below. + * @param {string} [options.parentURL] Base to use when resolving `specifier` + * @param {any} [options.data] Arbitrary data passed to the loader's `initialize` hook + * @param {any[]} [options.transferList] Objects in `data` that are changing ownership + * @returns {any} The result of the loader's initialize hook, if any + * @example + * ```js + * register('./myLoader.js'); + * register('ts-node/esm', { parentURL: import.meta.url }); + * register('./myLoader.js', { parentURL: import.meta.url }); + * register('ts-node/esm', import.meta.url); + * register('./myLoader.js', import.meta.url); + * register(new URL('./myLoader.js', import.meta.url)); + * register('./myLoader.js', { + * parentURL: import.meta.url, + * data: { banana: 'tasty' }, + * }); + * register('./myLoader.js', { + * parentURL: import.meta.url, + * data: someArrayBuffer, + * transferList: [someArrayBuffer], + * }); + * ``` + */ +function register(specifier, parentURL = undefined, options) { + const moduleLoader = require('internal/process/esm_loader').esmLoader; + if (parentURL != null && typeof parentURL === 'object') { + options = parentURL; + parentURL = options.parentURL; + } + return moduleLoader.register( + `${specifier}`, + parentURL ?? 'data:', + options?.data, + options?.transferList, + ); } module.exports = { - DefaultModuleLoader, createModuleLoader, + getHooksProxy, + register, }; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 2cf2813a6dcf7f..8ac5ff240bdb37 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -21,7 +21,7 @@ const { const { ModuleWrap } = internalBinding('module_wrap'); -const { decorateErrorStack } = require('internal/util'); +const { decorateErrorStack, kEmptyObject } = require('internal/util'); const { getSourceMapsEnabled, } = require('internal/source_map/source_map_cache'); @@ -51,17 +51,26 @@ class ModuleJob { // `loader` is the Loader instance used for loading dependencies. // `moduleProvider` is a function constructor(loader, url, importAssertions = { __proto__: null }, - moduleProvider, isMain, inspectBrk) { + moduleProvider, isMain, inspectBrk, sync = false) { this.loader = loader; this.importAssertions = importAssertions; this.isMain = isMain; this.inspectBrk = inspectBrk; + this.url = url; + this.module = undefined; // Expose the promise to the ModuleWrap directly for linking below. // `this.module` is also filled in below. this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]); + if (sync) { + this.module = this.modulePromise; + this.modulePromise = PromiseResolve(this.module); + } else { + this.modulePromise = PromiseResolve(this.modulePromise); + } + // Wait for the ModuleWrap instance being linked with all dependencies. const link = async () => { this.module = await this.modulePromise; @@ -72,8 +81,8 @@ class ModuleJob { // so that circular dependencies can't cause a deadlock by two of // these `link` callbacks depending on each other. const dependencyJobs = []; - const promises = this.module.link((specifier, assertions) => { - const job = this.loader.getModuleJob(specifier, url, assertions); + const promises = this.module.link(async (specifier, assertions) => { + const job = await this.loader.getModuleJob(specifier, url, assertions); ArrayPrototypePush(dependencyJobs, job); return job.modulePromise; }); @@ -140,7 +149,9 @@ class ModuleJob { /module '(.*)' does not provide an export named '(.+)'/, e.message); const { url: childFileURL } = await this.loader.resolve( - childSpecifier, parentFileUrl, + childSpecifier, + parentFileUrl, + kEmptyObject, ); let format; try { @@ -184,6 +195,20 @@ class ModuleJob { } } + runSync() { + assert(this.module instanceof ModuleWrap); + if (this.instantiated !== undefined) { + return { __proto__: null, module: this.module }; + } + + this.module.instantiate(); + this.instantiated = PromiseResolve(); + const timeout = -1; + const breakOnSigint = false; + this.module.evaluate(timeout, breakOnSigint); + return { __proto__: null, module: this.module }; + } + async run() { await this.instantiate(); const timeout = -1; diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js index ac6d95445ae757..12a1a526178a7e 100644 --- a/lib/internal/modules/esm/module_map.js +++ b/lib/internal/modules/esm/module_map.js @@ -1,17 +1,92 @@ 'use strict'; -const { kImplicitAssertType } = require('internal/modules/esm/assert'); const { + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypeSort, + JSONStringify, + ObjectKeys, SafeMap, } = primordials; +const { kImplicitAssertType } = require('internal/modules/esm/assert'); let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { debug = fn; }); const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes; const { validateString } = require('internal/validators'); -// Tracks the state of the loader-level module cache -class ModuleMap extends SafeMap { +/** + * Cache the results of the `resolve` step of the module resolution and loading process. + * Future resolutions of the same input (specifier, parent URL and import assertions) + * must return the same result if the first attempt was successful, per + * https://tc39.es/ecma262/#sec-HostLoadImportedModule. + * This cache is *not* used when custom loaders are registered. + */ +class ResolveCache extends SafeMap { + constructor(i) { super(i); } // eslint-disable-line no-useless-constructor + + /** + * Generates the internal serialized cache key and returns it along the actual cache object. + * + * It is exposed to allow more efficient read and overwrite a cache entry. + * @param {string} specifier + * @param {Record} importAssertions + * @returns {string} + */ + serializeKey(specifier, importAssertions) { + // To serialize the ModuleRequest (specifier + list of import assertions), + // we need to sort the assertions by key, then stringifying, + // so that different import statements with the same assertions are always treated + // as identical. + const keys = ObjectKeys(importAssertions); + + if (keys.length === 0) { + return specifier + '::'; + } + + return specifier + '::' + ArrayPrototypeJoin( + ArrayPrototypeMap( + ArrayPrototypeSort(keys), + (key) => JSONStringify(key) + JSONStringify(importAssertions[key])), + ','); + } + + #getModuleCachedImports(parentURL) { + let internalCache = super.get(parentURL); + if (internalCache == null) { + super.set(parentURL, internalCache = { __proto__: null }); + } + return internalCache; + } + + /** + * @param {string} serializedKey + * @param {string} parentURL + * @returns {import('./loader').ModuleExports | Promise} + */ + get(serializedKey, parentURL) { + return this.#getModuleCachedImports(parentURL)[serializedKey]; + } + + /** + * @param {string} serializedKey + * @param {string} parentURL + * @param {{ format: string, url: URL['href'] }} result + */ + set(serializedKey, parentURL, result) { + this.#getModuleCachedImports(parentURL)[serializedKey] = result; + return this; + } + + has(serializedKey, parentURL) { + return serializedKey in this.#getModuleCachedImports(parentURL); + } +} + +/** + * Cache the results of the `load` step of the module resolution and loading process. + */ +class LoadCache extends SafeMap { constructor(i) { super(i); } // eslint-disable-line no-useless-constructor get(url, type = kImplicitAssertType) { validateString(url, 'url'); @@ -29,7 +104,7 @@ class ModuleMap extends SafeMap { } debug(`Storing ${url} (${ type === kImplicitAssertType ? 'implicit type' : type - }) in ModuleMap`); + }) in ModuleLoadMap`); const cachedJobsForUrl = super.get(url) ?? { __proto__: null }; cachedJobsForUrl[type] = job; return super.set(url, cachedJobsForUrl); @@ -40,4 +115,8 @@ class ModuleMap extends SafeMap { return super.get(url)?.[type] !== undefined; } } -module.exports = ModuleMap; + +module.exports = { + LoadCache, + ResolveCache, +}; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index ce5e4e27fd5128..6d1793c06a4270 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -215,16 +215,14 @@ function finalizeResolution(resolved, base, preserveSymlinks) { // Check for stats.isDirectory() if (stats === 1) { - const err = new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base)); - err.url = String(resolved); - throw err; + throw new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base), String(resolved)); } else if (stats !== 0) { // Check for !stats.isFile() if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { process.send({ 'watch:require': [path || resolved.pathname] }); } throw new ERR_MODULE_NOT_FOUND( - path || resolved.pathname, base && fileURLToPath(base), 'module'); + path || resolved.pathname, base && fileURLToPath(base), resolved); } if (!preserveSymlinks) { @@ -779,7 +777,7 @@ function packageResolve(specifier, base, conditions) { // eslint can't handle the above code. // eslint-disable-next-line no-unreachable - throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base)); + throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null); } /** diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 267d89f1d44730..c0125cd84c37c0 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -1,13 +1,13 @@ 'use strict'; const { - ArrayPrototypeForEach, ArrayPrototypeMap, Boolean, JSONParse, ObjectGetPrototypeOf, ObjectPrototypeHasOwnProperty, ObjectKeys, + ReflectApply, SafeArrayIterator, SafeMap, SafeSet, @@ -24,8 +24,9 @@ function lazyTypes() { return _TYPES = require('internal/util/types'); } +const assert = require('internal/assert'); const { readFileSync } = require('fs'); -const { extname, isAbsolute } = require('path'); +const { dirname, extname, isAbsolute } = require('path'); const { hasEsmSyntax, loadBuiltinModule, @@ -35,11 +36,11 @@ const { Module: CJSModule, cjsParseCache, } = require('internal/modules/cjs/loader'); -const { fileURLToPath, URL } = require('internal/url'); +const { fileURLToPath, pathToFileURL, URL } = require('internal/url'); let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { debug = fn; }); -const { emitExperimentalWarning } = require('internal/util'); +const { emitExperimentalWarning, kEmptyObject, setOwnProperty } = require('internal/util'); const { ERR_UNKNOWN_BUILTIN_MODULE, ERR_INVALID_RETURN_PROPERTY_VALUE, @@ -49,6 +50,7 @@ const moduleWrap = internalBinding('module_wrap'); const { ModuleWrap } = moduleWrap; const asyncESM = require('internal/process/esm_loader'); const { emitWarningSync } = require('internal/process/warning'); +const { internalCompileFunction } = require('internal/vm'); let cjsParse; async function initCJSParse() { @@ -140,35 +142,104 @@ function enrichCJSError(err, content, filename) { } } -// Strategy for loading a node-style CommonJS module -const isWindows = process.platform === 'win32'; -translators.set('commonjs', async function commonjsStrategy(url, source, - isMain) { +/** + * Loads a CommonJS module via the ESM Loader sync CommonJS translator. + * This translator creates its own version of the `require` function passed into CommonJS modules. + * Any monkey patches applied to the CommonJS Loader will not affect this module. + * Any `require` calls in this module will load all children in the same way. + */ +function loadCJSModule(module, source, url, filename) { + let compiledWrapper; + try { + compiledWrapper = internalCompileFunction(source, [ + 'exports', + 'require', + 'module', + '__filename', + '__dirname', + ], { + filename, + importModuleDynamically(specifier, _, importAssertions) { + return asyncESM.esmLoader.import(specifier, url, importAssertions); + }, + }).function; + } catch (err) { + enrichCJSError(err, source, url); + throw err; + } + + const __dirname = dirname(filename); + // eslint-disable-next-line func-name-matching,func-style + const requireFn = function require(specifier) { + let importAssertions = kEmptyObject; + if (!StringPrototypeStartsWith(specifier, 'node:')) { + // TODO: do not depend on the monkey-patchable CJS loader here. + const path = CJSModule._resolveFilename(specifier, module); + if (specifier !== path) { + switch (extname(path)) { + case '.json': + importAssertions = { __proto__: null, type: 'json' }; + break; + case '.node': + return CJSModule._load(specifier, module); + default: + // fall through + } + specifier = `${pathToFileURL(path)}`; + } + } + const job = asyncESM.esmLoader.getModuleJobSync(specifier, url, importAssertions); + job.runSync(); + return cjsCache.get(job.url).exports; + }; + setOwnProperty(requireFn, 'resolve', function resolve(specifier) { + if (!StringPrototypeStartsWith(specifier, 'node:')) { + const path = CJSModule._resolveFilename(specifier, module); + if (specifier !== path) { + specifier = `${pathToFileURL(path)}`; + } + } + const { url: resolvedURL } = asyncESM.esmLoader.resolveSync(specifier, url, kEmptyObject); + return StringPrototypeStartsWith(resolvedURL, 'file://') ? fileURLToPath(resolvedURL) : resolvedURL; + }); + setOwnProperty(requireFn, 'main', process.mainModule); + + ReflectApply(compiledWrapper, module.exports, + [module.exports, requireFn, module, filename, __dirname]); + setOwnProperty(module, 'loaded', true); +} + +// TODO: can we use a weak map instead? +const cjsCache = new SafeMap(); +function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) { debug(`Translating CJSModule ${url}`); - const filename = fileURLToPath(new URL(url)); + const filename = StringPrototypeStartsWith(url, 'file://') ? fileURLToPath(url) : url; + source = stringify(source); - if (!cjsParse) await initCJSParse(); - const { module, exportNames } = cjsPreparseModuleExports(filename); + const { exportNames, module } = cjsPreparseModuleExports(filename, source); + cjsCache.set(url, module); const namesWithDefault = exportNames.has('default') ? [...exportNames] : ['default', ...exportNames]; + if (isMain) { + setOwnProperty(process, 'mainModule', module); + } + return new ModuleWrap(url, undefined, namesWithDefault, function() { debug(`Loading CJSModule ${url}`); + if (!module.loaded) { + loadCJS(module, source, url, filename); + } + let exports; if (asyncESM.esmLoader.cjsCache.has(module)) { exports = asyncESM.esmLoader.cjsCache.get(module); asyncESM.esmLoader.cjsCache.delete(module); } else { - try { - exports = CJSModule._load(filename, undefined, isMain); - } catch (err) { - enrichCJSError(err, undefined, filename); - throw err; - } + ({ exports } = module); } - for (const exportName of exportNames) { if (!ObjectPrototypeHasOwnProperty(exports, exportName) || exportName === 'default') @@ -184,9 +255,49 @@ translators.set('commonjs', async function commonjsStrategy(url, source, } this.setExport('default', exports); }); + +} + +// Handle CommonJS modules referenced by `require` calls. +// This translator function must be sync, as `require` is sync. +translators.set('require-commonjs', (url, source, isMain) => { + assert(cjsParse); + + return createCJSModuleWrap(url, source); }); -function cjsPreparseModuleExports(filename) { +// Handle CommonJS modules referenced by `import` statements or expressions, +// or as the initial entry point when the ESM loader handles a CommonJS entry. +translators.set('commonjs', async function commonjsStrategy(url, source, + isMain) { + if (!cjsParse) { + await initCJSParse(); + } + + // For backward-compatibility, it's possible to return a nullish value for + // CJS source associated with a file: URL. In this case, the source is + // obtained by calling the monkey-patchable CJS loader. + const cjsLoader = source == null ? (module, source, url, filename) => { + try { + module.load(filename); + } catch (err) { + enrichCJSError(err, source, url); + throw err; + } + } : loadCJSModule; + + try { + // We still need to read the FS to detect the exports. + source ??= readFileSync(new URL(url), 'utf8'); + } catch { + // Continue regardless of error. + } + return createCJSModuleWrap(url, source, isMain, cjsLoader); + +}); + +function cjsPreparseModuleExports(filename, source) { + // TODO: Do we want to keep hitting the user mutable CJS loader here? let module = CJSModule._cache[filename]; if (module) { const cached = cjsParseCache.get(module); @@ -201,13 +312,6 @@ function cjsPreparseModuleExports(filename) { CJSModule._cache[filename] = module; } - let source; - try { - source = readFileSync(filename, 'utf8'); - } catch { - // Continue regardless of error. - } - let exports, reexports; try { ({ exports, reexports } = cjsParse(source || '')); @@ -219,38 +323,53 @@ function cjsPreparseModuleExports(filename) { const exportNames = new SafeSet(new SafeArrayIterator(exports)); // Set first for cycles. - cjsParseCache.set(module, { source, exportNames, loaded }); + cjsParseCache.set(module, { source, exportNames }); if (reexports.length) { module.filename = filename; module.paths = CJSModule._nodeModulePaths(module.path); - } - ArrayPrototypeForEach(reexports, (reexport) => { - let resolved; - try { - resolved = CJSModule._resolveFilename(reexport, module); - } catch { - return; - } - const ext = extname(resolved); - if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) && - isAbsolute(resolved)) { - const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved); - for (const name of reexportNames) - exportNames.add(name); + for (let i = 0; i < reexports.length; i++) { + const reexport = reexports[i]; + let resolved; + try { + // TODO: this should be calling the `resolve` hook chain instead. + // Doing so would mean dropping support for CJS in the loader thread, as + // this call needs to be sync from the perspective of the main thread, + // which we can do via HooksProxy and Atomics, but we can't do within + // the loaders thread. Until this is done, the lexer will use the + // monkey-patchable CJS loader to get the path to the module file to + // load (which may or may not be aligned with the URL that the `resolve` + // hook have returned). + resolved = CJSModule._resolveFilename(reexport, module); + } catch { + continue; + } + // TODO: this should be calling the `load` hook chain and check if it returns + // `format: 'commonjs'` instead of relying on file extensions. + const ext = extname(resolved); + if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) && + isAbsolute(resolved)) { + // TODO: this should be calling the `load` hook chain to get the source + // (and fallback to reading the FS only if the source is nullish). + const source = readFileSync(resolved, 'utf-8'); + const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, source); + for (const name of reexportNames) + exportNames.add(name); + } } - }); + } return { module, exportNames }; } // Strategy for loading a node builtin CommonJS module that isn't // through normal resolution -translators.set('builtin', async function builtinStrategy(url) { +translators.set('builtin', function builtinStrategy(url) { debug(`Translating BuiltinModule ${url}`); // Slice 'node:' scheme const id = StringPrototypeSlice(url, 5); const module = loadBuiltinModule(id, url); + cjsCache.set(url, module); if (!StringPrototypeStartsWith(url, 'node:') || !module) { throw new ERR_UNKNOWN_BUILTIN_MODULE(url); } @@ -259,7 +378,8 @@ translators.set('builtin', async function builtinStrategy(url) { }); // Strategy for loading a JSON file -translators.set('json', async function jsonStrategy(url, source) { +const isWindows = process.platform === 'win32'; +translators.set('json', function jsonStrategy(url, source) { emitExperimentalWarning('Importing JSON modules'); assertBufferSource(source, true, 'load'); debug(`Loading JSONModule ${url}`); @@ -283,6 +403,7 @@ translators.set('json', async function jsonStrategy(url, source) { // A require call could have been called on the same file during loading and // that resolves synchronously. To make sure we always return the identical // export, we have to check again if the module already exists or not. + // TODO: remove CJS loader from here as well. module = CJSModule._cache[modulePath]; if (module && module.loaded) { const exports = module.exports; @@ -308,6 +429,7 @@ translators.set('json', async function jsonStrategy(url, source) { if (pathname) { CJSModule._cache[modulePath] = module; } + cjsCache.set(url, module); return new ModuleWrap(url, undefined, ['default'], function() { debug(`Parsing JSONModule ${url}`); this.setExport('default', module.exports); diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 0fd833b6544945..5014c99b2a9eb3 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -2,7 +2,6 @@ const { ArrayIsArray, - PromisePrototypeThen, SafeSet, SafeWeakMap, ObjectFreeze, @@ -13,8 +12,11 @@ const { ERR_INVALID_ARG_VALUE, } = require('internal/errors').codes; const { getOptionValue } = require('internal/options'); +const { + loadPreloadModules, + initializeFrozenIntrinsics, +} = require('internal/process/pre_execution'); const { pathToFileURL } = require('internal/url'); -const { kEmptyObject } = require('internal/util'); const { setImportModuleDynamicallyCallback, setInitializeImportMetaObjectCallback, @@ -120,46 +122,27 @@ async function initializeHooks() { const { Hooks } = require('internal/modules/esm/hooks'); - const hooks = new Hooks(); + const esmLoader = require('internal/process/esm_loader').esmLoader; - const { DefaultModuleLoader } = require('internal/modules/esm/loader'); - class ModuleLoader extends DefaultModuleLoader { - loaderType = 'internal'; - async #getModuleJob(specifier, parentURL, importAssertions) { - const resolveResult = await hooks.resolve(specifier, parentURL, importAssertions); - return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); - } - getModuleJob(specifier, parentURL, importAssertions) { - const jobPromise = this.#getModuleJob(specifier, parentURL, importAssertions); - return { - run() { - return PromisePrototypeThen(jobPromise, (job) => job.run()); - }, - get modulePromise() { - return PromisePrototypeThen(jobPromise, (job) => job.modulePromise); - }, - get linked() { - return PromisePrototypeThen(jobPromise, (job) => job.linked); - }, - }; - } - load(url, context) { return hooks.load(url, context); } - } - const privateModuleLoader = new ModuleLoader(); + const hooks = new Hooks(); + esmLoader.setCustomizations(hooks); + + // We need the loader customizations to be set _before_ we start invoking + // `--require`, otherwise loops can happen because a `--require` script + // might call `register(...)` before we've installed ourselves. These + // global values are magically set in `setupUserModules` just for us and + // we call them in the correct order. + // N.B. This block appears here specifically in order to ensure that + // `--require` calls occur before `--loader` ones do. + loadPreloadModules(); + initializeFrozenIntrinsics(); const parentURL = pathToFileURL(cwd).href; - for (let i = 0; i < customLoaderURLs.length; i++) { - const customLoaderURL = customLoaderURLs[i]; - - // Importation must be handled by internal loader to avoid polluting user-land - const keyedExports = await privateModuleLoader.import( - customLoaderURL, + await hooks.register( + customLoaderURLs[i], parentURL, - kEmptyObject, ); - - hooks.addCustomLoader(customLoaderURL, keyedExports); } const preloadScripts = hooks.initializeGlobalPreload(); diff --git a/lib/internal/perf/utils.js b/lib/internal/perf/utils.js index a92e177e4246e9..a0b7955c70481c 100644 --- a/lib/internal/perf/utils.js +++ b/lib/internal/perf/utils.js @@ -1,33 +1,33 @@ 'use strict'; -const binding = internalBinding('performance'); const { + constants: { + NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN, + }, milestones, - getTimeOrigin, -} = binding; +} = internalBinding('performance'); -// TODO(joyeecheung): we may want to warn about access to -// this during snapshot building. -let timeOrigin = getTimeOrigin(); +function getTimeOrigin() { + // Do not cache this to prevent it from being serialized into the + // snapshot. + return milestones[NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN] / 1e6; +} +// Returns the time relative to the process start time in milliseconds. function now() { const hr = process.hrtime(); - return (hr[0] * 1000 + hr[1] / 1e6) - timeOrigin; + return (hr[0] * 1000 + hr[1] / 1e6) - getTimeOrigin(); } +// Returns the milestone relative to the process start time in milliseconds. function getMilestoneTimestamp(milestoneIdx) { const ns = milestones[milestoneIdx]; if (ns === -1) return ns; - return ns / 1e6 - timeOrigin; -} - -function refreshTimeOrigin() { - timeOrigin = getTimeOrigin(); + return ns / 1e6 - getTimeOrigin(); } module.exports = { now, getMilestoneTimestamp, - refreshTimeOrigin, }; diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 9a84ed944e87c4..e735101ab18c47 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -36,6 +36,8 @@ module.exports = { parentURL, kEmptyObject, )); + } else { + esmLoader.forceLoadHooks(); } await callback(esmLoader); } catch (err) { diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 306315acb8d78d..d79aa41c53e7b6 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -68,7 +68,6 @@ function prepareExecution(options) { // Patch the process object with legacy properties and normalizations patchProcessObject(expandArgv1); setupTraceCategoryState(); - setupPerfHooks(); setupInspectorHooks(); setupWarningHandler(); setupFetch(); @@ -156,6 +155,10 @@ function setupUserModules(isLoaderWorker = false) { initializeESMLoader(isLoaderWorker); const CJSLoader = require('internal/modules/cjs/loader'); assert(!CJSLoader.hasLoadedAnyUserCJSModule); + // Loader workers are responsible for doing this themselves. + if (isLoaderWorker) { + return; + } loadPreloadModules(); // Need to be done after --require setup. initializeFrozenIntrinsics(); @@ -175,7 +178,7 @@ function patchProcessObject(expandArgv1) { __proto__: null, enumerable: true, // Only set it to true during snapshot building. - configurable: getOptionValue('--build-snapshot'), + configurable: isBuildingSnapshot(), value: process.argv[0], }); @@ -424,10 +427,6 @@ function setupTraceCategoryState() { toggleTraceCategoryState(isTraceCategoryEnabled('node.async_hooks')); } -function setupPerfHooks() { - require('internal/perf/utils').refreshTimeOrigin(); -} - function setupInspectorHooks() { // If Debugger.setAsyncCallStackDepth is sent during bootstrap, // we cannot immediately call into JS to enable the hooks, which could @@ -689,4 +688,6 @@ module.exports = { prepareMainThreadExecution, prepareWorkerThreadExecution, markBootstrapComplete, + loadPreloadModules, + initializeFrozenIntrinsics, }; diff --git a/lib/internal/readline/utils.js b/lib/internal/readline/utils.js index a546516d76b66c..124a5382a0ddae 100644 --- a/lib/internal/readline/utils.js +++ b/lib/internal/readline/utils.js @@ -148,8 +148,10 @@ function* emitKeys(stream) { * * - `;5` part is optional, e.g. it could be `\x1b[24~` * - first part can contain one or two digits + * - there is also special case when there can be 3 digits + * but without modifier. They are the case of paste bracket mode * - * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/ + * So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/ * * * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 } @@ -170,6 +172,10 @@ function* emitKeys(stream) { if (ch >= '0' && ch <= '9') { s += (ch = yield); + + if (ch >= '0' && ch <= '9') { + s += (ch = yield); + } } } @@ -189,9 +195,13 @@ function* emitKeys(stream) { const cmd = StringPrototypeSlice(s, cmdStart); let match; - if ((match = RegExpPrototypeExec(/^(\d\d?)(;(\d))?([~^$])$/, cmd))) { - code += match[1] + match[4]; - modifier = (match[3] || 1) - 1; + if ((match = RegExpPrototypeExec(/^(?:(\d\d?)(?:;(\d))?([~^$])|(\d{3}~))$/, cmd))) { + if (match[4]) { + code += match[4]; + } else { + code += match[1] + match[3]; + modifier = (match[2] || 1) - 1; + } } else if ( (match = RegExpPrototypeExec(/^((\d;)?(\d))?([A-Za-z])$/, cmd)) ) { @@ -228,6 +238,10 @@ function* emitKeys(stream) { case '[13~': key.name = 'f3'; break; case '[14~': key.name = 'f4'; break; + /* paste bracket mode */ + case '[200~': key.name = 'paste-start'; break; + case '[201~': key.name = 'paste-end'; break; + /* from Cygwin and used in libuv */ case '[[A': key.name = 'f1'; break; case '[[B': key.name = 'f2'; break; diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index a20055fa91b601..70f88984c82150 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -250,7 +250,7 @@ class TestCoverage { let dir; try { - mkdirSync(this.originalCoverageDirectory, { recursive: true }); + mkdirSync(this.originalCoverageDirectory, { __proto__: null, recursive: true }); dir = opendirSync(this.coverageDirectory); for (let entry; (entry = dir.readSync()) !== null;) { diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 246620f6628d88..36c36f2de14b04 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -116,6 +116,7 @@ function setup(root) { const globalOptions = parseCommandLine(); const hook = createHook({ + __proto__: null, init(asyncId, type, triggerAsyncId, resource) { if (resource instanceof Test) { testResources.set(asyncId, resource); @@ -190,8 +191,10 @@ let reportersSetup; function getGlobalRoot() { if (!globalRoot) { globalRoot = createTestTree(); - globalRoot.reporter.once('test:fail', () => { - process.exitCode = kGenericUserError; + globalRoot.reporter.on('test:fail', (data) => { + if (data.todo === undefined || data.todo === false) { + process.exitCode = kGenericUserError; + } }); reportersSetup = setupTestReporters(globalRoot); } @@ -216,7 +219,7 @@ function runInParentContext(Factory) { const test = (name, options, fn) => run(name, options, fn); ArrayPrototypeForEach(['skip', 'todo', 'only'], (keyword) => { - test[keyword] = (name, options, fn) => run(name, options, fn, { [keyword]: true }); + test[keyword] = (name, options, fn) => run(name, options, fn, { __proto__: null, [keyword]: true }); }); return test; } diff --git a/lib/internal/test_runner/mock/mock.js b/lib/internal/test_runner/mock/mock.js index 71363154c88328..a704b41996e6d2 100644 --- a/lib/internal/test_runner/mock/mock.js +++ b/lib/internal/test_runner/mock/mock.js @@ -133,7 +133,7 @@ class MockTracker { validateObject(options, 'options'); const { times = Infinity } = options; validateTimes(times, 'options.times'); - const ctx = new MockFunctionContext(implementation, { original }, times); + const ctx = new MockFunctionContext(implementation, { __proto__: null, original }, times); return this.#setupMock(ctx, original); } @@ -189,7 +189,7 @@ class MockTracker { ); } - const restore = { descriptor, object: objectOrFunction, methodName }; + const restore = { __proto__: null, descriptor, object: objectOrFunction, methodName }; const impl = implementation === kDefaultFunction ? original : implementation; const ctx = new MockFunctionContext(impl, restore, times); @@ -238,6 +238,7 @@ class MockTracker { } return this.method(object, methodName, implementation, { + __proto__: null, ...options, getter, }); @@ -265,6 +266,7 @@ class MockTracker { } return this.method(object, methodName, implementation, { + __proto__: null, ...options, setter, }); @@ -297,6 +299,7 @@ class MockTracker { throw err; } finally { FunctionPrototypeCall(trackCall, ctx, { + __proto__: null, arguments: argList, error, result, @@ -321,6 +324,7 @@ class MockTracker { throw err; } finally { FunctionPrototypeCall(trackCall, ctx, { + __proto__: null, arguments: argList, error, result, diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 790c48e663b387..311c30684d0ce6 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -45,7 +45,7 @@ function setPosition(node, pos) { } function abortIt(signal) { - return new AbortError(undefined, { cause: signal.reason }); + return new AbortError(undefined, { __proto__: null, cause: signal.reason }); } const SUPPORTED_TIMERS = ['setTimeout', 'setInterval']; @@ -194,7 +194,9 @@ class MockTimers { #toggleEnableTimers(activate) { const options = { + __proto__: null, toFake: { + __proto__: null, setTimeout: () => { this.#realSetTimeout = globalThis.setTimeout; this.#realClearTimeout = globalThis.clearTimeout; @@ -233,6 +235,7 @@ class MockTimers { }, }, toReal: { + __proto__: null, setTimeout: () => { globalThis.setTimeout = this.#realSetTimeout; globalThis.clearTimeout = this.#realClearTimeout; diff --git a/lib/internal/test_runner/reporter/spec.js b/lib/internal/test_runner/reporter/spec.js index 16cbdf1d5aa901..f15b3eaa40aa27 100644 --- a/lib/internal/test_runner/reporter/spec.js +++ b/lib/internal/test_runner/reporter/spec.js @@ -42,7 +42,7 @@ class SpecReporter extends Transform { #failedTests = []; constructor() { - super({ writableObjectMode: true }); + super({ __proto__: null, writableObjectMode: true }); } #indent(nesting) { @@ -64,17 +64,24 @@ class SpecReporter extends Transform { ), `\n${indent} `); return `\n${indent} ${message}\n`; } - #formatTestReport(type, data, prefix = '', indent = '', hasChildren = false, skippedSubtest = false) { + #formatTestReport(type, data, prefix = '', indent = '', hasChildren = false) { let color = colors[type] ?? white; let symbol = symbols[type] ?? ' '; + const { skip, todo } = data; const duration_ms = data.details?.duration_ms ? ` ${gray}(${data.details.duration_ms}ms)${white}` : ''; - const title = `${data.name}${duration_ms}${skippedSubtest ? ' # SKIP' : ''}`; + let title = `${data.name}${duration_ms}`; + + if (skip !== undefined) { + title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`; + } else if (todo !== undefined) { + title += ` # ${typeof todo === 'string' && todo.length ? todo : 'TODO'}`; + } if (hasChildren) { // If this test has had children - it was already reported, so slightly modify the output return `${prefix}${indent}${color}${symbols['arrow:right']}${white}${title}\n`; } const error = this.#formatError(data.details?.error, indent); - if (skippedSubtest) { + if (skip !== undefined) { color = gray; symbol = symbols['hyphen:minus']; } @@ -101,9 +108,8 @@ class SpecReporter extends Transform { ArrayPrototypeShift(this.#reported); hasChildren = true; } - const skippedSubtest = subtest && data.skip && data.skip !== undefined; const indent = this.#indent(data.nesting); - return `${this.#formatTestReport(type, data, prefix, indent, hasChildren, skippedSubtest)}\n`; + return `${this.#formatTestReport(type, data, prefix, indent, hasChildren)}\n`; } #handleEvent({ type, data }) { switch (type) { @@ -127,7 +133,7 @@ class SpecReporter extends Transform { } } _transform({ type, data }, encoding, callback) { - callback(null, this.#handleEvent({ type, data })); + callback(null, this.#handleEvent({ __proto__: null, type, data })); } _flush(callback) { if (this.#failedTests.length === 0) { diff --git a/lib/internal/test_runner/reporter/tap.js b/lib/internal/test_runner/reporter/tap.js index 4aec4ba072d954..de8188c58dd31e 100644 --- a/lib/internal/test_runner/reporter/tap.js +++ b/lib/internal/test_runner/reporter/tap.js @@ -18,7 +18,7 @@ const kDefaultIndent = ' '; // 4 spaces const kFrameStartRegExp = /^ {4}at /; const kLineBreakRegExp = /\n|\r\n/; const kDefaultTAPVersion = 13; -const inspectOptions = { colors: false, breakLength: Infinity }; +const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity }; let testModule; // Lazy loaded due to circular dependency. function lazyLoadTest() { @@ -171,7 +171,7 @@ function jsToYaml(indent, name, value, seen) { } if (isErrorObj) { - const { kTestCodeFailure, kUnwrapErrors } = lazyLoadTest(); + const { kUnwrapErrors } = lazyLoadTest(); const { cause, code, @@ -198,15 +198,14 @@ function jsToYaml(indent, name, value, seen) { errStack = cause?.stack ?? errStack; errCode = cause?.code ?? errCode; errName = cause?.name ?? errName; + errMsg = cause?.message ?? errMsg; + if (isAssertionLike(cause)) { errExpected = cause.expected; errActual = cause.actual; errOperator = cause.operator ?? errOperator; errIsAssertion = true; } - if (failureType === kTestCodeFailure) { - errMsg = cause?.message ?? errMsg; - } } result += jsToYaml(indent, 'error', errMsg, seen); diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 0f2749fd15f0ea..fdaa981eece4e3 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -135,7 +135,7 @@ function createTestFileList() { for (let i = 0; i < testPaths.length; i++) { const absolutePath = resolve(testPaths[i]); - processPath(absolutePath, testFiles, { userSupplied: true }); + processPath(absolutePath, testFiles, { __proto__: null, userSupplied: true }); } } catch (err) { if (err?.code === 'ENOENT') { @@ -348,9 +348,9 @@ class FileTest extends Test { function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { const watchMode = filesWatcher != null; const subtest = root.createSubtest(FileTest, path, async (t) => { - const args = getRunArgs({ path, inspectPort, testNamePatterns }); + const args = getRunArgs({ __proto__: null, path, inspectPort, testNamePatterns }); const stdio = ['pipe', 'pipe', 'pipe']; - const env = { ...process.env, NODE_TEST_CONTEXT: 'child-v8' }; + const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8' }; if (watchMode) { stdio.push('ipc'); env.WATCH_REPORT_DEPENDENCIES = '1'; @@ -359,7 +359,7 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { env.FORCE_COLOR = '1'; } - const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8', env, stdio }); + const child = spawn(process.execPath, args, { __proto__: null, signal: t.signal, encoding: 'utf8', env, stdio }); if (watchMode) { filesWatcher.runningProcesses.set(path, child); filesWatcher.watcher.watchChildProcessModules(child, path); @@ -376,7 +376,7 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { subtest.parseMessage(data); }); - const rl = createInterface({ input: child.stderr }); + const rl = createInterface({ __proto__: null, input: child.stderr }); rl.on('line', (line) => { if (isInspectorMessage(line)) { process.stderr.write(line + '\n'); @@ -394,8 +394,8 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { }); const { 0: { 0: code, 1: signal } } = await SafePromiseAll([ - once(child, 'exit', { signal: t.signal }), - finished(child.stdout, { signal: t.signal }), + once(child, 'exit', { __proto__: null, signal: t.signal }), + finished(child.stdout, { __proto__: null, signal: t.signal }), ]); if (watchMode) { @@ -428,7 +428,7 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { function watchFiles(testFiles, root, inspectPort, signal, testNamePatterns) { const runningProcesses = new SafeMap(); const runningSubtests = new SafeMap(); - const watcher = new FilesWatcher({ throttle: 500, mode: 'filter', signal }); + const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'filter', signal }); const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests }; watcher.on('changed', ({ owners }) => { @@ -513,7 +513,7 @@ function run(options) { }); } - const root = createTestTree({ concurrency, timeout, signal }); + const root = createTestTree({ __proto__: null, concurrency, timeout, signal }); let testFiles = files ?? createTestFileList(); if (shard) { diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index f5cc0fb98f6271..eb2ccf9c9a22c3 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -13,6 +13,7 @@ const { ObjectSeal, PromisePrototypeThen, PromiseResolve, + SafePromisePrototypeFinally, ReflectApply, RegExpPrototypeExec, SafeMap, @@ -78,7 +79,7 @@ function stopTest(timeout, signal) { if (timeout === kDefaultTimeout) { return once(signal, 'abort'); } - return PromisePrototypeThen(setTimeout(timeout, null, { ref: false, signal }), () => { + return PromisePrototypeThen(setTimeout(timeout, null, { __proto__: null, ref: false, signal }), () => { throw new ERR_TEST_FAILURE( `test timed out after ${timeout}ms`, kTestTimeoutFailure, @@ -281,8 +282,8 @@ class Test extends AsyncResource { this.harness = null; // Configured on the root test by the test harness. this.mock = null; this.cancelled = false; - this.skipped = !!skip; - this.isTodo = !!todo; + this.skipped = skip !== undefined && skip !== false; + this.isTodo = todo !== undefined && todo !== false; this.startTime = null; this.endTime = null; this.passed = false; @@ -505,7 +506,7 @@ class Test extends AsyncResource { getRunArgs() { const ctx = new TestContext(this); - return { ctx, args: [ctx] }; + return { __proto__: null, ctx, args: [ctx] }; } async runHook(hook, args) { @@ -539,12 +540,12 @@ class Test extends AsyncResource { const { args, ctx } = this.getRunArgs(); const after = async () => { if (this.hooks.after.length > 0) { - await this.runHook('after', { args, ctx }); + await this.runHook('after', { __proto__: null, args, ctx }); } }; const afterEach = runOnce(async () => { if (this.parent?.hooks.afterEach.length > 0) { - await this.parent.runHook('afterEach', { args, ctx }); + await this.parent.runHook('afterEach', { __proto__: null, args, ctx }); } }); @@ -553,7 +554,7 @@ class Test extends AsyncResource { await this.parent.runHook('before', this.parent.getRunArgs()); } if (this.parent?.hooks.beforeEach.length > 0) { - await this.parent.runHook('beforeEach', { args, ctx }); + await this.parent.runHook('beforeEach', { __proto__: null, args, ctx }); } const stopPromise = stopTest(this.timeout, this.signal); const runArgs = ArrayPrototypeSlice(args); @@ -601,6 +602,12 @@ class Test extends AsyncResource { } else { this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); } + } finally { + // Do not abort hooks and the root test as hooks instance are shared between tests suite so aborting them will + // cause them to not run for further tests. + if (this.parent !== null) { + this.#abortController.abort(); + } } // Clean up the test. Then, try to report the results and execute any @@ -627,7 +634,7 @@ class Test extends AsyncResource { subtest.#cancel(pendingSubtestsError); subtest.postRun(pendingSubtestsError); } - if (!subtest.passed) { + if (!subtest.passed && !subtest.isTodo) { failed++; } } @@ -783,22 +790,28 @@ class Suite extends Test { const { ctx, args } = this.getRunArgs(); const runArgs = [this.fn, ctx]; ArrayPrototypePushApply(runArgs, args); - this.buildSuite = PromisePrototypeThen( - PromiseResolve(ReflectApply(this.runInAsyncScope, this, runArgs)), - undefined, - (err) => { - this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); - }); + this.buildSuite = SafePromisePrototypeFinally( + PromisePrototypeThen( + PromiseResolve(ReflectApply(this.runInAsyncScope, this, runArgs)), + undefined, + (err) => { + this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); + }), + () => { + this.buildPhaseFinished = true; + }, + ); } catch (err) { this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); + + this.buildPhaseFinished = true; } this.fn = () => {}; - this.buildPhaseFinished = true; } getRunArgs() { const ctx = new SuiteContext(this); - return { ctx, args: [ctx] }; + return { __proto__: null, ctx, args: [ctx] }; } async run() { diff --git a/lib/internal/test_runner/tests_stream.js b/lib/internal/test_runner/tests_stream.js index 20e7d458704b20..901987681f319b 100644 --- a/lib/internal/test_runner/tests_stream.js +++ b/lib/internal/test_runner/tests_stream.js @@ -12,7 +12,7 @@ class TestsStream extends Readable { #canPush; constructor() { - super({ objectMode: true }); + super({ __proto__: null, objectMode: true }); this.#buffer = []; this.#canPush = true; } @@ -83,6 +83,8 @@ class TestsStream extends Readable { [kEmitMessage](type, data) { this.emit(type, data); + // Disabling as this going to the user-land + // eslint-disable-next-line node-core/set-proto-to-null-in-object this.#tryPush({ type, data }); } diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index da429b5421a45a..ace204c79bd785 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -81,7 +81,7 @@ function createDeferredCallback() { resolve(); }; - return { promise, cb }; + return { __proto__: null, promise, cb }; } function isTestFailureError(err) { diff --git a/lib/internal/url.js b/lib/internal/url.js index 946393fda4eb26..f777e753b68de3 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -747,13 +747,13 @@ ObjectDefineProperties(URLSearchParams.prototype, { * We use `href` and `protocol` as they are the only properties that are * easy to retrieve and calculate due to the lazy nature of the getters. * - * We check for auth attribute to distinguish legacy url instance with + * We check for `auth` and `path` attribute to distinguish legacy url instance with * WHATWG URL instance. * @param {*} self * @returns {self is URL} */ function isURL(self) { - return Boolean(self?.href && self.protocol && self.auth === undefined); + return Boolean(self?.href && self.protocol && self.auth === undefined && self.path === undefined); } class URL { @@ -1046,9 +1046,11 @@ class URL { url = `${url}`; if (base !== undefined) { - return bindingUrl.canParseWithBase(url, `${base}`); + return bindingUrl.canParse(url, `${base}`); } + // It is important to differentiate the canParse call statements + // since they resolve into different v8 fast api overloads. return bindingUrl.canParse(url); } } @@ -1414,25 +1416,27 @@ const backslashRegEx = /\\/g; const newlineRegEx = /\n/g; const carriageReturnRegEx = /\r/g; const tabRegEx = /\t/g; +const questionRegex = /\?/g; +const hashRegex = /#/g; function encodePathChars(filepath) { - if (StringPrototypeIncludes(filepath, '%')) + if (StringPrototypeIndexOf(filepath, '%') !== -1) filepath = RegExpPrototypeSymbolReplace(percentRegEx, filepath, '%25'); // In posix, backslash is a valid character in paths: - if (!isWindows && StringPrototypeIncludes(filepath, '\\')) + if (!isWindows && StringPrototypeIndexOf(filepath, '\\') !== -1) filepath = RegExpPrototypeSymbolReplace(backslashRegEx, filepath, '%5C'); - if (StringPrototypeIncludes(filepath, '\n')) + if (StringPrototypeIndexOf(filepath, '\n') !== -1) filepath = RegExpPrototypeSymbolReplace(newlineRegEx, filepath, '%0A'); - if (StringPrototypeIncludes(filepath, '\r')) + if (StringPrototypeIndexOf(filepath, '\r') !== -1) filepath = RegExpPrototypeSymbolReplace(carriageReturnRegEx, filepath, '%0D'); - if (StringPrototypeIncludes(filepath, '\t')) + if (StringPrototypeIndexOf(filepath, '\t') !== -1) filepath = RegExpPrototypeSymbolReplace(tabRegEx, filepath, '%09'); return filepath; } function pathToFileURL(filepath) { - const outURL = new URL('file://'); if (isWindows && StringPrototypeStartsWith(filepath, '\\\\')) { + const outURL = new URL('file://'); // UNC path format: \\server\share\resource const hostnameEndIndex = StringPrototypeIndexOf(filepath, '\\', 2); if (hostnameEndIndex === -1) { @@ -1453,18 +1457,29 @@ function pathToFileURL(filepath) { outURL.hostname = domainToASCII(hostname); outURL.pathname = encodePathChars( RegExpPrototypeSymbolReplace(backslashRegEx, StringPrototypeSlice(filepath, hostnameEndIndex), '/')); - } else { - let resolved = path.resolve(filepath); - // path.resolve strips trailing slashes so we must add them back - const filePathLast = StringPrototypeCharCodeAt(filepath, - filepath.length - 1); - if ((filePathLast === CHAR_FORWARD_SLASH || - (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) && - resolved[resolved.length - 1] !== path.sep) - resolved += '/'; - outURL.pathname = encodePathChars(resolved); - } - return outURL; + return outURL; + } + let resolved = path.resolve(filepath); + // path.resolve strips trailing slashes so we must add them back + const filePathLast = StringPrototypeCharCodeAt(filepath, + filepath.length - 1); + if ((filePathLast === CHAR_FORWARD_SLASH || + (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) && + resolved[resolved.length - 1] !== path.sep) + resolved += '/'; + + // Call encodePathChars first to avoid encoding % again for ? and #. + resolved = encodePathChars(resolved); + + // Question and hash character should be included in pathname. + // Therefore, encoding is required to eliminate parsing them in different states. + // This is done as an optimization to not creating a URL instance and + // later triggering pathname setter, which impacts performance + if (StringPrototypeIndexOf(resolved, '?') !== -1) + resolved = RegExpPrototypeSymbolReplace(questionRegex, resolved, '%3F'); + if (StringPrototypeIndexOf(resolved, '#') !== -1) + resolved = RegExpPrototypeSymbolReplace(hashRegex, resolved, '%23'); + return new URL(`file://${resolved}`); } function toPathIfFileURL(fileURLOrPath) { diff --git a/lib/internal/util/embedding.js b/lib/internal/util/embedding.js index e2e67202477bc7..be310f401ad115 100644 --- a/lib/internal/util/embedding.js +++ b/lib/internal/util/embedding.js @@ -1,7 +1,8 @@ 'use strict'; -const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors'); const { BuiltinModule: { normalizeRequirableId } } = require('internal/bootstrap/realm'); const { Module, wrapSafe } = require('internal/modules/cjs/loader'); +const { codes: { ERR_UNKNOWN_BUILTIN_MODULE } } = require('internal/errors'); +const { getCodeCache, getCodePath, isSea } = internalBinding('sea'); // This is roughly the same as: // @@ -15,7 +16,11 @@ const { Module, wrapSafe } = require('internal/modules/cjs/loader'); function embedderRunCjs(contents) { const filename = process.execPath; - const compiledWrapper = wrapSafe(filename, contents); + const compiledWrapper = wrapSafe( + isSea() ? getCodePath() : filename, + contents, + undefined, + getCodeCache()); const customModule = new Module(filename, null); customModule.filename = filename; diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 5bb671e6ad6a35..2e3e1b60be871c 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1215,7 +1215,7 @@ function getFunctionBase(value, constructor, tag) { function identicalSequenceRange(a, b) { for (let i = 0; i < a.length - 3; i++) { // Find the first entry of b that matches the current entry of a. - const pos = b.indexOf(a[i]); + const pos = ArrayPrototypeIndexOf(b, a[i]); if (pos !== -1) { const rest = b.length - pos; if (rest > 3) { diff --git a/lib/internal/util/inspector.js b/lib/internal/util/inspector.js index 0d9580c83224e4..6ff042af71f124 100644 --- a/lib/internal/util/inspector.js +++ b/lib/internal/util/inspector.js @@ -12,6 +12,7 @@ const { } = primordials; const { validatePort } = require('internal/validators'); +const permission = require('internal/process/permission'); const kMinPort = 1024; const kMaxPort = 65535; @@ -47,6 +48,10 @@ let session; function sendInspectorCommand(cb, onError) { const { hasInspector } = internalBinding('config'); if (!hasInspector) return onError(); + // Do not preview when the permission model is enabled + // because this feature require access to the inspector, + // which is unavailable in this case. + if (permission.isEnabled()) return onError(); const inspector = require('inspector'); if (session === undefined) session = new inspector.Session(); session.connect(); diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 848c17f4115616..b38f94d7cc8051 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -24,19 +24,19 @@ const supportsRecursiveWatching = process.platform === 'win32' || class FilesWatcher extends EventEmitter { #watchers = new SafeMap(); #filteredFiles = new SafeSet(); - #throttling = new SafeSet(); + #debouncing = new SafeSet(); #depencencyOwners = new SafeMap(); #ownerDependencies = new SafeMap(); - #throttle; + #debounce; #mode; #signal; - constructor({ throttle = 500, mode = 'filter', signal } = kEmptyObject) { + constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) { super(); - validateNumber(throttle, 'options.throttle', 0, TIMEOUT_MAX); + validateNumber(debounce, 'options.debounce', 0, TIMEOUT_MAX); validateOneOf(mode, 'options.mode', ['filter', 'all']); - this.#throttle = throttle; + this.#debounce = debounce; this.#mode = mode; this.#signal = signal; @@ -74,16 +74,18 @@ class FilesWatcher extends EventEmitter { } #onChange(trigger) { - if (this.#throttling.has(trigger)) { + if (this.#debouncing.has(trigger)) { return; } if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) { return; } - this.#throttling.add(trigger); + this.#debouncing.add(trigger); const owners = this.#depencencyOwners.get(trigger); - this.emit('changed', { owners }); - setTimeout(() => this.#throttling.delete(trigger), this.#throttle).unref(); + setTimeout(() => { + this.#debouncing.delete(trigger); + this.emit('changed', { owners }); + }, this.#debounce).unref(); } get watchedPaths() { diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index 9af63227e0496f..28a1a60b1dac2f 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -111,6 +111,8 @@ const { nonOpCancel, nonOpPull, nonOpStart, + getIterator, + iteratorNext, kType, kState, } = require('internal/webstreams/util'); @@ -138,6 +140,7 @@ const kChunk = Symbol('kChunk'); const kError = Symbol('kError'); const kPull = Symbol('kPull'); const kRelease = Symbol('kRelease'); +const kSkipThrow = Symbol('kSkipThrow'); let releasedError; let releasingError; @@ -316,6 +319,10 @@ class ReadableStream { return isReadableStreamLocked(this); } + static from(iterable) { + return readableStreamFromIterable(iterable); + } + /** * @param {any} [reason] * @returns { Promise } @@ -670,8 +677,10 @@ TransferredReadableStream.prototype[kDeserialize] = () => {}; class ReadableStreamBYOBRequest { [kType] = 'ReadableStreamBYOBRequest'; - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + constructor(skipThrowSymbol = undefined) { + if (skipThrowSymbol !== kSkipThrow) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } } /** @@ -753,17 +762,14 @@ ObjectDefineProperties(ReadableStreamBYOBRequest.prototype, { }); function createReadableStreamBYOBRequest(controller, view) { - return ReflectConstruct( - function() { - this[kType] = 'ReadableStreamBYOBRequest'; - this[kState] = { - controller, - view, - }; - }, - [], - ReadableStreamBYOBRequest, - ); + const stream = new ReadableStreamBYOBRequest(kSkipThrow); + + stream[kState] = { + controller, + view, + }; + + return stream; } class DefaultReadRequest { @@ -1013,9 +1019,12 @@ ObjectDefineProperties(ReadableStreamBYOBReader.prototype, { class ReadableStreamDefaultController { [kType] = 'ReadableStreamDefaultController'; + [kState] = {}; - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + constructor(skipThrowSymbol = undefined) { + if (skipThrowSymbol !== kSkipThrow) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } } /** @@ -1071,22 +1080,14 @@ ObjectDefineProperties(ReadableStreamDefaultController.prototype, { [SymbolToStringTag]: getNonWritablePropertyDescriptor(ReadableStreamDefaultController.name), }); -function createReadableStreamDefaultController() { - return ReflectConstruct( - function() { - this[kType] = 'ReadableStreamDefaultController'; - this[kState] = {}; - }, - [], - ReadableStreamDefaultController, - ); -} - class ReadableByteStreamController { [kType] = 'ReadableByteStreamController'; + [kState] = {}; - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + constructor(skipThrowSymbol = undefined) { + if (skipThrowSymbol !== kSkipThrow) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } } /** @@ -1197,17 +1198,6 @@ ObjectDefineProperties(ReadableByteStreamController.prototype, { [SymbolToStringTag]: getNonWritablePropertyDescriptor(ReadableByteStreamController.name), }); -function createReadableByteStreamController() { - return ReflectConstruct( - function() { - this[kType] = 'ReadableByteStreamController'; - this[kState] = {}; - }, - [], - ReadableByteStreamController, - ); -} - function createTeeReadableStream(start, pull, cancel) { return ReflectConstruct( function() { @@ -1251,6 +1241,59 @@ const isReadableStreamBYOBReader = // ---- ReadableStream Implementation +function readableStreamFromIterable(iterable) { + let stream; + const iteratorRecord = getIterator(iterable, 'async'); + + const startAlgorithm = nonOpStart; + + async function pullAlgorithm() { + const nextResult = iteratorNext(iteratorRecord); + const nextPromise = PromiseResolve(nextResult); + return PromisePrototypeThen(nextPromise, (iterResult) => { + if (typeof iterResult !== 'object' || iterResult === null) { + throw new ERR_INVALID_STATE.TypeError( + 'The promise returned by the iterator.next() method must fulfill with an object'); + } + if (iterResult.done) { + readableStreamDefaultControllerClose(stream[kState].controller); + } else { + readableStreamDefaultControllerEnqueue(stream[kState].controller, iterResult.value); + } + }); + } + + async function cancelAlgorithm(reason) { + const iterator = iteratorRecord.iterator; + const returnMethod = iterator.return; + if (returnMethod === undefined) { + return PromiseResolve(); + } + const returnResult = FunctionPrototypeCall(returnMethod, iterator, reason); + const returnPromise = PromiseResolve(returnResult); + return PromisePrototypeThen(returnPromise, (iterResult) => { + if (typeof iterResult !== 'object' || iterResult === null) { + throw new ERR_INVALID_STATE.TypeError( + 'The promise returned by the iterator.return() method must fulfill with an object'); + } + return undefined; + }); + } + + stream = new ReadableStream({ + start: startAlgorithm, + pull: pullAlgorithm, + cancel: cancelAlgorithm, + }, { + size() { + return 1; + }, + highWaterMark: 0, + }); + + return stream; +} + function readableStreamPipeTo( source, dest, @@ -2357,7 +2400,7 @@ function setupReadableStreamDefaultControllerFromSource( source, highWaterMark, sizeAlgorithm) { - const controller = createReadableStreamDefaultController(); + const controller = new ReadableStreamDefaultController(kSkipThrow); const start = source?.start; const pull = source?.pull; const cancel = source?.cancel; @@ -3155,7 +3198,7 @@ function setupReadableByteStreamControllerFromSource( stream, source, highWaterMark) { - const controller = createReadableByteStreamController(); + const controller = new ReadableByteStreamController(kSkipThrow); const start = source?.start; const pull = source?.pull; const cancel = source?.cancel; diff --git a/lib/internal/webstreams/transformstream.js b/lib/internal/webstreams/transformstream.js index 3a7fbebc042a22..bcccf90af91a0f 100644 --- a/lib/internal/webstreams/transformstream.js +++ b/lib/internal/webstreams/transformstream.js @@ -8,6 +8,7 @@ const { PromiseResolve, ReflectConstruct, SymbolToStringTag, + Symbol, } = primordials; const { @@ -65,6 +66,8 @@ const { const assert = require('internal/assert'); +const kSkipThrow = Symbol('kSkipThrow'); + const getNonWritablePropertyDescriptor = (value) => { return { __proto__: null, @@ -269,8 +272,10 @@ TransferredTransformStream.prototype[kDeserialize] = () => {}; class TransformStreamDefaultController { [kType] = 'TransformStreamDefaultController'; - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + constructor(skipThrowSymbol = undefined) { + if (skipThrowSymbol !== kSkipThrow) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } } /** @@ -331,15 +336,6 @@ ObjectDefineProperties(TransformStreamDefaultController.prototype, { [SymbolToStringTag]: getNonWritablePropertyDescriptor(TransformStreamDefaultController.name), }); -function createTransformStreamDefaultController() { - return ReflectConstruct( - function() { - this[kType] = 'TransformStreamDefaultController'; - }, - [], - TransformStreamDefaultController); -} - const isTransformStream = isBrandCheck('TransformStream'); const isTransformStreamDefaultController = @@ -454,7 +450,7 @@ function setupTransformStreamDefaultController( function setupTransformStreamDefaultControllerFromTransformer( stream, transformer) { - const controller = createTransformStreamDefaultController(); + const controller = new TransformStreamDefaultController(kSkipThrow); const transform = transformer?.transform || defaultTransformAlgorithm; const flush = transformer?.flush || nonOpFlush; const transformAlgorithm = diff --git a/lib/internal/webstreams/util.js b/lib/internal/webstreams/util.js index abc064170926dd..1979c55667b167 100644 --- a/lib/internal/webstreams/util.js +++ b/lib/internal/webstreams/util.js @@ -13,6 +13,8 @@ const { PromiseReject, ReflectGet, Symbol, + SymbolAsyncIterator, + SymbolIterator, Uint8Array, } = primordials; @@ -20,6 +22,7 @@ const { codes: { ERR_INVALID_ARG_VALUE, ERR_OPERATION_FAILED, + ERR_INVALID_STATE, }, } = require('internal/errors'); @@ -217,6 +220,54 @@ function lazyTransfer() { return transfer; } +function createAsyncFromSyncIterator(syncIteratorRecord) { + const syncIterable = { + [SymbolIterator]: () => syncIteratorRecord.iterator, + }; + + const asyncIterator = (async function* () { + return yield* syncIterable; + }()); + + const nextMethod = asyncIterator.next; + return { iterator: asyncIterator, nextMethod, done: false }; +} + +function getIterator(obj, kind = 'sync', method) { + if (method === undefined) { + if (kind === 'async') { + method = obj[SymbolAsyncIterator]; + if (method === undefined) { + const syncMethod = obj[SymbolIterator]; + const syncIteratorRecord = getIterator(obj, 'sync', syncMethod); + return createAsyncFromSyncIterator(syncIteratorRecord); + } + } else { + method = obj[SymbolIterator]; + } + } + + const iterator = FunctionPrototypeCall(method, obj); + if (typeof iterator !== 'object' || iterator === null) { + throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object'); + } + const nextMethod = iterator.next; + return { iterator, nextMethod, done: false }; +} + +function iteratorNext(iteratorRecord, value) { + let result; + if (value === undefined) { + result = FunctionPrototypeCall(iteratorRecord.nextMethod, iteratorRecord.iterator); + } else { + result = FunctionPrototypeCall(iteratorRecord.nextMethod, iteratorRecord.iterator, [value]); + } + if (typeof result !== 'object' || result === null) { + throw new ERR_INVALID_STATE.TypeError('The iterator.next() method must return an object'); + } + return result; +} + module.exports = { ArrayBufferViewGetBuffer, ArrayBufferViewGetByteLength, @@ -243,6 +294,8 @@ module.exports = { nonOpPull, nonOpStart, nonOpWrite, + getIterator, + iteratorNext, kType, kState, }; diff --git a/lib/internal/webstreams/writablestream.js b/lib/internal/webstreams/writablestream.js index b0eb5f20abb80e..44810d533c7092 100644 --- a/lib/internal/webstreams/writablestream.js +++ b/lib/internal/webstreams/writablestream.js @@ -81,6 +81,7 @@ const assert = require('internal/assert'); const kAbort = Symbol('kAbort'); const kCloseSentinel = Symbol('kCloseSentinel'); const kError = Symbol('kError'); +const kSkipThrow = Symbol('kSkipThrow'); let releasedError; @@ -523,8 +524,10 @@ ObjectDefineProperties(WritableStreamDefaultWriter.prototype, { class WritableStreamDefaultController { [kType] = 'WritableStreamDefaultController'; - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); + constructor(skipThrowSymbol = undefined) { + if (skipThrowSymbol !== kSkipThrow) { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } } [kAbort](reason) { @@ -570,14 +573,6 @@ ObjectDefineProperties(WritableStreamDefaultController.prototype, { [SymbolToStringTag]: getNonWritablePropertyDescriptor(WritableStreamDefaultController.name), }); -function createWritableStreamDefaultController() { - return ReflectConstruct( - function() { - this[kType] = 'WritableStreamDefaultController'; - }, - [], WritableStreamDefaultController); -} - const isWritableStream = isBrandCheck('WritableStream'); const isWritableStreamDefaultWriter = @@ -1234,7 +1229,7 @@ function setupWritableStreamDefaultControllerFromSink( sink, highWaterMark, sizeAlgorithm) { - const controller = createWritableStreamDefaultController(); + const controller = new WritableStreamDefaultController(kSkipThrow); const start = sink?.start; const write = sink?.write; const close = sink?.close; diff --git a/lib/module.js b/lib/module.js index b4a6dd7d18de56..ee90e92f53093c 100644 --- a/lib/module.js +++ b/lib/module.js @@ -2,8 +2,10 @@ const { findSourceMap } = require('internal/source_map/source_map_cache'); const { Module } = require('internal/modules/cjs/loader'); +const { register } = require('internal/modules/esm/loader'); const { SourceMap } = require('internal/source_map/source_map'); Module.findSourceMap = findSourceMap; +Module.register = register; Module.SourceMap = SourceMap; module.exports = Module; diff --git a/lib/url.js b/lib/url.js index 2cb51ff362a295..1cf27ec371e581 100644 --- a/lib/url.js +++ b/lib/url.js @@ -401,10 +401,7 @@ Url.prototype.parse = function parse(url, parseQueryString, slashesDenoteHost) { // It only converts parts of the domain name that // have non-ASCII characters, i.e. it doesn't matter if // you call it with a domain that already is ASCII-only. - - // Use lenient mode (`true`) to try to support even non-compliant - // URLs. - this.hostname = toASCII(this.hostname, true); + this.hostname = toASCII(this.hostname); // Prevent two potential routes of hostname spoofing. // 1. If this.hostname is empty, it must have become empty due to toASCII diff --git a/node.gyp b/node.gyp index 533d860a1e3cfa..49e39c3ce8b2e2 100644 --- a/node.gyp +++ b/node.gyp @@ -100,6 +100,7 @@ 'src/node_contextify.cc', 'src/node_credentials.cc', 'src/node_dir.cc', + 'src/node_dotenv.cc', 'src/node_env_var.cc', 'src/node_errors.cc', 'src/node_external_reference.cc', @@ -214,6 +215,7 @@ 'src/node_context_data.h', 'src/node_contextify.h', 'src/node_dir.h', + 'src/node_dotenv.h', 'src/node_errors.h', 'src/node_exit_code.h', 'src/node_external_reference.h', @@ -877,9 +879,6 @@ 'node_target_type=="executable"', { 'defines': [ 'NODE_ENABLE_LARGE_CODE_PAGES=1' ], }], - ['OS in "linux mac"', { - 'defines': [ 'NODE_MKSNAPSHOT_USE_STRING_LITERALS' ], - }], [ 'use_openssl_def==1', { # TODO(bnoordhuis) Make all platforms export the same list of symbols. # Teach mkssldef.py to generate linker maps that UNIX linkers understand. @@ -1039,6 +1038,7 @@ 'test/cctest/test_aliased_buffer.cc', 'test/cctest/test_base64.cc', 'test/cctest/test_base_object_ptr.cc', + 'test/cctest/test_cppgc.cc', 'test/cctest/test_node_postmortem_metadata.cc', 'test/cctest/test_environment.cc', 'test/cctest/test_linked_binding.cc', @@ -1248,6 +1248,9 @@ ], 'conditions': [ + ['OS in "linux mac"', { + 'defines': [ 'NODE_MKSNAPSHOT_USE_STRING_LITERALS=1' ], + }], [ 'node_use_openssl=="true"', { 'defines': [ 'HAVE_OPENSSL=1', diff --git a/src/README.md b/src/README.md index 15317f06ae0ca3..5853e1efa2396b 100644 --- a/src/README.md +++ b/src/README.md @@ -574,8 +574,7 @@ void InitializeHttpParser(Local target, Local context, void* priv) { Realm* realm = Realm::GetCurrent(context); - BindingData* const binding_data = - realm->AddBindingData(context, target); + BindingData* const binding_data = realm->AddBindingData(target); if (binding_data == nullptr) return; Local t = NewFunctionTemplate(realm->isolate(), Parser::New); diff --git a/src/api/environment.cc b/src/api/environment.cc index 2128ca6c4ebb75..6a6164b6d29443 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -408,8 +408,6 @@ void FreeIsolateData(IsolateData* isolate_data) { delete isolate_data; } -InspectorParentHandle::~InspectorParentHandle() {} - // Hide the internal handle class from the public API. #if HAVE_INSPECTOR struct InspectorParentHandleImpl : public InspectorParentHandle { diff --git a/src/base_object-inl.h b/src/base_object-inl.h index 99a438a4963717..2ca432e2dfd3c6 100644 --- a/src/base_object-inl.h +++ b/src/base_object-inl.h @@ -70,23 +70,28 @@ Realm* BaseObject::realm() const { return realm_; } -bool BaseObject::IsBaseObject(v8::Local obj) { +bool BaseObject::IsBaseObject(IsolateData* isolate_data, + v8::Local obj) { if (obj->InternalFieldCount() < BaseObject::kInternalFieldCount) { return false; } - void* ptr = - obj->GetAlignedPointerFromInternalField(BaseObject::kEmbedderType); - return ptr == &kNodeEmbedderId; + + uint16_t* ptr = static_cast( + obj->GetAlignedPointerFromInternalField(BaseObject::kEmbedderType)); + return ptr == isolate_data->embedder_id_for_non_cppgc(); } -void BaseObject::TagBaseObject(v8::Local object) { +void BaseObject::TagBaseObject(IsolateData* isolate_data, + v8::Local object) { DCHECK_GE(object->InternalFieldCount(), BaseObject::kInternalFieldCount); - object->SetAlignedPointerInInternalField(BaseObject::kEmbedderType, - &kNodeEmbedderId); + object->SetAlignedPointerInInternalField( + BaseObject::kEmbedderType, isolate_data->embedder_id_for_non_cppgc()); } -void BaseObject::SetInternalFields(v8::Local object, void* slot) { - TagBaseObject(object); +void BaseObject::SetInternalFields(IsolateData* isolate_data, + v8::Local object, + void* slot) { + TagBaseObject(isolate_data, object); object->SetAlignedPointerInInternalField(BaseObject::kSlot, slot); } @@ -130,7 +135,8 @@ template void BaseObject::InternalFieldGet( v8::Local property, const v8::PropertyCallbackInfo& info) { - info.GetReturnValue().Set(info.This()->GetInternalField(Field)); + info.GetReturnValue().Set( + info.This()->GetInternalField(Field).As()); } template diff --git a/src/base_object.cc b/src/base_object.cc index ed0fafe74d43a9..70c77eea442e7f 100644 --- a/src/base_object.cc +++ b/src/base_object.cc @@ -17,7 +17,7 @@ BaseObject::BaseObject(Realm* realm, Local object) : persistent_handle_(realm->isolate(), object), realm_(realm) { CHECK_EQ(false, object.IsEmpty()); CHECK_GE(object->InternalFieldCount(), BaseObject::kInternalFieldCount); - SetInternalFields(object, static_cast(this)); + SetInternalFields(realm->isolate_data(), object, static_cast(this)); realm->AddCleanupHook(DeleteMe, static_cast(this)); realm->modify_base_object_count(1); } @@ -66,18 +66,13 @@ void BaseObject::MakeWeak() { WeakCallbackType::kParameter); } -// This just has to be different from the Chromium ones: -// https://source.chromium.org/chromium/chromium/src/+/main:gin/public/gin_embedders.h;l=18-23;drc=5a758a97032f0b656c3c36a3497560762495501a -// Otherwise, when Node is loaded in an isolate which uses cppgc, cppgc will -// misinterpret the data stored in the embedder fields and try to garbage -// collect them. -uint16_t kNodeEmbedderId = 0x90de; - void BaseObject::LazilyInitializedJSTemplateConstructor( const FunctionCallbackInfo& args) { DCHECK(args.IsConstructCall()); CHECK_GE(args.This()->InternalFieldCount(), BaseObject::kInternalFieldCount); - SetInternalFields(args.This(), nullptr); + Environment* env = Environment::GetCurrent(args); + DCHECK_NOT_NULL(env); + SetInternalFields(env->isolate_data(), args.This(), nullptr); } Local BaseObject::MakeLazilyInitializedJSTemplate( diff --git a/src/base_object.h b/src/base_object.h index cfd6af673cec97..5968694e8393d8 100644 --- a/src/base_object.h +++ b/src/base_object.h @@ -41,8 +41,6 @@ namespace worker { class TransferData; } -extern uint16_t kNodeEmbedderId; - class BaseObject : public MemoryRetainer { public: enum InternalFields { kEmbedderType, kSlot, kInternalFieldCount }; @@ -74,10 +72,13 @@ class BaseObject : public MemoryRetainer { // was also passed to the `BaseObject()` constructor initially. // This may return `nullptr` if the C++ object has not been constructed yet, // e.g. when the JS object used `MakeLazilyInitializedJSTemplate`. - static inline void SetInternalFields(v8::Local object, + static inline void SetInternalFields(IsolateData* isolate_data, + v8::Local object, void* slot); - static inline bool IsBaseObject(v8::Local object); - static inline void TagBaseObject(v8::Local object); + static inline bool IsBaseObject(IsolateData* isolate_data, + v8::Local object); + static inline void TagBaseObject(IsolateData* isolate_data, + v8::Local object); static void LazilyInitializedJSTemplateConstructor( const v8::FunctionCallbackInfo& args); static inline BaseObject* FromJSObject(v8::Local object); diff --git a/src/blob_serializer_deserializer.h b/src/blob_serializer_deserializer.h index aa07bee54fd1bc..fe7989e22a3536 100644 --- a/src/blob_serializer_deserializer.h +++ b/src/blob_serializer_deserializer.h @@ -39,7 +39,7 @@ class BlobDeserializer : public BlobSerializerDeserializer { public: explicit BlobDeserializer(bool is_debug_v, std::string_view s) : BlobSerializerDeserializer(is_debug_v), sink(s) {} - ~BlobDeserializer() {} + ~BlobDeserializer() = default; size_t read_total = 0; std::string_view sink; @@ -85,7 +85,7 @@ class BlobSerializer : public BlobSerializerDeserializer { public: explicit BlobSerializer(bool is_debug_v) : BlobSerializerDeserializer(is_debug_v) {} - ~BlobSerializer() {} + ~BlobSerializer() = default; Impl* impl() { return static_cast(this); } const Impl* impl() const { return static_cast(this); } diff --git a/src/callback_queue.h b/src/callback_queue.h index e5694d5e1fe56a..9b649c04d7e106 100644 --- a/src/callback_queue.h +++ b/src/callback_queue.h @@ -4,6 +4,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include +#include namespace node { diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 96aee6df99850f..433c5822953071 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -1052,7 +1052,7 @@ int AnyTraits::Parse( return status; wrap->CallOnComplete(ret); - return 0; + return ARES_SUCCESS; } int ATraits::Parse( @@ -1086,7 +1086,7 @@ int ATraits::Parse( Local ttls = AddrTTLToArray(env, addrttls, naddrttls); wrap->CallOnComplete(ret, ttls); - return 0; + return ARES_SUCCESS; } int AaaaTraits::Parse( @@ -1120,7 +1120,7 @@ int AaaaTraits::Parse( Local ttls = AddrTTLToArray(env, addrttls, naddrttls); wrap->CallOnComplete(ret, ttls); - return 0; + return ARES_SUCCESS; } int CaaTraits::Parse( @@ -1142,7 +1142,7 @@ int CaaTraits::Parse( return status; wrap->CallOnComplete(ret); - return 0; + return ARES_SUCCESS; } int CnameTraits::Parse( @@ -1165,7 +1165,7 @@ int CnameTraits::Parse( return status; wrap->CallOnComplete(ret); - return 0; + return ARES_SUCCESS; } int MxTraits::Parse( @@ -1188,7 +1188,7 @@ int MxTraits::Parse( return status; wrap->CallOnComplete(mx_records); - return 0; + return ARES_SUCCESS; } int NsTraits::Parse( @@ -1211,7 +1211,7 @@ int NsTraits::Parse( return status; wrap->CallOnComplete(names); - return 0; + return ARES_SUCCESS; } int TxtTraits::Parse( @@ -1233,7 +1233,7 @@ int TxtTraits::Parse( return status; wrap->CallOnComplete(txt_records); - return 0; + return ARES_SUCCESS; } int SrvTraits::Parse( @@ -1255,7 +1255,7 @@ int SrvTraits::Parse( return status; wrap->CallOnComplete(srv_records); - return 0; + return ARES_SUCCESS; } int PtrTraits::Parse( @@ -1279,7 +1279,7 @@ int PtrTraits::Parse( return status; wrap->CallOnComplete(aliases); - return 0; + return ARES_SUCCESS; } int NaptrTraits::Parse( @@ -1301,7 +1301,7 @@ int NaptrTraits::Parse( return status; wrap->CallOnComplete(naptr_records); - return 0; + return ARES_SUCCESS; } int SoaTraits::Parse( @@ -1352,7 +1352,7 @@ int SoaTraits::Parse( ares_free_data(soa_out); wrap->CallOnComplete(soa_record); - return 0; + return ARES_SUCCESS; } int ReverseTraits::Send(GetHostByAddrWrap* wrap, const char* name) { @@ -1396,7 +1396,7 @@ int ReverseTraits::Parse( HandleScope handle_scope(env->isolate()); Context::Scope context_scope(env->context()); wrap->CallOnComplete(HostentToNames(env, host)); - return 0; + return ARES_SUCCESS; } namespace { diff --git a/src/cleanup_queue-inl.h b/src/cleanup_queue-inl.h index d1fbd8241d9919..dad23ecf3aec24 100644 --- a/src/cleanup_queue-inl.h +++ b/src/cleanup_queue-inl.h @@ -26,8 +26,8 @@ bool CleanupQueue::empty() const { } void CleanupQueue::Add(Callback cb, void* arg) { - auto insertion_info = cleanup_hooks_.emplace( - CleanupHookCallback{cb, arg, cleanup_hook_counter_++}); + auto insertion_info = + cleanup_hooks_.emplace(cb, arg, cleanup_hook_counter_++); // Make sure there was no existing element with these values. CHECK_EQ(insertion_info.second, true); } diff --git a/src/cleanup_queue.h b/src/cleanup_queue.h index 2ca333aca855ff..15451ab9473c81 100644 --- a/src/cleanup_queue.h +++ b/src/cleanup_queue.h @@ -18,7 +18,7 @@ class CleanupQueue : public MemoryRetainer { public: typedef void (*Callback)(void*); - CleanupQueue() {} + CleanupQueue() = default; // Not copyable. CleanupQueue(const CleanupQueue&) = delete; diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 415464be04db6b..b3a73f5c9d10a6 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -130,8 +130,6 @@ void ECDH::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("key", key_ ? kSizeOf_EC_KEY : 0); } -ECDH::~ECDH() {} - void ECDH::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); diff --git a/src/crypto/crypto_ec.h b/src/crypto/crypto_ec.h index 9782ce0bf35a66..f9570bd41f92eb 100644 --- a/src/crypto/crypto_ec.h +++ b/src/crypto/crypto_ec.h @@ -20,7 +20,7 @@ int GetOKPCurveFromName(const char* name); class ECDH final : public BaseObject { public: - ~ECDH() override; + ~ECDH() override = default; static void Initialize(Environment* env, v8::Local target); static void RegisterExternalReferences(ExternalReferenceRegistry* registry); diff --git a/src/crypto/crypto_tls.cc b/src/crypto/crypto_tls.cc index 028b251b3e7799..43a661cca7c313 100644 --- a/src/crypto/crypto_tls.cc +++ b/src/crypto/crypto_tls.cc @@ -357,12 +357,15 @@ TLSWrap::TLSWrap(Environment* env, Local obj, Kind kind, StreamBase* stream, - SecureContext* sc) + SecureContext* sc, + UnderlyingStreamWriteStatus under_stream_ws) : AsyncWrap(env, obj, AsyncWrap::PROVIDER_TLSWRAP), StreamBase(env), env_(env), kind_(kind), - sc_(sc) { + sc_(sc), + has_active_write_issued_by_prev_listener_( + under_stream_ws == UnderlyingStreamWriteStatus::kHasActive) { MakeWeak(); CHECK(sc_); ssl_ = sc_->CreateSSL(); @@ -472,14 +475,19 @@ void TLSWrap::InitSSL() { void TLSWrap::Wrap(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - CHECK_EQ(args.Length(), 3); + CHECK_EQ(args.Length(), 4); CHECK(args[0]->IsObject()); CHECK(args[1]->IsObject()); CHECK(args[2]->IsBoolean()); + CHECK(args[3]->IsBoolean()); Local sc = args[1].As(); Kind kind = args[2]->IsTrue() ? Kind::kServer : Kind::kClient; + UnderlyingStreamWriteStatus under_stream_ws = + args[3]->IsTrue() ? UnderlyingStreamWriteStatus::kHasActive + : UnderlyingStreamWriteStatus::kVacancy; + StreamBase* stream = StreamBase::FromObject(args[0].As()); CHECK_NOT_NULL(stream); @@ -490,7 +498,8 @@ void TLSWrap::Wrap(const FunctionCallbackInfo& args) { return; } - TLSWrap* res = new TLSWrap(env, obj, kind, stream, Unwrap(sc)); + TLSWrap* res = new TLSWrap( + env, obj, kind, stream, Unwrap(sc), under_stream_ws); args.GetReturnValue().Set(res->object()); } @@ -596,6 +605,13 @@ void TLSWrap::EncOut() { return; } + if (UNLIKELY(has_active_write_issued_by_prev_listener_)) { + Debug(this, + "Returning from EncOut(), " + "has_active_write_issued_by_prev_listener_ is true"); + return; + } + // Split-off queue if (established_ && current_write_) { Debug(this, "EncOut() write is scheduled"); @@ -666,6 +682,15 @@ void TLSWrap::EncOut() { void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) { Debug(this, "OnStreamAfterWrite(status = %d)", status); + + if (UNLIKELY(has_active_write_issued_by_prev_listener_)) { + Debug(this, "Notify write finish to the previous_listener_"); + CHECK_EQ(write_size_, 0); // we must have restrained writes + + previous_listener_->OnStreamAfterWrite(req_wrap, status); + return; + } + if (current_empty_write_) { Debug(this, "Had empty write"); BaseObjectPtr current_empty_write = @@ -2021,6 +2046,16 @@ void TLSWrap::GetALPNNegotiatedProto(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } +void TLSWrap::WritesIssuedByPrevListenerDone( + const FunctionCallbackInfo& args) { + TLSWrap* w; + ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder()); + + Debug(w, "WritesIssuedByPrevListenerDone is called"); + w->has_active_write_issued_by_prev_listener_ = false; + w->EncOut(); // resume all of our restrained writes +} + void TLSWrap::Cycle() { // Prevent recursion if (++cycle_depth_ > 1) @@ -2098,6 +2133,10 @@ void TLSWrap::Initialize( SetProtoMethod(isolate, t, "setSession", SetSession); SetProtoMethod(isolate, t, "setVerifyMode", SetVerifyMode); SetProtoMethod(isolate, t, "start", Start); + SetProtoMethod(isolate, + t, + "writesIssuedByPrevListenerDone", + WritesIssuedByPrevListenerDone); SetProtoMethodNoSideEffect( isolate, t, "exportKeyingMaterial", ExportKeyingMaterial); @@ -2180,6 +2219,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(GetSharedSigalgs); registry->Register(GetTLSTicket); registry->Register(VerifyError); + registry->Register(WritesIssuedByPrevListenerDone); #ifdef SSL_set_max_send_fragment registry->Register(SetMaxSendFragment); diff --git a/src/crypto/crypto_tls.h b/src/crypto/crypto_tls.h index a1a7f2ef240cc0..b65cea22255248 100644 --- a/src/crypto/crypto_tls.h +++ b/src/crypto/crypto_tls.h @@ -48,6 +48,8 @@ class TLSWrap : public AsyncWrap, kServer }; + enum class UnderlyingStreamWriteStatus { kHasActive, kVacancy }; + static void Initialize(v8::Local target, v8::Local unused, v8::Local context, @@ -136,7 +138,8 @@ class TLSWrap : public AsyncWrap, v8::Local obj, Kind kind, StreamBase* stream, - SecureContext* sc); + SecureContext* sc, + UnderlyingStreamWriteStatus under_stream_ws); static void SSLInfoCallback(const SSL* ssl_, int where, int ret); void InitSSL(); @@ -217,6 +220,8 @@ class TLSWrap : public AsyncWrap, static void Start(const v8::FunctionCallbackInfo& args); static void VerifyError(const v8::FunctionCallbackInfo& args); static void Wrap(const v8::FunctionCallbackInfo& args); + static void WritesIssuedByPrevListenerDone( + const v8::FunctionCallbackInfo& args); #ifdef SSL_set_max_send_fragment static void SetMaxSendFragment( @@ -284,6 +289,8 @@ class TLSWrap : public AsyncWrap, BIOPointer bio_trace_; + bool has_active_write_issued_by_prev_listener_ = false; + public: std::vector alpn_protos_; // Accessed by SelectALPNCallback. bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback. diff --git a/src/encoding_binding.cc b/src/encoding_binding.cc index 0cf588295405ac..b65a4f868e2b26 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -78,7 +78,7 @@ void BindingData::Deserialize(Local context, // Recreate the buffer in the constructor. InternalFieldInfo* casted_info = static_cast(info); BindingData* binding = - realm->AddBindingData(context, holder, casted_info); + realm->AddBindingData(holder, casted_info); CHECK_NOT_NULL(binding); } @@ -232,7 +232,7 @@ void BindingData::CreatePerContextProperties(Local target, Local context, void* priv) { Realm* realm = Realm::GetCurrent(context); - realm->AddBindingData(context, target); + realm->AddBindingData(target); } void BindingData::RegisterTimerExternalReferences( diff --git a/src/env-inl.h b/src/env-inl.h index 43fc11217133c2..7802304b1891ae 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -34,6 +34,7 @@ #include "node_realm-inl.h" #include "util-inl.h" #include "uv.h" +#include "v8-cppgc.h" #include "v8.h" #include @@ -61,6 +62,39 @@ inline uv_loop_t* IsolateData::event_loop() const { return event_loop_; } +inline void IsolateData::SetCppgcReference(v8::Isolate* isolate, + v8::Local object, + void* wrappable) { + v8::CppHeap* heap = isolate->GetCppHeap(); + CHECK_NOT_NULL(heap); + v8::WrapperDescriptor descriptor = heap->wrapper_descriptor(); + uint16_t required_size = std::max(descriptor.wrappable_instance_index, + descriptor.wrappable_type_index); + CHECK_GT(object->InternalFieldCount(), required_size); + + uint16_t* id_ptr = nullptr; + { + Mutex::ScopedLock lock(isolate_data_mutex_); + auto it = + wrapper_data_map_.find(descriptor.embedder_id_for_garbage_collected); + CHECK_NE(it, wrapper_data_map_.end()); + id_ptr = &(it->second->cppgc_id); + } + + object->SetAlignedPointerInInternalField(descriptor.wrappable_type_index, + id_ptr); + object->SetAlignedPointerInInternalField(descriptor.wrappable_instance_index, + wrappable); +} + +inline uint16_t* IsolateData::embedder_id_for_cppgc() const { + return &(wrapper_data_->cppgc_id); +} + +inline uint16_t* IsolateData::embedder_id_for_non_cppgc() const { + return &(wrapper_data_->non_cppgc_id); +} + inline NodeArrayBufferAllocator* IsolateData::node_allocator() const { return node_allocator_; } diff --git a/src/env.cc b/src/env.cc index 56f4344d9e1b5d..7e3d3aca2d5f96 100644 --- a/src/env.cc +++ b/src/env.cc @@ -19,6 +19,7 @@ #include "tracing/agent.h" #include "tracing/traced_value.h" #include "util-inl.h" +#include "v8-cppgc.h" #include "v8-profiler.h" #include @@ -28,6 +29,7 @@ #include #include #include +#include namespace node { @@ -35,6 +37,8 @@ using errors::TryCatchScope; using v8::Array; using v8::Boolean; using v8::Context; +using v8::CppHeap; +using v8::CppHeapCreateParams; using v8::EmbedderGraph; using v8::EscapableHandleScope; using v8::Function; @@ -59,6 +63,7 @@ using v8::TracingController; using v8::TryCatch; using v8::Undefined; using v8::Value; +using v8::WrapperDescriptor; using worker::Worker; int const ContextEmbedderTag::kNodeContextTag = 0x6e6f64; @@ -497,6 +502,11 @@ void IsolateData::CreateProperties() { contextify::ContextifyContext::InitializeGlobalTemplates(this); } +constexpr uint16_t kDefaultCppGCEmebdderID = 0x90de; +Mutex IsolateData::isolate_data_mutex_; +std::unordered_map> + IsolateData::wrapper_data_map_; + IsolateData::IsolateData(Isolate* isolate, uv_loop_t* event_loop, MultiIsolatePlatform* platform, @@ -510,6 +520,54 @@ IsolateData::IsolateData(Isolate* isolate, snapshot_data_(snapshot_data) { options_.reset( new PerIsolateOptions(*(per_process::cli_options->per_isolate))); + v8::CppHeap* cpp_heap = isolate->GetCppHeap(); + + uint16_t cppgc_id = kDefaultCppGCEmebdderID; + if (cpp_heap != nullptr) { + // The general convention of the wrappable layout for cppgc in the + // ecosystem is: + // [ 0 ] -> embedder id + // [ 1 ] -> wrappable instance + // If the Isolate includes a CppHeap attached by another embedder, + // And if they also use the field 0 for the ID, we DCHECK that + // the layout matches our layout, and record the embedder ID for cppgc + // to avoid accidentally enabling cppgc on non-cppgc-managed wrappers . + v8::WrapperDescriptor descriptor = cpp_heap->wrapper_descriptor(); + if (descriptor.wrappable_type_index == BaseObject::kEmbedderType) { + cppgc_id = descriptor.embedder_id_for_garbage_collected; + DCHECK_EQ(descriptor.wrappable_instance_index, BaseObject::kSlot); + } + // If the CppHeap uses the slot we use to put non-cppgc-traced BaseObject + // for embedder ID, V8 could accidentally enable cppgc on them. So + // safe guard against this. + DCHECK_NE(descriptor.wrappable_type_index, BaseObject::kSlot); + } else { + cpp_heap_ = CppHeap::Create( + platform, + CppHeapCreateParams{ + {}, + WrapperDescriptor( + BaseObject::kEmbedderType, BaseObject::kSlot, cppgc_id)}); + isolate->AttachCppHeap(cpp_heap_.get()); + } + // We do not care about overflow since we just want this to be different + // from the cppgc id. + uint16_t non_cppgc_id = cppgc_id + 1; + + { + // GC could still be run after the IsolateData is destroyed, so we store + // the ids in a static map to ensure pointers to them are still valid + // then. In practice there should be very few variants of the cppgc id + // in one process so the size of this map should be very small. + node::Mutex::ScopedLock lock(isolate_data_mutex_); + auto it = wrapper_data_map_.find(cppgc_id); + if (it == wrapper_data_map_.end()) { + auto pair = wrapper_data_map_.emplace( + cppgc_id, new PerIsolateWrapperData{cppgc_id, non_cppgc_id}); + it = pair.first; + } + wrapper_data_ = it->second.get(); + } if (snapshot_data == nullptr) { CreateProperties(); @@ -518,6 +576,21 @@ IsolateData::IsolateData(Isolate* isolate, } } +IsolateData::~IsolateData() { + if (cpp_heap_ != nullptr) { + // The CppHeap must be detached before being terminated. + isolate_->DetachCppHeap(); + cpp_heap_->Terminate(); + } +} + +// Public API +void SetCppgcReference(Isolate* isolate, + Local object, + void* wrappable) { + IsolateData::SetCppgcReference(isolate, object, wrappable); +} + void IsolateData::MemoryInfo(MemoryTracker* tracker) const { #define V(PropertyName, StringValue) \ tracker->TrackField(#PropertyName, PropertyName()); @@ -572,10 +645,6 @@ void Environment::AssignToContext(Local context, context->SetAlignedPointerInEmbedderData(ContextEmbedderIndex::kEnvironment, this); context->SetAlignedPointerInEmbedderData(ContextEmbedderIndex::kRealm, realm); - // Used to retrieve bindings - context->SetAlignedPointerInEmbedderData( - ContextEmbedderIndex::kBindingDataStoreIndex, - realm->binding_data_store()); // ContextifyContexts will update this to a pointer to the native object. context->SetAlignedPointerInEmbedderData( @@ -598,8 +667,6 @@ void Environment::UnassignFromContext(Local context) { nullptr); context->SetAlignedPointerInEmbedderData(ContextEmbedderIndex::kRealm, nullptr); - context->SetAlignedPointerInEmbedderData( - ContextEmbedderIndex::kBindingDataStoreIndex, nullptr); context->SetAlignedPointerInEmbedderData( ContextEmbedderIndex::kContextifyContext, nullptr); } @@ -616,7 +683,7 @@ void Environment::TryLoadAddon( } } -std::string Environment::GetCwd() { +std::string Environment::GetCwd(const std::string& exec_path) { char cwd[PATH_MAX_BYTES]; size_t size = PATH_MAX_BYTES; const int err = uv_cwd(cwd, &size); @@ -628,7 +695,6 @@ std::string Environment::GetCwd() { // This can fail if the cwd is deleted. In that case, fall back to // exec_path. - const std::string& exec_path = exec_path_; return exec_path.substr(0, exec_path.find_last_of(kPathSeparator)); } @@ -662,7 +728,7 @@ std::unique_ptr Environment::release_managed_buffer( return bs; } -std::string GetExecPath(const std::vector& argv) { +std::string Environment::GetExecPath(const std::vector& argv) { char exec_path_buf[2 * PATH_MAX]; size_t exec_path_len = sizeof(exec_path_buf); std::string exec_path; @@ -704,7 +770,7 @@ Environment::Environment(IsolateData* isolate_data, timer_base_(uv_now(isolate_data->event_loop())), exec_argv_(exec_args), argv_(args), - exec_path_(GetExecPath(args)), + exec_path_(Environment::GetExecPath(args)), exit_info_( isolate_, kExitInfoFieldCount, MAYBE_FIELD_PTR(env_info, exit_info)), should_abort_on_uncaught_toggle_( @@ -783,7 +849,7 @@ Environment::Environment(IsolateData* isolate_data, destroy_async_id_list_.reserve(512); performance_state_ = std::make_unique( - isolate, MAYBE_FIELD_PTR(env_info, performance_state)); + isolate, time_origin_, MAYBE_FIELD_PTR(env_info, performance_state)); if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED( TRACING_CATEGORY_NODE1(environment)) != 0) { @@ -803,19 +869,17 @@ Environment::Environment(IsolateData* isolate_data, if (options_->experimental_permission) { permission()->EnablePermissions(); - // If any permission is set the process shouldn't be able to neither + // The process shouldn't be able to neither // spawn/worker nor use addons or enable inspector // unless explicitly allowed by the user - if (!options_->allow_fs_read.empty() || !options_->allow_fs_write.empty()) { - options_->allow_native_addons = false; - flags_ = flags_ | EnvironmentFlags::kNoCreateInspector; - permission()->Apply("*", permission::PermissionScope::kInspector); - if (!options_->allow_child_process) { - permission()->Apply("*", permission::PermissionScope::kChildProcess); - } - if (!options_->allow_worker_threads) { - permission()->Apply("*", permission::PermissionScope::kWorkerThreads); - } + options_->allow_native_addons = false; + flags_ = flags_ | EnvironmentFlags::kNoCreateInspector; + permission()->Apply("*", permission::PermissionScope::kInspector); + if (!options_->allow_child_process) { + permission()->Apply("*", permission::PermissionScope::kChildProcess); + } + if (!options_->allow_worker_threads) { + permission()->Apply("*", permission::PermissionScope::kWorkerThreads); } if (!options_->allow_fs_read.empty()) { @@ -1732,7 +1796,7 @@ void Environment::DeserializeProperties(const EnvSerializeInfo* info) { immediate_info_.Deserialize(ctx); timeout_info_.Deserialize(ctx); tick_info_.Deserialize(ctx); - performance_state_->Deserialize(ctx); + performance_state_->Deserialize(ctx, time_origin_); exit_info_.Deserialize(ctx); stream_base_state_.Deserialize(ctx); should_abort_on_uncaught_toggle_.Deserialize(ctx); @@ -1856,7 +1920,7 @@ size_t Environment::NearHeapLimitCallback(void* data, std::string dir = env->options()->diagnostic_dir; if (dir.empty()) { - dir = env->GetCwd(); + dir = Environment::GetCwd(env->exec_path_); } DiagnosticFilename name(env, "Heap", "heapsnapshot"); std::string filename = dir + kPathSeparator + (*name); diff --git a/src/env.h b/src/env.h index 231ca64db38e90..c02fc6bd62dd78 100644 --- a/src/env.h +++ b/src/env.h @@ -62,6 +62,10 @@ #include #include +namespace v8 { +class CppHeap; +} + namespace node { namespace shadow_realm { @@ -124,6 +128,11 @@ struct IsolateDataSerializeInfo { const IsolateDataSerializeInfo& i); }; +struct PerIsolateWrapperData { + uint16_t cppgc_id; + uint16_t non_cppgc_id; +}; + class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { public: IsolateData(v8::Isolate* isolate, @@ -131,6 +140,8 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { MultiIsolatePlatform* platform = nullptr, ArrayBufferAllocator* node_allocator = nullptr, const SnapshotData* snapshot_data = nullptr); + ~IsolateData(); + SET_MEMORY_INFO_NAME(IsolateData) SET_SELF_SIZE(IsolateData) void MemoryInfo(MemoryTracker* tracker) const override; @@ -139,6 +150,13 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { bool is_building_snapshot() const { return is_building_snapshot_; } void set_is_building_snapshot(bool value) { is_building_snapshot_ = value; } + uint16_t* embedder_id_for_cppgc() const; + uint16_t* embedder_id_for_non_cppgc() const; + + static inline void SetCppgcReference(v8::Isolate* isolate, + v8::Local object, + void* wrappable); + inline uv_loop_t* event_loop() const; inline MultiIsolatePlatform* platform() const; inline const SnapshotData* snapshot_data() const; @@ -220,9 +238,15 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { NodeArrayBufferAllocator* const node_allocator_; MultiIsolatePlatform* platform_; const SnapshotData* snapshot_data_; + std::unique_ptr cpp_heap_; std::shared_ptr options_; worker::Worker* worker_context_ = nullptr; bool is_building_snapshot_ = false; + PerIsolateWrapperData* wrapper_data_; + + static Mutex isolate_data_mutex_; + static std::unordered_map> + wrapper_data_map_; }; struct ContextInfo { @@ -564,6 +588,9 @@ class Environment : public MemoryRetainer { SET_MEMORY_INFO_NAME(Environment) + static std::string GetExecPath(const std::vector& argv); + static std::string GetCwd(const std::string& exec_path); + inline size_t SelfSize() const override; bool IsRootNode() const override { return true; } void MemoryInfo(MemoryTracker* tracker) const override; @@ -580,8 +607,6 @@ class Environment : public MemoryRetainer { // Should be called before InitializeInspector() void InitializeDiagnostics(); - std::string GetCwd(); - #if HAVE_INSPECTOR // If the environment is created for a worker, pass parent_handle and // the ownership if transferred into the Environment. diff --git a/src/heap_utils.cc b/src/heap_utils.cc index b423d97345e956..e385955a5d5fce 100644 --- a/src/heap_utils.cc +++ b/src/heap_utils.cc @@ -456,7 +456,9 @@ void TriggerHeapSnapshot(const FunctionCallbackInfo& args) { if (filename_v->IsUndefined()) { DiagnosticFilename name(env, "Heap", "heapsnapshot"); THROW_IF_INSUFFICIENT_PERMISSIONS( - env, permission::PermissionScope::kFileSystemWrite, env->GetCwd()); + env, + permission::PermissionScope::kFileSystemWrite, + Environment::GetCwd(env->exec_path())); if (WriteSnapshot(env, *name, options).IsNothing()) return; if (String::NewFromUtf8(isolate, *name).ToLocal(&filename_v)) { args.GetReturnValue().Set(filename_v); diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index fe1e573242f6e2..f0b4cc43c864ae 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -687,7 +687,7 @@ Agent::Agent(Environment* env) debug_options_(env->options()->debug_options()), host_port_(env->inspector_host_port()) {} -Agent::~Agent() {} +Agent::~Agent() = default; bool Agent::Start(const std::string& path, const DebugOptions& options, diff --git a/src/inspector_profiler.cc b/src/inspector_profiler.cc index 56e588ceefa17a..307049bf7c83c5 100644 --- a/src/inspector_profiler.cc +++ b/src/inspector_profiler.cc @@ -431,7 +431,8 @@ void StartProfilers(Environment* env) { if (env->options()->cpu_prof) { const std::string& dir = env->options()->cpu_prof_dir; env->set_cpu_prof_interval(env->options()->cpu_prof_interval); - env->set_cpu_prof_dir(dir.empty() ? env->GetCwd() : dir); + env->set_cpu_prof_dir(dir.empty() ? Environment::GetCwd(env->exec_path()) + : dir); if (env->options()->cpu_prof_name.empty()) { DiagnosticFilename filename(env, "CPU", "cpuprofile"); env->set_cpu_prof_name(*filename); @@ -446,7 +447,8 @@ void StartProfilers(Environment* env) { if (env->options()->heap_prof) { const std::string& dir = env->options()->heap_prof_dir; env->set_heap_prof_interval(env->options()->heap_prof_interval); - env->set_heap_prof_dir(dir.empty() ? env->GetCwd() : dir); + env->set_heap_prof_dir(dir.empty() ? Environment::GetCwd(env->exec_path()) + : dir); if (env->options()->heap_prof_name.empty()) { DiagnosticFilename filename(env, "Heap", "heapprofile"); env->set_heap_prof_name(*filename); diff --git a/src/js_native_api_v8.h b/src/js_native_api_v8.h index f7646778311bfd..1ac738af2adb88 100644 --- a/src/js_native_api_v8.h +++ b/src/js_native_api_v8.h @@ -10,8 +10,8 @@ namespace v8impl { class RefTracker { public: - RefTracker() {} - virtual ~RefTracker() {} + RefTracker() = default; + virtual ~RefTracker() = default; virtual void Finalize() {} typedef RefTracker RefList; @@ -268,14 +268,6 @@ inline napi_status napi_set_last_error(napi_env env, } \ } while (0) -#define RETURN_STATUS_IF_FALSE_WITH_PREAMBLE(env, condition, status) \ - do { \ - if (!(condition)) { \ - return napi_set_last_error( \ - (env), try_catch.HasCaught() ? napi_pending_exception : (status)); \ - } \ - } while (0) - #define CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe, status) \ RETURN_STATUS_IF_FALSE_WITH_PREAMBLE((env), !((maybe).IsEmpty()), (status)) diff --git a/src/json_parser.cc b/src/json_parser.cc index a9973c099087e5..1e19e174833fa5 100644 --- a/src/json_parser.cc +++ b/src/json_parser.cc @@ -4,7 +4,6 @@ #include "util-inl.h" namespace node { -using v8::ArrayBuffer; using v8::Context; using v8::Isolate; using v8::Local; @@ -12,26 +11,8 @@ using v8::Object; using v8::String; using v8::Value; -static Isolate* NewIsolate(v8::ArrayBuffer::Allocator* allocator) { - Isolate* isolate = Isolate::Allocate(); - CHECK_NOT_NULL(isolate); - per_process::v8_platform.Platform()->RegisterIsolate(isolate, - uv_default_loop()); - Isolate::CreateParams params; - params.array_buffer_allocator = allocator; - Isolate::Initialize(isolate, params); - return isolate; -} - -void JSONParser::FreeIsolate(Isolate* isolate) { - per_process::v8_platform.Platform()->UnregisterIsolate(isolate); - isolate->Dispose(); -} - JSONParser::JSONParser() - : allocator_(ArrayBuffer::Allocator::NewDefaultAllocator()), - isolate_(NewIsolate(allocator_.get())), - handle_scope_(isolate_.get()), + : handle_scope_(isolate_.get()), context_(isolate_.get(), Context::New(isolate_.get())), context_scope_(context_.Get(isolate_.get())) {} diff --git a/src/json_parser.h b/src/json_parser.h index 555f539acf3076..f2e7d592d9aaa7 100644 --- a/src/json_parser.h +++ b/src/json_parser.h @@ -16,7 +16,7 @@ namespace node { class JSONParser { public: JSONParser(); - ~JSONParser() {} + ~JSONParser() = default; bool Parse(const std::string& content); std::optional GetTopLevelStringField(std::string_view field); std::optional GetTopLevelBoolField(std::string_view field); @@ -24,9 +24,7 @@ class JSONParser { private: // We might want a lighter-weight JSON parser for this use case. But for now // using V8 is good enough. - static void FreeIsolate(v8::Isolate* isolate); - std::unique_ptr allocator_; - DeleteFnPtr isolate_; + RAIIIsolate isolate_; v8::HandleScope handle_scope_; v8::Global context_; v8::Context::Scope context_scope_; diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 77ee0dc9109ebb..2dca349bd97089 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -78,7 +78,7 @@ ModuleWrap::~ModuleWrap() { } Local ModuleWrap::context() const { - Local obj = object()->GetInternalField(kContextObjectSlot); + Local obj = object()->GetInternalField(kContextObjectSlot).As(); if (obj.IsEmpty()) return {}; return obj.As()->GetCreationContext().ToLocalChecked(); } @@ -684,7 +684,9 @@ MaybeLocal ModuleWrap::SyntheticModuleEvaluationStepsCallback( TryCatchScope try_catch(env); Local synthetic_evaluation_steps = - obj->object()->GetInternalField(kSyntheticEvaluationStepsSlot) + obj->object() + ->GetInternalField(kSyntheticEvaluationStepsSlot) + .As() .As(); obj->object()->SetInternalField( kSyntheticEvaluationStepsSlot, Undefined(isolate)); diff --git a/src/node.cc b/src/node.cc index f483e59dd155a8..e6be00eeb3c185 100644 --- a/src/node.cc +++ b/src/node.cc @@ -20,6 +20,7 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. #include "node.h" +#include "node_dotenv.h" // ========== local headers ========== @@ -62,6 +63,8 @@ #endif // NODE_USE_V8_PLATFORM #include "v8-profiler.h" +#include "cppgc/platform.h" + #if HAVE_INSPECTOR #include "inspector/worker_inspector.h" // ParentInspectorHandle #endif @@ -138,6 +141,10 @@ using v8::Value; namespace per_process { +// node_dotenv.h +// Instance is used to store environment variables including NODE_OPTIONS. +node::Dotenv dotenv_file = Dotenv(); + // node_revert.h // Bit flag used to track security reverts. unsigned int reverted_cve = 0; @@ -292,6 +299,21 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { CHECK(!env->isolate_data()->is_building_snapshot()); +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION + if (sea::IsSingleExecutable()) { + sea::SeaResource sea = sea::FindSingleExecutableResource(); + // The SEA preparation blob building process should already enforce this, + // this check is just here to guard against the unlikely case where + // the SEA preparation blob has been manually modified by someone. + CHECK_IMPLIES(sea.use_snapshot(), + !env->snapshot_deserialize_main().IsEmpty()); + } +#endif + + if (env->options()->has_env_file_string) { + per_process::dotenv_file.SetEnvironment(env); + } + // TODO(joyeecheung): move these conditions into JS land and let the // deserialize main function take precedence. For workers, we need to // move the pre-execution part into a different file that can be @@ -818,11 +840,22 @@ static ExitCode InitializeNodeWithArgsInternal( HandleEnvOptions(per_process::cli_options->per_isolate->per_env); + std::string node_options; + auto file_path = node::Dotenv::GetPathFromArgs(*argv); + + if (file_path.has_value()) { + auto cwd = Environment::GetCwd(Environment::GetExecPath(*argv)); + std::string path = cwd + kPathSeparator + file_path.value(); + CHECK(!per_process::v8_initialized); + per_process::dotenv_file.ParsePath(path); + per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options); + } + #if !defined(NODE_WITHOUT_NODE_OPTIONS) if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) { - std::string node_options; - - if (credentials::SafeGetenv("NODE_OPTIONS", &node_options)) { + // NODE_OPTIONS environment variable is preferred over the file one. + if (credentials::SafeGetenv("NODE_OPTIONS", &node_options) || + !node_options.empty()) { std::vector env_argv = ParseNodeOptionsEnvVar(node_options, errors); @@ -1085,6 +1118,14 @@ InitializeOncePerProcessInternal(const std::vector& args, V8::Initialize(); } + if (!(flags & ProcessInitializationFlags::kNoInitializeCppgc)) { + v8::PageAllocator* allocator = nullptr; + if (result->platform_ != nullptr) { + allocator = result->platform_->GetPageAllocator(); + } + cppgc::InitializeProcess(allocator); + } + performance::performance_v8_start = PERFORMANCE_NOW(); per_process::v8_initialized = true; @@ -1104,6 +1145,10 @@ void TearDownOncePerProcess() { ResetSignalHandlers(); } + if (!(flags & ProcessInitializationFlags::kNoInitializeCppgc)) { + cppgc::ShutdownProcess(); + } + per_process::v8_initialized = false; if (!(flags & ProcessInitializationFlags::kNoInitializeV8)) { V8::Dispose(); @@ -1127,9 +1172,6 @@ void TearDownOncePerProcess() { } } -InitializationResult::~InitializationResult() {} -InitializationResultImpl::~InitializationResultImpl() {} - ExitCode GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr, const InitializationResultImpl* result) { ExitCode exit_code = result->exit_code_enum(); @@ -1198,49 +1240,66 @@ ExitCode GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr, return exit_code; } -ExitCode LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr, - const InitializationResultImpl* result) { - ExitCode exit_code = result->exit_code_enum(); +bool LoadSnapshotData(const SnapshotData** snapshot_data_ptr) { // nullptr indicates there's no snapshot data. DCHECK_NULL(*snapshot_data_ptr); + + bool is_sea = false; +#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION + if (sea::IsSingleExecutable()) { + is_sea = true; + sea::SeaResource sea = sea::FindSingleExecutableResource(); + if (sea.use_snapshot()) { + std::unique_ptr read_data = + std::make_unique(); + std::string_view snapshot = sea.main_code_or_snapshot; + if (SnapshotData::FromBlob(read_data.get(), snapshot)) { + *snapshot_data_ptr = read_data.release(); + return true; + } else { + fprintf(stderr, "Invalid snapshot data in single executable binary\n"); + return false; + } + } + } +#endif + // --snapshot-blob indicates that we are reading a customized snapshot. - if (!per_process::cli_options->snapshot_blob.empty()) { + // Ignore it when we are loading from SEA. + if (!is_sea && !per_process::cli_options->snapshot_blob.empty()) { std::string filename = per_process::cli_options->snapshot_blob; FILE* fp = fopen(filename.c_str(), "rb"); if (fp == nullptr) { fprintf(stderr, "Cannot open %s", filename.c_str()); - exit_code = ExitCode::kStartupSnapshotFailure; - return exit_code; + return false; } std::unique_ptr read_data = std::make_unique(); bool ok = SnapshotData::FromFile(read_data.get(), fp); fclose(fp); if (!ok) { - // If we fail to read the customized snapshot, - // simply exit with kStartupSnapshotFailure. - exit_code = ExitCode::kStartupSnapshotFailure; - return exit_code; + return false; } *snapshot_data_ptr = read_data.release(); - } else if (per_process::cli_options->node_snapshot) { - // If --snapshot-blob is not specified, we are reading the embedded - // snapshot, but we will skip it if --no-node-snapshot is specified. + return true; + } + + if (per_process::cli_options->node_snapshot) { + // If --snapshot-blob is not specified or if the SEA contains no snapshot, + // we are reading the embedded snapshot, but we will skip it if + // --no-node-snapshot is specified. const node::SnapshotData* read_data = SnapshotBuilder::GetEmbeddedSnapshotData(); - if (read_data != nullptr && read_data->Check()) { + if (read_data != nullptr) { + if (!read_data->Check()) { + return false; + } // If we fail to read the embedded snapshot, treat it as if Node.js // was built without one. *snapshot_data_ptr = read_data; } } - NodeMainInstance main_instance(*snapshot_data_ptr, - uv_default_loop(), - per_process::v8_platform.Platform(), - result->args(), - result->exec_args()); - exit_code = main_instance.Run(); - return exit_code; + return true; } static ExitCode StartInternal(int argc, char** argv) { @@ -1275,7 +1334,8 @@ static ExitCode StartInternal(int argc, char** argv) { std::string sea_config = per_process::cli_options->experimental_sea_config; if (!sea_config.empty()) { - return sea::BuildSingleExecutableBlob(sea_config); + return sea::BuildSingleExecutableBlob( + sea_config, result->args(), result->exec_args()); } // --build-snapshot indicates that we are in snapshot building mode. @@ -1290,7 +1350,15 @@ static ExitCode StartInternal(int argc, char** argv) { } // Without --build-snapshot, we are in snapshot loading mode. - return LoadSnapshotDataAndRun(&snapshot_data, result.get()); + if (!LoadSnapshotData(&snapshot_data)) { + return ExitCode::kStartupSnapshotFailure; + } + NodeMainInstance main_instance(snapshot_data, + uv_default_loop(), + per_process::v8_platform.Platform(), + result->args(), + result->exec_args()); + return main_instance.Run(); } int Start(int argc, char** argv) { diff --git a/src/node.h b/src/node.h index 846ec413f8e1fc..ca01c42e8af484 100644 --- a/src/node.h +++ b/src/node.h @@ -261,6 +261,10 @@ enum Flags : uint32_t { kNoUseLargePages = 1 << 11, // Skip printing output for --help, --version, --v8-options. kNoPrintHelpOrVersionOutput = 1 << 12, + // Do not perform cppgc initialization. If set, the embedder must call + // cppgc::InitializeProcess() before creating a Node.js environment + // and call cppgc::ShutdownProcess() before process shutdown. + kNoInitializeCppgc = 1 << 13, // Emulate the behavior of InitializeNodeWithArgs() when passing // a flags argument to the InitializeOncePerProcess() replacement @@ -269,7 +273,7 @@ enum Flags : uint32_t { kNoStdioInitialization | kNoDefaultSignalHandling | kNoInitializeV8 | kNoInitializeNodeV8Platform | kNoInitOpenSSL | kNoParseGlobalDebugVariables | kNoAdjustResourceLimits | - kNoUseLargePages | kNoPrintHelpOrVersionOutput, + kNoUseLargePages | kNoPrintHelpOrVersionOutput | kNoInitializeCppgc, }; } // namespace ProcessInitializationFlags namespace ProcessFlags = ProcessInitializationFlags; // Legacy alias. @@ -285,7 +289,7 @@ enum Flags : uint32_t { class NODE_EXTERN InitializationResult { public: - virtual ~InitializationResult(); + virtual ~InitializationResult() = default; // Returns a suggested process exit code. virtual int exit_code() const = 0; @@ -654,7 +658,7 @@ enum Flags : uint64_t { } // namespace EnvironmentFlags struct InspectorParentHandle { - virtual ~InspectorParentHandle(); + virtual ~InspectorParentHandle() = default; }; // TODO(addaleax): Maybe move per-Environment options parsing here. @@ -1486,6 +1490,25 @@ void RegisterSignalHandler(int signal, bool reset_handler = false); #endif // _WIN32 +// Configure the layout of the JavaScript object with a cppgc::GarbageCollected +// instance so that when the JavaScript object is reachable, the garbage +// collected instance would have its Trace() method invoked per the cppgc +// contract. To make it work, the process must have called +// cppgc::InitializeProcess() before, which is usually the case for addons +// loaded by the stand-alone Node.js executable. Embedders of Node.js can use +// either need to call it themselves or make sure that +// ProcessInitializationFlags::kNoInitializeCppgc is *not* set for cppgc to +// work. +// If the CppHeap is owned by Node.js, which is usually the case for addon, +// the object must be created with at least two internal fields available, +// and the first two internal fields would be configured by Node.js. +// This may be superseded by a V8 API in the future, see +// https://bugs.chromium.org/p/v8/issues/detail?id=13960. Until then this +// serves as a helper for Node.js isolates. +NODE_EXTERN void SetCppgcReference(v8::Isolate* isolate, + v8::Local object, + void* wrappable); + } // namespace node #endif // SRC_NODE_H_ diff --git a/src/node_blob.cc b/src/node_blob.cc index 7b04a658fc9e73..e4a3b2fe8b0f98 100644 --- a/src/node_blob.cc +++ b/src/node_blob.cc @@ -130,7 +130,7 @@ void Blob::CreatePerContextProperties(Local target, Local context, void* priv) { Realm* realm = Realm::GetCurrent(context); - realm->AddBindingData(context, target); + realm->AddBindingData(target); } Local Blob::GetConstructorTemplate(Environment* env) { @@ -535,8 +535,7 @@ void BlobBindingData::Deserialize(Local context, DCHECK_EQ(index, BaseObject::kEmbedderType); HandleScope scope(context->GetIsolate()); Realm* realm = Realm::GetCurrent(context); - BlobBindingData* binding = - realm->AddBindingData(context, holder); + BlobBindingData* binding = realm->AddBindingData(holder); CHECK_NOT_NULL(binding); } diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 875fb0979950b7..d78ad3dd811432 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -82,8 +82,8 @@ Local BuiltinLoader::GetConfigString(Isolate* isolate) { return config_.ToStringChecked(isolate); } -std::vector BuiltinLoader::GetBuiltinIds() const { - std::vector ids; +std::vector BuiltinLoader::GetBuiltinIds() const { + std::vector ids; auto source = source_.read(); ids.reserve(source->size()); for (auto const& x : *source) { @@ -95,7 +95,7 @@ std::vector BuiltinLoader::GetBuiltinIds() const { BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { BuiltinCategories builtin_categories; - std::vector prefixes = { + const std::vector prefixes = { #if !HAVE_OPENSSL "internal/crypto/", "internal/debugger/", @@ -475,7 +475,7 @@ MaybeLocal BuiltinLoader::CompileAndCall(Local context, } bool BuiltinLoader::CompileAllBuiltins(Local context) { - std::vector ids = GetBuiltinIds(); + std::vector ids = GetBuiltinIds(); bool all_succeeded = true; std::string v8_tools_prefix = "internal/deps/v8/tools/"; for (const auto& id : ids) { @@ -483,11 +483,11 @@ bool BuiltinLoader::CompileAllBuiltins(Local context) { continue; } v8::TryCatch bootstrapCatch(context->GetIsolate()); - USE(LookupAndCompile(context, id.c_str(), nullptr)); + USE(LookupAndCompile(context, id.data(), nullptr)); if (bootstrapCatch.HasCaught()) { per_process::Debug(DebugCategory::CODE_CACHE, "Failed to compile code cache for %s\n", - id.c_str()); + id.data()); all_succeeded = false; PrintCaughtException(context->GetIsolate(), context, bootstrapCatch); } @@ -607,7 +607,7 @@ void BuiltinLoader::BuiltinIdsGetter(Local property, Environment* env = Environment::GetCurrent(info); Isolate* isolate = env->isolate(); - std::vector ids = env->builtin_loader()->GetBuiltinIds(); + std::vector ids = env->builtin_loader()->GetBuiltinIds(); info.GetReturnValue().Set( ToV8Value(isolate->GetCurrentContext(), ids).ToLocalChecked()); } diff --git a/src/node_builtins.h b/src/node_builtins.h index 5e634254fc6560..f91c2a8105bfe5 100644 --- a/src/node_builtins.h +++ b/src/node_builtins.h @@ -126,7 +126,7 @@ class NODE_EXTERN_PRIVATE BuiltinLoader { void LoadJavaScriptSource(); // Loads data into source_ UnionBytes GetConfig(); // Return data for config.gypi - std::vector GetBuiltinIds() const; + std::vector GetBuiltinIds() const; struct BuiltinCategories { std::set can_be_required; diff --git a/src/node_context_data.h b/src/node_context_data.h index 009d46c34dc4ef..1854eb879ee9ff 100644 --- a/src/node_context_data.h +++ b/src/node_context_data.h @@ -51,7 +51,6 @@ enum ContextEmbedderIndex { kEnvironment = NODE_CONTEXT_EMBEDDER_DATA_INDEX, kSandboxObject = NODE_CONTEXT_SANDBOX_OBJECT_INDEX, kAllowWasmCodeGeneration = NODE_CONTEXT_ALLOW_WASM_CODE_GENERATION_INDEX, - kBindingDataStoreIndex = NODE_BINDING_DATA_STORE_INDEX, kAllowCodeGenerationFromStrings = NODE_CONTEXT_ALLOW_CODE_GENERATION_FROM_STRINGS_INDEX, kContextifyContext = NODE_CONTEXT_CONTEXTIFY_CONTEXT_INDEX, diff --git a/src/node_contextify.cc b/src/node_contextify.cc index f8bd2d9b7cd71d..ee68ed12795740 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -935,6 +935,22 @@ Maybe StoreCodeCacheResult( return Just(true); } +// TODO(RaisinTen): Reuse in ContextifyContext::CompileFunction(). +MaybeLocal CompileFunction(Local context, + Local filename, + Local content, + std::vector>* parameters) { + ScriptOrigin script_origin(context->GetIsolate(), filename, 0, 0, true); + ScriptCompiler::Source script_source(content, script_origin); + + return ScriptCompiler::CompileFunction(context, + &script_source, + parameters->size(), + parameters->data(), + 0, + nullptr); +} + bool ContextifyScript::InstanceOf(Environment* env, const Local& value) { return !value.IsEmpty() && diff --git a/src/node_contextify.h b/src/node_contextify.h index 3160160521e0fe..9a0cbe07d6e660 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -210,6 +210,12 @@ v8::Maybe StoreCodeCacheResult( bool produce_cached_data, std::unique_ptr new_cached_data); +v8::MaybeLocal CompileFunction( + v8::Local context, + v8::Local filename, + v8::Local content, + std::vector>* parameters); + } // namespace contextify } // namespace node diff --git a/src/node_credentials.cc b/src/node_credentials.cc index 52abaab7a635db..c1f7a4f2acbdf6 100644 --- a/src/node_credentials.cc +++ b/src/node_credentials.cc @@ -123,7 +123,6 @@ bool SafeGetenv(const char* key, } fail: - text->clear(); return false; } diff --git a/src/node_dotenv.cc b/src/node_dotenv.cc new file mode 100644 index 00000000000000..d8d6fc1d55d3de --- /dev/null +++ b/src/node_dotenv.cc @@ -0,0 +1,164 @@ +#include "node_dotenv.h" +#include "env-inl.h" +#include "node_file.h" +#include "uv.h" + +namespace node { + +using v8::NewStringType; +using v8::String; + +std::optional Dotenv::GetPathFromArgs( + const std::vector& args) { + std::string_view flag = "--env-file"; + // Match the last `--env-file` + // This is required to imitate the default behavior of Node.js CLI argument + // matching. + auto path = + std::find_if(args.rbegin(), args.rend(), [&flag](const std::string& arg) { + return strncmp(arg.c_str(), flag.data(), flag.size()) == 0; + }); + + if (path == args.rend()) { + return std::nullopt; + } + + auto equal_char = path->find('='); + + if (equal_char != std::string::npos) { + return path->substr(equal_char + 1); + } + + auto next_arg = std::prev(path); + + if (next_arg == args.rend()) { + return std::nullopt; + } + + return *next_arg; +} + +void Dotenv::SetEnvironment(node::Environment* env) { + if (store_.empty()) { + return; + } + + auto isolate = env->isolate(); + + for (const auto& entry : store_) { + auto key = entry.first; + auto value = entry.second; + env->env_vars()->Set( + isolate, + v8::String::NewFromUtf8( + isolate, key.data(), NewStringType::kNormal, key.size()) + .ToLocalChecked(), + v8::String::NewFromUtf8( + isolate, value.data(), NewStringType::kNormal, value.size()) + .ToLocalChecked()); + } +} + +void Dotenv::ParsePath(const std::string_view path) { + uv_fs_t req; + auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); }); + + uv_file file = uv_fs_open(nullptr, &req, path.data(), 0, 438, nullptr); + if (req.result < 0) { + // req will be cleaned up by scope leave. + return; + } + uv_fs_req_cleanup(&req); + + auto defer_close = OnScopeLeave([file]() { + uv_fs_t close_req; + CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr)); + uv_fs_req_cleanup(&close_req); + }); + + std::string result{}; + char buffer[8192]; + uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer)); + + while (true) { + auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr); + if (req.result < 0) { + // req will be cleaned up by scope leave. + return; + } + uv_fs_req_cleanup(&req); + if (r <= 0) { + break; + } + result.append(buf.base, r); + } + + using std::string_view_literals::operator""sv; + auto lines = SplitString(result, "\n"sv); + + for (const auto& line : lines) { + ParseLine(line); + } +} + +void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) { + auto match = store_.find("NODE_OPTIONS"); + + if (match != store_.end()) { + *node_options = match->second; + } +} + +void Dotenv::ParseLine(const std::string_view line) { + auto equal_index = line.find('='); + + if (equal_index == std::string_view::npos) { + return; + } + + auto key = line.substr(0, equal_index); + + // Remove leading and trailing space characters from key. + while (!key.empty() && std::isspace(key.front())) key.remove_prefix(1); + while (!key.empty() && std::isspace(key.back())) key.remove_suffix(1); + + // Omit lines with comments + if (key.front() == '#' || key.empty()) { + return; + } + + auto value = std::string(line.substr(equal_index + 1)); + + // Might start and end with `"' characters. + auto quotation_index = value.find_first_of("`\"'"); + + if (quotation_index == 0) { + auto quote_character = value[quotation_index]; + value.erase(0, 1); + + auto end_quotation_index = value.find_last_of(quote_character); + + // We couldn't find the closing quotation character. Terminate. + if (end_quotation_index == std::string::npos) { + return; + } + + value.erase(end_quotation_index); + } else { + auto hash_index = value.find('#'); + + // Remove any inline comments + if (hash_index != std::string::npos) { + value.erase(hash_index); + } + + // Remove any leading/trailing spaces from unquoted values. + while (!value.empty() && std::isspace(value.front())) value.erase(0, 1); + while (!value.empty() && std::isspace(value.back())) + value.erase(value.size() - 1); + } + + store_.emplace(key, value); +} + +} // namespace node diff --git a/src/node_dotenv.h b/src/node_dotenv.h new file mode 100644 index 00000000000000..2fb810386324fc --- /dev/null +++ b/src/node_dotenv.h @@ -0,0 +1,38 @@ +#ifndef SRC_NODE_DOTENV_H_ +#define SRC_NODE_DOTENV_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "util-inl.h" + +#include +#include + +namespace node { + +class Dotenv { + public: + Dotenv() = default; + Dotenv(const Dotenv& d) = default; + Dotenv(Dotenv&& d) noexcept = default; + Dotenv& operator=(Dotenv&& d) noexcept = default; + Dotenv& operator=(const Dotenv& d) = default; + ~Dotenv() = default; + + void ParsePath(const std::string_view path); + void AssignNodeOptionsIfAvailable(std::string* node_options); + void SetEnvironment(Environment* env); + + static std::optional GetPathFromArgs( + const std::vector& args); + + private: + void ParseLine(const std::string_view line); + std::map store_; +}; + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_DOTENV_H_ diff --git a/src/node_file.cc b/src/node_file.cc index 6405ebbde4aec0..4c21cc7467ccf2 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -456,7 +456,7 @@ MaybeLocal FileHandle::ClosePromise() { Local context = env()->context(); Local close_resolver = - object()->GetInternalField(FileHandle::kClosingPromiseSlot); + object()->GetInternalField(FileHandle::kClosingPromiseSlot).As(); if (!close_resolver.IsEmpty() && !close_resolver->IsUndefined()) { CHECK(close_resolver->IsPromise()); return close_resolver.As(); @@ -3079,10 +3079,11 @@ void BindingData::LegacyMainResolve(const FunctionCallbackInfo& args) { return; } - std::string err_module_message = - "Cannot find package '" + module_path + "' imported from " + module_base; env->isolate()->ThrowException( - ERR_MODULE_NOT_FOUND(env->isolate(), err_module_message.c_str())); + ERR_MODULE_NOT_FOUND(env->isolate(), + "Cannot find package '%s' imported from %s", + module_path, + module_base)); } void BindingData::MemoryInfo(MemoryTracker* tracker) const { @@ -3155,7 +3156,7 @@ void BindingData::Deserialize(Local context, Realm* realm = Realm::GetCurrent(context); InternalFieldInfo* casted_info = static_cast(info); BindingData* binding = - realm->AddBindingData(context, holder, casted_info); + realm->AddBindingData(holder, casted_info); CHECK_NOT_NULL(binding); } @@ -3308,7 +3309,7 @@ static void CreatePerContextProperties(Local target, Local context, void* priv) { Realm* realm = Realm::GetCurrent(context); - realm->AddBindingData(context, target); + realm->AddBindingData(target); } BindingData* FSReqBase::binding_data() { diff --git a/src/node_http2.cc b/src/node_http2.cc index 01d0eb3418b39f..0a8f2271f25689 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -3180,7 +3180,7 @@ void Initialize(Local target, Isolate* isolate = env->isolate(); HandleScope handle_scope(isolate); - Http2State* const state = realm->AddBindingData(context, target); + Http2State* const state = realm->AddBindingData(target); if (state == nullptr) return; #define SET_STATE_TYPEDARRAY(name, field) \ diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index e1944d90557316..a12d89c3cd6cb4 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -1203,8 +1203,7 @@ void InitializeHttpParser(Local target, Realm* realm = Realm::GetCurrent(context); Environment* env = realm->env(); Isolate* isolate = env->isolate(); - BindingData* const binding_data = - realm->AddBindingData(context, target); + BindingData* const binding_data = realm->AddBindingData(target); if (binding_data == nullptr) return; Local t = NewFunctionTemplate(isolate, Parser::New); diff --git a/src/node_internals.h b/src/node_internals.h index 9243344eb788b5..d7f78664615fcf 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -318,7 +318,7 @@ void MarkBootstrapComplete(const v8::FunctionCallbackInfo& args); class InitializationResultImpl final : public InitializationResult { public: - ~InitializationResultImpl(); + ~InitializationResultImpl() = default; int exit_code() const { return static_cast(exit_code_enum()); } ExitCode exit_code_enum() const { return exit_code_; } bool early_return() const { return early_return_; } diff --git a/src/node_main_instance.cc b/src/node_main_instance.cc index 41e5bee353a579..e1e456cfad9325 100644 --- a/src/node_main_instance.cc +++ b/src/node_main_instance.cc @@ -68,6 +68,8 @@ NodeMainInstance::~NodeMainInstance() { return; } // This should only be done on a main instance that owns its isolate. + // IsolateData must be freed before UnregisterIsolate() is called. + isolate_data_.reset(); platform_->UnregisterIsolate(isolate_); isolate_->Dispose(); } @@ -92,12 +94,16 @@ void NodeMainInstance::Run(ExitCode* exit_code, Environment* env) { bool runs_sea_code = false; #ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION if (sea::IsSingleExecutable()) { - runs_sea_code = true; sea::SeaResource sea = sea::FindSingleExecutableResource(); - std::string_view code = sea.code; - LoadEnvironment(env, code); + if (!sea.use_snapshot()) { + runs_sea_code = true; + std::string_view code = sea.main_code_or_snapshot; + LoadEnvironment(env, code); + } } #endif + // Either there is already a snapshot main function from SEA, or it's not + // a SEA at all. if (!runs_sea_code) { LoadEnvironment(env, StartExecutionCallback{}); } diff --git a/src/node_messaging.cc b/src/node_messaging.cc index 509c516286674b..f015f86ec2a121 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -318,7 +318,7 @@ class SerializerDelegate : public ValueSerializer::Delegate { } Maybe WriteHostObject(Isolate* isolate, Local object) override { - if (BaseObject::IsBaseObject(object)) { + if (BaseObject::IsBaseObject(env_->isolate_data(), object)) { return WriteHostObject( BaseObjectPtr { Unwrap(object) }); } @@ -514,7 +514,8 @@ Maybe Message::Serialize(Environment* env, serializer.TransferArrayBuffer(id, ab); continue; } else if (entry->IsObject() && - BaseObject::IsBaseObject(entry.As())) { + BaseObject::IsBaseObject(env->isolate_data(), + entry.As())) { // Check if the source MessagePort is being transferred. if (!source_port.IsEmpty() && entry == source_port) { ThrowDataCloneException( @@ -1281,7 +1282,8 @@ JSTransferable::NestedTransferables() const { Local value; if (!list->Get(context, i).ToLocal(&value)) return Nothing(); - if (value->IsObject() && BaseObject::IsBaseObject(value.As())) + if (value->IsObject() && + BaseObject::IsBaseObject(env()->isolate_data(), value.As())) ret.emplace_back(Unwrap(value)); } return Just(ret); @@ -1336,7 +1338,8 @@ BaseObjectPtr JSTransferable::Data::Deserialize( if (!env->messaging_deserialize_create_object() ->Call(context, Null(env->isolate()), 1, &info) .ToLocal(&ret) || - !ret->IsObject() || !BaseObject::IsBaseObject(ret.As())) { + !ret->IsObject() || + !BaseObject::IsBaseObject(env->isolate_data(), ret.As())) { return {}; } diff --git a/src/node_options.cc b/src/node_options.cc index c02752464c4ab5..6ea85e3399be69 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -396,7 +396,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_wasm_modules, kAllowedInEnvvar); AddOption("--experimental-import-meta-resolve", - "experimental ES Module import.meta.resolve() support", + "experimental ES Module import.meta.resolve() parentURL support", &EnvironmentOptions::experimental_import_meta_resolve, kAllowedInEnvvar); AddOption("--experimental-permission", @@ -575,6 +575,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "write warnings to file instead of stderr", &EnvironmentOptions::redirect_warnings, kAllowedInEnvvar); + AddOption( + "[has_env_file_string]", "", &EnvironmentOptions::has_env_file_string); + AddOption("--env-file", + "set environment variables from supplied file", + &EnvironmentOptions::env_file); + Implies("--env-file", "[has_env_file_string]"); AddOption("--test", "launch test runner on startup", &EnvironmentOptions::test_runner); diff --git a/src/node_options.h b/src/node_options.h index bb8b68894b4430..1cc575bb9c7e3c 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -158,6 +158,8 @@ class EnvironmentOptions : public Options { #endif // HAVE_INSPECTOR std::string redirect_warnings; std::string diagnostic_dir; + std::string env_file; + bool has_env_file_string = false; bool test_runner = false; bool test_runner_coverage = false; std::vector test_name_pattern; diff --git a/src/node_perf.cc b/src/node_perf.cc index 555a90b0a76091..1acaa9dfe47145 100644 --- a/src/node_perf.cc +++ b/src/node_perf.cc @@ -20,7 +20,6 @@ using v8::Function; using v8::FunctionCallbackInfo; using v8::GCCallbackFlags; using v8::GCType; -using v8::Int32; using v8::Integer; using v8::Isolate; using v8::Local; @@ -43,6 +42,7 @@ const double performance_process_start_timestamp = uint64_t performance_v8_start; PerformanceState::PerformanceState(Isolate* isolate, + uint64_t time_origin, const PerformanceState::SerializeInfo* info) : root(isolate, sizeof(performance_state_internal), @@ -58,24 +58,51 @@ PerformanceState::PerformanceState(Isolate* isolate, root, MAYBE_FIELD_PTR(info, observers)) { if (info == nullptr) { - for (size_t i = 0; i < milestones.Length(); i++) milestones[i] = -1.; + // For performance states initialized from scratch, reset + // all the milestones and initialize the time origin. + // For deserialized performance states, we will do the + // initialization in the deserialize callback. + ResetMilestones(); + Initialize(time_origin); + } +} + +void PerformanceState::ResetMilestones() { + size_t milestones_length = milestones.Length(); + for (size_t i = 0; i < milestones_length; ++i) { + milestones[i] = -1; } } PerformanceState::SerializeInfo PerformanceState::Serialize( v8::Local context, v8::SnapshotCreator* creator) { + // Reset all the milestones to improve determinism in the snapshot. + // We'll re-initialize them after deserialization. + ResetMilestones(); + SerializeInfo info{root.Serialize(context, creator), milestones.Serialize(context, creator), observers.Serialize(context, creator)}; return info; } -void PerformanceState::Deserialize(v8::Local context) { +void PerformanceState::Initialize(uint64_t time_origin) { + // We are only reusing the milestone array to store the time origin, so do + // not use the Mark() method. The time origin milestone is not exposed + // to user land. + this->milestones[NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN] = + static_cast(time_origin); +} + +void PerformanceState::Deserialize(v8::Local context, + uint64_t time_origin) { + // Resets the pointers. root.Deserialize(context); - // This is just done to set up the pointers, we will actually reset - // all the milestones after deserialization. milestones.Deserialize(context); observers.Deserialize(context); + + // Re-initialize the time origin i.e. the process start time. + Initialize(time_origin); } std::ostream& operator<<(std::ostream& o, @@ -96,18 +123,6 @@ void PerformanceState::Mark(PerformanceMilestone milestone, uint64_t ts) { TRACE_EVENT_SCOPE_THREAD, ts / 1000); } -// Allows specific Node.js lifecycle milestones to be set from JavaScript -void MarkMilestone(const FunctionCallbackInfo& args) { - Realm* realm = Realm::GetCurrent(args); - // TODO(legendecas): Remove this check once the sub-realms are supported. - CHECK_EQ(realm->kind(), Realm::Kind::kPrincipal); - Environment* env = realm->env(); - PerformanceMilestone milestone = - static_cast(args[0].As()->Value()); - if (milestone != NODE_PERFORMANCE_MILESTONE_INVALID) - env->performance_state()->Mark(milestone); -} - void SetupPerformanceObservers(const FunctionCallbackInfo& args) { Realm* realm = Realm::GetCurrent(args); // TODO(legendecas): Remove this check once the sub-realms are supported. @@ -275,12 +290,6 @@ void CreateELDHistogram(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(histogram->object()); } -void GetTimeOrigin(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - args.GetReturnValue().Set( - Number::New(args.GetIsolate(), env->time_origin() / NANOS_PER_MILLIS)); -} - void GetTimeOriginTimeStamp(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); args.GetReturnValue().Set(Number::New( @@ -300,7 +309,6 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, HistogramBase::Initialize(isolate_data, target); - SetMethod(isolate, target, "markMilestone", MarkMilestone); SetMethod(isolate, target, "setupObservers", SetupPerformanceObservers); SetMethod(isolate, target, @@ -312,7 +320,6 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, RemoveGarbageCollectionTracking); SetMethod(isolate, target, "notify", Notify); SetMethod(isolate, target, "loopIdleTime", LoopIdleTime); - SetMethod(isolate, target, "getTimeOrigin", GetTimeOrigin); SetMethod(isolate, target, "getTimeOriginTimestamp", GetTimeOriginTimeStamp); SetMethod(isolate, target, "createELDHistogram", CreateELDHistogram); SetMethod(isolate, target, "markBootstrapComplete", MarkBootstrapComplete); @@ -373,13 +380,11 @@ void CreatePerContextProperties(Local target, } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { - registry->Register(MarkMilestone); registry->Register(SetupPerformanceObservers); registry->Register(InstallGarbageCollectionTracking); registry->Register(RemoveGarbageCollectionTracking); registry->Register(Notify); registry->Register(LoopIdleTime); - registry->Register(GetTimeOrigin); registry->Register(GetTimeOriginTimeStamp); registry->Register(CreateELDHistogram); registry->Register(MarkBootstrapComplete); diff --git a/src/node_perf_common.h b/src/node_perf_common.h index d519222616a174..dd757651c09e9f 100644 --- a/src/node_perf_common.h +++ b/src/node_perf_common.h @@ -24,15 +24,15 @@ extern const uint64_t performance_process_start; extern const double performance_process_start_timestamp; extern uint64_t performance_v8_start; -#define NODE_PERFORMANCE_MILESTONES(V) \ - V(ENVIRONMENT, "environment") \ - V(NODE_START, "nodeStart") \ - V(V8_START, "v8Start") \ - V(LOOP_START, "loopStart") \ - V(LOOP_EXIT, "loopExit") \ +#define NODE_PERFORMANCE_MILESTONES(V) \ + V(TIME_ORIGIN, "timeOrigin") \ + V(ENVIRONMENT, "environment") \ + V(NODE_START, "nodeStart") \ + V(V8_START, "v8Start") \ + V(LOOP_START, "loopStart") \ + V(LOOP_EXIT, "loopExit") \ V(BOOTSTRAP_COMPLETE, "bootstrapComplete") - #define NODE_PERFORMANCE_ENTRY_TYPES(V) \ V(GC, "gc") \ V(HTTP, "http") \ @@ -62,10 +62,12 @@ class PerformanceState { AliasedBufferIndex observers; }; - explicit PerformanceState(v8::Isolate* isolate, const SerializeInfo* info); + explicit PerformanceState(v8::Isolate* isolate, + uint64_t time_origin, + const SerializeInfo* info); SerializeInfo Serialize(v8::Local context, v8::SnapshotCreator* creator); - void Deserialize(v8::Local context); + void Deserialize(v8::Local context, uint64_t time_origin); friend std::ostream& operator<<(std::ostream& o, const SerializeInfo& i); AliasedUint8Array root; @@ -79,6 +81,8 @@ class PerformanceState { uint64_t ts = PERFORMANCE_NOW()); private: + void Initialize(uint64_t time_origin); + void ResetMilestones(); struct performance_state_internal { // doubles first so that they are always sizeof(double)-aligned double milestones[NODE_PERFORMANCE_MILESTONE_INVALID]; diff --git a/src/node_process_methods.cc b/src/node_process_methods.cc index eca0b343baef3a..1b68207f3e3ba6 100644 --- a/src/node_process_methods.cc +++ b/src/node_process_methods.cc @@ -566,7 +566,7 @@ void BindingData::Deserialize(Local context, v8::HandleScope scope(context->GetIsolate()); Realm* realm = Realm::GetCurrent(context); // Recreate the buffer in the constructor. - BindingData* binding = realm->AddBindingData(context, holder); + BindingData* binding = realm->AddBindingData(holder); CHECK_NOT_NULL(binding); } @@ -607,7 +607,7 @@ static void CreatePerContextProperties(Local target, Local context, void* priv) { Realm* realm = Realm::GetCurrent(context); - realm->AddBindingData(context, target); + realm->AddBindingData(target); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { diff --git a/src/node_realm-inl.h b/src/node_realm-inl.h index 748ecfe262afa2..5ccd76fc56673c 100644 --- a/src/node_realm-inl.h +++ b/src/node_realm-inl.h @@ -66,9 +66,9 @@ inline T* Realm::GetBindingData( // static template inline T* Realm::GetBindingData(v8::Local context) { - BindingDataStore* map = - static_cast(context->GetAlignedPointerFromEmbedderData( - ContextEmbedderIndex::kBindingDataStoreIndex)); + Realm* realm = GetCurrent(context); + DCHECK_NOT_NULL(realm); + BindingDataStore* map = realm->binding_data_store(); DCHECK_NOT_NULL(map); constexpr size_t binding_index = static_cast(T::binding_type_int); static_assert(binding_index < std::tuple_size_v); @@ -81,10 +81,7 @@ inline T* Realm::GetBindingData(v8::Local context) { } template -inline T* Realm::AddBindingData(v8::Local context, - v8::Local target, - Args&&... args) { - DCHECK_EQ(GetCurrent(context), this); +inline T* Realm::AddBindingData(v8::Local target, Args&&... args) { // This won't compile if T is not a BaseObject subclass. static_assert(std::is_base_of_v); // The binding data must be weak so that it won't keep the realm reachable @@ -93,15 +90,11 @@ inline T* Realm::AddBindingData(v8::Local context, // reachable throughout the lifetime of the realm. BaseObjectWeakPtr item = MakeWeakBaseObject(this, target, std::forward(args)...); - DCHECK_EQ(context->GetAlignedPointerFromEmbedderData( - ContextEmbedderIndex::kBindingDataStoreIndex), - &binding_data_store_); constexpr size_t binding_index = static_cast(T::binding_type_int); static_assert(binding_index < std::tuple_size_v); - // Should not insert the binding twice. + // Each slot is expected to be assigned only once. CHECK(!binding_data_store_[binding_index]); binding_data_store_[binding_index] = item; - DCHECK_EQ(GetBindingData(context), item.get()); return item.get(); } diff --git a/src/node_realm.h b/src/node_realm.h index 6cca9d5041d3fb..a75cd610692183 100644 --- a/src/node_realm.h +++ b/src/node_realm.h @@ -93,9 +93,7 @@ class Realm : public MemoryRetainer { // this scope can access the created T* object using // GetBindingData(args) later. template - T* AddBindingData(v8::Local context, - v8::Local target, - Args&&... args); + T* AddBindingData(v8::Local target, Args&&... args); template static inline T* GetBindingData(const v8::PropertyCallbackInfo& info); template diff --git a/src/node_report.cc b/src/node_report.cc index dcaa6922070b92..76b5d4448267ff 100644 --- a/src/node_report.cc +++ b/src/node_report.cc @@ -878,7 +878,7 @@ std::string TriggerNodeReport(Isolate* isolate, THROW_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kFileSystemWrite, - std::string_view(env->GetCwd()), + std::string_view(Environment::GetCwd(env->exec_path())), filename); } } diff --git a/src/node_sea.cc b/src/node_sea.cc index 88741a5fce9d48..a8dbfeaa424943 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -4,9 +4,14 @@ #include "debug_utils-inl.h" #include "env-inl.h" #include "json_parser.h" +#include "node_contextify.h" +#include "node_errors.h" #include "node_external_reference.h" #include "node_internals.h" +#include "node_snapshot_builder.h" #include "node_union_bytes.h" +#include "node_v8_platform-inl.h" +#include "util-inl.h" // The POSTJECT_SENTINEL_FUSE macro is a string of random characters selected by // the Node.js project that is present only once in the entire binary. It is @@ -25,10 +30,19 @@ #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) using node::ExitCode; +using v8::ArrayBuffer; +using v8::BackingStore; using v8::Context; +using v8::DataView; +using v8::Function; using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; using v8::Local; +using v8::NewStringType; using v8::Object; +using v8::ScriptCompiler; +using v8::String; using v8::Value; namespace node { @@ -64,7 +78,7 @@ class SeaSerializer : public BlobSerializer { template <> size_t SeaSerializer::Write(const SeaResource& sea) { - sink.reserve(SeaResource::kHeaderSize + sea.code.size()); + sink.reserve(SeaResource::kHeaderSize + sea.main_code_or_snapshot.size()); Debug("Write SEA magic %x\n", kMagic); size_t written_total = WriteArithmetic(kMagic); @@ -74,10 +88,28 @@ size_t SeaSerializer::Write(const SeaResource& sea) { written_total += WriteArithmetic(flags); DCHECK_EQ(written_total, SeaResource::kHeaderSize); - Debug("Write SEA resource code %p, size=%zu\n", - sea.code.data(), - sea.code.size()); - written_total += WriteStringView(sea.code, StringLogMode::kAddressAndContent); + Debug("Write SEA code path %p, size=%zu\n", + sea.code_path.data(), + sea.code_path.size()); + written_total += + WriteStringView(sea.code_path, StringLogMode::kAddressAndContent); + + Debug("Write SEA resource %s %p, size=%zu\n", + sea.use_snapshot() ? "snapshot" : "code", + sea.main_code_or_snapshot.data(), + sea.main_code_or_snapshot.size()); + written_total += + WriteStringView(sea.main_code_or_snapshot, + sea.use_snapshot() ? StringLogMode::kAddressOnly + : StringLogMode::kAddressAndContent); + + if (sea.code_cache.has_value()) { + Debug("Write SEA resource code cache %p, size=%zu\n", + sea.code_cache->data(), + sea.code_cache->size()); + written_total += + WriteStringView(sea.code_cache.value(), StringLogMode::kAddressOnly); + } return written_total; } @@ -103,9 +135,29 @@ SeaResource SeaDeserializer::Read() { Debug("Read SEA flags %x\n", static_cast(flags)); CHECK_EQ(read_total, SeaResource::kHeaderSize); - std::string_view code = ReadStringView(StringLogMode::kAddressAndContent); - Debug("Read SEA resource code %p, size=%zu\n", code.data(), code.size()); - return {flags, code}; + std::string_view code_path = + ReadStringView(StringLogMode::kAddressAndContent); + Debug( + "Read SEA code path %p, size=%zu\n", code_path.data(), code_path.size()); + + bool use_snapshot = static_cast(flags & SeaFlags::kUseSnapshot); + std::string_view code = + ReadStringView(use_snapshot ? StringLogMode::kAddressOnly + : StringLogMode::kAddressAndContent); + + Debug("Read SEA resource %s %p, size=%zu\n", + use_snapshot ? "snapshot" : "code", + code.data(), + code.size()); + + std::string_view code_cache; + if (static_cast(flags & SeaFlags::kUseCodeCache)) { + code_cache = ReadStringView(StringLogMode::kAddressOnly); + Debug("Read SEA resource code cache %p, size=%zu\n", + code_cache.data(), + code_cache.size()); + } + return {flags, code_path, code, code_cache}; } std::string_view FindSingleExecutableBlob() { @@ -133,6 +185,10 @@ std::string_view FindSingleExecutableBlob() { } // anonymous namespace +bool SeaResource::use_snapshot() const { + return static_cast(flags & SeaFlags::kUseSnapshot); +} + SeaResource FindSingleExecutableResource() { static const SeaResource sea_resource = []() -> SeaResource { std::string_view blob = FindSingleExecutableBlob(); @@ -150,7 +206,18 @@ bool IsSingleExecutable() { return postject_has_resource(); } +void IsSea(const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(IsSingleExecutable()); +} + void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo& args) { + bool is_building_sea = + !per_process::cli_options->experimental_sea_config.empty(); + if (is_building_sea) { + args.GetReturnValue().Set(true); + return; + } + if (!IsSingleExecutable()) { args.GetReturnValue().Set(false); return; @@ -161,6 +228,54 @@ void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo& args) { sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning)); } +void GetCodeCache(const FunctionCallbackInfo& args) { + if (!IsSingleExecutable()) { + return; + } + + Isolate* isolate = args.GetIsolate(); + + SeaResource sea_resource = FindSingleExecutableResource(); + + if (!static_cast(sea_resource.flags & SeaFlags::kUseCodeCache)) { + return; + } + + std::shared_ptr backing_store = ArrayBuffer::NewBackingStore( + const_cast( + static_cast(sea_resource.code_cache->data())), + sea_resource.code_cache->length(), + [](void* /* data */, size_t /* length */, void* /* deleter_data */) { + // The code cache data blob is not freed here because it is a static + // blob which is not allocated by the BackingStore allocator. + }, + nullptr); + Local array_buffer = ArrayBuffer::New(isolate, backing_store); + Local data_view = + DataView::New(array_buffer, 0, array_buffer->ByteLength()); + + args.GetReturnValue().Set(data_view); +} + +void GetCodePath(const FunctionCallbackInfo& args) { + DCHECK(IsSingleExecutable()); + + Isolate* isolate = args.GetIsolate(); + + SeaResource sea_resource = FindSingleExecutableResource(); + + Local code_path; + if (!String::NewFromUtf8(isolate, + sea_resource.code_path.data(), + NewStringType::kNormal, + sea_resource.code_path.length()) + .ToLocal(&code_path)) { + return; + } + + args.GetReturnValue().Set(code_path); +} + std::tuple FixupArgsForSEA(int argc, char** argv) { // Repeats argv[0] at position 1 on argv as a replacement for the missing // entry point file path. @@ -235,10 +350,122 @@ std::optional ParseSingleExecutableConfig( result.flags |= SeaFlags::kDisableExperimentalSeaWarning; } + std::optional use_snapshot = parser.GetTopLevelBoolField("useSnapshot"); + if (!use_snapshot.has_value()) { + FPrintF( + stderr, "\"useSnapshot\" field of %s is not a Boolean\n", config_path); + return std::nullopt; + } + if (use_snapshot.value()) { + result.flags |= SeaFlags::kUseSnapshot; + } + + std::optional use_code_cache = + parser.GetTopLevelBoolField("useCodeCache"); + if (!use_code_cache.has_value()) { + FPrintF( + stderr, "\"useCodeCache\" field of %s is not a Boolean\n", config_path); + return std::nullopt; + } + if (use_code_cache.value()) { + result.flags |= SeaFlags::kUseCodeCache; + } + return result; } -ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) { +ExitCode GenerateSnapshotForSEA(const SeaConfig& config, + const std::vector& args, + const std::vector& exec_args, + const std::string& main_script, + std::vector* snapshot_blob) { + SnapshotData snapshot; + // TODO(joyeecheung): make the arguments configurable through the JSON + // config or a programmatic API. + std::vector patched_args = {args[0], config.main_path}; + ExitCode exit_code = SnapshotBuilder::Generate( + &snapshot, patched_args, exec_args, main_script); + if (exit_code != ExitCode::kNoFailure) { + return exit_code; + } + auto& persistents = snapshot.env_info.principal_realm.persistent_values; + auto it = std::find_if( + persistents.begin(), persistents.end(), [](const PropInfo& prop) { + return prop.name == "snapshot_deserialize_main"; + }); + if (it == persistents.end()) { + FPrintF( + stderr, + "%s does not invoke " + "v8.startupSnapshot.setDeserializeMainFunction(), which is required " + "for snapshot scripts used to build single executable applications." + "\n", + config.main_path); + return ExitCode::kGenericUserError; + } + // We need the temporary variable for copy elision. + std::vector temp = snapshot.ToBlob(); + *snapshot_blob = std::move(temp); + return ExitCode::kNoFailure; +} + +std::optional GenerateCodeCache(std::string_view main_path, + std::string_view main_script) { + RAIIIsolate raii_isolate; + Isolate* isolate = raii_isolate.get(); + + HandleScope handle_scope(isolate); + Local context = Context::New(isolate); + Context::Scope context_scope(context); + + errors::PrinterTryCatch bootstrapCatch( + isolate, errors::PrinterTryCatch::kPrintSourceLine); + + Local filename; + if (!String::NewFromUtf8(isolate, + main_path.data(), + NewStringType::kNormal, + main_path.length()) + .ToLocal(&filename)) { + return std::nullopt; + } + + Local content; + if (!String::NewFromUtf8(isolate, + main_script.data(), + NewStringType::kNormal, + main_script.length()) + .ToLocal(&content)) { + return std::nullopt; + } + + std::vector> parameters = { + FIXED_ONE_BYTE_STRING(isolate, "exports"), + FIXED_ONE_BYTE_STRING(isolate, "require"), + FIXED_ONE_BYTE_STRING(isolate, "module"), + FIXED_ONE_BYTE_STRING(isolate, "__filename"), + FIXED_ONE_BYTE_STRING(isolate, "__dirname"), + }; + + // TODO(RaisinTen): Using the V8 code cache prevents us from using `import()` + // in the SEA code. Support it. + // Refs: https://github.com/nodejs/node/pull/48191#discussion_r1213271430 + Local fn; + if (!contextify::CompileFunction(context, filename, content, ¶meters) + .ToLocal(&fn)) { + return std::nullopt; + } + + std::unique_ptr cache{ + ScriptCompiler::CreateCodeCacheForFunction(fn)}; + std::string code_cache(cache->data, cache->data + cache->length); + return code_cache; +} + +ExitCode GenerateSingleExecutableBlob( + const SeaConfig& config, + const std::vector& args, + const std::vector& exec_args) { std::string main_script; // TODO(joyeecheung): unify the file utils. int r = ReadFileSync(&main_script, config.main_path.c_str()); @@ -248,7 +475,37 @@ ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) { return ExitCode::kGenericUserError; } - SeaResource sea{config.flags, main_script}; + std::vector snapshot_blob; + bool builds_snapshot_from_main = + static_cast(config.flags & SeaFlags::kUseSnapshot); + if (builds_snapshot_from_main) { + ExitCode exit_code = GenerateSnapshotForSEA( + config, args, exec_args, main_script, &snapshot_blob); + if (exit_code != ExitCode::kNoFailure) { + return exit_code; + } + } + + std::optional optional_sv_code_cache; + std::string code_cache; + if (static_cast(config.flags & SeaFlags::kUseCodeCache)) { + std::optional optional_code_cache = + GenerateCodeCache(config.main_path, main_script); + if (!optional_code_cache.has_value()) { + FPrintF(stderr, "Cannot generate V8 code cache\n"); + return ExitCode::kGenericUserError; + } + code_cache = optional_code_cache.value(); + optional_sv_code_cache = code_cache; + } + + SeaResource sea{ + config.flags, + config.main_path, + builds_snapshot_from_main + ? std::string_view{snapshot_blob.data(), snapshot_blob.size()} + : std::string_view{main_script.data(), main_script.size()}, + optional_sv_code_cache}; SeaSerializer serializer; serializer.Write(sea); @@ -269,11 +526,14 @@ ExitCode GenerateSingleExecutableBlob(const SeaConfig& config) { } // anonymous namespace -ExitCode BuildSingleExecutableBlob(const std::string& config_path) { +ExitCode BuildSingleExecutableBlob(const std::string& config_path, + const std::vector& args, + const std::vector& exec_args) { std::optional config_opt = ParseSingleExecutableConfig(config_path); if (config_opt.has_value()) { - ExitCode code = GenerateSingleExecutableBlob(config_opt.value()); + ExitCode code = + GenerateSingleExecutableBlob(config_opt.value(), args, exec_args); return code; } @@ -284,14 +544,20 @@ void Initialize(Local target, Local unused, Local context, void* priv) { + SetMethod(context, target, "isSea", IsSea); SetMethod(context, target, "isExperimentalSeaWarningNeeded", IsExperimentalSeaWarningNeeded); + SetMethod(context, target, "getCodePath", GetCodePath); + SetMethod(context, target, "getCodeCache", GetCodeCache); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(IsSea); registry->Register(IsExperimentalSeaWarningNeeded); + registry->Register(GetCodePath); + registry->Register(GetCodeCache); } } // namespace sea diff --git a/src/node_sea.h b/src/node_sea.h index 8b0877df3eb0d7..c443de9e4d0adc 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -6,8 +6,12 @@ #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) #include +#include +#include #include #include +#include + #include "node_exit_code.h" namespace node { @@ -21,19 +25,27 @@ const uint32_t kMagic = 0x143da20; enum class SeaFlags : uint32_t { kDefault = 0, kDisableExperimentalSeaWarning = 1 << 0, + kUseSnapshot = 1 << 1, + kUseCodeCache = 1 << 2, }; struct SeaResource { SeaFlags flags = SeaFlags::kDefault; - std::string_view code; + std::string_view code_path; + std::string_view main_code_or_snapshot; + std::optional code_cache; + bool use_snapshot() const; static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags); }; bool IsSingleExecutable(); SeaResource FindSingleExecutableResource(); std::tuple FixupArgsForSEA(int argc, char** argv); -node::ExitCode BuildSingleExecutableBlob(const std::string& config_path); +node::ExitCode BuildSingleExecutableBlob( + const std::string& config_path, + const std::vector& args, + const std::vector& exec_args); } // namespace sea } // namespace node diff --git a/src/node_snapshot_builder.h b/src/node_snapshot_builder.h index f433bc52c28864..22d37242c96d22 100644 --- a/src/node_snapshot_builder.h +++ b/src/node_snapshot_builder.h @@ -18,10 +18,12 @@ struct SnapshotData; class NODE_EXTERN_PRIVATE SnapshotBuilder { public: - static ExitCode Generate(std::ostream& out, - const std::vector& args, - const std::vector& exec_args, - std::optional main_script); + static ExitCode GenerateAsSource( + const char* out_path, + const std::vector& args, + const std::vector& exec_args, + std::optional main_script_path = std::nullopt, + bool use_string_literals = true); // Generate the snapshot into out. static ExitCode Generate(SnapshotData* out, diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 59bcea00d3e7a6..1f066c7d5bb9ff 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -1,5 +1,6 @@ #include "node_snapshotable.h" +#include #include #include #include @@ -42,7 +43,6 @@ using v8::MaybeLocal; using v8::Object; using v8::ObjectTemplate; using v8::ScriptCompiler; -using v8::ScriptOrigin; using v8::SnapshotCreator; using v8::StartupData; using v8::String; @@ -585,7 +585,9 @@ size_t SnapshotSerializer::Write(const SnapshotMetadata& data) { // [ ... ] code_cache std::vector SnapshotData::ToBlob() const { + std::vector result; SnapshotSerializer w; + w.Debug("SnapshotData::ToBlob()\n"); size_t written_total = 0; @@ -603,7 +605,10 @@ std::vector SnapshotData::ToBlob() const { w.Debug("Write code_cache\n"); written_total += w.WriteVector(code_cache); w.Debug("SnapshotData::ToBlob() Wrote %d bytes\n", written_total); - return w.sink; + + // Return using the temporary value to enable copy elision. + std::swap(result, w.sink); + return result; } void SnapshotData::ToFile(FILE* out) const { @@ -711,13 +716,6 @@ SnapshotData::~SnapshotData() { } } -template -void WriteVector(std::ostream* ss, const T* vec, size_t size) { - for (size_t i = 0; i < size; i++) { - *ss << std::to_string(vec[i]) << (i == size - 1 ? '\n' : ','); - } -} - static std::string GetCodeCacheDefName(const std::string& id) { char buf[64] = {0}; size_t size = id.size(); @@ -742,48 +740,71 @@ static std::string FormatSize(size_t size) { return buf; } -#ifdef NODE_MKSNAPSHOT_USE_STRING_LITERALS -static void WriteDataAsCharString(std::ostream* ss, - const uint8_t* data, - size_t length) { - for (size_t i = 0; i < length; i++) { - const uint8_t ch = data[i]; - // We can print most printable characters directly. The exceptions are '\' - // (escape characters), " (would end the string), and ? (trigraphs). The - // latter may be overly conservative: we compile with C++17 which doesn't - // support trigraphs. - if (ch >= ' ' && ch <= '~' && ch != '\\' && ch != '"' && ch != '?') { - *ss << ch; - } else { - // All other characters are blindly output as octal. - const char c0 = '0' + ((ch >> 6) & 7); - const char c1 = '0' + ((ch >> 3) & 7); - const char c2 = '0' + (ch & 7); - *ss << "\\" << c0 << c1 << c2; - } - if (i % 64 == 63) { - // Go to a newline every 64 bytes since many text editors have - // problems with very long lines. - *ss << "\"\n\""; - } +std::string ToOctalString(const uint8_t ch) { + // We can print most printable characters directly. The exceptions are '\' + // (escape characters), " (would end the string), and ? (trigraphs). The + // latter may be overly conservative: we compile with C++17 which doesn't + // support trigraphs. + if (ch >= ' ' && ch <= '~' && ch != '\\' && ch != '"' && ch != '?') { + return std::string(1, static_cast(ch)); } + // All other characters are blindly output as octal. + const char c0 = '0' + ((ch >> 6) & 7); + const char c1 = '0' + ((ch >> 3) & 7); + const char c2 = '0' + (ch & 7); + return std::string("\\") + c0 + c1 + c2; } -static void WriteStaticCodeCacheDataAsStringLiteral( - std::ostream* ss, const builtins::CodeCacheInfo& info) { - *ss << "static const uint8_t *" << GetCodeCacheDefName(info.id) - << "= reinterpret_cast(\""; - WriteDataAsCharString(ss, info.data.data, info.data.length); - *ss << "\");\n"; +std::vector GetOctalTable() { + size_t size = 1 << 8; + std::vector code_table(size); + for (size_t i = 0; i < size; ++i) { + code_table[i] = ToOctalString(static_cast(i)); + } + return code_table; } -#else -static void WriteStaticCodeCacheDataAsArray( - std::ostream* ss, const builtins::CodeCacheInfo& info) { - *ss << "static const uint8_t " << GetCodeCacheDefName(info.id) << "[] = {\n"; - WriteVector(ss, info.data.data, info.data.length); - *ss << "};\n"; + +const std::string& GetOctalCode(uint8_t index) { + static std::vector table = GetOctalTable(); + return table[index]; +} + +template +void WriteByteVectorLiteral(std::ostream* ss, + const T* vec, + size_t size, + const char* var_name, + bool use_string_literals) { + constexpr bool is_uint8_t = std::is_same_v; + static_assert(is_uint8_t || std::is_same_v); + constexpr const char* type_name = is_uint8_t ? "uint8_t" : "char"; + if (use_string_literals) { + const uint8_t* data = reinterpret_cast(vec); + *ss << "static const " << type_name << " *" << var_name << " = "; + *ss << (is_uint8_t ? R"(reinterpret_cast(")" : "\""); + for (size_t i = 0; i < size; i++) { + const uint8_t ch = data[i]; + *ss << GetOctalCode(ch); + if (i % 64 == 63) { + // Go to a newline every 64 bytes since many text editors have + // problems with very long lines. + *ss << "\"\n\""; + } + } + *ss << (is_uint8_t ? "\");\n" : "\";\n"); + } else { + *ss << "static const " << type_name << " " << var_name << "[] = {"; + for (size_t i = 0; i < size; i++) { + *ss << std::to_string(vec[i]) << (i == size - 1 ? '\n' : ','); + if (i % 64 == 63) { + // Print a newline every 64 units and a offset to improve + // readability. + *ss << " // " << (i / 64) << "\n"; + } + } + *ss << "};\n"; + } } -#endif static void WriteCodeCacheInitializer(std::ostream* ss, const std::string& id, @@ -796,7 +817,9 @@ static void WriteCodeCacheInitializer(std::ostream* ss, *ss << " },\n"; } -void FormatBlob(std::ostream& ss, const SnapshotData* data) { +void FormatBlob(std::ostream& ss, + const SnapshotData* data, + bool use_string_literals) { ss << R"(#include #include "env.h" #include "node_snapshot_builder.h" @@ -807,32 +830,24 @@ void FormatBlob(std::ostream& ss, const SnapshotData* data) { namespace node { )"; -#ifdef NODE_MKSNAPSHOT_USE_STRING_LITERALS - ss << R"(static const char *v8_snapshot_blob_data = ")"; - WriteDataAsCharString( - &ss, - reinterpret_cast(data->v8_snapshot_blob_data.data), - data->v8_snapshot_blob_data.raw_size); - ss << R"(";)"; -#else - ss << R"(static const char v8_snapshot_blob_data[] = {)"; - WriteVector(&ss, - data->v8_snapshot_blob_data.data, - data->v8_snapshot_blob_data.raw_size); - ss << R"(};)"; -#endif + WriteByteVectorLiteral(&ss, + data->v8_snapshot_blob_data.data, + data->v8_snapshot_blob_data.raw_size, + "v8_snapshot_blob_data", + use_string_literals); ss << R"(static const int v8_snapshot_blob_size = )" - << data->v8_snapshot_blob_data.raw_size << ";"; + << data->v8_snapshot_blob_data.raw_size << ";\n"; + // Windows can't deal with too many large vector initializers. + // Store the data into static arrays first. for (const auto& item : data->code_cache) { -#ifdef NODE_MKSNAPSHOT_USE_STRING_LITERALS - WriteStaticCodeCacheDataAsStringLiteral(&ss, item); -#else - // Windows can't deal with too many large vector initializers. - // Store the data into static arrays first. - WriteStaticCodeCacheDataAsArray(&ss, item); -#endif + std::string var_name = GetCodeCacheDefName(item.id); + WriteByteVectorLiteral(&ss, + item.data.data, + item.data.length, + var_name.c_str(), + use_string_literals); } ss << R"(const SnapshotData snapshot_data { @@ -1069,17 +1084,45 @@ ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out, return ExitCode::kNoFailure; } -ExitCode SnapshotBuilder::Generate( - std::ostream& out, +ExitCode SnapshotBuilder::GenerateAsSource( + const char* out_path, const std::vector& args, const std::vector& exec_args, - std::optional main_script) { + std::optional main_script_path, + bool use_string_literals) { + std::string main_script_content; + std::optional main_script_optional; + if (main_script_path.has_value()) { + int r = ReadFileSync(&main_script_content, main_script_path.value().data()); + if (r != 0) { + FPrintF(stderr, + "Cannot read main script %s for building snapshot. %s: %s", + main_script_path.value(), + uv_err_name(r), + uv_strerror(r)); + return ExitCode::kGenericUserError; + } + main_script_optional = main_script_content; + } + + std::ofstream out(out_path, std::ios::out | std::ios::binary); + if (!out) { + FPrintF(stderr, "Cannot open %s for output.\n", out_path); + return ExitCode::kGenericUserError; + } + SnapshotData data; - ExitCode exit_code = Generate(&data, args, exec_args, main_script); + ExitCode exit_code = Generate(&data, args, exec_args, main_script_optional); if (exit_code != ExitCode::kNoFailure) { return exit_code; } - FormatBlob(out, &data); + FormatBlob(out, &data, use_string_literals); + + if (!out) { + std::cerr << "Failed to write to " << out_path << "\n"; + exit_code = node::ExitCode::kGenericUserError; + } + return exit_code; } @@ -1159,14 +1202,16 @@ void DeserializeNodeInternalFields(Local holder, StartupData SerializeNodeContextInternalFields(Local holder, int index, - void* env) { + void* callback_data) { // We only do one serialization for the kEmbedderType slot, the result // contains everything necessary for deserializing the entire object, // including the fields whose index is bigger than kEmbedderType // (most importantly, BaseObject::kSlot). // For Node.js this design is enough for all the native binding that are // serializable. - if (index != BaseObject::kEmbedderType || !BaseObject::IsBaseObject(holder)) { + Environment* env = static_cast(callback_data); + if (index != BaseObject::kEmbedderType || + !BaseObject::IsBaseObject(env->isolate_data(), holder)) { return StartupData{nullptr, 0}; } @@ -1254,7 +1299,6 @@ void CompileSerializeMain(const FunctionCallbackInfo& args) { Local source = args[1].As(); Isolate* isolate = args.GetIsolate(); Local context = isolate->GetCurrentContext(); - ScriptOrigin origin(isolate, filename, 0, 0, true); // TODO(joyeecheung): do we need all of these? Maybe we would want a less // internal version of them. std::vector> parameters = { @@ -1262,15 +1306,8 @@ void CompileSerializeMain(const FunctionCallbackInfo& args) { FIXED_ONE_BYTE_STRING(isolate, "__filename"), FIXED_ONE_BYTE_STRING(isolate, "__dirname"), }; - ScriptCompiler::Source script_source(source, origin); Local fn; - if (ScriptCompiler::CompileFunction(context, - &script_source, - parameters.size(), - parameters.data(), - 0, - nullptr, - ScriptCompiler::kNoCompileOptions) + if (contextify::CompileFunction(context, filename, source, ¶meters) .ToLocal(&fn)) { args.GetReturnValue().Set(fn); } @@ -1359,7 +1396,7 @@ void BindingData::Deserialize(Local context, // Recreate the buffer in the constructor. InternalFieldInfo* casted_info = static_cast(info); BindingData* binding = - realm->AddBindingData(context, holder, casted_info); + realm->AddBindingData(holder, casted_info); CHECK_NOT_NULL(binding); } @@ -1373,7 +1410,7 @@ void CreatePerContextProperties(Local target, Local context, void* priv) { Realm* realm = Realm::GetCurrent(context); - realm->AddBindingData(context, target); + realm->AddBindingData(target); } void CreatePerIsolateProperties(IsolateData* isolate_data, diff --git a/src/node_task_queue.cc b/src/node_task_queue.cc index 5d0e2b0d4c7ba1..1a0cb082a2534f 100644 --- a/src/node_task_queue.cc +++ b/src/node_task_queue.cc @@ -50,7 +50,7 @@ static Maybe GetAssignedPromiseWrapAsyncId(Environment* env, // be an object. If it's not, we just ignore it. Ideally v8 would // have had GetInternalField returning a MaybeLocal but this works // for now. - Local promiseWrap = promise->GetInternalField(0); + Local promiseWrap = promise->GetInternalField(0).As(); if (promiseWrap->IsObject()) { Local maybe_async_id; if (!promiseWrap.As()->Get(env->context(), id_symbol) diff --git a/src/node_url.cc b/src/node_url.cc index 483fbb04a47f34..f055acd51c323c 100644 --- a/src/node_url.cc +++ b/src/node_url.cc @@ -67,7 +67,7 @@ void BindingData::Deserialize(v8::Local context, DCHECK_EQ(index, BaseObject::kEmbedderType); v8::HandleScope scope(context->GetIsolate()); Realm* realm = Realm::GetCurrent(context); - BindingData* binding = realm->AddBindingData(context, holder); + BindingData* binding = realm->AddBindingData(holder); CHECK_NOT_NULL(binding); } @@ -170,8 +170,6 @@ bool BindingData::FastCanParse(Local receiver, return ada::can_parse(std::string_view(input.data, input.length)); } -CFunction BindingData::fast_can_parse_(CFunction::Make(FastCanParse)); - bool BindingData::FastCanParseWithBase(Local receiver, const FastOneByteString& input, const FastOneByteString& base) { @@ -179,8 +177,8 @@ bool BindingData::FastCanParseWithBase(Local receiver, return ada::can_parse(std::string_view(input.data, input.length), &base_view); } -CFunction BindingData::fast_can_parse_with_base_( - CFunction::Make(FastCanParseWithBase)); +CFunction BindingData::fast_can_parse_methods_[] = { + CFunction::Make(FastCanParse), CFunction::Make(FastCanParseWithBase)}; void BindingData::Format(const FunctionCallbackInfo& args) { CHECK_GT(args.Length(), 4); @@ -361,12 +359,7 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, SetMethod(isolate, target, "parse", Parse); SetMethod(isolate, target, "update", Update); SetFastMethodNoSideEffect( - isolate, target, "canParse", CanParse, &fast_can_parse_); - SetFastMethodNoSideEffect(isolate, - target, - "canParseWithBase", - CanParse, - &fast_can_parse_with_base_); + isolate, target, "canParse", CanParse, {fast_can_parse_methods_, 2}); } void BindingData::CreatePerContextProperties(Local target, @@ -374,7 +367,7 @@ void BindingData::CreatePerContextProperties(Local target, Local context, void* priv) { Realm* realm = Realm::GetCurrent(context); - realm->AddBindingData(context, target); + realm->AddBindingData(target); } void BindingData::RegisterExternalReferences( @@ -387,9 +380,11 @@ void BindingData::RegisterExternalReferences( registry->Register(Update); registry->Register(CanParse); registry->Register(FastCanParse); - registry->Register(fast_can_parse_.GetTypeInfo()); registry->Register(FastCanParseWithBase); - registry->Register(fast_can_parse_with_base_.GetTypeInfo()); + + for (const CFunction& method : fast_can_parse_methods_) { + registry->Register(method.GetTypeInfo()); + } } std::string FromFilePath(const std::string_view file_path) { diff --git a/src/node_url.h b/src/node_url.h index fc0a302778ae73..c485caa2eb0343 100644 --- a/src/node_url.h +++ b/src/node_url.h @@ -75,8 +75,7 @@ class BindingData : public SnapshotableObject { void UpdateComponents(const ada::url_components& components, const ada::scheme::type type); - static v8::CFunction fast_can_parse_; - static v8::CFunction fast_can_parse_with_base_; + static v8::CFunction fast_can_parse_methods_[]; }; std::string FromFilePath(const std::string_view file_path); diff --git a/src/node_util.cc b/src/node_util.cc index ea991873b9a033..dc2c730fdf042c 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -284,74 +284,44 @@ void WeakReference::DecRef(const FunctionCallbackInfo& args) { v8::Number::New(args.GetIsolate(), weak_ref->reference_count_)); } -static void GuessHandleType(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - int fd; - if (!args[0]->Int32Value(env->context()).To(&fd)) return; - CHECK_GE(fd, 0); - - uv_handle_type t = uv_guess_handle(fd); +static uint32_t GetUVHandleTypeCode(const uv_handle_type type) { // TODO(anonrig): We can use an enum here and then create the array in the // binding, which will remove the hard-coding in C++ and JS land. - uint32_t type{0}; // Currently, the return type of this function corresponds to the index of the // array defined in the JS land. This is done as an optimization to reduce the // string serialization overhead. - switch (t) { + switch (type) { case UV_TCP: - type = 0; - break; + return 0; case UV_TTY: - type = 1; - break; + return 1; case UV_UDP: - type = 2; - break; + return 2; case UV_FILE: - type = 3; - break; + return 3; case UV_NAMED_PIPE: - type = 4; - break; + return 4; case UV_UNKNOWN_HANDLE: - type = 5; - break; + return 5; default: ABORT(); } +} + +static void GuessHandleType(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + int fd; + if (!args[0]->Int32Value(env->context()).To(&fd)) return; + CHECK_GE(fd, 0); - args.GetReturnValue().Set(type); + uv_handle_type t = uv_guess_handle(fd); + args.GetReturnValue().Set(GetUVHandleTypeCode(t)); } static uint32_t FastGuessHandleType(Local receiver, const uint32_t fd) { uv_handle_type t = uv_guess_handle(fd); - uint32_t type{0}; - - switch (t) { - case UV_TCP: - type = 0; - break; - case UV_TTY: - type = 1; - break; - case UV_UDP: - type = 2; - break; - case UV_FILE: - type = 3; - break; - case UV_NAMED_PIPE: - type = 4; - break; - case UV_UNKNOWN_HANDLE: - type = 5; - break; - default: - ABORT(); - } - - return type; + return GetUVHandleTypeCode(t); } CFunction fast_guess_handle_type_(CFunction::Make(FastGuessHandleType)); diff --git a/src/node_v8.cc b/src/node_v8.cc index 308d851ef62278..a5e91f5b8ca624 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -158,7 +158,7 @@ void BindingData::Deserialize(Local context, // Recreate the buffer in the constructor. InternalFieldInfo* casted_info = static_cast(info); BindingData* binding = - realm->AddBindingData(context, holder, casted_info); + realm->AddBindingData(holder, casted_info); CHECK_NOT_NULL(binding); } @@ -422,8 +422,7 @@ void Initialize(Local target, void* priv) { Realm* realm = Realm::GetCurrent(context); Environment* env = realm->env(); - BindingData* const binding_data = - realm->AddBindingData(context, target); + BindingData* const binding_data = realm->AddBindingData(target); if (binding_data == nullptr) return; SetMethodNoSideEffect( diff --git a/src/node_version.h b/src/node_version.h index d6c0fb8dae6737..f5c9406d949c86 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -23,8 +23,8 @@ #define SRC_NODE_VERSION_H_ #define NODE_MAJOR_VERSION 20 -#define NODE_MINOR_VERSION 5 -#define NODE_PATCH_VERSION 2 +#define NODE_MINOR_VERSION 6 +#define NODE_PATCH_VERSION 1 #define NODE_VERSION_IS_LTS 0 #define NODE_VERSION_LTS_CODENAME "" diff --git a/src/node_worker.cc b/src/node_worker.cc index 478e80be505bca..900674bbe4c90e 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -11,6 +11,7 @@ #include "node_snapshot_builder.h" #include "permission/permission.h" #include "util-inl.h" +#include "v8-cppgc.h" #include #include diff --git a/src/node_zlib.cc b/src/node_zlib.cc index fac116f9e6b3e2..0c4ae0fc794347 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -423,7 +423,8 @@ class CompressionStream : public AsyncWrap, public ThreadPoolWork { UpdateWriteResult(); // call the write() cb - Local cb = object()->GetInternalField(kWriteJSCallback); + Local cb = + object()->GetInternalField(kWriteJSCallback).template As(); MakeCallback(cb.As(), 0, nullptr); if (pending_close_) diff --git a/src/permission/fs_permission.cc b/src/permission/fs_permission.cc index 7ae4a93fc4fcb4..91c63dff6582a8 100644 --- a/src/permission/fs_permission.cc +++ b/src/permission/fs_permission.cc @@ -67,52 +67,53 @@ bool is_tree_granted(node::permission::FSPermission::RadixTree* granted_tree, return granted_tree->Lookup(param, true); } -} // namespace - -namespace node { - -namespace permission { - -void PrintTree(FSPermission::RadixTree::Node* node, int spaces = 0) { +void PrintTree(const node::permission::FSPermission::RadixTree::Node* node, + size_t spaces = 0) { std::string whitespace(spaces, ' '); if (node == nullptr) { return; } if (node->wildcard_child != nullptr) { - per_process::Debug(DebugCategory::PERMISSION_MODEL, - "%s Wildcard: %s\n", - whitespace, - node->prefix); + node::per_process::Debug(node::DebugCategory::PERMISSION_MODEL, + "%s Wildcard: %s\n", + whitespace, + node->prefix); } else { - per_process::Debug(DebugCategory::PERMISSION_MODEL, - "%s Prefix: %s\n", - whitespace, - node->prefix); + node::per_process::Debug(node::DebugCategory::PERMISSION_MODEL, + "%s Prefix: %s\n", + whitespace, + node->prefix); if (node->children.size()) { - int child = 0; - for (const auto pair : node->children) { + size_t child = 0; + for (const auto& pair : node->children) { ++child; - per_process::Debug(DebugCategory::PERMISSION_MODEL, - "%s Child(%s): %s\n", - whitespace, - child, - std::string(1, pair.first)); + node::per_process::Debug(node::DebugCategory::PERMISSION_MODEL, + "%s Child(%s): %s\n", + whitespace, + child, + std::string(1, pair.first)); PrintTree(pair.second, spaces + 2); } - per_process::Debug(DebugCategory::PERMISSION_MODEL, - "%s End of tree - child(%s)\n", - whitespace, - child); + node::per_process::Debug(node::DebugCategory::PERMISSION_MODEL, + "%s End of tree - child(%s)\n", + whitespace, + child); } else { - per_process::Debug(DebugCategory::PERMISSION_MODEL, - "%s End of tree: %s\n", - whitespace, - node->prefix); + node::per_process::Debug(node::DebugCategory::PERMISSION_MODEL, + "%s End of tree: %s\n", + whitespace, + node->prefix); } } } +} // namespace + +namespace node { + +namespace permission { + // allow = '*' // allow = '/tmp/,/home/example.js' void FSPermission::Apply(const std::string& allow, PermissionScope scope) { diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index 9690031773781b..af3642c1c16f7e 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -59,8 +59,7 @@ void BindingData::DecreaseAllocatedSize(size_t size) { void BindingData::Initialize(Environment* env, Local target) { SetMethod(env->context(), target, "setCallbacks", SetCallbacks); SetMethod(env->context(), target, "flushPacketFreelist", FlushPacketFreelist); - Realm::GetCurrent(env->context()) - ->AddBindingData(env->context(), target); + Realm::GetCurrent(env->context())->AddBindingData(target); } void BindingData::RegisterExternalReferences( diff --git a/src/stream_base.cc b/src/stream_base.cc index 65dab1ccac7cde..cfb1e3ae6a2d7d 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -470,8 +470,9 @@ MaybeLocal StreamBase::CallJSOnreadMethod(ssize_t nread, AsyncWrap* wrap = GetAsyncWrap(); CHECK_NOT_NULL(wrap); - Local onread = wrap->object()->GetInternalField( - StreamBase::kOnReadFunctionField); + Local onread = wrap->object() + ->GetInternalField(StreamBase::kOnReadFunctionField) + .As(); CHECK(onread->IsFunction()); return wrap->MakeCallback(onread.As(), arraysize(argv), argv); } diff --git a/src/timers.cc b/src/timers.cc index 59b4770fa219e8..27fa18ec4d3f86 100644 --- a/src/timers.cc +++ b/src/timers.cc @@ -108,7 +108,7 @@ void BindingData::Deserialize(Local context, v8::HandleScope scope(context->GetIsolate()); Realm* realm = Realm::GetCurrent(context); // Recreate the buffer in the constructor. - BindingData* binding = realm->AddBindingData(context, holder); + BindingData* binding = realm->AddBindingData(holder); CHECK_NOT_NULL(binding); } @@ -151,8 +151,7 @@ void BindingData::CreatePerContextProperties(Local target, void* priv) { Realm* realm = Realm::GetCurrent(context); Environment* env = realm->env(); - BindingData* const binding_data = - realm->AddBindingData(context, target); + BindingData* const binding_data = realm->AddBindingData(target); if (binding_data == nullptr) return; // TODO(joyeecheung): move these into BindingData. diff --git a/src/undici_version.h b/src/undici_version.h index af56664e3f04b3..47aef25a73212d 100644 --- a/src/undici_version.h +++ b/src/undici_version.h @@ -2,5 +2,5 @@ // Refer to tools/update-undici.sh #ifndef SRC_UNDICI_VERSION_H_ #define SRC_UNDICI_VERSION_H_ -#define UNDICI_VERSION "5.22.1" +#define UNDICI_VERSION "5.23.0" #endif // SRC_UNDICI_VERSION_H_ diff --git a/src/util.cc b/src/util.cc index 91c69a98c49c41..8140c177490c33 100644 --- a/src/util.cc +++ b/src/util.cc @@ -28,6 +28,7 @@ #include "node_errors.h" #include "node_internals.h" #include "node_util.h" +#include "node_v8_platform-inl.h" #include "string_bytes.h" #include "uv.h" @@ -55,6 +56,7 @@ static std::atomic_int seq = {0}; // Sequence number for diagnostic filenames. namespace node { +using v8::ArrayBuffer; using v8::ArrayBufferView; using v8::Context; using v8::FunctionTemplate; @@ -358,7 +360,7 @@ Local NewFunctionTemplate( void SetMethod(Local context, Local that, - const char* name, + const std::string_view name, v8::FunctionCallback callback) { Isolate* isolate = context->GetIsolate(); Local function = @@ -372,14 +374,15 @@ void SetMethod(Local context, // kInternalized strings are created in the old space. const v8::NewStringType type = v8::NewStringType::kInternalized; Local name_string = - v8::String::NewFromUtf8(isolate, name, type).ToLocalChecked(); + v8::String::NewFromUtf8(isolate, name.data(), type, name.size()) + .ToLocalChecked(); that->Set(context, name_string, function).Check(); function->SetName(name_string); // NODE_SET_METHOD() compatibility. } void SetMethod(v8::Isolate* isolate, v8::Local that, - const char* name, + const std::string_view name, v8::FunctionCallback callback) { Local t = NewFunctionTemplate(isolate, @@ -390,13 +393,14 @@ void SetMethod(v8::Isolate* isolate, // kInternalized strings are created in the old space. const v8::NewStringType type = v8::NewStringType::kInternalized; Local name_string = - v8::String::NewFromUtf8(isolate, name, type).ToLocalChecked(); + v8::String::NewFromUtf8(isolate, name.data(), type, name.size()) + .ToLocalChecked(); that->Set(name_string, t); } void SetFastMethod(Isolate* isolate, Local