Skip to main content

max / makenotwork

10.1 KB · 237 lines History Blame Raw
1 //! Forward fence, the frontend-globals ratchet.
2 //!
3 //! The legacy `static/*.js` files register behavior as `window.<name> = fn`
4 //! globals (the pre-module `data-action` dispatcher pattern). The frontend
5 //! restructure (`_private/docs/mnw/frontend/`) migrates these into typed ES
6 //! modules under `frontend/src`. This seal keeps the count monotonically
7 //! non-increasing: adding a new global fails the build, and every migrated file
8 //! must lower `HIGH_WATER`. Same idea as the migration HIGH_WATER seal.
9 //!
10 //! When you remove globals, lower `HIGH_WATER` to the new count (the failure
11 //! message reports it). Never raise it.
12 //!
13 //! # What counts as a global
14 //!
15 //! Both trees are read, `static/*.js` and `frontend/src`, and an alias bound to
16 //! `window` in a file is followed within that file: a typed module assigning
17 //! through `const w = window as unknown as Record<string, unknown>` is counted
18 //! like a direct `window.x =`. Without that, moving a file out of `static/`
19 //! while keeping its global lowers the seal without lowering the global
20 //! surface, and a conversion batch reports progress it did not make.
21 //!
22 //! A bare top-level `function foo() {}` in a classic script is a global too,
23 //! with no assignment to match. Every `static/*.js` file is loaded by a plain
24 //! `<script src>` with no `type="module"`, so those declarations count.
25 //!
26 //! **The rule is per tree, and that is the point.** A top-level function in an
27 //! ES module under `frontend/src` is scoped to the module and is not a global,
28 //! so [`count_script_globals`] runs only over `static/*.js` while
29 //! [`count_globals`] runs over both. Applying it everywhere would count the
30 //! typed tree's own functions as the surface the typed tree exists to remove.
31 //!
32 //! `HIGH_WATER` moves down when globals are removed, and is restated only when
33 //! a correction changes what this test can see. It must not move for any other
34 //! reason.
35
36 use std::fs;
37 use std::path::{Path, PathBuf};
38
39 /// Ratchets down only, never up. Lower it to the count the failure message
40 /// reports whenever globals are removed.
41 ///
42 /// A ratchet that only refuses to grow lets removals go unrecorded, so this
43 /// number is a ceiling rather than a progress figure: measure before trusting
44 /// it as one.
45 const HIGH_WATER: usize = 124;
46
47 /// Every file whose global assignments count: the legacy scripts and the typed
48 /// modules that replaced them.
49 ///
50 /// `frontend/src` is walked rather than listed, because the whole point of the
51 /// restructure is that files move there, and a seal that has to be told about
52 /// each new one is a seal that silently stops covering the tree.
53 fn sources() -> Vec<PathBuf> {
54 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
55 let mut files = Vec::new();
56
57 for entry in fs::read_dir(root.join("static")).expect("read static/ dir") {
58 let path = entry.expect("dir entry").path();
59 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
60 continue;
61 };
62 // Vendored libraries are not our global surface, and a minified one
63 // aliases `window` internally dozens of times, so the alias rule below
64 // would read htmx's own bookkeeping as names this server put on the
65 // page. `htmx.min.js` did contribute one direct `window.htmx =` to the
66 // old count; that was noise the narrower regex happened to admit.
67 let is_js = path.extension().and_then(|e| e.to_str()) == Some("js");
68 if name.ends_with(".min.js") || !is_js {
69 continue;
70 }
71 files.push(path);
72 }
73 collect_ts(&root.join("frontend/src"), &mut files);
74
75 files
76 }
77
78 /// The typed tree, recursively. Type declarations and tests are skipped: a
79 /// `.d.ts` describes globals rather than creating them, and a test asserting on
80 /// one is not a page that ships one.
81 fn collect_ts(dir: &Path, out: &mut Vec<PathBuf>) {
82 let Ok(entries) = fs::read_dir(dir) else {
83 return;
84 };
85 for entry in entries {
86 let path = entry.expect("dir entry").path();
87 if path.is_dir() {
88 collect_ts(&path, out);
89 continue;
90 }
91 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
92 continue;
93 };
94 if name.ends_with(".d.ts") || name.contains(".test.") {
95 continue;
96 }
97 if matches!(path.extension().and_then(|e| e.to_str()), Some("ts" | "js")) {
98 out.push(path);
99 }
100 }
101 }
102
103 /// Count bare top-level function declarations, in one CLASSIC-script file.
104 ///
105 /// `82315c45`. A `<script src>` with no `type="module"` shares the global
106 /// scope, so `function foo() {}` at the top level of one lands `foo` on the
107 /// global object without ever writing an assignment. Neither rule in
108 /// [`count_globals`] can see that, and 55 of this server's globals are that
109 /// shape.
110 ///
111 /// **Only for `static/*.js`.** The same declaration in an ES module under
112 /// `frontend/src` is scoped to the module and is not a global, so counting it
113 /// there would charge the typed tree for the surface it exists to remove.
114 fn count_script_globals(src: &str) -> usize {
115 // Column zero is the whole test for "top level", and it is enough because
116 // every one of the 55 is written that way. A declaration indented inside an
117 // IIFE is correctly not counted: that is a function scope, not the global
118 // object. A regex cannot know the difference and does not have to.
119 regex::Regex::new(r"(?m)^(?:async )?function [A-Za-z_][A-Za-z0-9_]*\s*\(")
120 .unwrap()
121 .find_iter(src)
122 .count()
123 }
124
125 /// Count assignments that land a name on `window`, in one file.
126 ///
127 /// Two forms, because the code uses two. The direct `window.foo = …` is what
128 /// the legacy scripts write. The aliased form is what a typed module writes,
129 /// since assigning to `window.foo` in TypeScript needs the index signature the
130 /// alias provides, so the bridge binds `window` to a local first.
131 ///
132 /// The alias is followed only inside the file that binds it, and only when it
133 /// was bound to `window` itself, so a local named `w` holding anything else
134 /// contributes nothing.
135 ///
136 /// Runs over both trees, unlike [`count_script_globals`]: an assignment to
137 /// `window` is a global wherever it is written.
138 fn count_globals(src: &str) -> usize {
139 // `=[^=]` matches an assignment `=` while excluding the first `=` of a
140 // comparison operator. The regex crate has no lookahead, so this is the
141 // portable form.
142 let direct = regex::Regex::new(r"window\.[A-Za-z_][A-Za-z0-9_]*\s*=[^=]").unwrap();
143 let binding =
144 regex::Regex::new(r"(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*window\b").unwrap();
145
146 let mut total = direct.find_iter(src).count();
147
148 for bound in binding.captures_iter(src) {
149 let alias = &bound[1];
150 let through =
151 regex::Regex::new(&format!(r"\b{alias}\.[A-Za-z_][A-Za-z0-9_]*\s*=[^=]")).unwrap();
152 total += through.find_iter(src).count();
153 }
154
155 total
156 }
157
158 #[test]
159 fn frontend_globals_do_not_grow() {
160 let count: usize = sources()
161 .iter()
162 .map(|path| {
163 let src = fs::read_to_string(path).unwrap_or_default();
164 // A bare top-level function is a global only in a classic script,
165 // which is what `static/` holds and `frontend/src` does not. See
166 // `count_script_globals`.
167 let classic = path.parent().and_then(Path::file_name) == Some("static".as_ref());
168 count_globals(&src)
169 + if classic {
170 count_script_globals(&src)
171 } else {
172 0
173 }
174 })
175 .sum();
176
177 assert!(
178 count <= HIGH_WATER,
179 "globals across static/*.js and frontend/src rose to {count} \
180 (HIGH_WATER {HIGH_WATER}). New frontend code must be a typed ES module in \
181 frontend/src registered through the dispatcher, not a global -- and note \
182 that a bare top-level `function` in a classic script is one too. If you \
183 REMOVED globals, lower HIGH_WATER to {count}."
184 );
185 }
186
187 #[test]
188 fn the_seal_sees_a_bare_top_level_function_in_a_classic_script() {
189 // `82315c45`. The shape that was invisible until 2026-08-26: no assignment
190 // anywhere, and `exportItemSalesCSV` was still a global the dispatcher
191 // called by name.
192 assert_eq!(
193 count_script_globals("function exportItemSalesCSV() {\n}\n"),
194 1
195 );
196 assert_eq!(count_script_globals("async function loadThing(id) {}"), 1);
197
198 // Two shapes that are NOT the global scope, and must not be counted.
199 assert_eq!(
200 count_script_globals("(function () {\n function inner() {}\n})();"),
201 0,
202 "a declaration inside an IIFE is a function scope"
203 );
204 assert_eq!(count_script_globals("const f = function named() {};"), 0);
205 }
206
207 #[test]
208 fn the_two_counters_do_not_double_count_one_name() {
209 // A file that both declares and assigns is two globals by two routes, and
210 // the seal should say two rather than collapsing them. The point is that
211 // neither counter reaches into the other's shape.
212 let src = "function draw() {}\nwindow.draw = draw;\n";
213 assert_eq!(count_script_globals(src), 1);
214 assert_eq!(count_globals(src), 1);
215 }
216
217 #[test]
218 fn the_seal_sees_a_global_assigned_through_an_alias() {
219 // The hole `7df1a7de` names, as a unit. Without this the count is a
220 // measure of which directory a file sits in rather than of how many names
221 // reach `window`, and a migration that moves a file and keeps its global
222 // reads as progress.
223 let bridged = "const w = window as unknown as Record<string, unknown>;\n\
224 w.escapeHtml = escapeHtml;\n\
225 w.showToast = showToast;\n";
226 assert_eq!(count_globals(bridged), 2);
227
228 // A local that is not `window` contributes nothing, which is what keeps the
229 // alias rule from counting every object property assignment in the tree.
230 let ordinary = "const w = document.body;\nw.className = 'x';\n";
231 assert_eq!(count_globals(ordinary), 0);
232
233 // And a comparison is not an assignment, in either form.
234 let compared = "if (window.foo === bar) {}\nconst w = window;\nif (w.foo == bar) {}\n";
235 assert_eq!(count_globals(compared), 0);
236 }
237