Html monolith refactor
Skill prithvi020397/agent-refactor-skills/skills/html-monolith-refactor
Community agent skills for safe, behavior-preserving refactoring (OpenCode, Claude Code, and any SKILL.md-compatible agent)
npx -y skills add prithvi020397/agent-refactor-skills --skill html-monolith-refactorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 18 days oldThe repository was created 18 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use this skill whenever the user asks to refactor, split, restructure, or clean up an HTML file that contains inline <style> and/or <script> blocks — especially a single-file frontend served by Flask/Jinja (or Django) via render_template. Covers extracting CSS/JS into static files, splitting a monolithic script into ordered modules, and centralizing global state, all WITHOUT changing behavior, adding a bundler, or touching the backend.
SKILL.md
9.4 KB, as published. Nobody here has run it
HTML Monolith Refactor
Safely split a single-file HTML frontend (inline CSS + inline vanilla JS, no framework, no build step) into separate static assets, keeping the app fully runnable and deployable after every step.
Scope
- Modify ONLY the template and static assets. NEVER modify backend routes/handlers. Sole exception: injecting an asset version string for cache busting — and only with explicit user approval.
- Do not introduce npm, bundlers, transpilers, or frameworks.
- Do not proceed to any "optional cleanup" step without explicit user opt-in.
Non-Negotiable Rules
1. Classic scripts only
Extracted JS files are CLASSIC scripts loaded with defer, in explicit
numbered order:
00-config.js constants, endpoint paths
01-state.js central App.state
02-utils.js fetch wrapper, DOM helpers
10..79-*.js feature modules (auth, data, media, canvas, render, export...)
90-main.js ALL top-level executable statements / init
NEVER use type="module". Inline handlers (onclick="fn()") require
top-level functions to be global; modules break that silently.
2. DOM contract is frozen
Existing element IDs, class names, and inline event-handler attributes are a
public API. Never rename or remove them. Every function referenced by an
inline handler stays global. 90-main.js must assert them at load:
["startPractice", "toggleMic" /* every inline-handler fn */].forEach(f =>
console.assert(typeof window[f] === "function", `missing global: ${f}`));
3. Jinja safety
BEFORE extracting any <style> or <script> block, grep it for {{ and
{%. Templated fragments must NOT move to /static (Flask does not render
static files — they will be served literally and fail silently).
- Route dynamic values through ONE inline bootstrap script:
window.APP_BOOT = { ... }— the only inline JS that remains. - Jinja-dependent CSS stays in a small inline
<style>block. - All asset URLs in the template use
url_for('static', filename=...).
4. Cache busting
Every <link> and <script src> gets a ?v= version string. Warn the user:
deploying split assets WITHOUT cache busting will break returning users who
load new HTML with stale cached JS.
Migration Order
Each step = one commit = app fully runnable = smoke checklist passes. Copy CHECKLIST.md from this skill folder into the target repo and log each step there.
-
Step 0 — Baseline. Commit current state. Write a smoke checklist covering every major user flow (auth, data loading, media recording, canvas, rendering, export — whatever the app has). Record any PRE-EXISTING console errors so later regressions are distinguishable.
-
Step 1 — Extract CSS. Move the
<style>block verbatim tostatic/css/app.css; add the<link>. Grep for Jinja first (Rule 3). -
Step 2 — Split JS verbatim. Cut the monolithic
<script>into the numbered files IN THE ORIGINAL SOURCE ORDER. Zero logic changes. Load withdeferin order. (Classic deferred scripts share one global scope and execute in document order — behaviorally identical to one block.)Two valid forms of this step:
- Numbered files now — if you can identify clean feature boundaries,
cut straight into
00-config.js…90-main.js. - Single deferred bundle first — if boundaries are unclear, extract the
whole block verbatim to one
app.bundle.jsloaded withdefer, verify it, then do the numbered split in Step 3. This is the most verifiable verbatim split (one file, onenode --check) and is explicitly allowed. Either form is acceptable as long as the result is behaviorally identical.
- Numbered files now — if you can identify clean feature boundaries,
cut straight into
-
Step 3 — Reorganize. Move functions into their logical modules. Rule: numbered module files may only DEFINE things; every top-level executable statement moves to
90-main.js(single init point). This is the only step where hoisting/load-order bugs can appear — smoke test thoroughly. As each module takes shape, add alog()call at its key entry points (see Step 3.5).If the source is feature-tangled (e.g. canvas, recording, grading, and scenario logic interleaved with no clean seams), do NOT force a semantic split — that requires risky cross-file moves. Instead do a lossless ordered slice: cut the block at points where the cumulative brace depth returns to 0 (so each slice is itself syntactically valid), emit files in original order, then verify (a)
node --checkpasses on every file and (b) concatenating the slices reproduces the original byte-for-byte. Name the files generically/ordered (e.g.00-config.js,20-module.js…120-module.js) rather than falsely semantic — the lossless +node --checkverification, not semantic naming, is the real safety net. -
Step 3.5 — Add logging (inline with the refactor, not after). Add a single
log()helper in02-utils.jsand emit logs as you migrate each module — do NOT add logging upfront (Step 0–2) or defer it to the end. This keeps logs next to the code that owns them and avoids logging code that is about to move.Define exactly one helper, debug-gated so it is silent for end users:
// 02-utils.js function log(...args) { if (window.APP_BOOT && window.APP_BOOT.debug) { console.debug("[loop]", ...args); } }-
Per-module entry points: sprinkle
log(...)at each module's key transitions as you touch it in Step 3 — e.g.20-scenario.jslogs when a question loads,30-recording.jslogs mic start/stop + transcribe result,50-coach.jslogs judge/coaching render. One module per commit, so logs land with the code that owns them. -
Every fetch, automatically: the
api()wrapper (Step 4) is the highest-value logging spot. Logmethod path status msin one place so all network calls are visible without touching call sites:// 02-utils.js async function api(path, opts = {}) { const t0 = performance.now(); const res = await fetch(path, opts); log(opts.method || "GET", path, res.status, Math.round(performance.now() - t0) + "ms"); return res; } -
Gate it: logs fire only when
window.APP_BOOT.debugis true, so production stays silent. SetAPP_BOOT.debug = trueduring local testing (see Risk 5 in the plan); remove or leave false before deploy. -
Separate concern: frontend
log()is for local debugging only. Durable request history lives on the backend (itsafter_requestlogger) and the host's log stream — do not build a frontend logging pipeline.
-
-
Step 4 — Fetch wrapper. Add one
api(path, opts)helper in02-utils.js(with the auto-log from Step 3.5). Migrate call sites ONE ENDPOINT AT A TIME, one commit each. Becauseapi()logs centrally, every migrated endpoint gains visibility for free. -
Step 5 — Centralize state. Move magic globals into
App.state. Keep old names alive during migration via aliases:Object.defineProperty(window, "__compareData", { get: () => App.state.compareData, set: v => { App.state.compareData = v; } });Delete each alias only after grep shows zero remaining direct uses. One commit per global.
-
Step 6 — OPTIONAL (explicit user opt-in required). Convert inline handlers to
addEventListener. Default: STOP before this step. It is the highest-risk, lowest-value change.
Risks — surface these to the user BEFORE starting
- Stale cache on deploy — new HTML + cached old JS for returning
users. Mitigation:
?v=versioning (Rule 4). - Jinja inside CSS/JS — silent
undefinedat runtime if moved to static. Mitigation: grep + APP_BOOT bridge (Rule 3). - Hoisting/load-order coupling — in one file, function declarations hoist across 5000 lines; after splitting, only file order preserves this. Mitigation: verbatim order in Step 2; definitions-only rule in Step 3.
- Double initialization — init code left in a module AND in 90-main.js causes double listeners/fetches. Mitigation: grep for stray top-level calls before each commit.
- Media/permission flows — getUserMedia/MediaRecorder behave differently off HTTPS. Test on the deployed HTTPS URL, not just localhost.
- Logging is silent by default. The
log()helper (Step 3.5) is gated behindwindow.APP_BOOT.debug. If you add logs but forget to setAPP_BOOT.debug = truelocally, you will see nothing — that is intended, not a bug. Never makelog()always-on in production; it is a local debugging aid, not telemetry.
Verification (after EVERY step)
- Run the smoke checklist end to end.
- Browser console: no NEW errors vs the Step-0 baseline.
git diffon the template shows markup unchanged except removed<style>/<script>bodies and added<link>/<script src>tags.- During local testing, add
window.onerror = e => alert(e)temporarily so nothing fails silently; remove before commit.