From f90c16155d8c87c0d8cc1bdf46b553e8aef144a5 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 15 Apr 2024 23:04:46 -0400 Subject: [PATCH] drop node support for < v18.17.0 (#3125) * drop node support for < v18.17.0 * fixup * fixup --- docs/docs/api/Fetch.md | 6 - index.js | 2 +- lib/core/util.js | 1 - lib/dispatcher/client.js | 2 +- lib/dispatcher/pool.js | 2 +- lib/web/fetch/file.js | 221 +---------------------------- lib/web/fetch/formdata-parser.js | 4 +- lib/web/fetch/formdata.js | 6 +- lib/web/fetch/index.js | 6 +- lib/web/websocket/util.js | 19 +-- package.json | 2 +- test/fetch/file.js | 190 ------------------------- test/fetch/resource-timing.js | 11 +- test/node-test/autoselectfamily.js | 3 +- 14 files changed, 27 insertions(+), 448 deletions(-) delete mode 100644 test/fetch/file.js diff --git a/docs/docs/api/Fetch.md b/docs/docs/api/Fetch.md index 5e480f5acfa..c3406f128dc 100644 --- a/docs/docs/api/Fetch.md +++ b/docs/docs/api/Fetch.md @@ -4,12 +4,6 @@ Undici exposes a fetch() method starts the process of fetching a resource from t Documentation and examples can be found on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch). -## File - -This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/File) - -In Node versions v18.13.0 and above and v19.2.0 and above, undici will default to using Node's [File](https://nodejs.org/api/buffer.html#class-file) class. In versions where it's not available, it will default to the undici one. - ## FormData This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData). diff --git a/index.js b/index.js index dcf0be90798..32879cdb743 100644 --- a/index.js +++ b/index.js @@ -116,7 +116,7 @@ module.exports.Headers = require('./lib/web/fetch/headers').Headers module.exports.Response = require('./lib/web/fetch/response').Response module.exports.Request = require('./lib/web/fetch/request').Request module.exports.FormData = require('./lib/web/fetch/formdata').FormData -module.exports.File = require('./lib/web/fetch/file').File +module.exports.File = globalThis.File ?? require('node:buffer').File module.exports.FileReader = require('./lib/web/fileapi/filereader').FileReader const { setGlobalOrigin, getGlobalOrigin } = require('./lib/web/fetch/global') diff --git a/lib/core/util.js b/lib/core/util.js index 47ad0485202..2bad24df2f6 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -634,6 +634,5 @@ module.exports = { isHttpOrHttpsPrefixed, nodeMajor, nodeMinor, - nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13), safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'] } diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 054198f65c4..3b3da96d712 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -203,7 +203,7 @@ class Client extends DispatcherBase { allowH2, socketPath, timeout: connectTimeout, - ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect }) } diff --git a/lib/dispatcher/pool.js b/lib/dispatcher/pool.js index 0ba3a2b5f3e..2d84cd96488 100644 --- a/lib/dispatcher/pool.js +++ b/lib/dispatcher/pool.js @@ -58,7 +58,7 @@ class Pool extends PoolBase { allowH2, socketPath, timeout: connectTimeout, - ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect }) } diff --git a/lib/web/fetch/file.js b/lib/web/fetch/file.js index 3cb4eebb3e0..31ba40718ec 100644 --- a/lib/web/fetch/file.js +++ b/lib/web/fetch/file.js @@ -1,100 +1,10 @@ 'use strict' -const { EOL } = require('node:os') -const { Blob, File: NativeFile } = require('node:buffer') -const { types } = require('node:util') +const { Blob, File } = require('node:buffer') const { kState } = require('./symbols') -const { isBlobLike } = require('./util') const { webidl } = require('./webidl') -const { parseMIMEType, serializeAMimeType } = require('./data-url') -const { kEnumerableProperty } = require('../../core/util') - -const encoder = new TextEncoder() - -class File extends Blob { - constructor (fileBits, fileName, options = {}) { - // The File constructor is invoked with two or three parameters, depending - // on whether the optional dictionary parameter is used. When the File() - // constructor is invoked, user agents must run the following steps: - webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' }) - - fileBits = webidl.converters['sequence'](fileBits) - fileName = webidl.converters.USVString(fileName) - options = webidl.converters.FilePropertyBag(options) - - // 1. Let bytes be the result of processing blob parts given fileBits and - // options. - // Note: Blob handles this for us - - // 2. Let n be the fileName argument to the constructor. - const n = fileName - - // 3. Process FilePropertyBag dictionary argument by running the following - // substeps: - - // 1. If the type member is provided and is not the empty string, let t - // be set to the type dictionary member. If t contains any characters - // outside the range U+0020 to U+007E, then set t to the empty string - // and return from these substeps. - // 2. Convert every character in t to ASCII lowercase. - let t = options.type - let d - - // eslint-disable-next-line no-labels - substep: { - if (t) { - t = parseMIMEType(t) - - if (t === 'failure') { - t = '' - // eslint-disable-next-line no-labels - break substep - } - - t = serializeAMimeType(t).toLowerCase() - } - - // 3. If the lastModified member is provided, let d be set to the - // lastModified dictionary member. If it is not provided, set d to the - // current date and time represented as the number of milliseconds since - // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). - d = options.lastModified - } - - // 4. Return a new File object F such that: - // F refers to the bytes byte sequence. - // F.size is set to the number of total bytes in bytes. - // F.name is set to n. - // F.type is set to t. - // F.lastModified is set to d. - - super(processBlobParts(fileBits, options), { type: t }) - this[kState] = { - name: n, - lastModified: d, - type: t - } - } - - get name () { - webidl.brandCheck(this, File) - - return this[kState].name - } - - get lastModified () { - webidl.brandCheck(this, File) - - return this[kState].lastModified - } - - get type () { - webidl.brandCheck(this, File) - - return this[kState].type - } -} +// TODO(@KhafraDev): remove class FileLike { constructor (blobLike, fileName, options = {}) { // TODO: argument idl type check @@ -196,136 +106,15 @@ class FileLike { } } -Object.defineProperties(File.prototype, { - [Symbol.toStringTag]: { - value: 'File', - configurable: true - }, - name: kEnumerableProperty, - lastModified: kEnumerableProperty -}) - webidl.converters.Blob = webidl.interfaceConverter(Blob) -webidl.converters.BlobPart = function (V, opts) { - if (webidl.util.Type(V) === 'Object') { - if (isBlobLike(V)) { - return webidl.converters.Blob(V, { strict: false }) - } - - if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) { - return webidl.converters.BufferSource(V, opts) - } - } - - return webidl.converters.USVString(V, opts) -} - -webidl.converters['sequence'] = webidl.sequenceConverter( - webidl.converters.BlobPart -) - -// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag -webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ - { - key: 'lastModified', - converter: webidl.converters['long long'], - get defaultValue () { - return Date.now() - } - }, - { - key: 'type', - converter: webidl.converters.DOMString, - defaultValue: '' - }, - { - key: 'endings', - converter: (value) => { - value = webidl.converters.DOMString(value) - value = value.toLowerCase() - - if (value !== 'native') { - value = 'transparent' - } - - return value - }, - defaultValue: 'transparent' - } -]) - -/** - * @see https://www.w3.org/TR/FileAPI/#process-blob-parts - * @param {(NodeJS.TypedArray|Blob|string)[]} parts - * @param {{ type: string, endings: string }} options - */ -function processBlobParts (parts, options) { - // 1. Let bytes be an empty sequence of bytes. - /** @type {NodeJS.TypedArray[]} */ - const bytes = [] - - // 2. For each element in parts: - for (const element of parts) { - // 1. If element is a USVString, run the following substeps: - if (typeof element === 'string') { - // 1. Let s be element. - let s = element - - // 2. If the endings member of options is "native", set s - // to the result of converting line endings to native - // of element. - if (options.endings === 'native') { - s = convertLineEndingsNative(s) - } - - // 3. Append the result of UTF-8 encoding s to bytes. - bytes.push(encoder.encode(s)) - } else if (ArrayBuffer.isView(element) || types.isArrayBuffer(element)) { - // 2. If element is a BufferSource, get a copy of the - // bytes held by the buffer source, and append those - // bytes to bytes. - if (element.buffer) { - bytes.push( - new Uint8Array(element.buffer, element.byteOffset, element.byteLength) - ) - } else { // ArrayBuffer - bytes.push(new Uint8Array(element)) - } - } else if (isBlobLike(element)) { - // 3. If element is a Blob, append the bytes it represents - // to bytes. - bytes.push(element) - } - } - - // 3. Return bytes. - return bytes -} - -/** - * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native - * @param {string} s - */ -function convertLineEndingsNative (s) { - // 1. Let native line ending be be the code point U+000A LF. - // 2. If the underlying platform’s conventions are to - // represent newlines as a carriage return and line feed - // sequence, set native line ending to the code point - // U+000D CR followed by the code point U+000A LF. - // NOTE: We are using the native line ending for the current - // platform, provided by node's os module. - - return s.replace(/\r?\n/g, EOL) -} - // If this function is moved to ./util.js, some tools (such as // rollup) will warn about circular dependencies. See: // https://github.com/nodejs/undici/issues/1629 function isFileLike (object) { return ( - (NativeFile && object instanceof NativeFile) || - object instanceof File || ( + (object instanceof File) || + ( object && (typeof object.stream === 'function' || typeof object.arrayBuffer === 'function') && @@ -334,4 +123,4 @@ function isFileLike (object) { ) } -module.exports = { File, FileLike, isFileLike } +module.exports = { FileLike, isFileLike } diff --git a/lib/web/fetch/formdata-parser.js b/lib/web/fetch/formdata-parser.js index 17c55753838..7e567e9ec65 100644 --- a/lib/web/fetch/formdata-parser.js +++ b/lib/web/fetch/formdata-parser.js @@ -3,12 +3,12 @@ const { isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util') const { utf8DecodeBytes } = require('./util') const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url') -const { isFileLike, File: UndiciFile } = require('./file') +const { isFileLike } = require('./file') const { makeEntry } = require('./formdata') const assert = require('node:assert') const { File: NodeFile } = require('node:buffer') -const File = globalThis.File ?? NodeFile ?? UndiciFile +const File = globalThis.File ?? NodeFile const formDataNameBuffer = Buffer.from('form-data; name="') const filenameBuffer = Buffer.from('; filename') diff --git a/lib/web/fetch/formdata.js b/lib/web/fetch/formdata.js index 9e7d6d0e4ad..029419ffe94 100644 --- a/lib/web/fetch/formdata.js +++ b/lib/web/fetch/formdata.js @@ -3,13 +3,13 @@ const { isBlobLike, iteratorMixin } = require('./util') const { kState } = require('./symbols') const { kEnumerableProperty } = require('../../core/util') -const { File: UndiciFile, FileLike, isFileLike } = require('./file') +const { FileLike, isFileLike } = require('./file') const { webidl } = require('./webidl') const { File: NativeFile } = require('node:buffer') const nodeUtil = require('node:util') /** @type {globalThis['File']} */ -const File = NativeFile ?? UndiciFile +const File = globalThis.File ?? NativeFile // https://xhr.spec.whatwg.org/#formdata class FormData { @@ -231,7 +231,7 @@ function makeEntry (name, value, filename) { lastModified: value.lastModified } - value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile + value = value instanceof NativeFile ? new File([value], filename, options) : new FileLike(value, filename, options) } diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index 76acc3d865b..0b584c5a5da 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -59,7 +59,7 @@ const { } = require('./constants') const EE = require('node:events') const { Readable, pipeline, finished } = require('node:stream') -const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor, bufferToLowerCasedHeaderName } = require('../../core/util') +const { addAbortListener, isErrored, isReadable, bufferToLowerCasedHeaderName } = require('../../core/util') const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./data-url') const { getGlobalDispatcher } = require('../../global') const { webidl } = require('./webidl') @@ -310,9 +310,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') { } // https://w3c.github.io/resource-timing/#dfn-mark-resource-timing -const markResourceTiming = (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) - ? performance.markResourceTiming - : () => {} +const markResourceTiming = performance.markResourceTiming // https://fetch.spec.whatwg.org/#abort-fetch function abortFetch (p, request, responseObject, error) { diff --git a/lib/web/websocket/util.js b/lib/web/websocket/util.js index 437a842cd96..20cdb995efe 100644 --- a/lib/web/websocket/util.js +++ b/lib/web/websocket/util.js @@ -211,19 +211,12 @@ const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undef */ const utf8Decode = hasIntl ? fatalDecoder.decode.bind(fatalDecoder) - : !isUtf8 - ? function () { // TODO: remove once node 18 or < node v18.14.0 is dropped - process.emitWarning('ICU is not supported and no fallback exists. Please upgrade to at least Node v18.14.0.', { - code: 'UNDICI-WS-NO-ICU' - }) - throw new TypeError('Invalid utf-8 received.') - } - : function (buffer) { - if (isUtf8(buffer)) { - return buffer.toString('utf-8') - } - throw new TypeError('Invalid utf-8 received.') - } + : function (buffer) { + if (isUtf8(buffer)) { + return buffer.toString('utf-8') + } + throw new TypeError('Invalid utf-8 received.') + } module.exports = { isConnecting, diff --git a/package.json b/package.json index 567d5b77795..a9b8fdb44be 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "ws": "^8.11.0" }, "engines": { - "node": ">=18.0" + "node": ">=18.17" }, "standard": { "env": [ diff --git a/test/fetch/file.js b/test/fetch/file.js deleted file mode 100644 index 4bf8e812d3b..00000000000 --- a/test/fetch/file.js +++ /dev/null @@ -1,190 +0,0 @@ -'use strict' - -const { Blob } = require('node:buffer') -const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') -const { File, FileLike } = require('../../lib/web/fetch/file') - -test('args validation', (t) => { - const { throws, doesNotThrow, strictEqual } = tspl(t, { plan: 14 }) - - throws(() => { - File.prototype.name.toString() - }, TypeError) - throws(() => { - File.prototype.lastModified.toString() - }, TypeError) - doesNotThrow(() => { - File.prototype[Symbol.toStringTag].charAt(0) - }, TypeError) - - throws(() => { - FileLike.prototype.stream.call(null) - }, TypeError) - throws(() => { - FileLike.prototype.arrayBuffer.call(null) - }, TypeError) - throws(() => { - FileLike.prototype.slice.call(null) - }, TypeError) - throws(() => { - FileLike.prototype.text.call(null) - }, TypeError) - throws(() => { - FileLike.prototype.size.toString() - }, TypeError) - throws(() => { - FileLike.prototype.type.toString() - }, TypeError) - throws(() => { - FileLike.prototype.name.toString() - }, TypeError) - throws(() => { - FileLike.prototype.lastModified.toString() - }, TypeError) - doesNotThrow(() => { - FileLike.prototype[Symbol.toStringTag].charAt(0) - }, TypeError) - - strictEqual(File.prototype[Symbol.toStringTag], 'File') - strictEqual(FileLike.prototype[Symbol.toStringTag], 'File') -}) - -test('return value of File.lastModified', (t) => { - const { ok } = tspl(t, { plan: 2 }) - - const f = new File(['asd1'], 'filename123') - const lastModified = f.lastModified - ok(typeof lastModified === typeof Date.now()) - ok(lastModified >= 0 && lastModified <= Date.now()) -}) - -test('Symbol.toStringTag', (t) => { - const { strictEqual } = tspl(t, { plan: 2 }) - strictEqual(new File([], '')[Symbol.toStringTag], 'File') - strictEqual(new FileLike()[Symbol.toStringTag], 'File') -}) - -test('arguments', () => { - assert.throws(() => { - new File() // eslint-disable-line no-new - }, TypeError) - - assert.throws(() => { - new File([]) // eslint-disable-line no-new - }, TypeError) -}) - -test('lastModified', () => { - const file = new File([], '') - const lastModified = Date.now() - 69_000 - - assert.ok(file !== 0) - - const file1 = new File([], '', { lastModified }) - assert.strictEqual(file1.lastModified, lastModified) - - assert.strictEqual(new File([], '', { lastModified: 0 }).lastModified, 0) - - assert.strictEqual( - new File([], '', { - lastModified: true - }).lastModified, - 1 - ) -}) - -test('File.prototype.text', async (t) => { - await t.test('With Blob', async () => { - const blob1 = new Blob(['hello']) - const blob2 = new Blob([' ']) - const blob3 = new Blob(['world']) - - const file = new File([blob1, blob2, blob3], 'hello_world.txt') - - assert.strictEqual(await file.text(), 'hello world') - }) - - /* eslint-disable camelcase */ - await t.test('With TypedArray', async () => { - const uint8_1 = new Uint8Array(Buffer.from('hello')) - const uint8_2 = new Uint8Array(Buffer.from(' ')) - const uint8_3 = new Uint8Array(Buffer.from('world')) - - const file = new File([uint8_1, uint8_2, uint8_3], 'hello_world.txt') - - assert.strictEqual(await file.text(), 'hello world') - }) - - await t.test('With TypedArray range', async () => { - const uint8_1 = new Uint8Array(Buffer.from('hello world')) - const uint8_2 = new Uint8Array(uint8_1.buffer, 1, 4) - - const file = new File([uint8_2], 'hello_world.txt') - - assert.strictEqual(await file.text(), 'ello') - }) - /* eslint-enable camelcase */ - - await t.test('With ArrayBuffer', async () => { - const uint8 = new Uint8Array([65, 66, 67]) - const ab = uint8.buffer - - const file = new File([ab], 'file.txt') - - assert.strictEqual(await file.text(), 'ABC') - }) - - await t.test('With string', async () => { - const string = 'hello world' - const file = new File([string], 'hello_world.txt') - - assert.strictEqual(await file.text(), 'hello world') - }) - - await t.test('With Buffer', async () => { - const buffer = Buffer.from('hello world') - - const file = new File([buffer], 'hello_world.txt') - - assert.strictEqual(await file.text(), 'hello world') - }) - - await t.test('Mixed', async () => { - const blob = new Blob(['Hello, ']) - const uint8 = new Uint8Array(Buffer.from('world! This')) - const string = ' is a test! Hope it passes!' - - const file = new File([blob, uint8, string], 'mixed-messages.txt') - - assert.strictEqual( - await file.text(), - 'Hello, world! This is a test! Hope it passes!' - ) - }) -}) - -test('endings=native', async () => { - const file = new File(['Hello\nWorld'], 'text.txt', { endings: 'native' }) - const text = await file.text() - - if (process.platform === 'win32') { - assert.strictEqual(text, 'Hello\r\nWorld', 'on windows, LF is replace with CRLF') - } else { - assert.strictEqual(text, 'Hello\nWorld', `on ${process.platform} LF stays LF`) - } -}) - -test('not allow SharedArrayBuffer', () => { - const buffer = new SharedArrayBuffer(0) - assert.throws(() => { - // eslint-disable-next-line no-new - new File([buffer], 'text.txt') - }, TypeError) - - assert.throws(() => { - // eslint-disable-next-line no-new - new File([new Uint8Array(buffer)], 'text.txt') - }, TypeError) -}) diff --git a/test/fetch/resource-timing.js b/test/fetch/resource-timing.js index 7617ff06328..b32fd08fb8b 100644 --- a/test/fetch/resource-timing.js +++ b/test/fetch/resource-timing.js @@ -3,7 +3,6 @@ const { test } = require('node:test') const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') -const { nodeMajor, nodeMinor } = require('../../lib/core/util') const { fetch } = require('../..') const { closeServerAsPromise } = require('../utils/node-http') @@ -12,9 +11,7 @@ const { performance } = require('node:perf_hooks') -const skip = nodeMajor === 18 && nodeMinor < 2 - -test('should create a PerformanceResourceTiming after each fetch request', { skip }, (t, done) => { +test('should create a PerformanceResourceTiming after each fetch request', (t, done) => { const { strictEqual, ok, deepStrictEqual } = tspl(t, { plan: 8 }) const obs = new PerformanceObserver(list => { @@ -50,7 +47,7 @@ test('should create a PerformanceResourceTiming after each fetch request', { ski t.after(closeServerAsPromise(server)) }) -test('should include encodedBodySize in performance entry', { skip }, (t, done) => { +test('should include encodedBodySize in performance entry', (t, done) => { const { strictEqual } = tspl(t, { plan: 4 }) const obs = new PerformanceObserver(list => { const [entry] = list.getEntries() @@ -75,7 +72,7 @@ test('should include encodedBodySize in performance entry', { skip }, (t, done) t.after(closeServerAsPromise(server)) }) -test('timing entries should be in order', { skip }, (t, done) => { +test('timing entries should be in order', (t, done) => { const { ok, strictEqual } = tspl(t, { plan: 13 }) const obs = new PerformanceObserver(list => { const [entry] = list.getEntries() @@ -111,7 +108,7 @@ test('timing entries should be in order', { skip }, (t, done) => { t.after(closeServerAsPromise(server)) }) -test('redirect timing entries should be included when redirecting', { skip }, (t, done) => { +test('redirect timing entries should be included when redirecting', (t, done) => { const { ok, strictEqual } = tspl(t, { plan: 4 }) const obs = new PerformanceObserver(list => { const [entry] = list.getEntries() diff --git a/test/node-test/autoselectfamily.js b/test/node-test/autoselectfamily.js index 196f219fc79..1c95296731f 100644 --- a/test/node-test/autoselectfamily.js +++ b/test/node-test/autoselectfamily.js @@ -6,7 +6,6 @@ const { Resolver } = require('node:dns') const dnsPacket = require('dns-packet') const { createServer } = require('node:http') const { Client, Agent, request } = require('../..') -const { nodeHasAutoSelectFamily } = require('../../lib/core/util') const { tspl } = require('@matteo.collina/tspl') /* @@ -16,7 +15,7 @@ const { tspl } = require('@matteo.collina/tspl') * explicitly passed in tests in this file to avoid compatibility problems across release lines. * */ -const skip = !nodeHasAutoSelectFamily +const skip = false function _lookup (resolver, hostname, options, cb) { resolver.resolve(hostname, 'ANY', (err, replies) => {