mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
refactor(app): carve the plugin loader out of app.js into static/js/ (R3a) (#878)
The first carve, and deliberately the riskiest: app.js IS the plugin loader (the
R0 host rails), so it goes first while the module graph is still one edge deep.
static/js/plugin-loader.js (829 lines) — bodies VERBATIM. app.js 12,217 → 11,439.
Core's first `static/js/` module, exactly as constitution II anticipates.
CLOSURE (measured with acorn, not regex — brace-matching stripped source drifted):
the block at app.js:11246-12031 is contiguous and self-contained. It needs only
TWO things from the rest of app.js, and exports only TWO:
exports: loadPlugins (the window contract), bootstrapPluginsAndUi (boot)
inbound: window.showScreen — already the public host contract (constitution II),
so it is called through `window`, not re-coupled as an import
_populateVizPicker — injected via configurePluginLoader()
WHY A SEAM, NOT AN IMPORT. plugin-loader must not import app.js: app.js imports
it, so that would close a cycle. I checked whether _populateVizPicker could just
move into the module instead (which would delete the seam entirely) — it drags 9
further symbols (_canRun3D, _autoMatchViz, _showPromotionNag, …), i.e. a whole
viz cluster. That is its own carve, so the seam stays.
THE SEAM'S DEFAULT IS LOUD, ON PURPOSE. A no-op stub is the classic silent
failure for this pattern (see the editor's setHostHooks trap, hit twice): drop the
wiring call and the loader keeps working while the viz picker quietly stops
refreshing — no test, no boot check says a word. The default now console.errors,
so the smoke harness catches it. VERIFIED BY BITE TEST: removing
configurePluginLoader() from app.js surfaces
"[plugin-loader] host seam not configured" at boot. The seam IS exercised on the
plugin-startup path, so an unwired hook cannot pass silently.
no-cycle is now LIVE on core's own graph for the first time. eslint.config.js
gains `static/app.js` + `static/js/**` to the module block — app.js now `import`s,
so parsing it as a script would be a syntax error. VERIFIED BY BITE TEST: making
plugin-loader import app.js back fails with "Dependency cycle detected".
HARNESSES (the R3a note said budget one conversion per carve — it was five):
retargeted capability_inspector_nav, plugin_hydration_wipe,
plugin_loader_script_type, plugin_style_injection, legacy_shim_hits (SPLIT — one
test needs the loader, one still needs app.js) + test_plugin_runtime_idempotence.
legacy_shim_hits was missed by a symbol-name grep because it greps for a code
STRING; only the failing run found it. test_capability_events' NEGATIVE asserts
now span app.js + the loader — carving code out of app.js would otherwise make
them vacuous instead of failing.
VERIFIED: A/B against origin/main in two browsers — mounted plugin screens, 14
loaded plugin scripts, the 3 module plugins injected as <script type="module">,
37 capability participants, 14 shims, window.loadPlugins: IDENTICAL, zero
console/page errors on both. /static/js/plugin-loader.js serves 200; R0 rails
intact (src/main.js 200, conditional GET 304, script_type passthrough).
pytest 2396, node 1032/1032, ESLint 0, Codex 0.
Codex preflight caught a REAL [P1] first pass: static/js/plugin-loader.js was
untracked, so a checkout would have served an app.js importing a nonexistent
module — a failed static import kills the whole module and every window handler
with it. Now tracked.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
92c86f5393
commit
38772f604a
@@ -4,7 +4,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..', '..');
|
||||
const APP_JS = path.join(ROOT, 'static', 'app.js');
|
||||
const PLUGIN_LOADER_JS = path.join(ROOT, 'static', 'js', 'plugin-loader.js');
|
||||
const MANIFEST = path.join(ROOT, 'plugins', 'capability_inspector', 'plugin.json');
|
||||
const SCREEN_HTML = path.join(ROOT, 'plugins', 'capability_inspector', 'screen.html');
|
||||
const SETTINGS_HTML = path.join(ROOT, 'plugins', 'capability_inspector', 'settings.html');
|
||||
@@ -28,7 +28,7 @@ test('capability inspector manifest ships settings but no default nav entry', ()
|
||||
});
|
||||
|
||||
test('capability inspector plugins menu entry is localStorage opt-in', () => {
|
||||
const src = source(APP_JS);
|
||||
const src = source(PLUGIN_LOADER_JS);
|
||||
const helper = region(src, "const CAPABILITY_INSPECTOR_NAV_SETTING = 'capability_inspector.showInPluginsMenu'", 1400);
|
||||
const menu = region(src, 'const navPlugins = plugins.map', 1000);
|
||||
const contributions = region(src, 'async function _registerLegacyPluginUiContributions(plugin)', 1400);
|
||||
@@ -106,7 +106,7 @@ test('capability inspector screen ships scoped graph lane CSS', () => {
|
||||
assert.match(html, /left: -1\.75rem/);
|
||||
});
|
||||
test('_navLabel resolves string, object, synthesized, and empty nav values', () => {
|
||||
const src = source(APP_JS);
|
||||
const src = source(PLUGIN_LOADER_JS);
|
||||
const m = src.match(/function _navLabel\(nav, plugin\) \{[\s\S]*?\n\}/);
|
||||
assert.ok(m, 'could not extract _navLabel from app.js');
|
||||
const _navLabel = new Function(`${m[0]}; return _navLabel;`)();
|
||||
@@ -123,7 +123,7 @@ test('_navLabel resolves string, object, synthesized, and empty nav values', ()
|
||||
});
|
||||
|
||||
test('plugin nav dropdown label uses the computed nav, not the raw plugin.nav', () => {
|
||||
const src = source(APP_JS);
|
||||
const src = source(PLUGIN_LOADER_JS);
|
||||
// Regression guard for the string/synthesized-nav label fix: the dropdown
|
||||
// label must derive from the loop's computed nav via _navLabel, not from
|
||||
// plugin.nav?.label (which drops string and synthesized labels).
|
||||
|
||||
@@ -71,6 +71,9 @@ test('native audio-mix participant suppresses matching legacy fader and records
|
||||
|
||||
const ROOT = path.join(__dirname, '..', '..');
|
||||
const APP_JS = path.join(ROOT, 'static', 'app.js');
|
||||
// The plugin loader was carved out of app.js into its own module (R3a); the
|
||||
// library-provider code below still lives in app.js.
|
||||
const PLUGIN_LOADER_JS = path.join(ROOT, 'static', 'js', 'plugin-loader.js');
|
||||
const LIBRARY_JS = path.join(ROOT, 'static', 'capabilities', 'library.js');
|
||||
|
||||
function source(file) {
|
||||
@@ -87,7 +90,7 @@ function region(src, needle, length = 1200) {
|
||||
}
|
||||
|
||||
test('plugin script hydration exposes the current plugin id for legacy registrations', () => {
|
||||
const src = source(APP_JS);
|
||||
const src = source(PLUGIN_LOADER_JS);
|
||||
const block = region(src, 'script.src = `/api/plugins/${plugin.id}/screen.js');
|
||||
assert.match(block, /window\.feedBack\._loadingPluginId\s*=\s*plugin\.id/);
|
||||
assert.match(block, /delete\s+window\.feedBack\._loadingPluginId/);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Verify loadPlugins' plugin-DOM wipe loops in static/app.js: a plugin that is
|
||||
// Verify loadPlugins' plugin-DOM wipe loops in static/js/plugin-loader.js: a plugin that is
|
||||
// merely ABSENT from the current /api/plugins response (transient partial
|
||||
// response while the backend's plugin registry is repopulating after a
|
||||
// restart) must keep its settings panel and screen DOM. Wiping it while its
|
||||
@@ -14,7 +14,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
||||
const PLUGIN_LOADER_JS = path.join(__dirname, '..', '..', 'static', 'js', 'plugin-loader.js');
|
||||
|
||||
// Slice the wipe block out of loadPlugins by its stable landmarks: from the
|
||||
// nav reset that opens it to the comment introducing the next section.
|
||||
@@ -40,7 +40,7 @@ function makeEl(pluginId, id) {
|
||||
}
|
||||
|
||||
function runWipe({ respondedIds, alreadyHydrated, settingsChildren, screens }) {
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const src = fs.readFileSync(PLUGIN_LOADER_JS, 'utf8');
|
||||
const block = extractWipeBlock(src);
|
||||
settingsChildren.forEach((el) => { el._parent = settingsChildren; });
|
||||
const container = { children: settingsChildren };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Guards the R0 module-migration loader change in static/app.js: a migrated
|
||||
// Guards the R0 module-migration loader change in static/js/plugin-loader.js: a migrated
|
||||
// plugin (manifest scriptType:"module", surfaced as plugin.script_type) must be
|
||||
// injected as <script type="module"> so its screen.js `import './src/main.js'`
|
||||
// graph loads, while classic plugins stay untouched.
|
||||
@@ -16,8 +16,8 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const PLUGIN_LOADER_JS = path.join(__dirname, '..', '..', 'static', 'js', 'plugin-loader.js');
|
||||
const src = fs.readFileSync(PLUGIN_LOADER_JS, 'utf8');
|
||||
|
||||
// Isolate the screen.js <script> injection block: from where its src is built
|
||||
// to where the element is appended.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Verify the plugin `styles` capability in static/app.js: _injectPluginStyles
|
||||
// Verify the plugin `styles` capability in static/js/plugin-loader.js: _injectPluginStyles
|
||||
// adds exactly one versioned <link rel="stylesheet"> per plugin, swaps it on a
|
||||
// version upgrade (no duplicates, no stale tags), injects nothing for a plugin
|
||||
// without `styles`, and routes the URL through the sandboxed asset endpoint.
|
||||
@@ -9,7 +9,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
||||
const PLUGIN_LOADER_JS = path.join(__dirname, '..', '..', 'static', 'js', 'plugin-loader.js');
|
||||
|
||||
// Brace-balanced extraction of a `const NAME = (...) => { ... }` arrow, so a
|
||||
// nested object/template literal can't make a naive regex stop early.
|
||||
@@ -84,7 +84,7 @@ function setupSandbox() {
|
||||
},
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||
const src = fs.readFileSync(PLUGIN_LOADER_JS, 'utf8');
|
||||
const removeSrc = extractConstArrow(src, '_removePluginStyleTags');
|
||||
const injectSrc = extractConstArrow(src, '_injectPluginStyles');
|
||||
const reconcileSrc = extractConstArrow(src, '_reconcilePluginStyles');
|
||||
|
||||
Reference in New Issue
Block a user