undici 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -185,13 +185,12 @@ Help us improve the test coverage by following instructions at [nodejs/undici/#9
185
185
  Basic usage example:
186
186
 
187
187
  ```js
188
- import {fetch} from 'undici';
188
+ import { fetch } from 'undici';
189
189
 
190
- async function fetchJson() {
191
- const res = await fetch('https://example.com')
192
- const json = await res.json()
193
- console.log(json);
194
- }
190
+
191
+ const res = await fetch('https://example.com')
192
+ const json = await res.json()
193
+ console.log(json);
195
194
  ```
196
195
 
197
196
  You can pass an optional dispatcher to `fetch` as:
@@ -235,9 +234,7 @@ const data = {
235
234
  },
236
235
  };
237
236
 
238
- (async () => {
239
- await fetch("https://example.com", { body: data, method: 'POST' });
240
- })();
237
+ await fetch("https://example.com", { body: data, method: 'POST' });
241
238
  ```
242
239
 
243
240
  #### `response.body`
@@ -245,14 +242,12 @@ const data = {
245
242
  Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
246
243
 
247
244
  ```js
248
- import {fetch} from 'undici';
249
- import {Readable} from 'node:stream';
245
+ import { fetch } from 'undici';
246
+ import { Readable } from 'node:stream';
250
247
 
251
- async function fetchStream() {
252
- const response = await fetch('https://example.com')
253
- const readableWebStream = response.body;
254
- const readableNodeStream = Readable.fromWeb(readableWebStream);
255
- }
248
+ const response = await fetch('https://example.com')
249
+ const readableWebStream = response.body;
250
+ const readableNodeStream = Readable.fromWeb(readableWebStream);
256
251
  ```
257
252
 
258
253
  #### Specification Compliance
@@ -461,14 +461,14 @@ Arguments:
461
461
  * **options** `RequestOptions`
462
462
  * **callback** `(error: Error | null, data: ResponseData) => void` (optional)
463
463
 
464
- Returns: `void | Promise<ResponseData>` - Only returns a `Promise` if no `callback` argument was passed
464
+ Returns: `void | Promise<ResponseData>` - Only returns a `Promise` if no `callback` argument was passed.
465
465
 
466
466
  #### Parameter: `RequestOptions`
467
467
 
468
468
  Extends: [`DispatchOptions`](#parameter-dispatchoptions)
469
469
 
470
- * **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`
471
- * **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`
470
+ * **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`.
471
+ * **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`.
472
472
  * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
473
473
 
474
474
  The `RequestOptions.method` property should not be value `'CONNECT'`.
@@ -476,7 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
476
476
  #### Parameter: `ResponseData`
477
477
 
478
478
  * **statusCode** `number`
479
- * **headers** `http.IncomingHttpHeaders`
479
+ * **headers** `http.IncomingHttpHeaders` - Note that all header keys are lower-cased, e. g. `content-type`.
480
480
  * **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
481
481
  * **trailers** `Record<string, string>` - This object starts out
482
482
  as empty and will be mutated to contain trailers after `body` has emitted `'end'`.
@@ -497,6 +497,8 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
497
497
 
498
498
  - `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 262144.
499
499
 
500
+ Note that body will still be a `Readable` even if it is empty, but attempting to deserialize it with `json()` will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and `content-type` header starts with `application/json`.
501
+
500
502
  #### Example 1 - Basic GET Request
501
503
 
502
504
  ```js
@@ -84,7 +84,8 @@ class RequestHandler extends AsyncResource {
84
84
  }
85
85
 
86
86
  const parsedHeaders = util.parseHeaders(rawHeaders)
87
- const body = new Readable(resume, abort, parsedHeaders['content-type'])
87
+ const contentType = parsedHeaders['content-type']
88
+ const body = new Readable(resume, abort, contentType)
88
89
 
89
90
  this.callback = null
90
91
  this.res = body
@@ -92,8 +93,8 @@ class RequestHandler extends AsyncResource {
92
93
 
93
94
  if (callback !== null) {
94
95
  if (this.throwOnError && statusCode >= 400) {
95
- this.runInAsyncScope(callback, null,
96
- new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)
96
+ this.runInAsyncScope(getResolveErrorBodyCallback, null,
97
+ { callback, body, contentType, statusCode, statusMessage, headers }
97
98
  )
98
99
  return
99
100
  }
@@ -152,6 +153,33 @@ class RequestHandler extends AsyncResource {
152
153
  }
153
154
  }
154
155
 
156
+ async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) {
157
+ if (statusCode === 204 || !contentType) {
158
+ body.dump()
159
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
160
+ return
161
+ }
162
+
163
+ try {
164
+ if (contentType.startsWith('application/json')) {
165
+ const payload = await body.json()
166
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
167
+ return
168
+ }
169
+
170
+ if (contentType.startsWith('text/')) {
171
+ const payload = await body.text()
172
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
173
+ return
174
+ }
175
+ } catch (err) {
176
+ // Process in a fallback if error
177
+ }
178
+
179
+ body.dump()
180
+ process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
181
+ }
182
+
155
183
  function request (opts, callback) {
156
184
  if (callback === undefined) {
157
185
  return new Promise((resolve, reject) => {
package/lib/client.js CHANGED
@@ -720,7 +720,7 @@ class Parser {
720
720
  }
721
721
  }
722
722
 
723
- if (request.method === 'CONNECT' && statusCode >= 200 && statusCode < 300) {
723
+ if (request.method === 'CONNECT') {
724
724
  assert(client[kRunning] === 1)
725
725
  this.upgrade = true
726
726
  return 2
@@ -889,10 +889,8 @@ function onParserTimeout (parser) {
889
889
 
890
890
  /* istanbul ignore else */
891
891
  if (timeoutType === TIMEOUT_HEADERS) {
892
- if (!socket[kWriting]) {
893
- assert(!parser.paused, 'cannot be paused while waiting for headers')
894
- util.destroy(socket, new HeadersTimeoutError())
895
- }
892
+ assert(!parser.paused, 'cannot be paused while waiting for headers')
893
+ util.destroy(socket, new HeadersTimeoutError())
896
894
  } else if (timeoutType === TIMEOUT_BODY) {
897
895
  if (!parser.paused) {
898
896
  util.destroy(socket, new BodyTimeoutError())
@@ -21,7 +21,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
21
21
  timeout = timeout == null ? 10e3 : timeout
22
22
  maxCachedSessions = maxCachedSessions == null ? 100 : maxCachedSessions
23
23
 
24
- return function connect ({ hostname, host, protocol, port, servername }, callback) {
24
+ return function connect ({ hostname, host, protocol, port, servername, httpSocket }, callback) {
25
25
  let socket
26
26
  if (protocol === 'https:') {
27
27
  if (!tls) {
@@ -39,6 +39,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
39
39
  ...options,
40
40
  servername,
41
41
  session,
42
+ socket: httpSocket, // upgrade socket connection
42
43
  port: port || 443,
43
44
  host: hostname
44
45
  })
@@ -65,6 +66,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
65
66
  }
66
67
  })
67
68
  } else {
69
+ assert(!httpSocket, 'httpSocket can only be sent on TLS update')
68
70
  socket = net.connect({
69
71
  highWaterMark: 64 * 1024, // Same as nodejs fs streams.
70
72
  ...options,
@@ -1,13 +1,5 @@
1
1
  'use strict'
2
2
 
3
- class AbortError extends Error {
4
- constructor () {
5
- super('The operation was aborted')
6
- this.code = 'ABORT_ERR'
7
- this.name = 'AbortError'
8
- }
9
- }
10
-
11
3
  class UndiciError extends Error {
12
4
  constructor (message) {
13
5
  super(message)
@@ -57,12 +49,13 @@ class BodyTimeoutError extends UndiciError {
57
49
  }
58
50
 
59
51
  class ResponseStatusCodeError extends UndiciError {
60
- constructor (message, statusCode, headers) {
52
+ constructor (message, statusCode, headers, body) {
61
53
  super(message)
62
54
  Error.captureStackTrace(this, ResponseStatusCodeError)
63
55
  this.name = 'ResponseStatusCodeError'
64
56
  this.message = message || 'Response Status Code Error'
65
57
  this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
58
+ this.body = body
66
59
  this.status = statusCode
67
60
  this.statusCode = statusCode
68
61
  this.headers = headers
@@ -191,7 +184,6 @@ class HTTPParserError extends Error {
191
184
  }
192
185
 
193
186
  module.exports = {
194
- AbortError,
195
187
  HTTPParserError,
196
188
  UndiciError,
197
189
  HeadersTimeoutError,
@@ -48,7 +48,11 @@ class Request {
48
48
  }, handler) {
49
49
  if (typeof path !== 'string') {
50
50
  throw new InvalidArgumentError('path must be a string')
51
- } else if (path[0] !== '/' && !(path.startsWith('http://') || path.startsWith('https://'))) {
51
+ } else if (
52
+ path[0] !== '/' &&
53
+ !(path.startsWith('http://') || path.startsWith('https://')) &&
54
+ method !== 'CONNECT'
55
+ ) {
52
56
  throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
53
57
  }
54
58
 
@@ -80,13 +84,12 @@ class Request {
80
84
  this.body = null
81
85
  } else if (util.isStream(body)) {
82
86
  this.body = body
83
- } else if (body instanceof DataView) {
84
- // TODO: Why is DataView special?
85
- this.body = body.buffer.byteLength ? Buffer.from(body.buffer) : null
86
- } else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
87
- this.body = body.byteLength ? Buffer.from(body) : null
88
87
  } else if (util.isBuffer(body)) {
89
88
  this.body = body.byteLength ? body : null
89
+ } else if (ArrayBuffer.isView(body)) {
90
+ this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
91
+ } else if (body instanceof ArrayBuffer) {
92
+ this.body = body.byteLength ? Buffer.from(body) : null
90
93
  } else if (typeof body === 'string') {
91
94
  this.body = body.length ? Buffer.from(body) : null
92
95
  } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
package/lib/fetch/body.js CHANGED
@@ -4,12 +4,13 @@ const util = require('../core/util')
4
4
  const { ReadableStreamFrom, toUSVString, isBlobLike } = require('./util')
5
5
  const { FormData } = require('./formdata')
6
6
  const { kState } = require('./symbols')
7
+ const { webidl } = require('./webidl')
7
8
  const { Blob } = require('buffer')
8
9
  const { kBodyUsed } = require('../core/symbols')
9
10
  const assert = require('assert')
10
11
  const { NotSupportedError } = require('../core/errors')
11
12
  const { isErrored } = require('../core/util')
12
- const { isUint8Array } = require('util/types')
13
+ const { isUint8Array, isArrayBuffer } = require('util/types')
13
14
 
14
15
  let ReadableStream
15
16
 
@@ -61,7 +62,7 @@ function extractBody (object, keepalive = false) {
61
62
 
62
63
  // Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`.
63
64
  contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
64
- } else if (object instanceof ArrayBuffer || ArrayBuffer.isView(object)) {
65
+ } else if (isArrayBuffer(object) || ArrayBuffer.isView(object)) {
65
66
  // BufferSource
66
67
 
67
68
  if (object instanceof DataView) {
@@ -262,100 +263,135 @@ function cloneBody (body) {
262
263
  }
263
264
  }
264
265
 
265
- const methods = {
266
- async blob () {
267
- const chunks = []
266
+ function bodyMixinMethods (instance) {
267
+ const methods = {
268
+ async blob () {
269
+ if (!(this instanceof instance)) {
270
+ throw new TypeError('Illegal invocation')
271
+ }
268
272
 
269
- if (this[kState].body) {
270
- if (isUint8Array(this[kState].body)) {
271
- chunks.push(this[kState].body)
272
- } else {
273
- const stream = this[kState].body.stream
273
+ const chunks = []
274
274
 
275
- if (util.isDisturbed(stream)) {
276
- throw new TypeError('disturbed')
277
- }
275
+ if (this[kState].body) {
276
+ if (isUint8Array(this[kState].body)) {
277
+ chunks.push(this[kState].body)
278
+ } else {
279
+ const stream = this[kState].body.stream
278
280
 
279
- if (stream.locked) {
280
- throw new TypeError('locked')
281
- }
281
+ if (util.isDisturbed(stream)) {
282
+ throw new TypeError('disturbed')
283
+ }
284
+
285
+ if (stream.locked) {
286
+ throw new TypeError('locked')
287
+ }
282
288
 
283
- // Compat.
284
- stream[kBodyUsed] = true
289
+ // Compat.
290
+ stream[kBodyUsed] = true
285
291
 
286
- for await (const chunk of stream) {
287
- chunks.push(chunk)
292
+ for await (const chunk of stream) {
293
+ chunks.push(chunk)
294
+ }
288
295
  }
289
296
  }
290
- }
291
297
 
292
- return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
293
- },
298
+ return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
299
+ },
294
300
 
295
- async arrayBuffer () {
296
- const blob = await this.blob()
297
- return await blob.arrayBuffer()
298
- },
301
+ async arrayBuffer () {
302
+ if (!(this instanceof instance)) {
303
+ throw new TypeError('Illegal invocation')
304
+ }
299
305
 
300
- async text () {
301
- const blob = await this.blob()
302
- return toUSVString(await blob.text())
303
- },
306
+ const blob = await this.blob()
307
+ return await blob.arrayBuffer()
308
+ },
304
309
 
305
- async json () {
306
- return JSON.parse(await this.text())
307
- },
310
+ async text () {
311
+ if (!(this instanceof instance)) {
312
+ throw new TypeError('Illegal invocation')
313
+ }
314
+
315
+ const blob = await this.blob()
316
+ return toUSVString(await blob.text())
317
+ },
308
318
 
309
- async formData () {
310
- const contentType = this.headers.get('Content-Type')
311
-
312
- // If mimeType’s essence is "multipart/form-data", then:
313
- if (/multipart\/form-data/.test(contentType)) {
314
- throw new NotSupportedError('multipart/form-data not supported')
315
- } else if (/application\/x-www-form-urlencoded/.test(contentType)) {
316
- // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
317
-
318
- // 1. Let entries be the result of parsing bytes.
319
- let entries
320
- try {
321
- entries = new URLSearchParams(await this.text())
322
- } catch (err) {
323
- // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
324
- // 2. If entries is failure, then throw a TypeError.
325
- throw Object.assign(new TypeError(), { cause: err })
319
+ async json () {
320
+ if (!(this instanceof instance)) {
321
+ throw new TypeError('Illegal invocation')
326
322
  }
327
323
 
328
- // 3. Return a new FormData object whose entries are entries.
329
- const formData = new FormData()
330
- for (const [name, value] of entries) {
331
- formData.append(name, value)
324
+ return JSON.parse(await this.text())
325
+ },
326
+
327
+ async formData () {
328
+ if (!(this instanceof instance)) {
329
+ throw new TypeError('Illegal invocation')
330
+ }
331
+
332
+ const contentType = this.headers.get('Content-Type')
333
+
334
+ // If mimeType’s essence is "multipart/form-data", then:
335
+ if (/multipart\/form-data/.test(contentType)) {
336
+ throw new NotSupportedError('multipart/form-data not supported')
337
+ } else if (/application\/x-www-form-urlencoded/.test(contentType)) {
338
+ // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
339
+
340
+ // 1. Let entries be the result of parsing bytes.
341
+ let entries
342
+ try {
343
+ entries = new URLSearchParams(await this.text())
344
+ } catch (err) {
345
+ // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
346
+ // 2. If entries is failure, then throw a TypeError.
347
+ throw Object.assign(new TypeError(), { cause: err })
348
+ }
349
+
350
+ // 3. Return a new FormData object whose entries are entries.
351
+ const formData = new FormData()
352
+ for (const [name, value] of entries) {
353
+ formData.append(name, value)
354
+ }
355
+ return formData
356
+ } else {
357
+ // Otherwise, throw a TypeError.
358
+ webidl.errors.exception({
359
+ header: `${instance.name}.formData`,
360
+ value: 'Could not parse content as FormData.'
361
+ })
332
362
  }
333
- return formData
334
- } else {
335
- // Otherwise, throw a TypeError.
336
- throw new TypeError()
337
363
  }
338
364
  }
365
+
366
+ return methods
339
367
  }
340
368
 
341
369
  const properties = {
342
370
  body: {
343
371
  enumerable: true,
344
372
  get () {
373
+ if (!this || !this[kState]) {
374
+ throw new TypeError('Illegal invocation')
375
+ }
376
+
345
377
  return this[kState].body ? this[kState].body.stream : null
346
378
  }
347
379
  },
348
380
  bodyUsed: {
349
381
  enumerable: true,
350
382
  get () {
383
+ if (!this || !this[kState]) {
384
+ throw new TypeError('Illegal invocation')
385
+ }
386
+
351
387
  return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
352
388
  }
353
389
  }
354
390
  }
355
391
 
356
392
  function mixinBody (prototype) {
357
- Object.assign(prototype, methods)
358
- Object.defineProperties(prototype, properties)
393
+ Object.assign(prototype.prototype, bodyMixinMethods(prototype))
394
+ Object.defineProperties(prototype.prototype, properties)
359
395
  }
360
396
 
361
397
  module.exports = {
@@ -60,7 +60,19 @@ const subresource = [
60
60
  ''
61
61
  ]
62
62
 
63
+ /** @type {globalThis['DOMException']} */
64
+ const DOMException = globalThis.DOMException ?? (() => {
65
+ // DOMException was only made a global in Node v17.0.0,
66
+ // but fetch supports >= v16.5.
67
+ try {
68
+ atob('~')
69
+ } catch (err) {
70
+ return Object.getPrototypeOf(err).constructor
71
+ }
72
+ })()
73
+
63
74
  module.exports = {
75
+ DOMException,
64
76
  subresource,
65
77
  forbiddenMethods,
66
78
  requestBodyHeader,
package/lib/fetch/file.js CHANGED
@@ -1,19 +1,27 @@
1
1
  'use strict'
2
2
 
3
3
  const { Blob } = require('buffer')
4
+ const { types } = require('util')
4
5
  const { kState } = require('./symbols')
6
+ const { isBlobLike } = require('./util')
7
+ const { webidl } = require('./webidl')
5
8
 
6
9
  class File extends Blob {
7
10
  constructor (fileBits, fileName, options = {}) {
8
- // TODO: argument idl type check
9
-
10
11
  // The File constructor is invoked with two or three parameters, depending
11
12
  // on whether the optional dictionary parameter is used. When the File()
12
13
  // constructor is invoked, user agents must run the following steps:
14
+ if (arguments.length < 2) {
15
+ throw new TypeError('2 arguments required')
16
+ }
17
+
18
+ fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
19
+ fileName = webidl.converters.USVString(fileName)
20
+ options = webidl.converters.FilePropertyBag(options)
13
21
 
14
22
  // 1. Let bytes be the result of processing blob parts given fileBits and
15
23
  // options.
16
- // TODO
24
+ // Note: Blob handles this for us
17
25
 
18
26
  // 2. Let n be the fileName argument to the constructor.
19
27
  const n = fileName
@@ -25,17 +33,14 @@ class File extends Blob {
25
33
  // be set to the type dictionary member. If t contains any characters
26
34
  // outside the range U+0020 to U+007E, then set t to the empty string
27
35
  // and return from these substeps.
28
- // TODO
29
- const t = options.type
30
-
31
36
  // 2. Convert every character in t to ASCII lowercase.
32
- // TODO
37
+ // Note: Blob handles both of these steps for us
33
38
 
34
39
  // 3. If the lastModified member is provided, let d be set to the
35
40
  // lastModified dictionary member. If it is not provided, set d to the
36
41
  // current date and time represented as the number of milliseconds since
37
42
  // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
38
- const d = options.lastModified ?? Date.now()
43
+ const d = options.lastModified
39
44
 
40
45
  // 4. Return a new File object F such that:
41
46
  // F refers to the bytes byte sequence.
@@ -43,9 +48,8 @@ class File extends Blob {
43
48
  // F.name is set to n.
44
49
  // F.type is set to t.
45
50
  // F.lastModified is set to d.
46
- // TODO
47
51
 
48
- super(fileBits, { type: t })
52
+ super(processBlobParts(fileBits, options), { type: options.type })
49
53
  this[kState] = {
50
54
  name: n,
51
55
  lastModified: d
@@ -190,4 +194,120 @@ class FileLike {
190
194
  }
191
195
  }
192
196
 
193
- module.exports = { File: globalThis.File ?? File, FileLike }
197
+ webidl.converters.Blob = webidl.interfaceConverter(Blob)
198
+
199
+ webidl.converters.BlobPart = function (V, opts) {
200
+ if (webidl.util.Type(V) === 'Object') {
201
+ if (isBlobLike(V)) {
202
+ return webidl.converters.Blob(V, { strict: false })
203
+ }
204
+
205
+ return webidl.converters.BufferSource(V, opts)
206
+ } else {
207
+ return webidl.converters.USVString(V, opts)
208
+ }
209
+ }
210
+
211
+ webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
212
+ webidl.converters.BlobPart
213
+ )
214
+
215
+ // https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag
216
+ webidl.converters.FilePropertyBag = webidl.dictionaryConverter([
217
+ {
218
+ key: 'lastModified',
219
+ converter: webidl.converters['long long'],
220
+ get defaultValue () {
221
+ return Date.now()
222
+ }
223
+ },
224
+ {
225
+ key: 'type',
226
+ converter: webidl.converters.DOMString,
227
+ defaultValue: ''
228
+ },
229
+ {
230
+ key: 'endings',
231
+ converter: (value) => {
232
+ value = webidl.converters.DOMString(value)
233
+ value = value.toLowerCase()
234
+
235
+ if (value !== 'native') {
236
+ value = 'transparent'
237
+ }
238
+
239
+ return value
240
+ },
241
+ defaultValue: 'transparent'
242
+ }
243
+ ])
244
+
245
+ /**
246
+ * @see https://www.w3.org/TR/FileAPI/#process-blob-parts
247
+ * @param {(NodeJS.TypedArray|Blob|string)[]} parts
248
+ * @param {{ type: string, endings: string }} options
249
+ */
250
+ function processBlobParts (parts, options) {
251
+ // 1. Let bytes be an empty sequence of bytes.
252
+ /** @type {NodeJS.TypedArray[]} */
253
+ const bytes = []
254
+
255
+ // 2. For each element in parts:
256
+ for (const element of parts) {
257
+ // 1. If element is a USVString, run the following substeps:
258
+ if (typeof element === 'string') {
259
+ // 1. Let s be element.
260
+ let s = element
261
+
262
+ // 2. If the endings member of options is "native", set s
263
+ // to the result of converting line endings to native
264
+ // of element.
265
+ if (options.endings === 'native') {
266
+ s = convertLineEndingsNative(s)
267
+ }
268
+
269
+ // 3. Append the result of UTF-8 encoding s to bytes.
270
+ bytes.push(new TextEncoder().encode(s))
271
+ } else if (
272
+ types.isAnyArrayBuffer(element) ||
273
+ types.isTypedArray(element)
274
+ ) {
275
+ // 2. If element is a BufferSource, get a copy of the
276
+ // bytes held by the buffer source, and append those
277
+ // bytes to bytes.
278
+ if (!element.buffer) { // ArrayBuffer
279
+ bytes.push(new Uint8Array(element))
280
+ } else {
281
+ bytes.push(element.buffer)
282
+ }
283
+ } else if (isBlobLike(element)) {
284
+ // 3. If element is a Blob, append the bytes it represents
285
+ // to bytes.
286
+ bytes.push(element)
287
+ }
288
+ }
289
+
290
+ // 3. Return bytes.
291
+ return bytes
292
+ }
293
+
294
+ /**
295
+ * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native
296
+ * @param {string} s
297
+ */
298
+ function convertLineEndingsNative (s) {
299
+ // 1. Let native line ending be be the code point U+000A LF.
300
+ let nativeLineEnding = '\n'
301
+
302
+ // 2. If the underlying platform’s conventions are to
303
+ // represent newlines as a carriage return and line feed
304
+ // sequence, set native line ending to the code point
305
+ // U+000D CR followed by the code point U+000A LF.
306
+ if (process.platform === 'win32') {
307
+ nativeLineEnding = '\r\n'
308
+ }
309
+
310
+ return s.replace(/\r?\n/g, nativeLineEnding)
311
+ }
312
+
313
+ module.exports = { File, FileLike }