single-file-core 1.5.115 → 1.5.116
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/core/helper.js +12 -7
- package/core/index.js +19 -5
- package/core/lib/processor-helper-common.js +42 -18
- package/core/lib/processor-helper-inline.js +2 -10
- package/core/lib/processor-helper.js +25 -18
- package/doc/singlefile-archive.md +123 -26
- package/modules/css-fonts-minifier.js +114 -24
- package/modules/css-rules-minifier.js +9 -2
- package/package.json +2 -2
- package/processors/compression/compression-display.js +23 -2
- package/processors/compression/compression.js +12 -1
- package/processors/hooks/content/content-hooks-frames-web.js +27 -4
- package/test/sfz-harness/README.md +5 -2
- package/test/sfz-harness/adopted-stylesheets-hook.js +240 -0
- package/test/sfz-harness/css-fonts-minifier.js +184 -0
- package/test/sfz-harness/css-property-filter.js +85 -0
- package/test/sfz-harness/format-rules.js +28 -0
- package/test/sfz-harness/inlined-functions.js +82 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// removeUnusedFonts decides which @font-face rules to keep by reading the font-family declarations
|
|
2
|
+
// out of the stylesheets. When a family is written as var(--name), the value has to be resolved
|
|
3
|
+
// first, and the only resolution the module had was getComputedStyle(body).getPropertyValue(name).
|
|
4
|
+
// Custom properties inherit downward, so that answers for a property declared on :root and returns
|
|
5
|
+
// nothing for one declared on any descendant — and a single unresolved variable anywhere sets
|
|
6
|
+
// unusedFonts to [], which switches pruning off for the WHOLE document. One declaration moved from
|
|
7
|
+
// :root to .card took a probe page from 7,611 to 43,648 bytes with every unused font embedded.
|
|
8
|
+
//
|
|
9
|
+
// The fix keeps the computed value as the first answer and falls back to the union of every value
|
|
10
|
+
// the document declares for that property. The union is a superset of what any one element sees,
|
|
11
|
+
// so it can only keep too much, never too little — and options.usedFonts, which comes from the
|
|
12
|
+
// rendered computed styles, gates the result anyway.
|
|
13
|
+
//
|
|
14
|
+
// The other var() defects this pins, all found alongside it:
|
|
15
|
+
// - custom property names are case-sensitive; they were lowercased with the family names, so
|
|
16
|
+
// var(--ProbeFont) never resolved, not even from :root
|
|
17
|
+
// - the "font" shorthand's var() regexp required var(--name) exactly, so a fallback in it made
|
|
18
|
+
// the shorthand unreadable and switched pruning off
|
|
19
|
+
// - a var() fallback is one raw token, so a list in it was pushed as a single mangled name and
|
|
20
|
+
// the families it holds were dropped
|
|
21
|
+
import * as cssTree from "../../vendor/css-tree.js";
|
|
22
|
+
|
|
23
|
+
// the module reaches core/helper.js, which pulls in the frame hooks, and those install themselves
|
|
24
|
+
// against window and document as they are evaluated: the stubs go in before the dynamic import
|
|
25
|
+
globalThis.window = globalThis;
|
|
26
|
+
globalThis.document = {};
|
|
27
|
+
globalThis.Document = class Document { };
|
|
28
|
+
globalThis.MutationObserver = class MutationObserver { observe() { } };
|
|
29
|
+
const { process: removeUnusedFonts } = await import("../../modules/css-fonts-minifier.js");
|
|
30
|
+
|
|
31
|
+
const FONT_FACES = `
|
|
32
|
+
@font-face{font-family:"UsedOne";src:url(one.woff2)}
|
|
33
|
+
@font-face{font-family:"UsedTwo";src:url(two.woff2)}
|
|
34
|
+
@font-face{font-family:"UsedThree";src:url(three.woff2)}
|
|
35
|
+
@font-face{font-family:"Unused";src:url(unused.woff2)}`;
|
|
36
|
+
|
|
37
|
+
// what the rendered page reports as actually drawn: [family, weight, style, variant]. A family
|
|
38
|
+
// missing from this list is dropped whatever the stylesheets say, so it holds every family the
|
|
39
|
+
// tests below may legitimately keep
|
|
40
|
+
const USED_FONTS = [
|
|
41
|
+
["usedone", "400", "normal", "normal"],
|
|
42
|
+
["usedtwo", "400", "normal", "normal"],
|
|
43
|
+
["usedthree", "400", "normal", "normal"]
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const ALL_FAMILIES = ["usedone", "usedtwo", "usedthree", "unused"];
|
|
47
|
+
|
|
48
|
+
let failures = 0;
|
|
49
|
+
|
|
50
|
+
check("a plain family name prunes the rest",
|
|
51
|
+
run({ rules: ".card p{font-family:\"UsedOne\",serif}" }),
|
|
52
|
+
["usedone"]);
|
|
53
|
+
|
|
54
|
+
check("a property declared on :root resolves through the computed style",
|
|
55
|
+
run({ rules: ":root{--probe-font:\"UsedOne\"}.card p{font-family:var(--probe-font),serif}", computed: { "--probe-font": "\"UsedOne\"" } }),
|
|
56
|
+
["usedone"]);
|
|
57
|
+
|
|
58
|
+
check("a property declared on a descendant resolves through the declared values",
|
|
59
|
+
run({ rules: ".card{--probe-font:\"UsedOne\"}.card p{font-family:var(--probe-font),serif}" }),
|
|
60
|
+
["usedone"]);
|
|
61
|
+
|
|
62
|
+
check("a mixed-case property name is not lowercased away",
|
|
63
|
+
run({ rules: ".card{--ProbeFont:\"UsedOne\"}.card p{font-family:var(--ProbeFont),serif}" }),
|
|
64
|
+
["usedone"]);
|
|
65
|
+
|
|
66
|
+
check("a property declared inside a media query resolves too",
|
|
67
|
+
run({ rules: "@media screen{.card{--probe-font:\"UsedOne\"}}.card p{font-family:var(--probe-font),serif}" }),
|
|
68
|
+
["usedone"]);
|
|
69
|
+
|
|
70
|
+
check("several declared values are all kept as candidates",
|
|
71
|
+
run({ rules: ".card{--probe-font:\"UsedOne\"}.note{--probe-font:\"UsedTwo\"}p{font-family:var(--probe-font),serif}" }),
|
|
72
|
+
["usedone", "usedtwo"]);
|
|
73
|
+
|
|
74
|
+
check("a fallback list is parsed as a list, not as one name",
|
|
75
|
+
run({ rules: ":root{--probe-font:\"UsedOne\"}.card p{font-family:var(--probe-font,\"UsedTwo\",\"UsedThree\"),serif}", computed: { "--probe-font": "\"UsedOne\"" } }),
|
|
76
|
+
["usedone", "usedtwo", "usedthree"]);
|
|
77
|
+
|
|
78
|
+
check("the font shorthand resolves a uniquely declared property",
|
|
79
|
+
run({ rules: ".card{--probe-font:\"UsedOne\"}.card p{font:italic 1em var(--probe-font)}" }),
|
|
80
|
+
["usedone"]);
|
|
81
|
+
|
|
82
|
+
check("the font shorthand resolves a property written with a fallback",
|
|
83
|
+
run({ rules: ".card{--probe-font:\"UsedOne\"}.card p{font:italic 1em var(--probe-font,serif)}" }),
|
|
84
|
+
["usedone"]);
|
|
85
|
+
|
|
86
|
+
// the conservative half of the contract: when the value genuinely cannot be determined, every
|
|
87
|
+
// declared font stays, including the one no rule names. These are the cases the union does not
|
|
88
|
+
// cover, and getting them wrong loses fonts from the saved page rather than merely wasting bytes
|
|
89
|
+
check("a property declared nowhere keeps every font",
|
|
90
|
+
run({ rules: ".card p{font-family:var(--set-by-script),serif}" }),
|
|
91
|
+
ALL_FAMILIES);
|
|
92
|
+
|
|
93
|
+
// the shorthand cannot be substituted with several candidate values, but its var() sits in family
|
|
94
|
+
// position, so the parser hands it back as the family and the union answers it there instead
|
|
95
|
+
check("a font shorthand with several candidates resolves through the family",
|
|
96
|
+
run({ rules: ".card{--probe-font:\"UsedOne\"}.note{--probe-font:\"UsedTwo\"}p{font:italic 1em var(--probe-font)}" }),
|
|
97
|
+
["usedone", "usedtwo"]);
|
|
98
|
+
|
|
99
|
+
// a property holding the whole shorthand is the case the union must NOT touch: its values are not
|
|
100
|
+
// family lists, and reading them as such would drop every font in the document
|
|
101
|
+
check("a property holding a whole shorthand keeps every font",
|
|
102
|
+
run({ rules: ".card{--font:italic 1em \"UsedOne\"}.note{--font:italic 1em \"UsedTwo\"}p{font:var(--font)}" }),
|
|
103
|
+
ALL_FAMILIES);
|
|
104
|
+
|
|
105
|
+
check("a var() nested in a fallback keeps every font",
|
|
106
|
+
run({ rules: ".card{--probe-font:\"UsedOne\"}.card p{font-family:var(--other,var(--probe-font),serif)}" }),
|
|
107
|
+
ALL_FAMILIES);
|
|
108
|
+
|
|
109
|
+
check("a property whose value is another undetermined property keeps every font",
|
|
110
|
+
run({ rules: ".card{--probe-font:var(--set-by-script)}.card p{font-family:var(--probe-font),serif}" }),
|
|
111
|
+
ALL_FAMILIES);
|
|
112
|
+
|
|
113
|
+
// An unquoted family name is a sequence of identifier tokens, and the walk that joins them has to
|
|
114
|
+
// resume after the LAST of them. Resuming after the first pushed every word but that one again as a
|
|
115
|
+
// family in its own right, so "Helvetica Neue Light" also claimed fonts named "Neue Light" and
|
|
116
|
+
// "Light". It only ever kept too much, which is why it went unnoticed; a page declaring a family
|
|
117
|
+
// whose name is the tail of another one paid for it.
|
|
118
|
+
const TAIL_FACES = `
|
|
119
|
+
@font-face{font-family:"Helvetica Neue Light";src:url(one.woff2)}
|
|
120
|
+
@font-face{font-family:"Neue Light";src:url(two.woff2)}
|
|
121
|
+
@font-face{font-family:"Light";src:url(three.woff2)}`;
|
|
122
|
+
|
|
123
|
+
const TAIL_USED_FONTS = [
|
|
124
|
+
["helvetica neue light", "400", "normal", "normal"],
|
|
125
|
+
["neue light", "400", "normal", "normal"],
|
|
126
|
+
["light", "400", "normal", "normal"]
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
check("a multi-word family name is read whole",
|
|
130
|
+
run({ faces: TAIL_FACES, usedFonts: TAIL_USED_FONTS, rules: "p{font-family:Helvetica Neue Light,serif}" }),
|
|
131
|
+
["helvetica neue light"]);
|
|
132
|
+
|
|
133
|
+
check("a quoted name is still matched by its quoted declaration",
|
|
134
|
+
run({ faces: TAIL_FACES, usedFonts: TAIL_USED_FONTS, rules: "p{font-family:\"Neue Light\",serif}" }),
|
|
135
|
+
["neue light"]);
|
|
136
|
+
|
|
137
|
+
// the token that ends a name is not always the comma: a string or a number ends it too, and the
|
|
138
|
+
// walk used to append the token's absent `name` to the family as the text "undefined"
|
|
139
|
+
check("a name is ended by a token that is not an identifier",
|
|
140
|
+
run({ faces: TAIL_FACES, usedFonts: TAIL_USED_FONTS, rules: "p{font-family:Light \"Neue Light\"}" }),
|
|
141
|
+
["neue light", "light"]);
|
|
142
|
+
|
|
143
|
+
console.log(failures ? `\n${failures} check(s) FAILED` : "\nall checks passed");
|
|
144
|
+
Deno.exit(failures ? 1 : 0);
|
|
145
|
+
|
|
146
|
+
function run({ rules, computed, faces = FONT_FACES, usedFonts = USED_FONTS }) {
|
|
147
|
+
const stylesheet = cssTree.parse(faces + rules);
|
|
148
|
+
const options = { usedFonts };
|
|
149
|
+
const originalGetComputedStyle = globalThis.getComputedStyle;
|
|
150
|
+
if (computed) {
|
|
151
|
+
options.doc = { body: {} };
|
|
152
|
+
globalThis.getComputedStyle = () => ({ getPropertyValue: name => computed[name] || "" });
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
removeUnusedFonts(createStubDocument(), [{ stylesheet }], [], options);
|
|
156
|
+
} finally {
|
|
157
|
+
globalThis.getComputedStyle = originalGetComputedStyle;
|
|
158
|
+
}
|
|
159
|
+
const families = [];
|
|
160
|
+
stylesheet.children.forEach(ruleData => {
|
|
161
|
+
if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
|
|
162
|
+
const declaration = ruleData.block.children.filter(node => node.property == "font-family").tail;
|
|
163
|
+
families.push(cssTree.generate(declaration.data.value).replace(/^"|"$/g, "").toLowerCase());
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return families;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function check(label, actual, expected) {
|
|
170
|
+
const ok = actual.length == expected.length && actual.every((family, index) => family == expected[index]);
|
|
171
|
+
if (!ok) {
|
|
172
|
+
failures++;
|
|
173
|
+
}
|
|
174
|
+
console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual.join(", ") || "(none)"}${ok ? "" : " (expected " + expected.join(", ") + ")"}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// the module only reaches the document to borrow a <style> element for unescaping content values
|
|
178
|
+
// and to read the body text, so a full DOM is not needed here
|
|
179
|
+
function createStubDocument() {
|
|
180
|
+
return {
|
|
181
|
+
createElement: () => ({ textContent: "", remove() { } }),
|
|
182
|
+
body: { appendChild() { }, innerText: "" }
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The CSS-removal option runs every declaration through css-tree's lexer and drops the ones it
|
|
2
|
+
// judges invalid. css-tree reports two different failures the same way — a real value mismatch
|
|
3
|
+
// (SyntaxMatchError) and a property it simply has no entry for (SyntaxReferenceError) — so the
|
|
4
|
+
// original `!matchProperty(...).matched` deleted valid declarations whose property is missing from
|
|
5
|
+
// the vendored dictionary. That silently flattened every CSS-defined SVG gradient: stop-opacity was
|
|
6
|
+
// dropped, both stops became opaque, and the gradient rendered as a solid block. It hid for a long
|
|
7
|
+
// time because stop-color survived alongside it — a var() value takes a different branch and is
|
|
8
|
+
// never handed to the lexer at all, so the colour was right and only the fade was gone.
|
|
9
|
+
//
|
|
10
|
+
// The rule this pins: unknown must fail open. A dropped valid declaration breaks rendering; a kept
|
|
11
|
+
// invalid one is ignored by the browser.
|
|
12
|
+
import * as cssTree from "../../vendor/css-tree.js";
|
|
13
|
+
import { isUnsupportedPropertyValue } from "../../modules/css-rules-minifier.js";
|
|
14
|
+
|
|
15
|
+
// valid declarations whose property the vendored css-tree does not know. Every one of these was
|
|
16
|
+
// deleted before the fix. The SVG paint-server and filter properties are the ones that matter in
|
|
17
|
+
// practice; the last two are here as the moving target — any property newer than the vendored
|
|
18
|
+
// dictionary lands in this bucket, and the list will keep growing
|
|
19
|
+
const MUST_KEEP = [
|
|
20
|
+
["stop-color", "#2a78d6"],
|
|
21
|
+
["stop-opacity", ".20"],
|
|
22
|
+
["stop-opacity", "0"],
|
|
23
|
+
["flood-color", "red"],
|
|
24
|
+
["flood-opacity", "0.5"],
|
|
25
|
+
["lighting-color", "white"],
|
|
26
|
+
["text-box-trim", "trim-both"],
|
|
27
|
+
["corner-shape", "squircle"]
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// declarations css-tree knows and correctly rejects. These keep the filter honest: the fix must not
|
|
31
|
+
// turn the check off, only stop it firing on properties css-tree has never heard of
|
|
32
|
+
const MUST_DROP = [
|
|
33
|
+
["margin-trim", "block"],
|
|
34
|
+
["opacity", "not-a-number"],
|
|
35
|
+
["color", "12px"]
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// properties close enough to the broken ones to be worth stating outright, because they are the
|
|
39
|
+
// reason the bug looked like an SVG quirk rather than a dictionary gap: css-tree does know these,
|
|
40
|
+
// so gradients that used them were unaffected and the failure looked arbitrary
|
|
41
|
+
const KNOWN_GOOD = [
|
|
42
|
+
["fill-opacity", "0.5"],
|
|
43
|
+
["stroke-opacity", "0.5"],
|
|
44
|
+
["fill", "red"],
|
|
45
|
+
["vector-effect", "non-scaling-stroke"]
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
let failed = false;
|
|
49
|
+
|
|
50
|
+
function check(label, actual, expected) {
|
|
51
|
+
const ok = actual === expected;
|
|
52
|
+
console.log(`${ok ? "PASS" : "FAIL"} ${label}: ${actual}${ok ? "" : " (expected " + expected + ")"}`);
|
|
53
|
+
failed ||= !ok;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isDropped(property, declaration) {
|
|
57
|
+
return isUnsupportedPropertyValue(property, cssTree.parse(declaration, { context: "value" }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [property, declaration] of MUST_KEEP) {
|
|
61
|
+
check(`kept: ${property}: ${declaration}`, isDropped(property, declaration), false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const [property, declaration] of MUST_DROP) {
|
|
65
|
+
check(`dropped: ${property}: ${declaration}`, isDropped(property, declaration), true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const [property, declaration] of KNOWN_GOOD) {
|
|
69
|
+
check(`kept: ${property}: ${declaration}`, isDropped(property, declaration), false);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// the distinction the fix rests on. If a css-tree upgrade ever collapses these two error types into
|
|
73
|
+
// one, the predicate above cannot tell "unknown" from "wrong" any more and needs rewriting rather
|
|
74
|
+
// than adjusting
|
|
75
|
+
const unknownProperty = cssTree.lexer.matchProperty("stop-opacity", cssTree.parse("0", { context: "value" }));
|
|
76
|
+
const wrongValue = cssTree.lexer.matchProperty("margin-trim", cssTree.parse("block", { context: "value" }));
|
|
77
|
+
check("unknown property reports SyntaxReferenceError", unknownProperty.error && unknownProperty.error.name, "SyntaxReferenceError");
|
|
78
|
+
check("wrong value reports SyntaxMatchError", wrongValue.error && wrongValue.error.name, "SyntaxMatchError");
|
|
79
|
+
|
|
80
|
+
if (failed) {
|
|
81
|
+
console.log("FAILED");
|
|
82
|
+
Deno.exit(1);
|
|
83
|
+
} else {
|
|
84
|
+
console.log("PASSED");
|
|
85
|
+
}
|
|
@@ -207,6 +207,34 @@ function countIdentifiers(text) {
|
|
|
207
207
|
check("the cdata rung leaves the appended data placement alone", options.preventAppendedData !== true, true);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// the rung a caller names is resolved by its start tag, not by the identity of the array holding
|
|
211
|
+
// it. The option is internal and is normally set from the module's own EXTRA_DATA_TAGS, so an
|
|
212
|
+
// identity match worked by accident; a caller passing an equal pair of its own indexed the regexp
|
|
213
|
+
// table with -1 and threw a bare TypeError out of library internals
|
|
214
|
+
{
|
|
215
|
+
const options = makeOptions({ extractDataFromPageTags: ["<noframes>", "</noframes>"] });
|
|
216
|
+
const pageData = makePageData(31, 4 * 1024);
|
|
217
|
+
let error;
|
|
218
|
+
try {
|
|
219
|
+
await runProcess(pageData, options);
|
|
220
|
+
} catch (caught) {
|
|
221
|
+
error = caught;
|
|
222
|
+
}
|
|
223
|
+
check("a rung named by an equal pair is resolved", error, undefined);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
{
|
|
227
|
+
const options = makeOptions({ extractDataFromPageTags: ["<marquee>", "</marquee>"] });
|
|
228
|
+
const pageData = makePageData(32, 4 * 1024);
|
|
229
|
+
let message;
|
|
230
|
+
try {
|
|
231
|
+
await runProcess(pageData, options);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
message = error.message;
|
|
234
|
+
}
|
|
235
|
+
check("a rung that is not in the ladder names itself", message, "Unknown data tags: <marquee>");
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
// the terminator is the whole test for this rung, so a payload holding it must step past
|
|
211
239
|
{
|
|
212
240
|
const content = new Uint8Array(4096).fill(0x21);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2010-2026 Gildas Lormeau
|
|
3
|
+
* contact : gildas.lormeau <at> gmail.com
|
|
4
|
+
*
|
|
5
|
+
* This file is part of SingleFile.
|
|
6
|
+
*
|
|
7
|
+
* The code in this file is free software: you can redistribute it and/or
|
|
8
|
+
* modify it under the terms of the GNU Affero General Public License
|
|
9
|
+
* (GNU AGPL) as published by the Free Software Foundation, either version 3
|
|
10
|
+
* of the License, or (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* The code in this file is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
|
|
15
|
+
* General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* As additional permission under GNU AGPL version 3 section 7, you may
|
|
18
|
+
* distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
|
|
19
|
+
* AGPL normally required by section 4, provided you include this license
|
|
20
|
+
* notice and a URL through which recipients can access the Corresponding
|
|
21
|
+
* Source.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// `createArchive` builds the script of a self-extracting archive by serializing functions with
|
|
25
|
+
// toString() and pasting the text into the generated page. That page has no module scope, so
|
|
26
|
+
// anything such a function names has to be declared inside it. An import is the trap: it survives
|
|
27
|
+
// bundling, the call still reads correctly in the source, and the archive then fails at runtime
|
|
28
|
+
// with a bare ReferenceError while the page stays blank.
|
|
29
|
+
//
|
|
30
|
+
// It is worse than it looks, because a minifier can hide it. `display` called an imported
|
|
31
|
+
// `getDoctypeString`, and terser inlined that helper into the release bundle, so released
|
|
32
|
+
// archives worked while every unminified build produced archives that threw
|
|
33
|
+
// `ReferenceError: getDoctypeString is not defined` and rendered nothing.
|
|
34
|
+
//
|
|
35
|
+
// The invariant below is what makes that impossible to reintroduce: a module holding an inlined
|
|
36
|
+
// function declares that function and nothing else, so there is no module scope to close over.
|
|
37
|
+
|
|
38
|
+
const MODULES = [
|
|
39
|
+
{ path: "../../processors/compression/compression-display.js", functions: ["display"] },
|
|
40
|
+
{ path: "../../processors/compression/compression-extract.js", functions: ["extract"] },
|
|
41
|
+
{ path: "../../processors/compression/compression-router.js", functions: ["router"] }
|
|
42
|
+
];
|
|
43
|
+
const TOP_LEVEL_DECLARATION = /^(?:export\s+)?(?:async\s+)?(?:function|const|let|var|class)\s+([A-Za-z_$][\w$]*)/;
|
|
44
|
+
|
|
45
|
+
let failures = 0;
|
|
46
|
+
|
|
47
|
+
for (const { path, functions } of MODULES) {
|
|
48
|
+
const source = await Deno.readTextFile(new URL(path, import.meta.url));
|
|
49
|
+
const name = path.split("/").pop();
|
|
50
|
+
const lines = source.split("\n");
|
|
51
|
+
const imports = lines.filter(line => /^import[\s{]/.test(line) || /^\s*import\s*\(/.test(line));
|
|
52
|
+
check(name + " imports nothing", imports.length === 0, imports.join(" | "));
|
|
53
|
+
const declared = [];
|
|
54
|
+
lines.forEach(line => {
|
|
55
|
+
if (line === line.trimStart()) {
|
|
56
|
+
const match = line.match(TOP_LEVEL_DECLARATION);
|
|
57
|
+
if (match) {
|
|
58
|
+
declared.push(match[1]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const unexpected = declared.filter(identifier => !functions.includes(identifier));
|
|
63
|
+
check(name + " declares nothing at module scope but " + functions.join(", "),
|
|
64
|
+
unexpected.length === 0, unexpected.join(", "));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// the one inlined function that does live beside module scope, in compression.js, states the rule
|
|
68
|
+
// in a comment and binds every global it uses itself; assert that binding is still there
|
|
69
|
+
const compression = await Deno.readTextFile(new URL("../../processors/compression/compression.js", import.meta.url));
|
|
70
|
+
const getContentBody = compression.slice(compression.indexOf("async function getContent()"));
|
|
71
|
+
check("getContent binds its globals locally",
|
|
72
|
+
/const \{[^}]*\} = globalThis;/.test(getContentBody.slice(0, 2000)));
|
|
73
|
+
|
|
74
|
+
console.log(failures ? `\n${failures} check(s) FAILED` : "\nall checks passed");
|
|
75
|
+
Deno.exit(failures ? 1 : 0);
|
|
76
|
+
|
|
77
|
+
function check(label, condition, detail) {
|
|
78
|
+
if (!condition) {
|
|
79
|
+
failures++;
|
|
80
|
+
}
|
|
81
|
+
console.log((condition ? "PASS" : "FAIL") + " " + label + (condition || !detail ? "" : ": " + detail));
|
|
82
|
+
}
|