Stress-test skeleton: multi-MFE workspace, theme variants, init-skills#27
Merged
Conversation
…-test tier Mechanical move of all single-framework playgrounds under a new simple/ subdirectory, plus the shared justfile and DEMOS.md. The stress-test tier (single-spa host + per-framework MFEs + multi-language services) will land under playgrounds/stress-test/ in a follow-up. No behavior changes. Workspace glob, gitignore, root justfile, doc trees, and lockfile updated to match the new paths. Dev scripts in package.json filter by package name so they did not need updating, and Playwright/CI use the same scripts.
…ical slice Adds the playgrounds/stress-test/ tier described in the stress lab plan. This first pass lands the full directory skeleton (all 7 apps, 6 services, 5 packages, e2e/) with README placeholders, plus one bootable end-to-end slice to prove the pattern: host-single-spa (:4200) Vue shell with nav + iframe viewport mfe-vue-data-lab (:4220) Vue MFE, annotask mfe id 'vue-data-lab' services/fastapi (:4320) FastAPI with /api/health + OpenAPI The host is iframe-based for now. Real single-spa parcel/module-federation wiring lands once at least two MFEs are implemented — the single-spa root label is kept for the directory so the target is clear. Workspace globs extended to playgrounds/stress-test/apps/* and playgrounds/stress-test/packages/*. Root scripts added: pnpm dev:stress-host pnpm dev:stress-vue-data-lab pnpm dev:stress-fastapi docker-compose.yml stubs the compose path — only FastAPI is wired so far; other services are commented TODOs until their implementations land. Verified: pnpm install, pnpm typecheck, all three services boot and the Vue MFE proxies /api/health through to FastAPI.
Fills in the stress-test skeleton with bootable code for every slice that works without PHP/Java toolchains, and adds a Playwright suite that boots every server and verifies each slice end-to-end. Apps (all on their planned ports with distinct annotask mfe ids): - mfe-react-workflows (:4210) — React 19 + Vite - mfe-svelte-streaming (:4230) — Svelte 5 + Vite - mfe-solid-component-lab (:4240) — Solid + Vite (scaffolded, see below) - mfe-blade-legacy-lab (:4250) — Vite placeholder; real Blade via Laravel - mfe-htmx-partials (:4260) — plain HTML + htmx + Vite Each MFE has a minimal What-this-stresses panel and an Upstream-health probe that fetches its service's /api/health. Services (native dev + Dockerfile for compose): - node-api (:4340) — zero-dep std-lib http server - go-api (:4330) — net/http JSON health - rust-api (:4360) — std-only HTTP, serves JSON + an HTML fragment for htmx - java-api, laravel — Docker-only stubs (no native toolchain available) docker-compose.yml wires the four native services; Java and Laravel stay TODO-commented until their real implementations land. Playwright: - playgrounds/stress-test/e2e/playwright.config.ts boots all 11 webServers - host.spec.ts, mfes.spec.ts, services.spec.ts cover host + 5 MFEs + 4 services - 13 tests pass; the Solid MFE spec is test.skip'd (see below) - new root script: pnpm test:e2e:stress Known issue: The Solid MFE fails to render at runtime with 'React is not defined'. The annotask plugin's JSX transform emits React.createElement calls and pre-empts vite-plugin-solid's Babel-based transform. The same failure exists in playgrounds/simple/solid-vite/ but went uncaught because the simple tier doesn't e2e-test Solid. Fixing this belongs in the core annotask plugin, not in this skeleton.
…m to load
Annotask's transform hook and vite-plugin-solid both run in the pre-enforce
phase. Which one runs first depends on plugin array order, and when the
user puts solid() before annotask() (as the docs and every example show),
Solid transforms JSX to _tmpl$ output first, then annotask re-reads the
raw .tsx from disk and returns it unchanged — discarding Solid's transform.
Vite's internal esbuild then re-transforms the raw JSX with its default
React-automatic config, producing React.createElement calls that fail at
runtime with 'React is not defined'.
The same failure mode hit playgrounds/simple/solid-vite/ but went uncaught
because the simple tier had no Solid e2e test.
Fix: inject data-annotask-* attributes in the load hook instead of
transform. Load runs before any plugin's transform hook regardless of
enforce or plugin order, so we get the raw source file, return it with
attrs injected, and then vite-plugin-solid (or vite-plugin-react,
vite-plugin-vue, @sveltejs/vite-plugin-svelte) transforms the annotated
source normally. The transform hook is now slim — only the main.{ts,js}
runtime-import injection remains.
Regression-verified against simple/solid-vite (renders cleanly, zero
console errors) and the full stress-test Playwright suite (14/14 pass,
including the previously-skipped Solid spec).
Mirrors the playgrounds/simple/justfile pattern: short recipes per MFE and service, a `just up` recipe that starts the whole local stack (4 native services + host + every MFE) with per-process logs to /tmp and a trap that tears everything down on Ctrl-C, and a `just down` that frees any stuck ports. Recipes are a thin wrapper over the existing root pnpm scripts (dev:stress-*, test:e2e:stress, stress-test:up/down) — no new orchestration code — so calling pnpm directly from the repo root still works. README updated to point at the justfile as the primary entry point.
Makes the two docker-only stubs real and retires the Vite Blade placeholder.
services/java-api (Spring Boot 3.3, port 4310):
- Multi-stage Dockerfile: Maven build + Temurin 21 JRE runtime
- /api/health, /api/workflows, /api/workflows/:id, POST /api/workflows/:id/transitions
- springdoc-openapi wired: /api/docs, /api/openapi
- Seeded in-memory workflow store so React-workflows has something to list
- First build ~3-5 min, subsequent boots fast
services/laravel (Laravel 11, PHP 8.4, port 4350):
- Dockerfile scaffolds laravel/laravel via composer then layers an
overlay/ with our routes, controllers, and Blade views on top
- Pages: Blade workflow queue at /, create form at /tasks/new (with
validation round-trip), JSON /api/health
- The repo carries only the custom slice (~8 files) + Dockerfile; the
~150-file Laravel skeleton lives inside the image
- First build ~3-6 min, subsequent boots fast
Host:
- apps/mfe-blade-legacy-lab/ Vite placeholder removed
- Host now iframes http://localhost:4350/ for the Blade slot. When
Laravel isn't running the iframe shows a connection error; start it
with `just laravel`
docker-compose.yml: java-api and laravel added alongside the four native
services. Justfile: new `just java` and `just laravel` recipes (docker
compose up --build <svc>). Root package.json: dropped the dead
dev:stress-blade-legacy-lab script. Playwright: new gated tests for
java-api health, java-api workflows list, laravel health, and the Blade
home page — each skips when its service isn't reachable, so the native
dev loop stays green.
Verified: docker compose build both images (exit 0), started them, hit
every endpoint (both report healthy, Laravel Blade home renders the
workflow queue). Full Playwright suite: 18/18 pass with Docker up; 13/18
pass + 5 skipped with Docker down.
Every major framework now uses a distinct component library — Mantine
(React), Naive UI (Vue), bits-ui (Svelte 5), Kobalte (Solid), Pico.css
(htmx), native <x-*> Blade components (Laravel). Each MFE renders a real
multi-panel UI — workflow queue, product catalog, metric sparklines,
inspect-detail dialog, tabs — instead of the previous "health probe
only" page. Annotask's component discovery has real library breadth to
walk through.
Shared workspace packages are now populated instead of README stubs:
- @annotask/stress-contracts — Health, Workflow, Product, MetricSeries, ComponentUsage types
- @annotask/stress-fixtures — deterministic seed data for every MFE
- @annotask/stress-ui-tokens — cross-MFE CSS tokens (tokens.css)
- @annotask/stress-snapshots — pinned external-data snapshots
- @annotask/stress-upstream-adapters — live-or-snapshot fallback pattern
Every MFE imports the shared tokens CSS at entry, types from
stress-contracts, and seed data from stress-fixtures. Editing tokens in
one place cascades to every stack; editing fixtures re-renders every
MFE's list view.
Host: pulls in the shared tokens stylesheet so the chrome around the
iframe viewport uses the same accent colors as the children.
Blade (Laravel): new component library under resources/views/components/
— <x-card>, <x-button>, <x-status-pill>, <x-workflow-table>. Blade's
<x-*> is Laravel's native component system; pages compose these instead
of repeating markup.
Side-fix: worked around an annotask Svelte transform bug that
mis-parses '>' inside '{() => ...}' attribute expressions. The
findTagEnd scanner treats the arrow's '>' as a tag close and injects
data-annotask-* attrs mid-expression. Worked around by currying the
click handler (inspectFor(wf)) so the template attribute never contains
a '=>'. Fix belongs in src/plugin/transform.ts:findTagEnd (it needs to
track brace nesting when scanning attribute values).
Playwright: 18/18 pass with Docker up, 13/18 + 5 skipped without Docker
(the Docker-gated Java + Laravel tests). No new specs needed — existing
'mfe X boots' checks now also exercise the component libraries.
Verified: pnpm install resolves all new deps, pnpm typecheck clean,
docker compose build + up for Laravel picks up the new <x-*> components,
Blade home renders <x-card> + <x-status-pill> correctly.
Each JS MFE is now mounted in-place in the host's DOM via single-spa
ESM cross-origin module imports — not iframes. Clicking a nav link
routes via URL hash, single-spa resolves the registered application,
imports its lifecycle module from its own Vite dev server, and mounts
it into a single-spa-application:<name> container in the host.
Per-MFE lifecycle entries (kept alongside each MFE's solo main.*):
- single-spa-react → mfe-react-workflows/src/single-spa.tsx
- single-spa-vue → mfe-vue-data-lab/src/single-spa.ts
- hand-written svelte 5 mount/unmount → mfe-svelte-streaming/src/single-spa.js
- hand-written solid render/dispose → mfe-solid-component-lab/src/single-spa.tsx
- hand-written htmx HTML + htmx.process → mfe-htmx-partials/src/single-spa.js
- iframe lifecycle (legacy-app pattern) → inline in host for Blade/Laravel
Solo dev still works at each MFE's own port — main.* entries untouched,
Playwright 'mfe X boots at :42xx' tests still pass.
Under single-spa all MFE JS shares the host origin (:4200), so MFE
fetches now use absolute service URLs (Vue→:4320, React→:4310, etc.)
instead of relying on per-MFE Vite proxies. Every service already had
open CORS so cross-origin fetches just work.
React MFE switched to @vitejs/plugin-react-swc (plugin-react's Fast
Refresh preamble requirement clashes with cross-origin module import).
Host index.html installs the React Refresh runtime globals before
main.ts so plugin-react-swc's preamble check passes when the MFE mounts.
New Playwright specs under host.spec.ts:
- single-spa mounts <MFE> at #/<route> × 5 (React, Vue, Svelte, Solid, htmx)
- single-spa mounts Blade Legacy Lab as an iframe under #/blade
(Docker-gated — skips when Laravel isn't running)
The host's overview page still lists every MFE — now with stack +
library + route instead of iframe URL + status. Old iframe selection
test removed (no longer the pattern).
Verified:
- pnpm typecheck clean
- All 5 ESM MFEs mount in the host's DOM at their routes (browser probe)
- Playwright: 17/17 pass native, 23/23 pass with Docker + Laravel + Java up
Walked every route end-to-end in Playwright with the full stack up and caught three real bugs: 1. htmx MFE fragment never loaded under single-spa. Root cause: Vite's ESM bundle of htmx.org exports htmx as default but doesn't set `window.htmx`, so the `window.htmx.process()` call in single-spa.js was a no-op. Plus: htmx 2.x defaults `selfRequestsOnly = true` which rejected the cross-origin fetch from :4200 to the Rust service on :4360 as `htmx:invalidPath`. Fixed by importing the default, assigning to `window.htmx`, setting `htmx.config.selfRequestsOnly = false`, and kicking off the initial fetch via explicit `htmx.ajax(...)` (the declarative `hx-trigger="load"` doesn't fire on elements htmx sees via `htmx.process()` after DOMContentLoaded has already passed). Mirrored the window-htmx + selfRequestsOnly tweaks in the solo main.js for consistency. 2. Rust service didn't handle CORS preflight. htmx adds custom HX-* request headers which flips the browser into preflight mode. The Rust std-lib handler only answered GETs, so the OPTIONS request got a 404 and the whole fetch failed with "Response to preflight request doesn't pass access control check". Added an OPTIONS short-circuit that returns 204 with Access-Control-Allow-Methods/Headers, and added those same headers to the normal GET response for good measure. 3. Blade slot showed a silent blank iframe when Laravel was down. User-reported. Added a health probe inside the Blade single-spa lifecycle: before iframing, try `GET :4350/api/health` with a 2s timeout; if it fails, the host renders a friendly "service not running — start with \`just laravel\`" message instead of the dead iframe. Verified via a Playwright walkthrough that every route shows live service data when the stack is up: #/vue → fastapi (status, service, port, version) #/react → java-api + 8 workflow rows from Java #/svelte → go-api + seed metrics + workflow table #/solid → node-api + tabs with seed data #/htmx → rust-api fragment swapped in via hx-get #/blade → Laravel Blade home (iframe) with workflow queue Full Playwright suite: 23/23 pass with Docker up.
The host's index.html had a top-level-await <script> that imported
http://localhost:4210/@react-refresh as a React Fast Refresh preamble
before loading main.ts. When the React MFE wasn't running, that fetch
blocked main.ts for several seconds while it timed out, and anything
that iframed the host page — notably the annotask shell at
http://localhost:4200/__annotask/, which renders the app under test
inside an iframe — appeared broken or slow to load.
Moved the preamble into a lazy ensureReactPreamble() helper that runs
only when single-spa activates the React route. The host page now
boots immediately regardless of which MFEs are up, the $Refresh*
globals + preamble flag get installed the first time React mounts, and
a failed fetch to :4210 (when the React MFE is down) is swallowed
silently — the flag alone satisfies plugin-react-swc's runtime check.
Verified with Playwright:
- /__annotask on the host loads in ~2.6s with zero errors and all
services stopped (previously timed out with a visible reqfail)
- /__annotask/ available on every MFE port (host + 5 MFEs)
- single-spa React route still mounts correctly (23/23 tests pass)
Every MFE has its own framework, component library, and annotask server,
so each one needs its own .annotask/design-spec.json generated by the
/annotask-init skill. That skill is a Claude Code slash command (AI-
driven), not a CLI binary, so we can't invoke it from a shell loop —
init-list just prints the paths to cd into from Claude Code.
init-skills handles the CLI-scriptable setup that IS scriptable:
- annotask init-skills → installs .agents/skills/ + .claude/skills/
- annotask init-mcp --editor=claude → writes .mcp.json pointing at the
MFE's own annotask server (each MFE's MCP config uses its own port)
Also ran init-skills so the host + every MFE now has its Claude Code
skill scaffold and .mcp.json committed — matches the convention in
playgrounds/simple/vue-webpack/.claude/skills/.
Prior commit's init-skills run was interrupted after 3 of 6 MFEs. This commit adds the remaining three — symmetric with the first batch, each MFE gets its own .claude/skills/, .agents/skills/, and .mcp.json pointing at its own annotask server port (4220/4230/4240).
- Workspace/MFE catalog: server + useWorkspace composable + MfeFilterDropdown; thread MFE filter through CLI/MCP/API - Multi-theme variants in design-spec: per-variant token values, selector detection (attribute/class/media), ThemePage rewrite for variant editing - Data-source scanner: display_name preserves host:port, resolved_endpoint via vite proxy, hint_symbols for inline-fetch binding - API schemas: origin scoping so same-path endpoints on different services stay attributed correctly - Drop ElementContext/ElementNode/AncestorInfo in favor of rendered_html sidecar - Flesh out stress-test MFEs (React workflows, Vue data-lab, Svelte streaming, Solid component lab, htmx partials); add mfe-react-sidebar - Per-MFE init-skills pass with framework-specific SKILL.md scaffolds - Drop Laravel service; add Java ProductsController; openapi specs for go-api and rust-api - New e2e/annotask suite (annotate, task-lifecycle) + reports - Add data-source-scanner and path-safety tests - Broad docs refresh (README, architecture, CLI, api, skills, setup)
Comment on lines
+673
to
+678
| * `undefined` when no usable origin can be derived. | ||
| */ | ||
| function extractOpenApiOrigin(doc: Record<string, unknown>, location: string, source: ApiSchema['source']): string | undefined { | ||
| if (source === 'dev-server') { | ||
| try { return new URL(location).origin } catch { /* fall through */ } | ||
| } |
| for (const pkgDir of ws.packages) { | ||
| const srcDir = nodePath.join(pkgDir, 'src') | ||
| const scanRoot = fs.existsSync(srcDir) ? srcDir : pkgDir | ||
| await walk(scanRoot, files) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
15 commits building out a multi-MFE stress-test playground and the shell/server features it surfaced. Highlights:
useWorkspacecomposable,MfeFilterDropdown, MFE filter threaded through CLI/MCP/API)ThemePagerewrite,theme_updatecontext carriestheme_variant+theme_selectordisplay_namepreserves host:port for inline fetches,resolved_endpointvia nearestvite.configproxy,hint_symbolsso the binding analyzer can trace inline-fetch result vars into templates/JSXElementContext/ElementNode/AncestorInfofor element contextjustfileorchestration, Playwright e2e/annotask suiteinit-skills— each MFE gets its own.claude/skills/,.agents/skills/, and.mcp.jsonpointing at its own annotask server portselfRequestsOnly=false+ explicithtmx.ajax); Blade fallback when Laravel is downdata-source-scanner+path-safety; Playwright annotate + task-lifecycle suitesdocs/data-source-discovery.mdTest plan
pnpm install && pnpm buildcleanpnpm typecheckpassespnpm test— unit suite green (includes new data-source-scanner + path-safety tests)just upbrings host + MFEs + services online/__annotaskloads on host and on each MFE port with all services up/__annotaskon host loads fast even with MFEs/services stopped (regression check for the React-preamble fix)theme_updatetask withtheme_variant+theme_selectorpopulated--mfe, MCPmfearg) scopes results to the selected packagee2e/annotaskannotate + task-lifecycle suites green