feat: add optional extension hooks for private overlays

Three small, backwards-compatible hooks let anyone with private features
(velxio.dev's analytics, custom integrations, paid tiers, …) layer them
on top of the open-source build without forking files.

Backend (app/main.py):
- After standard router registration, try-import an optional `app.pro`
  module exposing `register_pro(app)`. ImportError is silently swallowed
  (the OSS image doesn't ship `app.pro`, so this is a no-op there).

Frontend:
- EditorToolbar: new optional `rightSlot` prop renders extra elements
  after the built-in right-group buttons (mirrors the existing
  `centerSlot` pattern).
- main.tsx: dynamic `import('@pro/index')` gated by VITE_PRO_BUILD env.
  When unset (OSS build), the branch is dead-code-eliminated and no pro
  chunk is emitted.
- vite.config.ts: `@pro` alias resolves to `src/__pro_stub__/` by default.
  Private builds set `VITE_PRO_BUILD=true` and `PRO_OVERLAY_PATH=<path>`
  to point at their real overlay tree.
- src/__pro_stub__/index.ts: 1-line no-op `mountPro` so TypeScript and
  Vite resolvers stay happy in OSS builds.

Verified: `npm run build:docker` succeeds; `npm test` passes 1161/1162;
the OSS bundle (43 MB) contains zero references to `__pro_stub__`,
`@pro`, or `pro/index` (verified via `grep dist/`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-05-04 14:03:20 -03:00
parent 156d89c61d
commit edd2ac32d5
6 changed files with 215 additions and 163 deletions

View File

@ -126,6 +126,17 @@ app.include_router(simulation.router, prefix="/api/simulation", tags=["simulatio
from app.api.routes import iot_gateway
app.include_router(iot_gateway.router, prefix="/api/gateway", tags=["iot-gateway"])
# Optional pro extension. The `app.pro` package only exists in private builds
# (overlaid at Docker build time by an external repo) — its absence in the
# open-source image is expected and silently ignored. Anyone with private
# extensions can drop a package at `backend/app/pro/` exposing
# `register_pro(app)` and have it auto-loaded here without further edits.
try:
from app.pro import register_pro # type: ignore[import-not-found]
register_pro(app)
except ImportError:
pass
@app.get("/")
def root():
return {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
// Open-source no-op stub for the @pro alias (see vite.config.ts).
// Private overlays (e.g. velxio-prod) replace this at build time by setting
// VITE_PRO_BUILD=true and PRO_OVERLAY_PATH to their real pro/frontend/src/pro
// directory. The dynamic `import('@pro/index')` in main.tsx is gated by
// VITE_PRO_BUILD, so this stub is unreachable in OSS builds — it exists only
// to keep the TypeScript and Vite resolvers happy.
export const mountPro = () => {};

View File

@ -33,6 +33,12 @@ interface EditorToolbarProps {
* regardless of how narrow the editor pane gets.
*/
centerSlot?: React.ReactNode;
/**
* Optional extra elements rendered after the built-in right-group buttons
* (Libraries / Import-Export / Output Console). Used by private overlays
* to add deployment-specific actions without forking the toolbar.
*/
rightSlot?: React.ReactNode;
}
const BOARD_PILL_ICON: Record<BoardKind, string> = {
@ -63,6 +69,7 @@ export const EditorToolbar = ({
compileLogs: _compileLogs,
setCompileLogs,
centerSlot,
rightSlot,
}: EditorToolbarProps) => {
const { files, codeChangedSinceLastCompile, markCompiled } = useEditorStore();
const {
@ -973,6 +980,7 @@ export const EditorToolbar = ({
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
</button>
{rightSlot}
</div>
</div>
</div>

View File

@ -15,3 +15,13 @@ import './components/velxio-components/EPaperElement';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(<App />);
// Optional pro overlay. The `@pro` import resolves to a no-op stub in the
// open-source build (see vite.config.ts) and to the real overlay only when
// VITE_PRO_BUILD=true at build time. The dynamic import keeps the pro chunk
// out of the OSS bundle entirely (Vite tree-shakes the never-taken branch).
if (import.meta.env.VITE_PRO_BUILD) {
import('@pro/index')
.then((m) => m.mountPro?.())
.catch((err) => console.warn('[pro] failed to load overlay:', err));
}

View File

@ -1,11 +1,27 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
// avr8js / rp2040js / @wokwi/elements are resolved from npm via package.json.
// (The third-party/ clones are reference-only — keep them updated for credits.)
//
// The `@pro` alias resolves to a no-op stub by default. Private overlays
// (e.g. velxio-prod) set VITE_PRO_BUILD=true and PRO_OVERLAY_PATH at build
// time to point at their actual pro source tree. See README's "Pro overlay"
// section.
const proOverlayPath =
process.env.VITE_PRO_BUILD && process.env.PRO_OVERLAY_PATH
? path.resolve(process.env.PRO_OVERLAY_PATH)
: path.resolve(__dirname, 'src/__pro_stub__')
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@pro': proOverlayPath,
},
},
server: {
proxy: {
'/api': {