@gjsify/fetch 0.0.2

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.
Files changed (58) hide show
  1. package/README.md +9 -0
  2. package/lib/cjs/body.js +284 -0
  3. package/lib/cjs/errors/abort-error.js +28 -0
  4. package/lib/cjs/errors/base.js +36 -0
  5. package/lib/cjs/errors/fetch-error.js +40 -0
  6. package/lib/cjs/headers.js +231 -0
  7. package/lib/cjs/index.js +246 -0
  8. package/lib/cjs/request.js +306 -0
  9. package/lib/cjs/response.js +162 -0
  10. package/lib/cjs/types/index.js +17 -0
  11. package/lib/cjs/types/system-error.js +16 -0
  12. package/lib/cjs/utils/blob-from.js +124 -0
  13. package/lib/cjs/utils/get-search.js +30 -0
  14. package/lib/cjs/utils/is-redirect.js +26 -0
  15. package/lib/cjs/utils/is.js +47 -0
  16. package/lib/cjs/utils/multipart-parser.js +372 -0
  17. package/lib/cjs/utils/referrer.js +172 -0
  18. package/lib/esm/body.js +255 -0
  19. package/lib/esm/errors/abort-error.js +9 -0
  20. package/lib/esm/errors/base.js +17 -0
  21. package/lib/esm/errors/fetch-error.js +21 -0
  22. package/lib/esm/headers.js +202 -0
  23. package/lib/esm/index.js +224 -0
  24. package/lib/esm/request.js +281 -0
  25. package/lib/esm/response.js +133 -0
  26. package/lib/esm/types/index.js +1 -0
  27. package/lib/esm/types/system-error.js +1 -0
  28. package/lib/esm/utils/blob-from.js +101 -0
  29. package/lib/esm/utils/get-search.js +11 -0
  30. package/lib/esm/utils/is-redirect.js +7 -0
  31. package/lib/esm/utils/is.js +28 -0
  32. package/lib/esm/utils/multipart-parser.js +353 -0
  33. package/lib/esm/utils/referrer.js +153 -0
  34. package/package.json +53 -0
  35. package/src/body.ts +415 -0
  36. package/src/errors/abort-error.ts +10 -0
  37. package/src/errors/base.ts +20 -0
  38. package/src/errors/fetch-error.ts +26 -0
  39. package/src/headers.ts +279 -0
  40. package/src/index.spec.ts +13 -0
  41. package/src/index.ts +367 -0
  42. package/src/request.ts +396 -0
  43. package/src/response.ts +197 -0
  44. package/src/test.mts +6 -0
  45. package/src/types/index.ts +1 -0
  46. package/src/types/system-error.ts +11 -0
  47. package/src/utils/blob-from.ts +168 -0
  48. package/src/utils/get-search.ts +9 -0
  49. package/src/utils/is-redirect.ts +11 -0
  50. package/src/utils/is.ts +88 -0
  51. package/src/utils/multipart-parser.ts +448 -0
  52. package/src/utils/referrer.ts +350 -0
  53. package/test.gjs.js +34758 -0
  54. package/test.gjs.mjs +53177 -0
  55. package/test.node.js +1226 -0
  56. package/test.node.mjs +6294 -0
  57. package/tsconfig.json +19 -0
  58. package/tsconfig.types.json +8 -0
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@gjsify/fetch",
3
+ "version": "0.0.2",
4
+ "description": "Web and Node.js fetch module for Gjs",
5
+ "main": "lib/cjs/index.js",
6
+ "module": "lib/esm/index.js",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./lib/types/index.d.ts",
12
+ "default": "./lib/esm/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./lib/types/index.d.ts",
16
+ "default": "./lib/cjs/index.js"
17
+ }
18
+ }
19
+ },
20
+ "scripts": {
21
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo || exit 0",
22
+ "print:name": "echo '@gjsify/fetch'",
23
+ "build": "yarn print:name && yarn build:gjsify",
24
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
25
+ "build:types": "tsc --project tsconfig.types.json",
26
+ "build:test": "yarn build:test:gjs && yarn build:test:node",
27
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
28
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
29
+ "test": "yarn print:name && yarn build:gjsify && yarn build:test && yarn test:node && yarn test:gjs",
30
+ "test:gjs": "gjs -m test.gjs.mjs",
31
+ "test:node": "node test.node.mjs"
32
+ },
33
+ "keywords": [
34
+ "gjs",
35
+ "node",
36
+ "fetch"
37
+ ],
38
+ "devDependencies": {
39
+ "@gjsify/cli": "^0.0.2",
40
+ "@gjsify/http": "^0.0.2",
41
+ "@gjsify/unit": "^0.0.2",
42
+ "@types/node": "^20.3.1"
43
+ },
44
+ "dependencies": {
45
+ "@gjsify/deno-runtime": "^0.0.2",
46
+ "@gjsify/gio-2.0": "^0.0.2",
47
+ "@gjsify/soup-3.0": "^0.0.2",
48
+ "@types/node-fetch": "^2.6.4",
49
+ "data-uri-to-buffer": "^5.0.1",
50
+ "formdata-polyfill": "^4.0.10",
51
+ "node-fetch": "^3.3.1"
52
+ }
53
+ }
package/src/body.ts ADDED
@@ -0,0 +1,415 @@
1
+
2
+ /**
3
+ * Body.js
4
+ *
5
+ * Body interface provides common methods for Request and Response
6
+ */
7
+
8
+ import { URLSearchParams } from '@gjsify/deno-runtime/ext/url/00_url';
9
+ import { Blob } from "@gjsify/deno-runtime/ext/web/09_file";
10
+
11
+ import { PassThrough, pipeline as pipelineCb, Readable, Stream, Writable } from 'stream';
12
+ import { ReadableStream as StreamWebReadableStream } from "stream/web";
13
+ import { types, deprecate, promisify } from 'util';
14
+ import { Buffer } from 'buffer';
15
+
16
+ import { FormData, formDataToBlob } from 'formdata-polyfill/esm.min.js';
17
+
18
+ import { FetchError } from './errors/fetch-error.js';
19
+ import { FetchBaseError } from './errors/base.js';
20
+ import { isBlob, isURLSearchParameters } from './utils/is.js';
21
+
22
+ import type { Request } from './request.js';
23
+ import type { Response } from './response.js';
24
+
25
+ const pipeline = promisify(pipelineCb);
26
+ const INTERNALS = Symbol('Body internals');
27
+
28
+ /**
29
+ * Body mixin
30
+ *
31
+ * Ref: https://fetch.spec.whatwg.org/#body
32
+ *
33
+ * @param body Readable stream
34
+ * @param opts Response options
35
+ */
36
+ export default class Body implements globalThis.Body {
37
+
38
+ [INTERNALS]: {
39
+ body: null | Buffer | Readable | Blob;
40
+ stream: Readable | null;
41
+ boundary: string;
42
+ disturbed: boolean,
43
+ error: null | FetchBaseError;
44
+ } = {
45
+ body: null,
46
+ stream: null,
47
+ boundary: '',
48
+ disturbed: false,
49
+ error: null,
50
+ }
51
+
52
+ size = 0;
53
+
54
+ constructor(body: BodyInit | Readable | Blob | Buffer, options: ResponseInit & { size?: number } = { size: 0 }) {
55
+ this.size = options.size || 0;
56
+ if (body === null) {
57
+ // Body is undefined or null
58
+ this[INTERNALS].body = null;
59
+ } else if (isURLSearchParameters(body)) {
60
+ // Body is a URLSearchParams
61
+ this[INTERNALS].body = Buffer.from(body.toString())
62
+ } else if (isBlob(body)) {
63
+ // Body is blob
64
+ } else if (Buffer.isBuffer(body)) {
65
+ // Body is Buffer
66
+ } else if (types.isAnyArrayBuffer(body)) {
67
+ // Body is ArrayBuffer
68
+ this[INTERNALS].body = Buffer.from(body);
69
+ } else if (ArrayBuffer.isView(body)) {
70
+ // Body is ArrayBufferView
71
+ this[INTERNALS].body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
72
+ } else if (body instanceof Readable) {
73
+ // Body is stream
74
+ this[INTERNALS].body = body;
75
+ } else if (body instanceof ReadableStream || body instanceof StreamWebReadableStream) {
76
+ // Body is web stream
77
+ this[INTERNALS].body = Readable.fromWeb(body as StreamWebReadableStream); // TODO check compatibility between ReadableStream (from lib.dom.d.ts) and StreamWebReadableStream (from stream/web)
78
+ } else if (body instanceof FormData) {
79
+ // Body is FormData
80
+ this[INTERNALS].body = formDataToBlob(body) as Blob & globalThis.Blob;
81
+ this[INTERNALS].boundary = this[INTERNALS].body.type.split('=')[1];
82
+ } else if (typeof body === 'string'){
83
+ // None of the above
84
+ // coerce to string then buffer
85
+ this[INTERNALS].body = Buffer.from(body);
86
+ } else if (body instanceof URLSearchParams){
87
+ // None of the above
88
+ // coerce to string then buffer
89
+ this[INTERNALS].body = Buffer.from(body.toString());
90
+ } else {
91
+ console.warn(`Unknown body type "${typeof body}", try to parse the body to string!`);
92
+ this[INTERNALS].body = Readable.from(typeof (body as any).toString === 'function' ? (body as any).toString() : body as any); // TODO
93
+ }
94
+
95
+ // ´this[INTERNALS].stream = body;
96
+
97
+ if (Buffer.isBuffer(body)) {
98
+ this[INTERNALS].stream = Readable.from(body);
99
+ } else if (isBlob(body)) {
100
+ // @ts-ignore
101
+ this[INTERNALS].stream = Readable.from(body.stream());
102
+ } else if (body instanceof Readable) {
103
+ this[INTERNALS].stream = body;
104
+ }
105
+
106
+ if (body instanceof Stream) {
107
+ body.on('error', error_ => {
108
+ const error = error_ instanceof FetchBaseError ?
109
+ error_ :
110
+ new FetchError(`Invalid response body while trying to fetch ${(this as any).url}: ${error_.message}`, 'system', error_ as any);
111
+ this[INTERNALS].error = error;
112
+ });
113
+ }
114
+ }
115
+
116
+ get body(): ReadableStream<Uint8Array> {
117
+ // @ts-ignore
118
+ return Readable.toWeb(this[INTERNALS].stream);
119
+ }
120
+
121
+ get _stream() {
122
+ return this[INTERNALS].stream;
123
+ }
124
+
125
+ get bodyUsed() {
126
+ return this[INTERNALS].disturbed;
127
+ }
128
+
129
+ /**
130
+ * Decode response as ArrayBuffer
131
+ *
132
+ * @return Promise
133
+ */
134
+ async arrayBuffer() {
135
+ const {buffer, byteOffset, byteLength} = await consumeBody(this);
136
+ return buffer.slice(byteOffset, byteOffset + byteLength);
137
+ }
138
+
139
+ async formData() {
140
+ const ct = (this as unknown as Request).headers?.get('content-type');
141
+
142
+ if (ct.startsWith('application/x-www-form-urlencoded')) {
143
+ const formData = new FormData();
144
+ const parameters = new URLSearchParams(await this.text());
145
+
146
+ for (const [name, value] of parameters) {
147
+ formData.append(name, value);
148
+ }
149
+
150
+ return formData;
151
+ }
152
+
153
+ const {toFormData} = await import('./utils/multipart-parser.js');
154
+ return toFormData(this.body, ct);
155
+ }
156
+
157
+ /**
158
+ * Return raw response as Blob
159
+ *
160
+ * @return Promise
161
+ */
162
+ // @ts-ignore
163
+ async blob() {
164
+ const ct = ((this as unknown as Request).headers?.get('content-type')) || (this[INTERNALS].body && (this[INTERNALS].body as Blob).type) || '';
165
+ const buf = await this.arrayBuffer();
166
+
167
+ return new Blob([buf], {
168
+ type: ct
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Decode response as json
174
+ *
175
+ * @return Promise
176
+ */
177
+ async json() {
178
+ const text = await this.text();
179
+ return JSON.parse(text);
180
+ }
181
+
182
+ /**
183
+ * Decode response as text
184
+ *
185
+ * @return Promise
186
+ */
187
+ async text() {
188
+ const buffer = await consumeBody(this);
189
+ return new TextDecoder().decode(buffer);
190
+ }
191
+ }
192
+
193
+ // In browsers, all properties are enumerable.
194
+ Object.defineProperties(Body.prototype, {
195
+ body: {enumerable: true},
196
+ bodyUsed: {enumerable: true},
197
+ arrayBuffer: {enumerable: true},
198
+ blob: {enumerable: true},
199
+ json: {enumerable: true},
200
+ text: {enumerable: true},
201
+ data: {get: deprecate(() => {},
202
+ 'data doesn\'t exist, use json(), text(), arrayBuffer(), or body instead',
203
+ 'https://github.com/node-fetch/node-fetch/issues/1000 (response)')}
204
+ });
205
+
206
+ /**
207
+ * Consume and convert an entire Body to a Buffer.
208
+ *
209
+ * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
210
+ *
211
+ * @return Promise
212
+ */
213
+ async function consumeBody(data: Body & Partial<Request>) {
214
+ if (data[INTERNALS].disturbed) {
215
+ throw new TypeError(`body used already for: ${data.url}`);
216
+ }
217
+
218
+ data[INTERNALS].disturbed = true;
219
+
220
+ if (data[INTERNALS].error) {
221
+ throw data[INTERNALS].error;
222
+ }
223
+
224
+ const { _stream: body } = data;
225
+
226
+ // Body is null
227
+ if (body === null) {
228
+ return Buffer.alloc(0);
229
+ }
230
+
231
+ /* c8 ignore next 3 */
232
+ if (!(body instanceof Stream)) {
233
+ return Buffer.alloc(0);
234
+ }
235
+
236
+ // Body is stream
237
+ // get ready to actually consume the body
238
+ const accum = [];
239
+ let accumBytes = 0;
240
+
241
+ try {
242
+ for await (const chunk of body) {
243
+ if (data.size > 0 && accumBytes + chunk.length > data.size) {
244
+ const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
245
+ body.destroy(error);
246
+ throw error;
247
+ }
248
+
249
+ accumBytes += chunk.length;
250
+ accum.push(chunk);
251
+ }
252
+ } catch (error) {
253
+ const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error);
254
+ throw error_;
255
+ }
256
+
257
+ if (body.readableEnded === true || (body as any)._readableState.ended === true) {
258
+ try {
259
+ if (accum.every(c => typeof c === 'string')) {
260
+ return Buffer.from(accum.join(''));
261
+ }
262
+
263
+ return Buffer.concat(accum, accumBytes);
264
+ } catch (error) {
265
+ throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error);
266
+ }
267
+ } else {
268
+ throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Clone body given Res/Req instance
274
+ *
275
+ * @param Mixed instance Response or Request instance
276
+ * @param highWaterMark highWaterMark for both PassThrough body streams
277
+ * @return Mixed
278
+ */
279
+ export const clone = <T extends Request | Response>(instance: T, highWaterMark?: number) => {
280
+ let p1: PassThrough;
281
+ let p2: PassThrough;
282
+ let {body} = instance[INTERNALS];
283
+
284
+ // Don't allow cloning a used body
285
+ if (instance.bodyUsed) {
286
+ throw new Error('cannot clone body after it is used');
287
+ }
288
+
289
+ // Check that body is a stream and not form-data object
290
+ // note: we can't clone the form-data object without having it as a dependency
291
+ if ((body instanceof Stream) && (typeof (body as any).getBoundary !== 'function')) {
292
+ // Tee instance body
293
+ p1 = new PassThrough({highWaterMark});
294
+ p2 = new PassThrough({highWaterMark});
295
+ body.pipe(p1);
296
+ body.pipe(p2);
297
+ // Set instance body to teed body and return the other teed body
298
+ instance[INTERNALS].stream = p1;
299
+ body = p2;
300
+ }
301
+
302
+ return body;
303
+ };
304
+
305
+ // const getNonSpecFormDataBoundary = deprecate(
306
+ // (body) => body.getBoundary(),
307
+ // 'form-data doesn\'t follow the spec and requires special treatment. Use alternative package',
308
+ // 'https://github.com/node-fetch/node-fetch/issues/1167'
309
+ // );
310
+
311
+ /**
312
+ * Performs the operation "extract a `Content-Type` value from |object|" as
313
+ * specified in the specification:
314
+ * https://fetch.spec.whatwg.org/#concept-bodyinit-extract
315
+ *
316
+ * This function assumes that instance.body is present.
317
+ *
318
+ * @param body Any options.body input
319
+ */
320
+ export const extractContentType = (body: BodyInit | string | ArrayBuffer | Readable | Blob | ArrayBufferView | Buffer | FormData | globalThis.ReadableStream<any> | null, request: Request | Response): string | null => {
321
+ // Body is null or undefined
322
+ if (body === null) {
323
+ return null;
324
+ }
325
+
326
+ // Body is string
327
+ if (typeof body === 'string') {
328
+ return 'text/plain;charset=UTF-8';
329
+ }
330
+
331
+ // Body is a URLSearchParams
332
+ if (isURLSearchParameters(body)) {
333
+ return 'application/x-www-form-urlencoded;charset=UTF-8';
334
+ }
335
+
336
+ // Body is blob
337
+ if (isBlob(body)) {
338
+ return (body as Blob & globalThis.Blob).type || null;
339
+ }
340
+
341
+ // Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView)
342
+ if (Buffer.isBuffer(body) || types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) {
343
+ return null;
344
+ }
345
+
346
+ if (body instanceof FormData) {
347
+ return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
348
+ }
349
+
350
+ // Detect form data input from form-data module
351
+ // if (body && typeof body.getBoundary === 'function') {
352
+ // return `multipart/form-data;boundary=${getNonSpecFormDataBoundary(body)}`;
353
+ // }
354
+
355
+ // Body is stream - can't really do much about this
356
+ if (body instanceof Stream) {
357
+ return null;
358
+ }
359
+
360
+ // Body constructor defaults other things to string
361
+ return 'text/plain;charset=UTF-8';
362
+ };
363
+
364
+ /**
365
+ * The Fetch Standard treats this as if "total bytes" is a property on the body.
366
+ * For us, we have to explicitly get it with a function.
367
+ *
368
+ * ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
369
+ *
370
+ * @param request Request object with the body property.
371
+ */
372
+ export const getTotalBytes = (request: Request): number | null => {
373
+ const { body } = request[INTERNALS];
374
+
375
+ // Body is null or undefined
376
+ if (body === null) {
377
+ return 0;
378
+ }
379
+
380
+ // Body is Blob
381
+ if (isBlob(body)) {
382
+ return (body as Blob).size;
383
+ }
384
+
385
+ // Body is Buffer
386
+ if (Buffer.isBuffer(body)) {
387
+ return body.length;
388
+ }
389
+
390
+ // Detect form data input from form-data module
391
+ if (body && typeof (body as any).getLengthSync === 'function') {
392
+ const anyBody = body as any;
393
+ return anyBody.hasKnownLength && anyBody.hasKnownLength() ? anyBody.getLengthSync() : null;
394
+ }
395
+
396
+ // Body is stream
397
+ return null;
398
+ };
399
+
400
+ /**
401
+ * Write a Body to a Node.js WritableStream (e.g. http.Request) object.
402
+ *
403
+ * @param dest The stream to write to.
404
+ * @param obj.body Body object from the Body instance.
405
+ */
406
+ export const writeToStream = async (dest: Writable, {body}): Promise<void> => {
407
+ if (body === null) {
408
+ // Body is null
409
+ dest.end();
410
+ } else {
411
+ // Body is stream
412
+ await pipeline(body, dest);
413
+ }
414
+ };
415
+
@@ -0,0 +1,10 @@
1
+ import { FetchBaseError } from './base.js';
2
+
3
+ /**
4
+ * AbortError interface for cancelled requests
5
+ */
6
+ export class AbortError extends FetchBaseError {
7
+ constructor(message: string, type = 'aborted') {
8
+ super(message, type);
9
+ }
10
+ }
@@ -0,0 +1,20 @@
1
+ export class FetchBaseError extends Error {
2
+
3
+ type?: 'aborted' | string;
4
+
5
+ constructor(message: string, type?: string) {
6
+ super(message);
7
+ // Hide custom error implementation details from end-users
8
+ Error.captureStackTrace(this, this.constructor);
9
+
10
+ this.type = type;
11
+ }
12
+
13
+ get name() {
14
+ return this.constructor.name;
15
+ }
16
+
17
+ get [Symbol.toStringTag]() {
18
+ return this.constructor.name;
19
+ }
20
+ }
@@ -0,0 +1,26 @@
1
+ import { FetchBaseError } from './base.js';
2
+ import type { SystemError } from '../types/index.js';
3
+
4
+ /**
5
+ * FetchError interface for operational errors
6
+ */
7
+ export class FetchError extends FetchBaseError {
8
+ code: string;
9
+ errno: string;
10
+ erroredSysCall: string;
11
+
12
+ /**
13
+ * @param message Error message for human
14
+ * @param type Error type for machine
15
+ * @param systemError For Node.js system error
16
+ */
17
+ constructor(message: string, type?: string, systemError?: SystemError) {
18
+ super(message, type);
19
+ // When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code
20
+ if (systemError) {
21
+ // eslint-disable-next-line no-multi-assign
22
+ this.code = this.errno = systemError.code;
23
+ this.erroredSysCall = systemError.syscall;
24
+ }
25
+ }
26
+ }