the last piece. installing a plugin now brings its UI with it.
a bundler cannot follow import(runtimeString), so which plugins have a frontend
cannot be answered from the database at render time — it has to be written into
source first. Plugins.gen.tsx is that file: concrete imports, generated from
what is installed, gitignored because it describes THIS machine.
App.tsx keeps its core routes and gains one map. the wildcard hands the whole
subtree to the plugin's own router, which react-router nests natively.
serving moved to build/ in production. the html import is bundled once when the
module graph loads and can never change after, which is precisely why a plugin's
frontend needed a restart; Bun.build measures ~900ms for a 25MB bundle, so an
install can just rebuild. development keeps the html import, because that is
what gives HMR and bun --watch restarts on every source change anyway.
verified end to end against a running server, no restart at any point: install
regenerated the module, rebuilt the bundle (chunk hash changed), and the
plugin's own markup was in it; /example and /example/deeper both served; disable
took it back out of both the module and the bundle and 404'd the api; enable put
it back.
three things worth recording because they were found rather than reasoned:
the shell output is named after the ENTRYPOINT — index.gen.html, not index.html
— and naming: { entry: '[name].[ext]' } does not change it because [name] is
'index.gen'. found as a 503 on the first boot after the switch.
App.tsx already destructured a `plugins`, from useServerSettings — the DEAD
plugin system that scans a directory which does not exist and always returns [].
it silently shadowed the import. the new one is `installedPlugins` and says why.
seedAppRegistry takes plugin panels as an argument rather than importing them:
officerdev is a dependency of the shell, so importing upward would invert that.
756 pass, same 10 pre-existing failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 lines
1.3 KiB
TypeScript
37 lines
1.3 KiB
TypeScript
import { Routes, Route, Link } from 'react-router';
|
|
|
|
// The plugin's own router, mounted by the shell at `<prefix>/*` — so everything below this point is the
|
|
// plugin's, and react-router nests it natively. The shell knows the prefix; this file does not need to.
|
|
|
|
const Home = () => (
|
|
<div className="p-8">
|
|
<h1 className="text-xl font-semibold text-duck-dark">Example plugin</h1>
|
|
<p className="mt-2 text-sm text-duck-dark/60">
|
|
Rendered from <code>plugins/example/web/Router.tsx</code>, compiled into the shell's bundle by the generated{' '}
|
|
<code>Plugins.gen.tsx</code>.
|
|
</p>
|
|
<Link className="mt-4 inline-block text-sm text-duck-teal underline" to="/example/deeper">
|
|
A nested route →
|
|
</Link>
|
|
</div>
|
|
);
|
|
|
|
const Deeper = () => (
|
|
<div className="p-8">
|
|
<h1 className="text-xl font-semibold text-duck-dark">Nested</h1>
|
|
<p className="mt-2 text-sm text-duck-dark/60">Proof the wildcard mount hands the whole subtree to the plugin.</p>
|
|
<Link className="mt-4 inline-block text-sm text-duck-teal underline" to="/example">
|
|
← back
|
|
</Link>
|
|
</div>
|
|
);
|
|
|
|
export default function ExampleRouter() {
|
|
return (
|
|
<Routes>
|
|
<Route path="/" element={<Home />} />
|
|
<Route path="/deeper" element={<Deeper />} />
|
|
</Routes>
|
|
);
|
|
}
|