Tutorial: 2026-07-11

2026-07-11

Newest on top.

De-duplication: one provider registry for every runtime (2.53.0)

Triggered by the owner looking at the mobile provider file: "this feels dumb — why isn't providers handled on an engine level? I'm actually wondering how much functionality that should be in the engine is not, and duplicated to the targets." They were right, and the diagnosis turned out to be structural rather than a case of someone being lazy. Full campaign: ../../plans/de-duplication.md.

The root cause. Every plugin pool here is drop-a-folder-in, so it needs discovery, and each runtime discovers differently — Vite has import.meta.glob, Node has fs.readdirSync, and Metro has neither (static module graph, no fs at runtime). The provider registry reached for the Vite glob, which locked the other two targets out; each then hand-ported it. The same 40 provider folders had grown three registries (Vite glob · CLI fs-scan re-port with a duplicated applySharedSettings · an 892-line mobile hand-port that re-declared every provider and re-implemented the transports). The mobile parity checks existed to detect that drift — treating the symptom, not the disease.

The fix. A generated static index (targets/shared/registry.generated.js) — plain import statements, the one construct Vite, Node and Metro all understand. scripts/build-provider-registry.mjs writes it (npm run registry), and npm run check:registry (in npm test) fails if it's stale, so drop-a-folder-in still needs no central edit. shared/index.js is now runtime-agnostic — no import.meta, no node: — which is precisely what lets a target import it instead of forking it. The one web-only concept (online-build gating) moved out to the web shim. CLI registry: 145 lines → a thin async facade.

The deeper blocker, and the seam that resolves it. Sharing the registry isn't enough, because the transport genuinely differs per platform — and that, not laziness, is what made a native port look unavoidable:

  • hosted-proxy hardcoded a relative fetch("/api/generate"). A browser has an origin; a native app does not.
  • local-direct tunnels through /api/forward purely to dodge browser CORS. React Native has no CORS at all and should call the user's ComfyUI/A1111 directly — no backend in the loop.
  • RN's fetch has no timeout, so a dead Wi-Fi hangs a generation forever.

So: share the providers, inject the platform_shared/transport/config.js with configureTransport({ apiBase, forward, timeoutMs }). Defaults reproduce the web's exact behavior, so the web is a no-op. 11 contract tests pin both sides; proven by re-introducing both bugs (breaking apiUrl → 2 fail, breaking the forward flag → 4 fail).

Also folded provider description / keyUrl onto the manifests — they were a hand-kept PROVIDER_META table in the web plus a second copy inside mobile's registry.

Landmine (cost me a revert). Ran the metadata codemod as an inline node -e string through PowerShell; PowerShell mangled the $1 regex backreference, so the replacement ate the label: line in 15 provider configs and wrote a literal \n. Caught it by reading the file back rather than trusting "injected into 15 configs", reverted with git checkout -- targets/shared, and redid it as a real script file. Rule: never inline node -e through PowerShell when the code contains $ — write a script file. Added to ../../reference/fix-patterns.md.

Verified green at every commit: local build 698 modules · online build + SSR prerender · prompt list providers 40 (settings fold intact through the CLI's JSON-loader hook) · web 430 tests (64 files) · unit 352 · mobile parity (Manage 22/22) · lint 0 errors.

Next (in ../../plans/de-duplication.md): mobile swaps its 892-line imageProviders.js for the shared registry. One real obstacle found while starting it — an impedance mismatch, not a missing piece: the shared manifest exposes settings asynchronously (loadSettings(), code-split, like the web's gear), while mobile's UI reads a synchronous flat .settings array. So mobile's provider-settings sheet must become load-on-open first; then the arrays and the re-implemented transports get deleted.