max / makenotwork
| 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 this used to miss, and why the number moved once |
| 14 | //! |
| 15 | //! Problem `7df1a7de`. Until 2026-08-10 this read `static/*.js` and nothing |
| 16 | //! else, and matched the literal text `window.<ident> =`. The typed core module |
| 17 | //! defeats both halves at once: it lives in `frontend/src`, and |
| 18 | //! `installLegacyBridge` assigns through an alias |
| 19 | //! (`const w = window as unknown as Record<string, unknown>; w.escapeHtml = …`). |
| 20 | //! So its globals were invisible, and **moving a file out of `static/` while |
| 21 | //! keeping its global lowered the seal without lowering the global surface**. |
| 22 | //! A conversion batch could report progress it did not make, which is the one |
| 23 | //! failure mode a ratchet exists to prevent. |
| 24 | //! |
| 25 | //! Both halves are fixed here: both trees are read, and an alias bound to |
| 26 | //! `window` in a file is followed within that file. `HIGH_WATER` was restated |
| 27 | //! once against the corrected measurement, which is not the same act as raising |
| 28 | //! it. It has not moved since and must not. |
| 29 | |
| 30 | use fs; |
| 31 | use ; |
| 32 | |
| 33 | /// Ratchets down only, never up. 115 at the start of the restructure |
| 34 | /// (2026-07-07); 113 after Phase 1 retired `mnw.js`. Restated to 124 on |
| 35 | /// 2026-08-10, when the measurement was corrected in two directions at once: |
| 36 | /// the typed core module's 12 bridged globals became visible, and htmx's one |
| 37 | /// vendored `window.htmx =` stopped counting. 113 + 12 - 1 = 124. The surface |
| 38 | /// did not change that day; what this test can see did. |
| 39 | const HIGH_WATER: usize = 124; |
| 40 | |
| 41 | /// Every file whose global assignments count: the legacy scripts and the typed |
| 42 | /// modules that replaced them. |
| 43 | /// |
| 44 | /// `frontend/src` is walked rather than listed, because the whole point of the |
| 45 | /// restructure is that files move there, and a seal that has to be told about |
| 46 | /// each new one is a seal that silently stops covering the tree. |
| 47 | |
| 48 | let root = new; |
| 49 | let mut files = Vecnew; |
| 50 | |
| 51 | for entry in read_dir.expect |
| 52 | let path = entry.expect.path; |
| 53 | let Some = path.file_name.and_then else |
| 54 | continue; |
| 55 | ; |
| 56 | // Vendored libraries are not our global surface, and a minified one |
| 57 | // aliases `window` internally dozens of times, so the alias rule below |
| 58 | // would read htmx's own bookkeeping as names this server put on the |
| 59 | // page. `htmx.min.js` did contribute one direct `window.htmx =` to the |
| 60 | // old count; that was noise the narrower regex happened to admit. |
| 61 | let is_js = path.extension.and_then == Some; |
| 62 | if name.ends_with || !is_js |
| 63 | continue; |
| 64 | |
| 65 | files.push; |
| 66 | |
| 67 | collect_ts; |
| 68 | |
| 69 | files |
| 70 | |
| 71 | |
| 72 | /// The typed tree, recursively. Type declarations and tests are skipped: a |
| 73 | /// `.d.ts` describes globals rather than creating them, and a test asserting on |
| 74 | /// one is not a page that ships one. |
| 75 | |
| 76 | let Ok = read_dir else |
| 77 | return; |
| 78 | ; |
| 79 | for entry in entries |
| 80 | let path = entry.expect.path; |
| 81 | if path.is_dir |
| 82 | collect_ts; |
| 83 | continue; |
| 84 | |
| 85 | let Some = path.file_name.and_then else |
| 86 | continue; |
| 87 | ; |
| 88 | if name.ends_with || name.contains |
| 89 | continue; |
| 90 | |
| 91 | if matches! |
| 92 | out.push; |
| 93 | |
| 94 | |
| 95 | |
| 96 | |
| 97 | /// Count assignments that land a name on `window`, in one file. |
| 98 | /// |
| 99 | /// Two forms, because the code uses two. The direct `window.foo = …` is what |
| 100 | /// the legacy scripts write. The aliased form is what a typed module writes, |
| 101 | /// since assigning to `window.foo` in TypeScript needs the index signature the |
| 102 | /// alias provides, so the bridge binds `window` to a local first. |
| 103 | /// |
| 104 | /// The alias is followed only inside the file that binds it, and only when it |
| 105 | /// was bound to `window` itself, so a local named `w` holding anything else |
| 106 | /// contributes nothing. |
| 107 | |
| 108 | // `=[^=]` matches an assignment `=` while excluding the first `=` of a |
| 109 | // comparison operator. The regex crate has no lookahead, so this is the |
| 110 | // portable form. |
| 111 | let direct = new.unwrap; |
| 112 | let binding = |
| 113 | new.unwrap; |
| 114 | |
| 115 | let mut total = direct.find_iter.count; |
| 116 | |
| 117 | for bound in binding.captures_iter |
| 118 | let alias = &bound; |
| 119 | let through = |
| 120 | new.unwrap; |
| 121 | total += through.find_iter.count; |
| 122 | |
| 123 | |
| 124 | total |
| 125 | |
| 126 | |
| 127 | |
| 128 | |
| 129 | let count: usize = sources |
| 130 | .iter |
| 131 | .map |
| 132 | .sum; |
| 133 | |
| 134 | assert! |
| 135 | count <= HIGH_WATER, |
| 136 | "global assignments across static/*.js and frontend/src rose to {count} \ |
| 137 | (HIGH_WATER {HIGH_WATER}). New frontend code must be a typed ES module in \ |
| 138 | frontend/src registered through the dispatcher, not a global. If you REMOVED \ |
| 139 | globals, lower HIGH_WATER to {count}." |
| 140 | ; |
| 141 | |
| 142 | |
| 143 | |
| 144 | |
| 145 | // The hole `7df1a7de` names, as a unit. Without this the count is a |
| 146 | // measure of which directory a file sits in rather than of how many names |
| 147 | // reach `window`, and a migration that moves a file and keeps its global |
| 148 | // reads as progress. |
| 149 | let bridged = "const w = window as unknown as Record<string, unknown>;\n\ |
| 150 | w.escapeHtml = escapeHtml;\n\ |
| 151 | w.showToast = showToast;\n"; |
| 152 | assert_eq!; |
| 153 | |
| 154 | // A local that is not `window` contributes nothing, which is what keeps the |
| 155 | // alias rule from counting every object property assignment in the tree. |
| 156 | let ordinary = "const w = document.body;\nw.className = 'x';\n"; |
| 157 | assert_eq!; |
| 158 | |
| 159 | // And a comparison is not an assignment, in either form. |
| 160 | let compared = "if (window.foo === bar) {}\nconst w = window;\nif (w.foo == bar) {}\n"; |
| 161 | assert_eq!; |
| 162 | |
| 163 |