Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 18b1388

Browse files
committed
Bump html-rewriter-wasm to 0.4.0
- Add support for `Element#onEndTag` - Add support for the [`html_rewriter_treats_esi_include_as_void_tag`](https://developers.cloudflare.com/workers/platform/compatibility-dates#htmlrewriter-handling-of-esiinclude) compatibility flag - Throw a `TypeError` instead of a `string` when a content token is used outside the relevant content handler
1 parent d93200a commit 18b1388

File tree

7 files changed

+150
-22
lines changed

7 files changed

+150
-22
lines changed

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/html-rewriter/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"dependencies": {
3838
"@miniflare/core": "2.1.0",
3939
"@miniflare/shared": "2.1.0",
40-
"html-rewriter-wasm": "^0.3.2",
40+
"html-rewriter-wasm": "^0.4.0",
4141
"undici": "4.12.1"
4242
},
4343
"devDependencies": {
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import { Plugin, PluginContext, SetupResult } from "@miniflare/shared";
2-
import { HTMLRewriter } from "./rewriter";
2+
import { HTMLRewriter, withEnableEsiTags } from "./rewriter";
3+
4+
const ESIHTMLRewriter = new Proxy(HTMLRewriter, {
5+
construct(target, args, newTarget) {
6+
const value = Reflect.construct(target, args, newTarget);
7+
return withEnableEsiTags(value);
8+
},
9+
});
310

411
export class HTMLRewriterPlugin extends Plugin {
512
constructor(ctx: PluginContext) {
613
super(ctx);
714
}
815

916
setup(): SetupResult {
10-
return { globals: { HTMLRewriter } };
17+
const enableEsiFlags = this.ctx.compat.isEnabled(
18+
"html_rewriter_treats_esi_include_as_void_tag"
19+
);
20+
const impl = enableEsiFlags ? ESIHTMLRewriter : HTMLRewriter;
21+
return { globals: { HTMLRewriter: impl } };
1122
}
1223
}

packages/html-rewriter/src/rewriter.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import { Response as BaseResponse } from "undici";
99

1010
type SelectorElementHandlers = [selector: string, handlers: ElementHandlers];
1111

12+
const kEnableEsiTags = Symbol("kEnableEsiTags");
13+
1214
// noinspection SuspiciousTypeOfGuard
1315
export class HTMLRewriter {
1416
readonly #elementHandlers: SelectorElementHandlers[] = [];
1517
readonly #documentHandlers: DocumentHandlers[] = [];
18+
[kEnableEsiTags] = false;
1619

1720
on(selector: string, handlers: ElementHandlers): this {
1821
this.#elementHandlers.push([selector, handlers]);
@@ -48,10 +51,13 @@ export class HTMLRewriter {
4851
const {
4952
HTMLRewriter: BaseHTMLRewriter,
5053
}: typeof import("html-rewriter-wasm") = require("html-rewriter-wasm");
51-
rewriter = new BaseHTMLRewriter((output) => {
52-
// enqueue will throw on empty chunks
53-
if (output.length !== 0) controller.enqueue(output);
54-
});
54+
rewriter = new BaseHTMLRewriter(
55+
(output) => {
56+
// enqueue will throw on empty chunks
57+
if (output.length !== 0) controller.enqueue(output);
58+
},
59+
{ enableEsiTags: this[kEnableEsiTags] }
60+
);
5561
// Add all registered handlers
5662
for (const [selector, handlers] of this.#elementHandlers) {
5763
rewriter.on(selector, handlers);
@@ -77,3 +83,8 @@ export class HTMLRewriter {
7783
return res;
7884
}
7985
}
86+
87+
export function withEnableEsiTags(rewriter: HTMLRewriter): HTMLRewriter {
88+
rewriter[kEnableEsiTags] = true;
89+
return rewriter;
90+
}
Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
1+
import { Response } from "@miniflare/core";
12
import { HTMLRewriter, HTMLRewriterPlugin } from "@miniflare/html-rewriter";
2-
import { Compatibility, NoOpLog } from "@miniflare/shared";
3+
import { Compatibility, NoOpLog, PluginContext } from "@miniflare/shared";
34
import test from "ava";
5+
import type { ElementHandlers } from "html-rewriter-wasm";
6+
7+
const log = new NoOpLog();
8+
const compat = new Compatibility();
9+
const rootPath = process.cwd();
10+
const ctx: PluginContext = { log, compat, rootPath };
411

512
test("HTMLRewriterPlugin: setup: includes HTMLRewriter in globals", (t) => {
6-
const plugin = new HTMLRewriterPlugin({
7-
log: new NoOpLog(),
8-
compat: new Compatibility(),
9-
rootPath: process.cwd(),
10-
});
13+
const plugin = new HTMLRewriterPlugin(ctx);
1114
const result = plugin.setup();
1215
t.is(result.globals?.HTMLRewriter, HTMLRewriter);
1316
});
17+
18+
test("HTMLRewriterPlugin: setup: treats esi tags as void only if compatibility flag enabled", async (t) => {
19+
const handlers: ElementHandlers = {
20+
element(element) {
21+
element.replace("replacement");
22+
},
23+
};
24+
const input = '<span><esi:include src="a" /> text<span>';
25+
26+
// Check with flag disabled
27+
let plugin = new HTMLRewriterPlugin(ctx);
28+
let result = plugin.setup();
29+
let impl: typeof HTMLRewriter = result.globals?.HTMLRewriter;
30+
let res = new impl()
31+
.on("esi\\:include", handlers)
32+
.transform(new Response(input));
33+
t.is(await res.text(), "<span>replacement");
34+
35+
// Check with flag enabled
36+
const compat = new Compatibility(undefined, [
37+
"html_rewriter_treats_esi_include_as_void_tag",
38+
]);
39+
plugin = new HTMLRewriterPlugin({ ...ctx, compat });
40+
result = plugin.setup();
41+
impl = result.globals?.HTMLRewriter;
42+
res = new impl().on("esi\\:include", handlers).transform(new Response(input));
43+
t.is(await res.text(), "<span>replacement text<span>");
44+
});

packages/html-rewriter/test/rewriter.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,77 @@ test("HTMLRewriter: handles element class handler", async (t) => {
221221
.transform(new Response("<p>test</p>"));
222222
t.is(await res.text(), "<p>new</p>");
223223
});
224+
225+
test("HTMLRewriter: handles end tag properties", async (t) => {
226+
const res = new HTMLRewriter()
227+
.on("p", {
228+
element(element) {
229+
element.onEndTag(function (end) {
230+
t.is(this, element);
231+
t.is(end.name, "p");
232+
end.name = "h1";
233+
});
234+
},
235+
})
236+
.transform(new Response("<p>test</p>"));
237+
t.is(await res.text(), "<p>test</h1>");
238+
});
239+
test("HTMLRewriter: handles end tag mutations", async (t) => {
240+
const input = "<p>test</p>";
241+
const beforeAfterExpected = [
242+
"<p>",
243+
"test",
244+
"&lt;span&gt;before&lt;/span&gt;",
245+
"<span>before html</span>",
246+
"</p>",
247+
"<span>after html</span>",
248+
"&lt;span&gt;after&lt;/span&gt;",
249+
].join("");
250+
const removeExpected = "<p>test";
251+
252+
// before/after
253+
let res = new HTMLRewriter()
254+
.on("p", {
255+
element(element) {
256+
// eslint-disable-next-line @typescript-eslint/no-this-alias
257+
const that = this;
258+
element.onEndTag((end) => {
259+
t.is(this, that);
260+
end.before("<span>before</span>");
261+
end.before("<span>before html</span>", { html: true });
262+
end.after("<span>after</span>");
263+
end.after("<span>after html</span>", { html: true });
264+
});
265+
},
266+
})
267+
.transform(new Response(input));
268+
t.is(await res.text(), beforeAfterExpected);
269+
270+
// remove
271+
res = new HTMLRewriter()
272+
.on("p", {
273+
element(element) {
274+
element.onEndTag((end) => {
275+
end.remove();
276+
});
277+
},
278+
})
279+
.transform(new Response(input));
280+
t.is(await res.text(), removeExpected);
281+
});
282+
test.serial("HTMLRewriter: handles end tag async handler", async (t) => {
283+
const res = new HTMLRewriter()
284+
.on("p", {
285+
element(element) {
286+
element.onEndTag(async (end) => {
287+
await setTimeout(50);
288+
end.before("!");
289+
});
290+
},
291+
})
292+
.transform(new Response("<p>test</p>"));
293+
t.is(await res.text(), "<p>test!</p>");
294+
});
224295
// endregion: element
225296

226297
// region: comments

packages/shared/src/compat.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export interface CompatibilityFeature {
1111
export type CompatibilityEnableFlag =
1212
| "durable_object_fetch_requires_full_url"
1313
| "fetch_refuses_unknown_protocols"
14-
| "formdata_parser_supports_files";
14+
| "formdata_parser_supports_files"
15+
| "html_rewriter_treats_esi_include_as_void_tag";
1516
export type CompatibilityDisableFlag =
1617
| "durable_object_fetch_allows_relative_url"
1718
| "fetch_treats_unknown_protocols_as_http"
@@ -36,6 +37,9 @@ const FEATURES: CompatibilityFeature[] = [
3637
enableFlag: "formdata_parser_supports_files",
3738
disableFlag: "formdata_parser_converts_files_to_strings",
3839
},
40+
{
41+
enableFlag: "html_rewriter_treats_esi_include_as_void_tag",
42+
},
3943
];
4044

4145
const collator = new Intl.Collator(undefined, { numeric: true });

0 commit comments

Comments
 (0)