@jsenv/fetch 1.0.0 → 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/fetch",
3
- "version": "1.0.0",
3
+ "version": "1.1.2",
4
4
  "description": "Unified url fetcher (http, http, file, data)",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -23,11 +23,13 @@
23
23
  "type": "module",
24
24
  "exports": {
25
25
  ".": {
26
- "import": "./src/main.js"
26
+ "node": "./src/node/fetch_url_node.js",
27
+ "browser": "./src/browser/fetch_url_browser.js"
27
28
  },
29
+ "./browser": "./src/browser/fetch_url_browser.js",
28
30
  "./*": "./*"
29
31
  },
30
- "main": "./src/main.js",
32
+ "main": "./src/node/fetch_url_node.js",
31
33
  "files": [
32
34
  "/src/"
33
35
  ],
@@ -35,8 +37,8 @@
35
37
  "test": "node --conditions=development ./scripts/test.mjs"
36
38
  },
37
39
  "dependencies": {
38
- "@jsenv/urls": "1.2.5",
39
- "@jsenv/server": "12.7.0",
40
+ "@jsenv/urls": "1.2.6",
41
+ "@jsenv/server": "12.7.2",
40
42
  "node-fetch": "2.6.7"
41
43
  }
42
44
  }
@@ -0,0 +1,27 @@
1
+ import { fetchUrl } from "./fetch_url_browser.js"
2
+
3
+ export const fetchAndEval = async (url) => {
4
+ const response = await fetchUrl(url)
5
+
6
+ if (response.status >= 200 && response.status <= 299) {
7
+ const text = await response.text()
8
+ // eslint-disable-next-line no-eval
9
+ window.eval(appendSourceURL(text, url))
10
+ } else {
11
+ const text = await response.text()
12
+ throw new Error(
13
+ `Unexpected response for script.
14
+ --- script url ---
15
+ ${url}
16
+ --- response body ---
17
+ ${text}
18
+ --- response status ---
19
+ ${response.status}`,
20
+ )
21
+ }
22
+ }
23
+
24
+ const appendSourceURL = (code, sourceURL) => {
25
+ return `${code}
26
+ ${"//#"} sourceURL=${sourceURL}`
27
+ }
@@ -0,0 +1,47 @@
1
+ export const fetchAndEvalUsingScript = async (src) => {
2
+ return new Promise((resolve, reject) => {
3
+ const script = document.createElement("script")
4
+
5
+ const onwindowerror = (errorEvent) => {
6
+ if (errorEvent.filename === src) {
7
+ cleanup()
8
+ reject(errorEvent.error)
9
+ }
10
+ }
11
+
12
+ const onscripterror = () => {
13
+ cleanup()
14
+ reject(new Error(`Error loading ${src}`))
15
+ }
16
+
17
+ const onscriptload = () => {
18
+ cleanup()
19
+ resolve()
20
+ }
21
+
22
+ const cleanup = () => {
23
+ removeOnWindowError()
24
+ removeOnScriptError()
25
+ removeOnScriptLoad()
26
+ document.head.removeChild(script)
27
+ }
28
+
29
+ const removeOnWindowError = () =>
30
+ window.removeEventListener("error", onwindowerror)
31
+
32
+ const removeOnScriptError = () =>
33
+ script.removeEventListener("error", onscripterror)
34
+
35
+ const removeOnScriptLoad = () =>
36
+ script.removeEventListener("load", onscriptload)
37
+
38
+ window.addEventListener("error", onwindowerror)
39
+ script.addEventListener("error", onscripterror)
40
+ script.addEventListener("load", onscriptload)
41
+ script.charset = "utf-8"
42
+ script.crossOrigin = "anonymous"
43
+ script.src = src
44
+
45
+ document.head.appendChild(script)
46
+ })
47
+ }
@@ -0,0 +1,7 @@
1
+ import { fetchUrl } from "./fetch_url_browser.js"
2
+
3
+ export const fetchJson = async (url, options = {}) => {
4
+ const response = await fetchUrl(url, options)
5
+ const object = await response.json()
6
+ return object
7
+ }
@@ -0,0 +1,8 @@
1
+ import { fetchUsingXHR } from "./fetch_using_xhr.js"
2
+ import { fetchUsingFetch } from "./fetch_using_fetch.js"
3
+
4
+ export const fetchUrl =
5
+ typeof window.fetch === "function" &&
6
+ typeof window.AbortController === "function"
7
+ ? fetchUsingFetch
8
+ : fetchUsingXHR
@@ -0,0 +1,28 @@
1
+ export const fetchUsingFetch = async (
2
+ url,
3
+ { mode = "cors", ...options } = {},
4
+ ) => {
5
+ const response = await window.fetch(url, {
6
+ mode,
7
+ ...options,
8
+ })
9
+ return {
10
+ url: response.url,
11
+ status: response.status,
12
+ statusText: "",
13
+ headers: responseToHeaders(response),
14
+ text: () => response.text(),
15
+ json: () => response.json(),
16
+ blob: () => response.blob(),
17
+ arrayBuffer: () => response.arrayBuffer(),
18
+ formData: () => response.formData(),
19
+ }
20
+ }
21
+
22
+ const responseToHeaders = (response) => {
23
+ const headers = {}
24
+ response.headers.forEach((value, name) => {
25
+ headers[name] = value
26
+ })
27
+ return headers
28
+ }
@@ -0,0 +1,403 @@
1
+ export const fetchUsingXHR = async (
2
+ url,
3
+ {
4
+ signal,
5
+ method = "GET",
6
+ credentials = "same-origin",
7
+ headers = {},
8
+ body = null,
9
+ } = {},
10
+ ) => {
11
+ const headersPromise = createPromiseAndHooks()
12
+ const bodyPromise = createPromiseAndHooks()
13
+
14
+ const xhr = new XMLHttpRequest()
15
+
16
+ const failure = (error) => {
17
+ // if it was already resolved, we must reject the body promise
18
+ if (headersPromise.settled) {
19
+ bodyPromise.reject(error)
20
+ } else {
21
+ headersPromise.reject(error)
22
+ }
23
+ }
24
+
25
+ const cleanup = () => {
26
+ xhr.ontimeout = null
27
+ xhr.onerror = null
28
+ xhr.onload = null
29
+ xhr.onreadystatechange = null
30
+ }
31
+
32
+ xhr.ontimeout = () => {
33
+ cleanup()
34
+ failure(new Error(`xhr request timeout on ${url}.`))
35
+ }
36
+
37
+ xhr.onerror = (error) => {
38
+ cleanup()
39
+ // unfortunately with have no clue why it fails
40
+ // might be cors for instance
41
+ failure(createRequestError(error, { url }))
42
+ }
43
+
44
+ xhr.onload = () => {
45
+ cleanup()
46
+ bodyPromise.resolve()
47
+ }
48
+
49
+ signal.addEventListener("abort", () => {
50
+ xhr.abort()
51
+ const abortError = new Error("aborted")
52
+ abortError.name = "AbortError"
53
+ failure(abortError)
54
+ })
55
+
56
+ xhr.onreadystatechange = () => {
57
+ // https://developer.mozilla.org/fr/docs/Web/API/XMLHttpRequest/readyState
58
+ const { readyState } = xhr
59
+
60
+ if (readyState === 2) {
61
+ headersPromise.resolve()
62
+ } else if (readyState === 4) {
63
+ cleanup()
64
+ bodyPromise.resolve()
65
+ }
66
+ }
67
+
68
+ xhr.open(method, url, true)
69
+ Object.keys(headers).forEach((key) => {
70
+ xhr.setRequestHeader(key, headers[key])
71
+ })
72
+ xhr.withCredentials = computeWithCredentials({ credentials, url })
73
+ if ("responseType" in xhr && hasBlob) {
74
+ xhr.responseType = "blob"
75
+ }
76
+ xhr.send(body)
77
+
78
+ await headersPromise
79
+
80
+ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseURL
81
+ const responseUrl =
82
+ "responseURL" in xhr ? xhr.responseURL : headers["x-request-url"]
83
+ let responseStatus = xhr.status
84
+ const responseStatusText = xhr.statusText
85
+ const responseHeaders = getHeadersFromXHR(xhr)
86
+
87
+ const readBody = async () => {
88
+ await bodyPromise
89
+
90
+ const { status } = xhr
91
+ // in Chrome on file:/// URLs, status is 0
92
+ if (status === 0) {
93
+ responseStatus = 200
94
+ }
95
+
96
+ const body = "response" in xhr ? xhr.response : xhr.responseText
97
+
98
+ return {
99
+ responseBody: body,
100
+ responseBodyType: detectBodyType(body),
101
+ }
102
+ }
103
+
104
+ const text = async () => {
105
+ const { responseBody, responseBodyType } = await readBody()
106
+
107
+ if (responseBodyType === "blob") {
108
+ return blobToText(responseBody)
109
+ }
110
+ if (responseBodyType === "formData") {
111
+ throw new Error("could not read FormData body as text")
112
+ }
113
+ if (responseBodyType === "dataView") {
114
+ return arrayBufferToText(responseBody.buffer)
115
+ }
116
+ if (responseBodyType === "arrayBuffer") {
117
+ return arrayBufferToText(responseBody)
118
+ }
119
+ // if (responseBodyType === "text" || responseBodyType === 'searchParams') {
120
+ // return body
121
+ // }
122
+ return String(responseBody)
123
+ }
124
+
125
+ const json = async () => {
126
+ const responseText = await text()
127
+ return JSON.parse(responseText)
128
+ }
129
+
130
+ const blob = async () => {
131
+ if (!hasBlob) {
132
+ throw new Error(`blob not supported`)
133
+ }
134
+
135
+ const { responseBody, responseBodyType } = await readBody()
136
+
137
+ if (responseBodyType === "blob") {
138
+ return responseBody
139
+ }
140
+ if (responseBodyType === "dataView") {
141
+ return new Blob([cloneBuffer(responseBody.buffer)])
142
+ }
143
+ if (responseBodyType === "arrayBuffer") {
144
+ return new Blob([cloneBuffer(responseBody)])
145
+ }
146
+ if (responseBodyType === "formData") {
147
+ throw new Error("could not read FormData body as blob")
148
+ }
149
+ return new Blob([String(responseBody)])
150
+ }
151
+
152
+ const arrayBuffer = async () => {
153
+ const { responseBody, responseBodyType } = await readBody()
154
+
155
+ if (responseBodyType === "arrayBuffer") {
156
+ return cloneBuffer(responseBody)
157
+ }
158
+ const responseBlob = await blob()
159
+ return blobToArrayBuffer(responseBlob)
160
+ }
161
+
162
+ const formData = async () => {
163
+ if (!hasFormData) {
164
+ throw new Error(`formData not supported`)
165
+ }
166
+ const responseText = await text()
167
+ return textToFormData(responseText)
168
+ }
169
+
170
+ return {
171
+ url: responseUrl,
172
+ status: responseStatus,
173
+ statusText: responseStatusText,
174
+ headers: responseHeaders,
175
+ text,
176
+ json,
177
+ blob,
178
+ arrayBuffer,
179
+ formData,
180
+ }
181
+ }
182
+
183
+ const canUseBlob = () => {
184
+ if (typeof window.FileReader !== "function") return false
185
+
186
+ if (typeof window.Blob !== "function") return false
187
+
188
+ try {
189
+ // eslint-disable-next-line no-new
190
+ new Blob()
191
+ return true
192
+ } catch (e) {
193
+ return false
194
+ }
195
+ }
196
+
197
+ const hasBlob = canUseBlob()
198
+
199
+ const hasFormData = typeof window.FormData === "function"
200
+
201
+ const hasArrayBuffer = typeof window.ArrayBuffer === "function"
202
+
203
+ const hasSearchParams = typeof window.URLSearchParams === "function"
204
+
205
+ const createRequestError = (error, { url }) => {
206
+ return new Error(
207
+ `error during xhr request on ${url}.
208
+ --- error stack ---
209
+ ${error.stack}`,
210
+ )
211
+ }
212
+
213
+ const createPromiseAndHooks = () => {
214
+ let resolve
215
+ let reject
216
+ const promise = new Promise((res, rej) => {
217
+ resolve = (value) => {
218
+ promise.settled = true
219
+ res(value)
220
+ }
221
+ reject = (value) => {
222
+ promise.settled = true
223
+ rej(value)
224
+ }
225
+ })
226
+ promise.resolve = resolve
227
+ promise.reject = reject
228
+ return promise
229
+ }
230
+
231
+ // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
232
+ const computeWithCredentials = ({ credentials, url }) => {
233
+ if (credentials === "same-origin") {
234
+ return originSameAsGlobalOrigin(url)
235
+ }
236
+ return credentials === "include"
237
+ }
238
+
239
+ const originSameAsGlobalOrigin = (url) => {
240
+ // if we cannot read globalOrigin from window.location.origin, let's consider it's ok
241
+ if (typeof window !== "object") return true
242
+ if (typeof window.location !== "object") return true
243
+ const globalOrigin = window.location.origin
244
+ if (globalOrigin === "null") return true
245
+ return hrefToOrigin(url) === globalOrigin
246
+ }
247
+
248
+ const detectBodyType = (body) => {
249
+ if (!body) {
250
+ return ""
251
+ }
252
+ if (typeof body === "string") {
253
+ return "text"
254
+ }
255
+ if (hasBlob && Blob.prototype.isPrototypeOf(body)) {
256
+ return "blob"
257
+ }
258
+ if (hasFormData && FormData.prototype.isPrototypeOf(body)) {
259
+ return "formData"
260
+ }
261
+ if (hasArrayBuffer) {
262
+ if (hasBlob && isDataView(body)) {
263
+ return `dataView`
264
+ }
265
+ if (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body)) {
266
+ return `arrayBuffer`
267
+ }
268
+ }
269
+ if (hasSearchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
270
+ return "searchParams"
271
+ }
272
+ return ""
273
+ }
274
+
275
+ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders#Example
276
+ const getHeadersFromXHR = (xhr) => {
277
+ const headerMap = {}
278
+
279
+ const headersString = xhr.getAllResponseHeaders()
280
+ if (headersString === "") return headerMap
281
+
282
+ const lines = headersString.trim().split(/[\r\n]+/)
283
+ lines.forEach((line) => {
284
+ const parts = line.split(": ")
285
+ const name = parts.shift()
286
+ const value = parts.join(": ")
287
+ headerMap[name.toLowerCase()] = value
288
+ })
289
+
290
+ return headerMap
291
+ }
292
+
293
+ const hrefToOrigin = (href) => {
294
+ const scheme = hrefToScheme(href)
295
+
296
+ if (scheme === "file") {
297
+ return "file://"
298
+ }
299
+
300
+ if (scheme === "http" || scheme === "https") {
301
+ const secondProtocolSlashIndex = scheme.length + "://".length
302
+ const pathnameSlashIndex = href.indexOf("/", secondProtocolSlashIndex)
303
+
304
+ if (pathnameSlashIndex === -1) return href
305
+ return href.slice(0, pathnameSlashIndex)
306
+ }
307
+
308
+ return href.slice(0, scheme.length + 1)
309
+ }
310
+
311
+ const hrefToScheme = (href) => {
312
+ const colonIndex = href.indexOf(":")
313
+ if (colonIndex === -1) return ""
314
+ return href.slice(0, colonIndex)
315
+ }
316
+
317
+ const isDataView = (obj) => {
318
+ return obj && DataView.prototype.isPrototypeOf(obj)
319
+ }
320
+
321
+ const isArrayBufferView =
322
+ ArrayBuffer.isView ||
323
+ (() => {
324
+ const viewClasses = [
325
+ "[object Int8Array]",
326
+ "[object Uint8Array]",
327
+ "[object Uint8ClampedArray]",
328
+ "[object Int16Array]",
329
+ "[object Uint16Array]",
330
+ "[object Int32Array]",
331
+ "[object Uint32Array]",
332
+ "[object Float32Array]",
333
+ "[object Float64Array]",
334
+ ]
335
+
336
+ return (value) => {
337
+ return (
338
+ value && viewClasses.includes(Object.prototype.toString.call(value))
339
+ )
340
+ }
341
+ })()
342
+
343
+ const textToFormData = (text) => {
344
+ const form = new FormData()
345
+ text
346
+ .trim()
347
+ .split("&")
348
+ .forEach(function (bytes) {
349
+ if (bytes) {
350
+ const split = bytes.split("=")
351
+ const name = split.shift().replace(/\+/g, " ")
352
+ const value = split.join("=").replace(/\+/g, " ")
353
+ form.append(decodeURIComponent(name), decodeURIComponent(value))
354
+ }
355
+ })
356
+ return form
357
+ }
358
+
359
+ const blobToArrayBuffer = async (blob) => {
360
+ const reader = new FileReader()
361
+ const promise = fileReaderReady(reader)
362
+ reader.readAsArrayBuffer(blob)
363
+ return promise
364
+ }
365
+
366
+ const blobToText = (blob) => {
367
+ const reader = new FileReader()
368
+ const promise = fileReaderReady(reader)
369
+ reader.readAsText(blob)
370
+ return promise
371
+ }
372
+
373
+ const arrayBufferToText = (arrayBuffer) => {
374
+ const view = new Uint8Array(arrayBuffer)
375
+ const chars = new Array(view.length)
376
+ let i = 0
377
+ while (i < view.length) {
378
+ chars[i] = String.fromCharCode(view[i])
379
+
380
+ i++
381
+ }
382
+ return chars.join("")
383
+ }
384
+
385
+ const fileReaderReady = (reader) => {
386
+ return new Promise(function (resolve, reject) {
387
+ reader.onload = function () {
388
+ resolve(reader.result)
389
+ }
390
+ reader.onerror = function () {
391
+ reject(reader.error)
392
+ }
393
+ })
394
+ }
395
+
396
+ const cloneBuffer = (buffer) => {
397
+ if (buffer.slice) {
398
+ return buffer.slice(0)
399
+ }
400
+ const view = new Uint8Array(buffer.byteLength)
401
+ view.set(new Uint8Array(buffer))
402
+ return view.buffer
403
+ }
File without changes
package/src/main.js DELETED
@@ -1 +0,0 @@
1
- export { fetchUrl } from "./fetch_url.js"