refactor(oss-split): remove auth/admin/profile frontend from OSS
Phase 3 of the OSS / pro split — frontend side. Phase 2 already moved
the auth/DB stack out of the OSS backend; this commit does the same
for the React app. After this, the OSS image is editor + simulator
+ landing + docs only.
What moved to the private overlay (pro/frontend/src/pro/):
pages/{Login,Register,ForgotPassword,ResetPassword}Page.tsx
pages/{Admin,UserProfile,Project,ProjectById}Page.tsx
components/admin/{AdminBoardsTab,AdminDashboardTab,UserActivityModal}.tsx
components/layout/{SaveProjectModal,LoginPromptModal}.tsx
services/{authService,adminService}.ts
store/useAuthStore.ts
hooks/autoSaveImpl.ts
New seams added so OSS components stay decoupled:
* lib/proRoutes.ts — registerProRoutes()/useProRoutes() via
useSyncExternalStore. mountPro() injects the moved pages at runtime;
App.tsx subscribes to the registry, so registration after the
initial render re-renders without a Not-Found flash.
* lib/proSession.ts — registerSessionCheck()/triggerSessionCheck().
App.tsx fires this on mount instead of useAuthStore.checkSession();
pure OSS no-ops.
* lib/proSaveAction.ts — installSaveActionImpl()/triggerSaveAction().
EditorPage's Save button dispatches through this; the overlay
decides whether to show SaveProjectModal or LoginPromptModal based
on auth state. In OSS without an overlay it's a no-op today; in
Phase 4 of the split it becomes the .vlx Export entry point.
OSS-side rewrites:
* App.tsx drops the 8 page imports + 8 route entries; uses
triggerSessionCheck() instead of useAuthStore directly.
* AppHeader.tsx drops the user/login/register block entirely. The
header-auth slot (introduced in Phase 1) now stays empty in OSS
and gets filled by the overlay's portal mount.
* EditorPage.tsx drops useAuthStore + SaveProjectModal +
LoginPromptModal imports. The Save handler is now triggerSaveAction().
* LandingPage.tsx drops the dead UserMenu component (defined but
never rendered) + its useAuthStore imports.
* main.tsx drops the side-effect import of hooks/autoSaveImpl — the
impl lives in pro now and self-registers via mountPro().
Build config:
* vite.config.ts adds @velxio alias → src/. Lets the overlay import
upstream modules (lib/proRoutes etc.) by stable name regardless of
whether it's symlinked (local dev) or COPYed (Docker).
* preserveSymlinks now gated on VITE_PRO_BUILD only (not on serve
mode). Needed so Rollup keeps the overlay logically inside src/pro/
during local junction-based builds.
Build verification:
* OSS-only: 20-ish routes, no /login, /admin, /:username — 285 SEO
pages prerendered. Bundle drops ~80-120 KB.
* OSS + overlay: full 38 routes (30 upstream + 8 from registerProRoutes),
HeaderAuth dropdown injected via slot, save action wired to the
overlay's modal flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:31:12 +07:00
|
|
|
/**
|
|
|
|
|
* Pro-route registry.
|
|
|
|
|
*
|
|
|
|
|
* App.tsx defines the OSS route table at module load. Routes that only
|
|
|
|
|
* make sense in a private deployment (login, register, admin, user
|
|
|
|
|
* profile, project-by-slug, etc.) are registered separately by the pro
|
|
|
|
|
* overlay's mountPro() and merged into the route tree via this module.
|
|
|
|
|
*
|
|
|
|
|
* Subscription is `useSyncExternalStore`-based so any registration that
|
|
|
|
|
* happens AFTER the initial App render still produces a synchronous
|
|
|
|
|
* re-render — the user never sees a "Not Found" flash for routes the
|
|
|
|
|
* overlay was about to add. In practice the dynamic import of
|
|
|
|
|
* `@pro/index` resolves before the user can navigate, but the contract
|
|
|
|
|
* guarantees correctness even if it didn't.
|
|
|
|
|
*
|
|
|
|
|
* Calling registerProRoutes() more than once REPLACES the previous set
|
|
|
|
|
* (idempotent — pro can re-register on hot reload without leaking
|
|
|
|
|
* duplicate entries). Pass [] to clear.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useSyncExternalStore, type ReactElement } from 'react';
|
|
|
|
|
|
|
|
|
|
export interface ProRoute {
|
|
|
|
|
/** Route path WITHOUT leading slash, matching App.tsx's ROUTES convention. */
|
|
|
|
|
path: string;
|
|
|
|
|
element: ReactElement;
|
|
|
|
|
/** True if this is the locale root (path === '' for /<locale>/). */
|
|
|
|
|
index?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let _routes: ProRoute[] = [];
|
|
|
|
|
const _listeners = new Set<() => void>();
|
|
|
|
|
|
|
|
|
|
export function registerProRoutes(routes: ProRoute[]): void {
|
|
|
|
|
_routes = routes;
|
|
|
|
|
for (const listener of _listeners) listener();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function subscribe(listener: () => void): () => void {
|
|
|
|
|
_listeners.add(listener);
|
|
|
|
|
return () => {
|
|
|
|
|
_listeners.delete(listener);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSnapshot(): ProRoute[] {
|
|
|
|
|
return _routes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useProRoutes(): ProRoute[] {
|
|
|
|
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
|
|
|
}
|
refactor(routes): OSS is the editor; marketing moves to the overlay
The open-source build now ships exactly its product surface: /editor, the
examples gallery (/examples, /examples/:id) and the per-example editor
(/example/:id). Landing, about, pricing, docs, the 14 keyword-targeted
simulator landings and the v2/v2.5/v3 showcases move to the private
overlay, registered through the same registerProRoutes seam that already
carries login/admin/classroom.
Root behaves per build: an overlay that registers an index route claims
'/' (velxio.dev keeps its landing); otherwise '/' redirects to /editor.
The redirect waits for the overlay import to settle — same contract as
markProExamplesSettled — so a velxio.dev visitor is never bounced into
the editor because the landing was 300ms away from registering.
Prerender and sitemap follow the split: entry-server pulls the marketing
page map from '@pro/pages/marketing' behind a VITE_PRO_BUILD-gated dynamic
import (the proven main.tsx pattern, with an OSS stub for tsc), and
generate-sitemap lists only served routes in OSS builds (2 URLs) while
pro builds stay byte-identical — verified against a pre-migration
baseline: sitemap (37 URLs) and prerendered /, /about, /docs,
/arduino-simulator, /esp32-simulator, /v3 all identical modulo hashed
asset names. OSS prerender drops exactly the 32 marketing pages (348→316).
The OSS header slims down to match: Editor, Examples, GitHub, Discord —
the marketing links only render in pro builds, where their routes exist.
The editor Help menu links those pages absolutely (velxio.dev) in OSS,
exactly like the desktop app's Help menu.
2026-07-30 22:50:14 +07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Has the overlay had its chance to register routes?
|
|
|
|
|
*
|
|
|
|
|
* The OSS root ('/') redirects to /editor — but only once it KNOWS no
|
|
|
|
|
* overlay is going to claim the home page. Redirecting on first render
|
|
|
|
|
* would race the overlay's dynamic import and bounce velxio.dev visitors
|
|
|
|
|
* into the editor before the landing page ever registered. main.tsx flips
|
|
|
|
|
* this when the overlay import settles, or immediately when no overlay is
|
|
|
|
|
* configured. Same contract as markProExamplesSettled in data/examples.
|
|
|
|
|
*/
|
|
|
|
|
let _settled = false;
|
|
|
|
|
|
|
|
|
|
export function markProRoutesSettled(): void {
|
|
|
|
|
if (_settled) return;
|
|
|
|
|
_settled = true;
|
|
|
|
|
for (const listener of _listeners) listener();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getSettled = (): boolean => _settled;
|
|
|
|
|
|
|
|
|
|
export function useProRoutesSettled(): boolean {
|
|
|
|
|
return useSyncExternalStore(subscribe, getSettled, getSettled);
|
|
|
|
|
}
|