Skip to content

Commit cee0a94

Browse files
committed
refactor: Split modules and use type guards
Signed-off-by: Richie Bendall <[email protected]>
1 parent ae6d6ca commit cee0a94

File tree

6 files changed

+150
-128
lines changed

6 files changed

+150
-128
lines changed

src/index.ts

Lines changed: 4 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -24,125 +24,7 @@
2424
* SOFTWARE.
2525
*/
2626

27-
import { decode as convert } from "iconv-lite"
28-
import getCharSet from "./utils/getCharSet"
29-
import { load as $ } from "cheerio"
30-
import { isURLSearchParams, isBlob, isArrayBuffer } from "./utils/is"
31-
import { Stream, Writable } from "stream"
32-
33-
/**
34-
* Detect buffer encoding and convert to target encoding
35-
* ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
36-
*
37-
* @param buffer Incoming buffer.
38-
* @param headers Headers provided with the request.
39-
*/
40-
export function convertBody(buffer: Buffer, headers?: Headers): string {
41-
const contentType = headers instanceof Headers ? headers.get("content-type") : null
42-
let charset: string
43-
44-
// Header
45-
if (contentType) charset = getCharSet(contentType)
46-
47-
// No charset in content type, peek at response body for at most 1024 bytes
48-
const res = buffer.slice(0, 1024).toString()
49-
50-
// HTML5, HTML4 and XML
51-
if (!charset && res) {
52-
charset = getCharSet(
53-
$(res)("meta[charset]").attr("charset") || // HTML5
54-
$(res)("meta[http-equiv][content]").attr("content") || // HTML4
55-
$(res.replace(/<\?(.*)\?>/im, "<$1>"), { xmlMode: true }).root().find("xml").attr("encoding") // XML
56-
)
57-
}
58-
59-
// Prevent decode issues when sites use incorrect encoding
60-
// ref: https://hsivonen.fi/encoding-menu/
61-
if (charset && charset.toLowerCase() in ["gb2312", "gbk"]) charset = "gb18030"
62-
63-
// Turn raw buffers into a single utf-8 buffer
64-
return convert(
65-
buffer,
66-
charset || "utf-8"
67-
)
68-
}
69-
70-
/**
71-
* Performs the operation "extract a `Content-Type` value from |object|" as
72-
* specified in the specification:
73-
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract
74-
*
75-
* This function assumes that instance.body is present.
76-
*
77-
* @param body Any options.body input
78-
*/
79-
export function extractContentType(body: any): string | null {
80-
// Body is string
81-
if (typeof body === "string") return "text/plain;charset=UTF-8"
82-
83-
// Body is a URLSearchParams
84-
if (isURLSearchParams(body)) return "application/x-www-form-urlencoded;charset=UTF-8"
85-
86-
// Body is blob
87-
if (isBlob(body)) return body.type || null
88-
89-
// Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView)
90-
if (Buffer.isBuffer(body) || isArrayBuffer(body) || ArrayBuffer.isView(body)) return null
91-
92-
// Detect form data input from form-data module
93-
if (typeof body.getBoundary === "function") return `multipart/form-data;boundary=${body.getBoundary()}`
94-
95-
// Body is stream - can't really do much about this
96-
if (body instanceof Stream) return null
97-
98-
// Body constructor defaults other things to string
99-
return "text/plain;charset=UTF-8"
100-
}
101-
102-
/**
103-
* The Fetch Standard treats this as if "total bytes" is a property on the body.
104-
* For us, we have to explicitly get it with a function.
105-
*
106-
* ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
107-
*
108-
* @param body Body object from the Body instance.
109-
*/
110-
export function getTotalBytes(body: any): number | null {
111-
// Body is null or undefined
112-
if (body == null) return 0
113-
114-
// Body is Blob
115-
if (isBlob(body)) return body.size
116-
117-
// Body is Buffer
118-
if (Buffer.isBuffer(body)) return body.length
119-
120-
// Detect form data input from form-data module
121-
if (body && typeof body.getLengthSync === "function") return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null
122-
123-
// Body is stream
124-
return null
125-
}
126-
127-
/**
128-
* Write a Body to a Node.js WritableStream (e.g. http.Request) object.
129-
*
130-
* @param body Body object from the Body instance.
131-
* @param dest The stream to write to.
132-
*/
133-
export function writeToStream(body: any, dest: Writable): void {
134-
// Body is null
135-
if (body == null) dest.end()
136-
137-
// Body is Blob
138-
else if (isBlob(body)) body.stream().pipe(dest)
139-
140-
// Body is buffer
141-
else if (Buffer.isBuffer(body)) {
142-
dest.write(body)
143-
dest.end()
144-
} else {
145-
// Body is stream
146-
body.pipe(dest)
147-
}
148-
}
27+
export { convertBody } from "./lib/convert-body"
28+
export { extractContentType } from "./lib/extract-content-type"
29+
export { getTotalBytes } from "./lib/get-total-bytes"
30+
export { writeToStream } from "./lib/write-to-stream"

src/lib/convert-body.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import getCharSet from "../utils/getCharSet"
2+
import { decode as convert } from "iconv-lite"
3+
import { load as $ } from "cheerio"
4+
5+
/**
6+
* Detect buffer encoding and convert to target encoding
7+
* ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
8+
*
9+
* @param buffer Incoming buffer.
10+
* @param headers Headers provided with the request.
11+
*/
12+
export function convertBody(buffer: Buffer, headers?: Headers): string {
13+
const contentType = headers instanceof Headers ? headers.get("content-type") : null
14+
let charset: string
15+
16+
// Header
17+
if (contentType) charset = getCharSet(contentType)
18+
19+
// No charset in content type, peek at response body for at most 1024 bytes
20+
const res = buffer.slice(0, 1024).toString()
21+
22+
// HTML5, HTML4 and XML
23+
if (!charset && res) {
24+
charset = getCharSet(
25+
$(res)("meta[charset]").attr("charset") || // HTML5
26+
$(res)("meta[http-equiv][content]").attr("content") || // HTML4
27+
$(res.replace(/<\?(.*)\?>/im, "<$1>"), { xmlMode: true }).root().find("xml").attr("encoding") // XML
28+
)
29+
}
30+
31+
// Prevent decode issues when sites use incorrect encoding
32+
// ref: https://hsivonen.fi/encoding-menu/
33+
if (charset && charset.toLowerCase() in ["gb2312", "gbk"]) charset = "gb18030"
34+
35+
// Turn raw buffers into a single utf-8 buffer
36+
return convert(
37+
buffer,
38+
charset || "utf-8"
39+
)
40+
}

src/lib/extract-content-type.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Stream } from "stream"
2+
import { isBlob, isURLSearchParams, isArrayBuffer } from "../utils/is"
3+
4+
/**
5+
* Performs the operation "extract a `Content-Type` value from |object|" as
6+
* specified in the specification:
7+
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract
8+
*
9+
* This function assumes that instance.body is present.
10+
*
11+
* @param body Any options.body input
12+
*/
13+
export function extractContentType(body: any): string | null {
14+
// Body is string
15+
if (typeof body === "string") return "text/plain;charset=UTF-8"
16+
17+
// Body is a URLSearchParams
18+
if (isURLSearchParams(body)) return "application/x-www-form-urlencoded;charset=UTF-8"
19+
20+
// Body is blob
21+
if (isBlob(body)) return body.type || null
22+
23+
// Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView)
24+
if (Buffer.isBuffer(body) || isArrayBuffer(body) || ArrayBuffer.isView(body)) return null
25+
26+
// Detect form data input from form-data module
27+
if (typeof body.getBoundary === "function") return `multipart/form-data;boundary=${body.getBoundary()}`
28+
29+
// Body is stream - can't really do much about this
30+
if (body instanceof Stream) return null
31+
32+
// Body constructor defaults other things to string
33+
return "text/plain;charset=UTF-8"
34+
}

src/lib/get-total-bytes.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { isBlob } from "../utils/is"
2+
3+
/**
4+
* The Fetch Standard treats this as if "total bytes" is a property on the body.
5+
* For us, we have to explicitly get it with a function.
6+
*
7+
* ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
8+
*
9+
* @param body Body object from the Body instance.
10+
*/
11+
export function getTotalBytes(body: any): number | null {
12+
// Body is null or undefined
13+
if (body == null) return 0
14+
15+
// Body is Blob
16+
if (isBlob(body)) return body.size
17+
18+
// Body is Buffer
19+
if (Buffer.isBuffer(body)) return body.length
20+
21+
// Detect form data input from form-data module
22+
if (body && typeof body.getLengthSync === "function") return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null
23+
24+
// Body is stream
25+
return null
26+
}

src/lib/write-to-stream.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Writable } from "stream"
2+
import { isBlob } from "../utils/is"
3+
4+
/**
5+
* Write a Body to a Node.js WritableStream (e.g. http.Request) object.
6+
*
7+
* @param body Body object from the Body instance.
8+
* @param dest The stream to write to.
9+
*/
10+
export function writeToStream(body: any, dest: Writable): void {
11+
// Body is null
12+
if (body == null) dest.end()
13+
14+
// Body is Blob
15+
else if (isBlob(body)) body.stream().pipe(dest)
16+
17+
// Body is buffer
18+
else if (Buffer.isBuffer(body)) {
19+
dest.write(body)
20+
dest.end()
21+
} else {
22+
// Body is stream
23+
body.pipe(dest)
24+
}
25+
}

src/utils/is.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const NAME = Symbol.toStringTag;
66
*
77
* @param obj The object to check.
88
*/
9-
export function isURLSearchParams(obj): boolean {
9+
export function isURLSearchParams(obj: any): obj is URLSearchParams {
1010
return (
1111
typeof obj === 'object' &&
1212
typeof obj.append === 'function' &&
@@ -20,12 +20,19 @@ export function isURLSearchParams(obj): boolean {
2020
);
2121
}
2222

23+
interface FetchBlob extends Blob {
24+
arrayBuffer: Function,
25+
type: string,
26+
stream: Function,
27+
constructor: Function
28+
}
29+
2330
/**
2431
* Check if `obj` is a W3C `Blob` object (which `File` inherits from)
2532
*
2633
* @param obj The object to check.
2734
*/
28-
export function isBlob(obj): boolean {
35+
export function isBlob(obj: any): obj is FetchBlob {
2936
return (
3037
typeof obj === 'object' &&
3138
typeof obj.arrayBuffer === 'function' &&
@@ -41,7 +48,7 @@ export function isBlob(obj): boolean {
4148
*
4249
* @param obj The object to check.
4350
*/
44-
export function isAbortSignal(obj): boolean {
51+
export function isAbortSignal(obj: any): obj is AbortSignal {
4552
return (
4653
typeof obj === 'object' &&
4754
obj[NAME] === 'AbortSignal'
@@ -53,15 +60,23 @@ export function isAbortSignal(obj): boolean {
5360
*
5461
* @param obj The object to check.
5562
*/
56-
export function isArrayBuffer(obj): boolean {
63+
export function isArrayBuffer(obj: any): obj is ArrayBuffer {
5764
return obj[NAME] === 'ArrayBuffer';
5865
}
5966

67+
interface AbortError extends Error {
68+
name: "AbortError";
69+
[Symbol.toStringTag]: "AbortError"
70+
constructor(message: string);
71+
type: string;
72+
message: string;
73+
}
74+
6075
/**
6176
* Check if `obj` is an instance of AbortError.
6277
*
6378
* @param obj The object to check.
6479
*/
65-
export function isAbortError(obj): boolean {
66-
return obj.name === 'AbortError';
80+
export function isAbortError(obj: any): obj is AbortError {
81+
return obj[NAME] === 'AbortError';
6782
}

0 commit comments

Comments
 (0)