From 05e4fcb89b1dcb011e64043206d54e4a8a836ec3 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Thu, 5 Nov 2026 10:46:31 -0500 Subject: [PATCH 1/4] lib: add FileAPI support Signed-off-by: avivkeller --- doc/api/globals.md | 58 ++ eslint.config.mjs | 4 + .../bootstrap/web/exposed-window-or-worker.js | 7 + lib/internal/file.js | 1 + lib/internal/filereader.js | 777 ++++++++++++++++++ lib/internal/progress_event.js | 142 ++++ lib/internal/webidl.js | 21 + lib/internal/webworker.js | 3 + test/parallel/test-filereader.js | 243 ++++++ test/wpt/status/FileAPI.json | 1 + .../status/FileAPI/reading-data-section.json | 11 + test/wpt/status/workers.json | 6 +- test/wpt/test-fileapi.js | 19 + test/wpt/test-filereader.js | 8 + 14 files changed, 1296 insertions(+), 5 deletions(-) create mode 100644 lib/internal/filereader.js create mode 100644 lib/internal/progress_event.js create mode 100644 test/parallel/test-filereader.js create mode 100644 test/wpt/status/FileAPI.json create mode 100644 test/wpt/status/FileAPI/reading-data-section.json create mode 100644 test/wpt/test-fileapi.js create mode 100644 test/wpt/test-filereader.js diff --git a/doc/api/globals.md b/doc/api/globals.md index f07ac97431b6..c22ca33a1599 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -607,6 +607,48 @@ added: v20.0.0 See {File}. +## Class: `FileList` + + + +A browser-compatible implementation of {FileList} from the [File API][]. The +interface has no constructor. It is a list of {File} objects, with a `length` +attribute and an `item(index)` method that returns the `File` at `index`, or +`null` when `index` is past the end of the list. + +## Class: `FileReader` + + + +A browser-compatible implementation of {FileReader} from the [File API][]. +`readAsArrayBuffer()`, `readAsBinaryString()`, `readAsText()`, and +`readAsDataURL()` read a {Blob} asynchronously and report the result through +`loadstart`, `progress`, `load`, `abort`, `error`, and `loadend` events. +`readyState` is `FileReader.EMPTY` (`0`), `FileReader.LOADING` (`1`), or +`FileReader.DONE` (`2`). + +`readAsDataURL()` produces a base64 `data:` URL. When the blob's type is the +empty string, the URL has no media type. + +## Class: `FileReaderSync` + + + +A browser-compatible implementation of {FileReaderSync} from the [File API][]. +It is exposed only inside a [Web Worker][] (`--experimental-web-worker`), +matching `[Exposed=(DedicatedWorker,SharedWorker)]`. It is not available on +the main thread or in threads created with `node:worker_threads`. + +`readAsArrayBuffer()`, `readAsBinaryString()`, `readAsText()`, and +`readAsDataURL()` read a {Blob} synchronously. A file-backed blob, such as +one returned by [`fs.openAsBlob()`][], throws a `NotReadableError`. + ## Class: `FormData` + +A browser-compatible implementation of {ProgressEvent} from the +[XMLHttpRequest Standard][]. `FileReader` dispatches `ProgressEvent`s for +`loadstart`, `progress`, `load`, `abort`, `error`, and `loadend`. Those events +do not bubble and are not cancelable. `lengthComputable`, `loaded`, and +`total` return the values they were initialized to (`false`, `0`, and `0` +when omitted). + ## `queueMicrotask(callback)` -A browser-compatible implementation of {FileList} from the [File API][]. The -interface has no constructor. It is a list of {File} objects, with a `length` -attribute and an `item(index)` method that returns the `File` at `index`, or -`null` when `index` is past the end of the list. +> Stability: 1 - Experimental -## Class: `FileReader` +An implementation of {FileReader} from the [File API][]. `readAsArrayBuffer()`, +`readAsBinaryString()`, `readAsText()`, and `readAsDataURL()` read a {Blob} +asynchronously and report the result through `loadstart`, `progress`, `load`, +`abort`, `error`, and `loadend` events. `readyState` is `FileReader.EMPTY` +(`0`), `FileReader.LOADING` (`1`), or `FileReader.DONE` (`2`). - +`readAsDataURL()` produces a base64 `data:` URL. When the blob's type is the +empty string, the URL has no media type (`data:;base64,...`). Chrome and +Firefox use `application/octet-stream` in that case. -A browser-compatible implementation of {FileReader} from the [File API][]. -`readAsArrayBuffer()`, `readAsBinaryString()`, `readAsText()`, and -`readAsDataURL()` read a {Blob} asynchronously and report the result through -`loadstart`, `progress`, `load`, `abort`, `error`, and `loadend` events. -`readyState` is `FileReader.EMPTY` (`0`), `FileReader.LOADING` (`1`), or -`FileReader.DONE` (`2`). +`readAsText()` follows {TextDecoder} for the chosen encoding, including its +ICU requirement. Labels that the Encoding Standard maps to the replacement +encoding, such as `'iso-2022-kr'`, decode to U+FFFD when the input is not +empty. -`readAsDataURL()` produces a base64 `data:` URL. When the blob's type is the -empty string, the URL has no media type. +```js +const reader = new FileReader(); +reader.addEventListener('load', () => { + console.log(reader.result); // Prints: hello +}); +reader.readAsText(new Blob(['hello'])); +``` ## Class: `FileReaderSync` @@ -640,15 +644,23 @@ empty string, the URL has no media type. added: REPLACEME --> -A browser-compatible implementation of {FileReaderSync} from the [File API][]. -It is exposed only inside a [Web Worker][] (`--experimental-web-worker`), -matching `[Exposed=(DedicatedWorker,SharedWorker)]`. It is not available on -the main thread or in threads created with `node:worker_threads`. +> Stability: 1 - Experimental + +An implementation of {FileReaderSync} from the [File API][]. It is +`[Exposed=(DedicatedWorker,SharedWorker)]`, so it exists only inside a +[Web Worker][] (`--experimental-web-worker`). It is not available on the main +thread or in threads created with `node:worker_threads`. `readAsArrayBuffer()`, `readAsBinaryString()`, `readAsText()`, and `readAsDataURL()` read a {Blob} synchronously. A file-backed blob, such as one returned by [`fs.openAsBlob()`][], throws a `NotReadableError`. +```js +// Inside a Web Worker (`--experimental-web-worker`): +const text = new FileReaderSync().readAsText(new Blob(['hello'])); +postMessage(text); // Sends 'hello' +``` + ## Class: `FormData` -A browser-compatible implementation of {ProgressEvent} from the -[XMLHttpRequest Standard][]. `FileReader` dispatches `ProgressEvent`s for -`loadstart`, `progress`, `load`, `abort`, `error`, and `loadend`. Those events -do not bubble and are not cancelable. `lengthComputable`, `loaded`, and -`total` return the values they were initialized to (`false`, `0`, and `0` -when omitted). +> Stability: 1 - Experimental + +An implementation of {ProgressEvent} from the [XMLHttpRequest Standard][]. +`FileReader` +dispatches `ProgressEvent`s for `loadstart`, `progress`, `load`, `abort`, +`error`, and `loadend`. Those events do not bubble and are not cancelable. +`lengthComputable`, `loaded`, and `total` return the values they were +initialized to (`false`, `0`, and `0` when omitted). The File API's +"fire a progress event" algorithm does not set those three fields, so events +fired by `FileReader` leave them at the defaults. + +```js +const event = new ProgressEvent('progress', { + lengthComputable: true, + loaded: 50, + total: 100, +}); +console.log(event.loaded / event.total); // Prints: 0.5 +``` ## `queueMicrotask(callback)` diff --git a/eslint.config.mjs b/eslint.config.mjs index dcd5e7833b63..cebc1b482e36 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -131,7 +131,6 @@ export default [ DisposableStack: 'readonly', EventSource: 'readonly', fetch: 'readonly', - FileList: 'readonly', FileReader: 'readonly', FileReaderSync: 'readonly', Float16Array: 'readonly', diff --git a/lib/internal/bootstrap/web/exposed-window-or-worker.js b/lib/internal/bootstrap/web/exposed-window-or-worker.js index b6052b212482..974261639ea9 100644 --- a/lib/internal/bootstrap/web/exposed-window-or-worker.js +++ b/lib/internal/bootstrap/web/exposed-window-or-worker.js @@ -57,11 +57,8 @@ exposeLazyInterfaces(globalThis, 'internal/webworker', ['Worker']); exposeLazyInterfaces(globalThis, 'internal/blob', ['Blob']); // https://www.w3.org/TR/FileAPI/#dfn-file exposeLazyInterfaces(globalThis, 'internal/file', ['File']); -// https://w3c.github.io/FileAPI/#filelist-section // https://w3c.github.io/FileAPI/#APIASynch -exposeLazyInterfaces(globalThis, 'internal/filereader', [ - 'FileList', 'FileReader', -]); +exposeLazyInterfaces(globalThis, 'internal/filereader', ['FileReader']); // https://xhr.spec.whatwg.org/#interface-progressevent exposeLazyInterfaces(globalThis, 'internal/progress_event', ['ProgressEvent']); // https://www.w3.org/TR/hr-time-2/#the-performance-attribute diff --git a/lib/internal/filereader.js b/lib/internal/filereader.js index ef20fb680716..26db994e70b8 100644 --- a/lib/internal/filereader.js +++ b/lib/internal/filereader.js @@ -13,6 +13,7 @@ const { PromisePrototypeThen, Symbol, SymbolToStringTag, + TypedArrayPrototypeGetByteLength, TypedArrayPrototypeSet, TypedArrayPrototypeSubarray, Uint8Array, @@ -25,26 +26,23 @@ const { setImmediate } = require('timers'); const { EventTarget, defineEventHandler, + kTrustEvent, } = require('internal/event_target'); const { + createBlobReaderStream, getBlobDataSync, isBlob, + kHandle, + kType, } = require('internal/blob'); const { TextDecoder, getEncodingFromLabel } = require('internal/encoding'); -const { isFile } = require('internal/file'); - const { MIMEType } = require('internal/mime'); const { ProgressEvent } = require('internal/progress_event'); -const { - ERR_ILLEGAL_CONSTRUCTOR, - ERR_INVALID_ARG_TYPE, -} = require('internal/errors').codes; - const { kEnumerableProperty, lazyDOMException, @@ -63,7 +61,6 @@ const { } = require('internal/webidl'); const kError = Symbol('error'); -const kFiles = Symbol('files'); const kGeneration = Symbol('generation'); const kReader = Symbol('reader'); const kResult = Symbol('result'); @@ -74,85 +71,18 @@ const kProgressInterval = 50; const convertBlob = createInterfaceConverter('Blob', isBlob); -// https://w3c.github.io/FileAPI/#filelist-section -// -// [Exposed=(Window,Worker), Serializable] -// interface FileList { -class FileList { - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } - - // getter File? item(unsigned long index); - item(index) { - const prefix = "Failed to execute 'item' on 'FileList'"; - validateThisInternalField(this, kFiles, 'FileList'); - requiredArguments(arguments.length, 1, { prefix }); - index = converters['unsigned long'](index, { - prefix, - context: 'Argument 1', - }); - // "must return the indexth File object in the FileList. - // If there is no indexth File object in the FileList, - // then this method must return null." - const files = this[kFiles]; - if (index >= files.length) { - return null; - } - return files[index]; - } - - // readonly attribute unsigned long length; - get length() { - validateThisInternalField(this, kFiles, 'FileList'); - // "must return the number of files in the FileList object. - // If there are no files, this attribute must return 0." - return this[kFiles].length; - } -} -// }; - -function createFileList(files) { - const list = { __proto__: FileList.prototype }; - const items = []; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (!isFile(file)) { - throw new ERR_INVALID_ARG_TYPE('files', 'File[]', files); - } - ArrayPrototypePush(items, file); - // "Supported property indices are the numbers in the range zero to one - // less than the number of File objects represented by the FileList - // object." - ObjectDefineProperty(list, `${i}`, { - __proto__: null, - configurable: true, - enumerable: true, - writable: false, - value: file, - }); - } - list[kFiles] = items; - return list; -} - -ObjectDefineProperties(FileList.prototype, { - item: kEnumerableProperty, - length: kEnumerableProperty, - [SymbolToStringTag]: { - __proto__: null, - configurable: true, - value: 'FileList', - }, -}); - // "When this specification says to fire a progress event called e (for some // ProgressEvent e at a given FileReader reader), the following are normative: // // * The progress event e does not bubble. e.bubbles must be false // * The progress event e is NOT cancelable. e.cancelable must be false" +const kTrustedProgressEventInit = { + __proto__: null, + [kTrustEvent]: true, +}; + function fireAProgressEvent(name, reader) { - reader.dispatchEvent(new ProgressEvent(name)); + reader.dispatchEvent(new ProgressEvent(name, kTrustedProgressEventInit)); } // "Use the file reading task source for all these tasks." @@ -171,7 +101,7 @@ function combineByteSequences(chunks, totalBytes) { for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; TypedArrayPrototypeSet(bytes, chunk, offset); - offset += chunk.byteLength; + offset += TypedArrayPrototypeGetByteLength(chunk); } return bytes; } @@ -182,10 +112,7 @@ function combineByteSequences(chunks, totalBytes) { function bomSniff(bytes) { // "Let BOM be the result of peeking 3 bytes from ioQueue, converted to a // byte sequence." - const length = bytes.byteLength; - const a = bytes[0]; - const b = bytes[1]; - const c = bytes[2]; + const length = TypedArrayPrototypeGetByteLength(bytes); // "For each of the rows in the table below, starting with the first one // and going down, if BOM starts with the bytes given in the first column, // then return the encoding given in the cell in the second column of that @@ -194,14 +121,19 @@ function bomSniff(bytes) { // 0xEF 0xBB 0xBF UTF-8 // 0xFE 0xFF UTF-16BE // 0xFF 0xFE UTF-16LE - if (length >= 3 && a === 0xEF && b === 0xBB && c === 0xBF) { + if (length >= 3 && + bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) { return 'utf-8'; } - if (length >= 2 && a === 0xFE && b === 0xFF) { - return 'utf-16be'; - } - if (length >= 2 && a === 0xFF && b === 0xFE) { - return 'utf-16le'; + if (length >= 2) { + const a = bytes[0]; + const b = bytes[1]; + if (a === 0xFE && b === 0xFF) { + return 'utf-16be'; + } + if (a === 0xFF && b === 0xFE) { + return 'utf-16le'; + } } return null; } @@ -222,11 +154,21 @@ function decode(bytes, encoding) { encoding = bomEncoding; offset = bomEncoding === 'utf-8' ? 3 : 2; } + const view = offset === 0 ? bytes : + TypedArrayPrototypeSubarray(bytes, offset); + // "replacement's decoder's handler, given unused and byte, runs these + // steps: + // 1. If byte is end-of-queue, then return finished. + // 2. If replacement error returned is false, then set replacement error + // returned to true and return error. + // 3. Return finished." + // Error mode "replacement" turns that error into U+FFFD. + if (encoding === 'replacement') { + return TypedArrayPrototypeGetByteLength(view) === 0 ? '' : '\uFFFD'; + } // "Process a queue with an instance of encoding's decoder, ioQueue, // output, and "replacement"." // "Return output." - const view = offset === 0 ? bytes : - TypedArrayPrototypeSubarray(bytes, offset); return new TextDecoder(encoding, { ignoreBOM: true }).decode(view); } @@ -331,6 +273,9 @@ function readOperation(fr, blob, type, encodingLabel) { 'The object is in an invalid state.', 'InvalidStateError'); } + const previousState = fr[kState]; + const previousResult = fr[kResult]; + const previousError = fr[kError]; // "2. Set fr's state to "loading"." fr[kState] = 'loading'; // "3. Set fr's result to null." @@ -340,7 +285,15 @@ function readOperation(fr, blob, type, encodingLabel) { const generation = ++fr[kGeneration]; // "5. Let stream be the result of calling get stream on blob." - const stream = blob.stream(); + let stream; + try { + stream = createBlobReaderStream(blob[kHandle].getReader()); + } catch (error) { + fr[kState] = previousState; + fr[kResult] = previousResult; + fr[kError] = previousError; + throw error; + } // "6. Let reader be the result of getting a reader from stream." const reader = stream.getReader(); fr[kReader] = reader; @@ -418,7 +371,7 @@ function readOperation(fr, blob, type, encodingLabel) { // abort this algorithm:" if (chunk.done === true) { const bytes = combineByteSequences(chunks, totalBytes); - const mimeType = blob.type; + const mimeType = blob[kType]; // "One progress event will fire when blob has been completely read // into memory." if (totalBytes > 0) { @@ -685,7 +638,7 @@ function readSync(blob, type, encodingLabel) { const buffer = getBlobDataSync(blob); // "If promise fulfilled with a byte sequence bytes:" if (buffer !== undefined) { - return packageData(new Uint8Array(buffer), type, blob.type, encodingLabel); + return packageData(new Uint8Array(buffer), type, blob[kType], encodingLabel); } // "Throw promise's rejection reason." throw lazyDOMException( @@ -770,8 +723,6 @@ ObjectDefineProperties(FileReaderSync.prototype, { }); module.exports = { - FileList, FileReader, FileReaderSync, - createFileList, }; diff --git a/lib/internal/progress_event.js b/lib/internal/progress_event.js index fd3dd002e561..a08278ef7c09 100644 --- a/lib/internal/progress_event.js +++ b/lib/internal/progress_event.js @@ -13,6 +13,7 @@ const { const { Event, + kTrustEvent, } = require('internal/event_target'); const { @@ -92,11 +93,17 @@ class ProgressEvent extends Event { constructor(type, eventInitDict = kEmptyObject) { const prefix = "Failed to construct 'ProgressEvent'"; requiredArguments(arguments.length, 1, { prefix }); + const trusted = eventInitDict != null && + typeof eventInitDict === 'object' && + eventInitDict[kTrustEvent] === true; type = converters.DOMString(type, { prefix, context: 'Argument 1' }); eventInitDict = convertProgressEventInit(eventInitDict, { prefix, context: 'Argument 2', }); + if (trusted) { + eventInitDict[kTrustEvent] = true; + } super(type, eventInitDict); // "The lengthComputable, loaded, and total getter steps are to return diff --git a/test/parallel/test-filereader.js b/test/parallel/test-filereader.js index b32dbe011507..5e3982794413 100644 --- a/test/parallel/test-filereader.js +++ b/test/parallel/test-filereader.js @@ -3,11 +3,9 @@ const common = require('../common'); const assert = require('node:assert'); -const { createFileList } = require('internal/filereader'); { assert.strictEqual(typeof FileReader, 'function'); - assert.strictEqual(typeof FileList, 'function'); assert.strictEqual(typeof ProgressEvent, 'function'); assert.strictEqual(typeof FileReaderSync, 'undefined'); } @@ -40,23 +38,31 @@ const { createFileList } = require('internal/filereader'); { const reader = new FileReader(); const events = []; - reader.addEventListener('loadstart', common.mustCall(() => { + reader.addEventListener('loadstart', common.mustCall((event) => { events.push('loadstart'); + assert.ok(event instanceof ProgressEvent); + assert.strictEqual(event.isTrusted, true); assert.strictEqual(reader.readyState, FileReader.LOADING); assert.strictEqual(reader.result, null); })); - reader.addEventListener('progress', common.mustCall(() => { + reader.addEventListener('progress', common.mustCall((event) => { events.push('progress'); + assert.ok(event instanceof ProgressEvent); + assert.strictEqual(event.isTrusted, true); assert.strictEqual(reader.readyState, FileReader.LOADING); assert.strictEqual(reader.result, null); })); - reader.addEventListener('load', common.mustCall(() => { + reader.addEventListener('load', common.mustCall((event) => { events.push('load'); + assert.ok(event instanceof ProgressEvent); + assert.strictEqual(event.isTrusted, true); assert.strictEqual(reader.readyState, FileReader.DONE); assert.strictEqual(reader.result, 'TEST'); })); - reader.addEventListener('loadend', common.mustCall(() => { + reader.addEventListener('loadend', common.mustCall((event) => { events.push('loadend'); + assert.ok(event instanceof ProgressEvent); + assert.strictEqual(event.isTrusted, true); assert.deepStrictEqual(events, ['loadstart', 'progress', 'load', 'loadend']); })); reader.readAsText(new Blob(['TEST'])); @@ -178,20 +184,6 @@ const { createFileList } = require('internal/filereader'); first.readAsText(new Blob(['first'])); } -{ - assert.throws(() => new FileList(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); - const file = new File(['hello'], 'hello.txt'); - const list = createFileList([file]); - assert.ok(list instanceof FileList); - assert.strictEqual(list.length, 1); - assert.strictEqual(list.item(0), file); - assert.strictEqual(list[0], file); - assert.strictEqual(list.item(1), null); - const empty = createFileList([]); - assert.strictEqual(empty.length, 0); - assert.strictEqual(empty.item(0), null); -} - { const event = new ProgressEvent('progress', { lengthComputable: true, @@ -211,6 +203,38 @@ const { createFileList } = require('internal/filereader'); assert.throws(() => ProgressEvent.prototype.loaded, { code: 'ERR_INVALID_THIS', }); + assert.strictEqual(new ProgressEvent('load').isTrusted, false); +} + +{ + const reader = new FileReader(); + reader.onload = common.mustCall(() => { + assert.strictEqual(reader.result, '\uFFFD'); + }); + reader.readAsText(new Blob(['hello']), 'iso-2022-kr'); +} + +{ + const reader = new FileReader(); + reader.onload = common.mustCall(() => { + assert.strictEqual(reader.result, ''); + }); + reader.readAsText(new Blob(), 'iso-2022-kr'); +} + +{ + const blob = new Blob(['TEST']); + blob.stream = () => { + throw new Error('public stream should not be used'); + }; + Object.defineProperty(blob, 'type', { + get() { return 'text/evil'; }, + }); + const reader = new FileReader(); + reader.onload = common.mustCall(() => { + assert.strictEqual(reader.result, 'data:;base64,VEVTVA=='); + }); + reader.readAsDataURL(blob); } if (common.hasCrypto) { diff --git a/test/wpt/status/workers.json b/test/wpt/status/workers.json index 64eb1fb4359d..86f2ec1c39db 100644 --- a/test/wpt/status/workers.json +++ b/test/wpt/status/workers.json @@ -112,7 +112,7 @@ }, "semantics/interface-objects/001.worker.js": { "fail": { - "note": "Canvas, XHR, EventSource, PromiseRejectionEvent, and IndexedDB are not implemented", + "note": "Canvas, XHR, EventSource, PromiseRejectionEvent, FileList, and IndexedDB are not implemented", "expected": [ "The ImageData interface object should be exposed.", "The ImageBitmap interface object should be exposed.", @@ -121,6 +121,7 @@ "The TextMetrics interface object should be exposed.", "The Path2D interface object should be exposed.", "The PromiseRejectionEvent interface object should be exposed.", + "The FileList interface object should be exposed.", "The EventSource interface object should be exposed.", "The XMLHttpRequestEventTarget interface object should be exposed.", "The XMLHttpRequestUpload interface object should be exposed.", From cd0e0fb1e9684afed3b6e77ad3d80b6cf18e7d58 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Fri, 6 Nov 2026 10:42:36 -0500 Subject: [PATCH 4/4] fixup! lib: add FileAPI support --- doc/api/cli.md | 12 ++++++++++++ doc/api/globals.md | 10 +++++++--- doc/node.1 | 7 +++++++ lib/internal/process/pre_execution.js | 10 ++++++++++ src/node_options.cc | 5 +++++ src/node_options.h | 1 + test/parallel/test-filereader-flag.js | 9 +++++++++ test/parallel/test-filereader.js | 2 +- test/wpt/test-fileapi.js | 1 + test/wpt/test-filereader.js | 1 + test/wpt/test-workers.js | 1 + 11 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-filereader-flag.js diff --git a/doc/api/cli.md b/doc/api/cli.md index d3309ae6192d..9545f40f3460 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1465,6 +1465,16 @@ added: Enable exposition of [EventSource Web API][] on the global scope. +### `--experimental-fileapi` + + + +Enable exposition of the [File API][] globals `FileReader` and `ProgressEvent` +on the global scope. `FileReaderSync` is exposed only inside a Web Worker +started with `--experimental-web-worker`. + ### `--experimental-import-meta-resolve` -> Stability: 1 - Experimental +> Stability: 1.1 - Active development. Enable this API with the +> [`--experimental-fileapi`][] CLI flag. An implementation of {FileReader} from the [File API][]. `readAsArrayBuffer()`, `readAsBinaryString()`, `readAsText()`, and `readAsDataURL()` read a {Blob} @@ -644,7 +645,8 @@ reader.readAsText(new Blob(['hello'])); added: REPLACEME --> -> Stability: 1 - Experimental +> Stability: 1.1 - Active development. Enable this API with the +> [`--experimental-fileapi`][] CLI flag. An implementation of {FileReaderSync} from the [File API][]. It is `[Exposed=(DedicatedWorker,SharedWorker)]`, so it exists only inside a @@ -1002,7 +1004,8 @@ The process object. See the [`process` object][] section. added: REPLACEME --> -> Stability: 1 - Experimental +> Stability: 1.1 - Active development. Enable this API with the +> [`--experimental-fileapi`][] CLI flag. An implementation of {ProgressEvent} from the [XMLHttpRequest Standard][]. `FileReader` @@ -1564,6 +1567,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][]. [Web Worker]: #class-worker [XMLHttpRequest Standard]: https://xhr.spec.whatwg.org/#interface-progressevent [`--experimental-eventsource`]: cli.md#--experimental-eventsource +[`--experimental-fileapi`]: cli.md#--experimental-fileapi [`--experimental-web-worker`]: cli.md#--experimental-web-worker [`--localstorage-file`]: cli.md#--localstorage-filefile [`--no-experimental-global-navigator`]: cli.md#--no-experimental-global-navigator diff --git a/doc/node.1 b/doc/node.1 index 444da29eed3d..31d05dbb4b88 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -858,6 +858,11 @@ dtls documentation for details. .It Fl -experimental-eventsource Enable exposition of EventSource Web API on the global scope. . +.It Fl -experimental-fileapi +Enable exposition of the File API globals \fBFileReader\fR and \fBProgressEvent\fR +on the global scope. \fBFileReaderSync\fR is exposed only inside a Web Worker +started with \fB--experimental-web-worker\fR. +. .It Fl -experimental-import-meta-resolve Enable experimental \fBimport.meta.resolve()\fR parent URL support, which allows passing a second \fBparentURL\fR argument for contextual resolution. @@ -2266,6 +2271,8 @@ one is included in the list below. .It \fB--experimental-eventsource\fR .It +\fB--experimental-fileapi\fR +.It \fB--experimental-import-meta-resolve\fR .It \fB--experimental-import-text\fR diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 0f5aaf857aeb..0fe8ca417482 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -122,6 +122,7 @@ function prepareExecution(options) { setupWebStorage(); removeWebWorkersIfDisabled(); setupEventsource(); + removeFileAPIIfDisabled(); setupCodeCoverage(); setupDebugEnv(); // Process initial diagnostic reporting configuration, if present. @@ -463,6 +464,15 @@ function removeWebWorkersIfDisabled() { } } +// https://w3c.github.io/FileAPI/ +function removeFileAPIIfDisabled() { + if (!getOptionValue('--experimental-fileapi')) { + delete globalThis.FileReader; + delete globalThis.FileReaderSync; + delete globalThis.ProgressEvent; + } +} + // https://html.spec.whatwg.org/multipage/server-sent-events.html function setupEventsource() { if (!getOptionValue('--experimental-eventsource')) { diff --git a/src/node_options.cc b/src/node_options.cc index 4ba4801ad1dc..11dda59af97c 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -776,6 +776,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { BOOL_FIELD(experimental_eventsource), kAllowedInEnvvar, false); + AddOption("--experimental-fileapi", + "experimental File API globals", + BOOL_FIELD(experimental_fileapi), + kAllowedInEnvvar, + false); AddOption("--experimental-fetch", "", NoOp{}, kAllowedInEnvvar); #if HAVE_FFI AddOption("--experimental-ffi", diff --git a/src/node_options.h b/src/node_options.h index f8aa58253862..ae75f9e301eb 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -210,6 +210,7 @@ class EnvironmentOptions : public Options { DEFINE_BOOL_FIELD(experimental_addon_modules) = true; DEFINE_BOOL_FIELD(experimental_bench) = EXPERIMENTALS_DEFAULT_VALUE; DEFINE_BOOL_FIELD(experimental_eventsource) = EXPERIMENTALS_DEFAULT_VALUE; + DEFINE_BOOL_FIELD(experimental_fileapi) = EXPERIMENTALS_DEFAULT_VALUE; DEFINE_BOOL_FIELD(experimental_ffi) = HAVE_FFI; DEFINE_BOOL_FIELD(experimental_web_worker) = EXPERIMENTALS_DEFAULT_VALUE; DEFINE_BOOL_FIELD(experimental_websocket) = true; diff --git a/test/parallel/test-filereader-flag.js b/test/parallel/test-filereader-flag.js new file mode 100644 index 000000000000..f660ee46f80a --- /dev/null +++ b/test/parallel/test-filereader-flag.js @@ -0,0 +1,9 @@ +'use strict'; + +require('../common'); +const assert = require('node:assert'); + +// Without --experimental-fileapi, the File API globals are not exposed. +assert.strictEqual(typeof FileReader, 'undefined'); +assert.strictEqual(typeof FileReaderSync, 'undefined'); +assert.strictEqual(typeof ProgressEvent, 'undefined'); diff --git a/test/parallel/test-filereader.js b/test/parallel/test-filereader.js index 5e3982794413..42b41895b1c7 100644 --- a/test/parallel/test-filereader.js +++ b/test/parallel/test-filereader.js @@ -1,4 +1,4 @@ -// Flags: --expose-internals --experimental-web-worker +// Flags: --expose-internals --experimental-web-worker --experimental-fileapi 'use strict'; const common = require('../common'); diff --git a/test/wpt/test-fileapi.js b/test/wpt/test-fileapi.js index cb3344d49578..7c7f6bc136a8 100644 --- a/test/wpt/test-fileapi.js +++ b/test/wpt/test-fileapi.js @@ -11,6 +11,7 @@ const included = new Set([ ]); const runner = new WPTRunner('FileAPI'); +runner.setFlags(['--experimental-fileapi']); for (const spec of runner.specs) { if (!included.has(spec.filename)) { runner.specs.delete(spec); diff --git a/test/wpt/test-filereader.js b/test/wpt/test-filereader.js index 6ecd466201f9..678a315c1f3a 100644 --- a/test/wpt/test-filereader.js +++ b/test/wpt/test-filereader.js @@ -4,5 +4,6 @@ require('../common'); const { WPTRunner } = require('../common/wpt'); const runner = new WPTRunner('FileAPI/reading-data-section'); +runner.setFlags(['--experimental-fileapi']); runner.runJsTests(); diff --git a/test/wpt/test-workers.js b/test/wpt/test-workers.js index 21ec86057b6e..d1595a716846 100644 --- a/test/wpt/test-workers.js +++ b/test/wpt/test-workers.js @@ -5,5 +5,6 @@ const { WPTRunner } = require('../common/wpt'); const runner = new WPTRunner('workers'); +runner.setFlags(['--experimental-fileapi']); runner.pretendGlobalThisAs('Window'); runner.runJsTests();