-
Notifications
You must be signed in to change notification settings - Fork 6
/
index.ts
133 lines (118 loc) · 4.38 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// FIXME: replace multiple 1 import from skypack!?
import type {
HTMLRewriter as BaseHTMLRewriter,
ContentTypeOptions,
Element,
EndTag,
Comment,
TextChunk,
Doctype,
DocumentEnd,
ElementHandlers,
DocumentHandlers,
} from "./vendor/html_rewriter.d.ts";
import * as _base from './vendor/html_rewriter.js'
const { default: initWASM } = _base;
const base: typeof import("./vendor/html_rewriter.d.ts") = _base;
export type {
ContentTypeOptions,
Element,
EndTag,
Comment,
TextChunk,
Doctype,
DocumentEnd,
ElementHandlers,
DocumentHandlers,
}
import { ResolvablePromise } from 'https://ghuc.cc/worker-tools/resolvable-promise/index.ts'
type SelectorElementHandlers = [selector: string, handlers: ElementHandlers];
const kEnableEsiTags = Symbol("kEnableEsiTags");
// In case a server doesn't return the proper mime type (e.g. githubusercontent.com)..
const toWASMResponse = (response: Response) => {
if (response.headers.get('content-type')?.startsWith('application/wasm')) return response;
const { body, headers: hs, ...props } = response
const headers = new Headers(hs)
headers.set('content-type', 'application/wasm')
return new Response(body, { ...props, headers })
}
const initialized = new ResolvablePromise<void>();
let executing = false;
export class HTMLRewriter {
readonly #elementHandlers: SelectorElementHandlers[] = [];
readonly #documentHandlers: DocumentHandlers[] = [];
[kEnableEsiTags] = false;
constructor() {
if (!initialized.settled && !executing) {
executing = true;
fetch(new URL("./vendor/html_rewriter_bg.wasm", import.meta.url).href)
.then(r => r.ok ? r : (() => { throw Error('WASM response not ok') })())
.then(toWASMResponse)
.then(initWASM)
.then(() => initialized.resolve())
.catch(err => {
executing = false;
console.error(err);
})
}
}
on(selector: string, handlers: ElementHandlers): this {
this.#elementHandlers.push([selector, handlers]);
return this;
}
onDocument(handlers: DocumentHandlers): this {
this.#documentHandlers.push(handlers);
return this;
}
transform(response: Response): Response {
const body = response.body as ReadableStream<Uint8Array> | null;
// HTMLRewriter doesn't run the end handler if the body is null, so it's
// pointless to setup the transform stream.
if (body === null) return new Response(body, response);
if (response instanceof Response) {
// Make sure we validate chunks are BufferSources and convert them to
// Uint8Arrays as required by the Rust glue code.
response = new Response(response.body, response);
}
let rewriter: BaseHTMLRewriter;
const transformStream = new TransformStream<Uint8Array, Uint8Array>({
start: async (controller) => {
// Create a rewriter instance for this transformation that writes its
// output to the transformed response's stream. Note that each
// BaseHTMLRewriter can only be used once.
await initialized;
rewriter = new base.HTMLRewriter(
(output) => {
// enqueue will throw on empty chunks
if (output.length !== 0) controller.enqueue(output);
},
{ enableEsiTags: this[kEnableEsiTags] }
);
// Add all registered handlers
for (const [selector, handlers] of this.#elementHandlers) {
rewriter.on(selector, handlers);
}
for (const handlers of this.#documentHandlers) {
rewriter.onDocument(handlers);
}
},
// The finally() below will ensure the rewriter is always freed.
// chunk is guaranteed to be a Uint8Array as we're using the
// @miniflare/core Response class, which transforms to a byte stream.
transform: (chunk) => rewriter.write(chunk),
flush: () => rewriter.end(),
});
const promise = body.pipeTo(transformStream.writable);
promise.catch(() => {}).finally(() => rewriter?.free());
// Return a response with the transformed body, copying over headers, etc
const res = new Response(transformStream.readable, response);
// If Content-Length is set, it's probably going to be wrong, since we're
// rewriting content, so remove it
res.headers.delete("Content-Length");
return res;
}
}
export function withEnableEsiTags(rewriter: HTMLRewriter): HTMLRewriter {
rewriter[kEnableEsiTags] = true;
return rewriter;
}