mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 21:01:40 +00:00
Some checks failed
ship-ci / ci (push) Has been cancelled
Host enablement for the plugin ES-module migration: sandboxed /api/plugins/{id}/src/ serving, no-cache+weak-ETag/304 live-edit caching on src/+screen.js+assets, scriptType:module loader injection + scriptType/minHost manifest passthrough; constitution v1.2.0 + module playbook + signed size-exemptions register + maintainer/CI-only ESLint gate; rerunnable perf-baseline harness. Reviewed by Codex (local), Copilot, and CodeRabbit.
104 lines
5.6 KiB
Markdown
104 lines
5.6 KiB
Markdown
# Plugin ES-module migration playbook
|
|
|
|
How to move a plugin off a single global-scope `screen.js` IIFE onto a native
|
|
ES-module graph — **no build step, no framework, no bundler**. This is the
|
|
mechanism the monolith-killing refactor uses; the host rails for it shipped in
|
|
R0 (see `.specify/memory/constitution.md` Principle II + the "Module load
|
|
contract" in Operating Constraints).
|
|
|
|
## The shape
|
|
|
|
```
|
|
my-plugin/
|
|
plugin.json + "scriptType": "module" ← opt in
|
|
screen.js import './src/main.js'; ← the entire file
|
|
src/
|
|
state.js (0) module state + accessors
|
|
util/… (1) pure helpers — real-import testable
|
|
…/… (2..4) model → render/audio/io → input
|
|
globals.js (5) THE ONLY file that writes window.*
|
|
main.js (5) boot: wire modules, register screen:changed
|
|
assets/… worklets / WASM / images (unchanged, served as today)
|
|
```
|
|
|
|
`screen.js` becomes a one-line static `import`. The host injects it as
|
|
`<script type="module">`, whose load event fires **only after the whole
|
|
static-import graph fetches and evaluates** — so the loader's
|
|
completion-by-`onload` + `_loadingPluginId` window + `playSong` wrapper-chain
|
|
order are all preserved. (A classic IIFE that fired a fire-and-forget
|
|
`import()` would break that contract — don't do that; use `scriptType:"module"`.)
|
|
|
|
## Non-negotiable rules
|
|
|
|
1. **Source-served, no build.** Modules are plain source files fetched from
|
|
`/api/plugins/<id>/src/<path>`. No bundler, transpiler, or TypeScript.
|
|
2. **Layering points downward** — `state → util → commands/model →
|
|
render/audio/io → input → globals/main`. A lint check (`import-x/no-cycle`)
|
|
enforces acyclicity; extract bottom-up so each move only imports
|
|
already-extracted layers.
|
|
3. **`globals.js` is the only writer of `window.*`.** The deliberate global
|
|
surface shrinks to one auditable file; everything else is module-scoped.
|
|
4. **Import-time purity.** `node --test` runs a module's top-level code on
|
|
import, so a module you want to unit-test must be side-effect-free at import:
|
|
no `document` / `window` / `localStorage` at module top level — lift init
|
|
into an exported `init()` called by `main.js`. (Constitution Principle V's
|
|
"no implicit IO at import time", applied to the frontend.) Tests are `.mjs`
|
|
and use real `import`, retiring the regex/`extractFunction` harness.
|
|
5. **Assets resolve via `import.meta.url`.** `document.currentScript` is `null`
|
|
inside a module. `assets/` lives at the plugin root, so a `src/` module must
|
|
climb out of `src/`: from `src/main.js`, `new URL('../assets/x.js',
|
|
import.meta.url)` (deeper modules need more `../`). Simpler and
|
|
depth-independent: the absolute route `/api/plugins/<id>/assets/x.js`.
|
|
Worklets run in a *separate* module graph (`AudioWorkletGlobalScope`) and
|
|
cannot share modules with `src/`.
|
|
6. **Re-init comes from `screen:changed`, not re-execution.** The host loads
|
|
`screen.js` once per version and `showScreen` re-injects nothing, so module
|
|
top-level code does **not** re-run when the user re-enters the screen at the
|
|
same version. Keep per-visit setup/teardown in a `window.feedBack.on(
|
|
'screen:changed', …)` handler — exactly as classic plugins (tuner,
|
|
minigames) already do. Do not rely on the IIFE re-running.
|
|
7. **Inline `onclick=` keeps working** during migration via `globals.js` (which
|
|
keeps every referenced symbol on `window`); retire inline handlers to
|
|
module-side `addEventListener` opportunistically, never as a blocking step.
|
|
|
|
## The live-edit loop
|
|
|
|
The host serves `screen.js`, `src/**`, and `assets/**` with
|
|
`Cache-Control: no-cache` + a weak `ETag` and honors `If-None-Match` → `304`.
|
|
So: edit a `src/` file → **refresh the browser** → the edited module returns
|
|
`200` and reloads while every unchanged module `304`s. There is no hot-reload;
|
|
the loop is edit → refresh → see change, exactly as before. The `?v=<version>`
|
|
query on `screen.js` is the legacy version buster; it does **not** propagate
|
|
into the `src/` graph and does not need to — ETag/mtime is the correctness
|
|
authority for the whole graph.
|
|
|
|
## Host-version floor (`minHost`)
|
|
|
|
A migrated plugin *requires* a host new enough to serve `src/` and inject
|
|
`type=module`. Declare the floor with `"minHost": "X.Y.Z"` in `plugin.json`.
|
|
(R0 plumbs the field through `/api/plugins`; enforcement — refuse-with-message
|
|
on an older host — is deferred, so bundled plugins are unaffected. Community
|
|
plugins should state the floor and not migrate below it.)
|
|
|
|
## Migration mechanics
|
|
|
|
- **Move-only PRs.** One slice extracts one module: cut code, add
|
|
imports/exports, update `globals.js` — zero behavior change. Behavior fixes
|
|
are separate PRs. (Init-lifts for import purity are the one non-pure move —
|
|
budget them.)
|
|
- **Bottom-up, layer by layer.** Within a layer, independent modules are
|
|
independent PRs (a DAG, not a chain); use a git worktree per branch.
|
|
- Tests move with their subject and convert to real `.mjs` imports in the same
|
|
PR (assertions unchanged).
|
|
- Size norm: no source file over **1,500 lines**; legitimate exceptions
|
|
(hot renderers, etc.) go in the signed register at `docs/size-exemptions.md`.
|
|
|
|
## Verifying a migration
|
|
|
|
`node --test <plugin>/tests/*.mjs`; load the plugin on the `:8000` testbed and
|
|
confirm it boots (`<script type=module>` in DevTools, the `src/` graph in
|
|
Network); edit a `src/` file → refresh → change visible (`200` on the edited
|
|
file, `304` on the rest); leave and re-enter the screen at the same version →
|
|
it re-inits via `screen:changed`. The R1 pilots (stems, then studio) certify
|
|
this end-to-end before the flagship repos migrate.
|