The goal is to explore an idea.
I have a task. I'm a senior developer. There's an app that uses a fork of an existing app, and the existing app is a full-stack app, not modularized. It's not intended to be customized, but the wrapper app is customizing it nevertheless. The challenge is that we would like to both keep up with upstream changes and be freely able to change the files we're working on. It's using Svelte, so this is a bit difficult. We surveyed our options.
Small permanent fork that injects formal extension points, then merge upstream as a git subtree. โข You fork once to add an official PluginHost with typed hooks/events. โข Upstreamโs future commits are pulled into the subtree; conflicts surface only where the hook layer lives. โข Your product repo contains only plug-ins, themes, and routes.
The hook contract is explicit & testableโavoids โalias drift.โ Subtree lets you track upstream SHA, not just a version string, so you can cherry-pick bug-fixes quickly. Requires initial negotiation (or brute-force) to land hooks upstream. Subtree UX in Git is less familiar than submodules; team needs a short how-to.
// upstream/src/lib/pluginHost.ts โ (tiny, permanent fork) import { setContext, getContext } from 'svelte';
type ScriptHooks<T = any> = { beforeInit?: (state: T) => T; mounted?: (ctx: { state: T; update: (next: Partial) => void }) => void; updated?: (ctx: { state: T }) => void; };
const key = Symbol('plugin-bus');
export function initPluginBus() { const map = new Map<string, ScriptHooks[]>(); setContext(key, { register(component: string, hooks: ScriptHooks) { const list = map.get(component) ?? []; list.push(hooks); map.set(component, list); }, get(component: string) { return map.get(component) ?? []; } }); }
export function scriptHooksFor(component: string): ScriptHooks[] { return getContext<{ get: (c: string) => ScriptHooks[] }>(key).get(component); }
<button on:click={() => update({ count: state.count + 1 })}> Clicked {state.count} times
// product/plugins/widget-analytics.ts โ lives outside the subtree import { scriptHooksFor } from 'upstream/lib/pluginHost'; // re-export in your barrel
// register at app start-up import '$lib/bootstrap/plugins'; // e.g. in main.ts
// product/src/bootstrap/plugins.ts import { initPluginBus } from 'upstream/lib/pluginHost'; const bus = initPluginBus();
bus.register('Widget', { beforeInit: s => ({ ...s, count: 42 }), // default to 42 mounted : ({ state }) => console.log('mount', state), updated : ({ state }) => sendMetric(state.count) // analytics });
"Simulate" the upstream and downstream as folders. Upstream is import mapped so that it can be importent "as if a package @deepend". Downstream is imported locally.
Use Svelte.
@sveltejs/adapter-auto: 3.2.2 @sveltejs/adapter-static: ^3.0.2 @sveltejs/kit: ^2.5.20 @sveltejs/vite-plugin-svelte: ^3.1.1 @tailwindcss/container-queries: ^0.1.1 @tailwindcss/postcss: ^4.0.0 @tailwindcss/typography: ^0.5.13 @typescript-eslint/eslint-plugin: ^8.31.1 @typescript-eslint/parser: ^8.31.1 cypress: ^13.15.0 eslint: ^8.56.0 eslint-config-prettier: ^9.1.0 eslint-plugin-cypress: ^3.4.0 eslint-plugin-svelte: ^2.43.0 i18next-parser: ^9.0.1 postcss: ^8.4.31 prettier: ^3.3.3 prettier-plugin-svelte: ^3.2.6 sass-embedded: ^1.81.0 svelte: ^4.2.18 svelte-check: ^3.8.5 svelte-confetti: ^1.3.2 tailwindcss: ^4.0.0 tslib: ^2.4.1 typescript: ^5.5.4 vite: ^5.4.14 vitest: ^1.6.1