lovable-ssr 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # lovable-ssr
2
+
3
+ SSR and route data engine for [Lovable](https://lovable.dev) projects. Provides a route registry (singleton), `getServerData`-based data loading for SSR and SPA navigation, and an Express + Vite server.
4
+
5
+ **Documentation:** [Documentação completa](https://calm-meadow-5cf6.github-8c8.workers.dev/)
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm i lovable-ssr
11
+ ```
12
+
13
+ Peer dependencies: `react`, `react-dom`, `react-router-dom` (^18 / ^6).
14
+
15
+ ## Quick start
16
+
17
+ ### 1. Register routes
18
+
19
+ Define your routes and call `registerRoutes` so the framework can match paths and run `getServerData`:
20
+
21
+ ```ts
22
+ // src/routes.ts (or wherever you define routes)
23
+ import { registerRoutes, type RouteConfig, type ComponentWithGetServerData } from 'lovable-ssr';
24
+ import HomePage from '@/pages/HomePage';
25
+ import VideoPage from '@/pages/VideoPage';
26
+
27
+ export const routes: RouteConfig[] = [
28
+ { path: '/', Component: HomePage, isSSR: true },
29
+ { path: '/video/:id', Component: VideoPage, isSSR: true },
30
+ { path: '*', Component: NotFound, isSSR: false },
31
+ ];
32
+
33
+ registerRoutes(routes);
34
+ ```
35
+
36
+ ### 2. App shell
37
+
38
+ Ensure routes are loaded (so the registry is filled), then use `BrowserRouteDataProvider` and `AppRoutes`. The framework reads `window.__PRELOADED_DATA__` and the current pathname and fills the route data context for you (no need to declare `Window` or compute initial route/params in the app):
39
+
40
+ ```tsx
41
+ // src/App.tsx
42
+ import './routes'; // runs registerRoutes(routes)
43
+ import { BrowserRouteDataProvider, AppRoutes } from 'lovable-ssr';
44
+ import { BrowserRouter } from 'react-router-dom';
45
+
46
+ export default function App() {
47
+ return (
48
+ <BrowserRouter>
49
+ <BrowserRouteDataProvider>
50
+ <AppRoutes />
51
+ </BrowserRouteDataProvider>
52
+ </BrowserRouter>
53
+ );
54
+ }
55
+ ```
56
+
57
+ The package augments `Window` with `__PRELOADED_DATA__?: Record<string, unknown>` so you don't need to declare it. If you prefer to wire `RouteDataProvider` yourself (e.g. for testing), use `RouterService`, `RouteDataProvider`, and the same initial data logic.
58
+
59
+ ### 3. SSR entry (optional)
60
+
61
+ For SSR, add an entry module that registers routes and calls the framework’s `render` with your app wrappers (e.g. QueryClient, Toaster):
62
+
63
+ ```tsx
64
+ // src/entry-server.tsx
65
+ import { registerRoutes, render as frameworkRender } from 'lovable-ssr';
66
+ import { routes } from '@/routes';
67
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
68
+
69
+ registerRoutes(routes);
70
+
71
+ export async function render(url: string) {
72
+ return frameworkRender(url, {
73
+ wrap: (children) => (
74
+ <QueryClientProvider client={new QueryClient()}>
75
+ {children}
76
+ </QueryClientProvider>
77
+ ),
78
+ });
79
+ }
80
+ ```
81
+
82
+ ### 4. SSR server (optional)
83
+
84
+ Run the Express + Vite server using the framework’s `createServer` (import from the `server` subpath so Node-only code is not bundled in the client):
85
+
86
+ ```ts
87
+ // src/ssr/server.ts
88
+ import path from 'node:path';
89
+ import { fileURLToPath } from 'node:url';
90
+ import { createServer } from 'lovable-ssr/server';
91
+
92
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
93
+ const root = path.resolve(__dirname, '../..');
94
+
95
+ createServer({
96
+ root,
97
+ entryPath: 'src/entry-server.tsx',
98
+ port: process.env.PORT ? Number(process.env.PORT) : 5173,
99
+ })
100
+ .then((s) => s.listen())
101
+ .catch(console.error);
102
+ ```
103
+
104
+ Scripts:
105
+
106
+ - **Dev SPA:** `vite`
107
+ - **Dev SSR:** `tsx src/ssr/server.ts`
108
+ - **Build SSR:** `vite build && vite build --ssr src/entry-server.tsx --outDir dist`
109
+ - **Preview SSR:** `npm run build:ssr && NODE_ENV=production tsx src/ssr/server.ts`
110
+
111
+ ## API
112
+
113
+ - **Types:** `RouteConfig`, `ComponentWithGetServerData`
114
+ - **Registry:** `registerRoutes(routes)`, `getRoutes()`
115
+ - **Router:** `RouterService.matchRoute(pathname)`, `RouterService.routeParams(path, pathname)`, `RouterService.isSsrRoute(pathname)`
116
+ - **Data:** `RouteDataProvider`, `useRouteData()`, `buildRouteKey(path, params)`, `RouteDataState`, `InitialRouteShape`
117
+ - **UI:** `AppRoutes` (no props), `BrowserRouteDataProvider` (wraps children with `RouteDataProvider` using `window.__PRELOADED_DATA__` and current pathname; use inside `BrowserRouter`)
118
+ - **SSR:** `render(url, options?)` with `options.wrap = (children) => ReactNode`
119
+ - **Server:** `createServer(config)` and `runServer(config?)` from `lovable-ssr/server`
120
+
121
+ ## Pages with `getServerData`
122
+
123
+ Attach a `getServerData` function to the route component; it runs on the server for SSR and on the client when navigating to that route if data is not already cached (keyed by path + params).
124
+
125
+ ```ts
126
+ async function getServerData(params?: Record<string, string>) {
127
+ const id = params?.id;
128
+ const data = await fetch(`/api/videos/${id}`).then((r) => r.json());
129
+ return { video: data };
130
+ }
131
+ VideoPage.getServerData = getServerData;
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,2 @@
1
+ export declare function AppRoutes(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=AppRoutes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppRoutes.d.ts","sourceRoot":"","sources":["../../src/components/AppRoutes.tsx"],"names":[],"mappings":"AAMA,wBAAgB,SAAS,4CA6CxB"}
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect } from 'react';
3
+ import { Route, Routes, useLocation } from 'react-router-dom';
4
+ import { getRoutes } from '../registry.js';
5
+ import { buildRouteKey, useRouteData } from '../router/RouteDataContext.js';
6
+ import RouterService from '../router/RouterService.js';
7
+ export function AppRoutes() {
8
+ const location = useLocation();
9
+ const pathname = location.pathname || '/';
10
+ const routes = getRoutes();
11
+ const matchedRoute = RouterService.matchRoute(pathname);
12
+ const params = matchedRoute ? RouterService.routeParams(matchedRoute.path, pathname) : {};
13
+ const routeKey = matchedRoute ? buildRouteKey(matchedRoute.path, params) : '';
14
+ const { data, setData } = useRouteData();
15
+ const currentData = routeKey ? data[routeKey] : undefined;
16
+ const getServerData = matchedRoute?.Component?.getServerData;
17
+ useEffect(() => {
18
+ if (!routeKey || !getServerData || currentData !== undefined)
19
+ return;
20
+ getServerData(params)
21
+ .then((d) => setData(routeKey, d))
22
+ .catch((e) => {
23
+ console.error('Client getServerData failed:', e);
24
+ setData(routeKey, {});
25
+ });
26
+ }, [routeKey, getServerData, params, currentData, setData]);
27
+ return (_jsx(Routes, { children: routes.map((route) => {
28
+ const isMatched = matchedRoute === route;
29
+ const getServerDataForRoute = route.Component.getServerData;
30
+ let element;
31
+ if (isMatched) {
32
+ if (typeof getServerDataForRoute === 'function') {
33
+ element =
34
+ currentData !== undefined ? (_jsx(route.Component, { ...currentData })) : null;
35
+ }
36
+ else {
37
+ element = _jsx(route.Component, {});
38
+ }
39
+ }
40
+ else {
41
+ element = _jsx(route.Component, {});
42
+ }
43
+ return _jsx(Route, { path: route.path, element: element }, route.path);
44
+ }) }));
45
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * Wraps children with RouteDataProvider using initial data from the browser:
4
+ * - window.__PRELOADED_DATA__ (from SSR)
5
+ * - window.location.pathname + RouterService for matchedRoute and routeParams
6
+ *
7
+ * Use this inside BrowserRouter so the app does not need to read __PRELOADED_DATA__
8
+ * or compute initial route/params manually.
9
+ */
10
+ export declare function BrowserRouteDataProvider({ children }: {
11
+ children: ReactNode;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=BrowserRouteDataProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrowserRouteDataProvider.d.ts","sourceRoot":"","sources":["../../src/components/BrowserRouteDataProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAmB7E"}
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import RouterService from '../router/RouterService.js';
3
+ import { RouteDataProvider } from '../router/RouteDataContext.js';
4
+ /**
5
+ * Wraps children with RouteDataProvider using initial data from the browser:
6
+ * - window.__PRELOADED_DATA__ (from SSR)
7
+ * - window.location.pathname + RouterService for matchedRoute and routeParams
8
+ *
9
+ * Use this inside BrowserRouter so the app does not need to read __PRELOADED_DATA__
10
+ * or compute initial route/params manually.
11
+ */
12
+ export function BrowserRouteDataProvider({ children }) {
13
+ const preloadedData = typeof window !== 'undefined' ? (window.__PRELOADED_DATA__ ?? {}) : {};
14
+ const pathname = typeof window !== 'undefined' ? window.location.pathname || '/' : '/';
15
+ const matchedRoute = RouterService.matchRoute(pathname);
16
+ const routeParams = matchedRoute
17
+ ? RouterService.routeParams(matchedRoute.path, pathname)
18
+ : {};
19
+ return (_jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: routeParams, children: children }));
20
+ }
@@ -0,0 +1,7 @@
1
+ declare global {
2
+ interface Window {
3
+ __PRELOADED_DATA__?: Record<string, unknown>;
4
+ }
5
+ }
6
+ export {};
7
+ //# sourceMappingURL=globals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC9C;CACF;AAED,OAAO,EAAE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import './globals.js';
2
+ export type { RouteConfig, ComponentWithGetServerData } from './types.js';
3
+ export { registerRoutes, getRoutes } from './registry.js';
4
+ export { BrowserRouteDataProvider } from './components/BrowserRouteDataProvider.js';
5
+ export { default as RouterService } from './router/RouterService.js';
6
+ export { RouteDataProvider, useRouteData, buildRouteKey, type RouteDataState, type InitialRouteShape, } from './router/RouteDataContext.js';
7
+ export { AppRoutes } from './components/AppRoutes.js';
8
+ export { render, type RenderResult, type RenderOptions, } from './ssr/render.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AACtB,YAAY,EAAE,WAAW,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AACpF,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EACL,MAAM,EACN,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import './globals.js';
2
+ export { registerRoutes, getRoutes } from './registry.js';
3
+ export { BrowserRouteDataProvider } from './components/BrowserRouteDataProvider.js';
4
+ export { default as RouterService } from './router/RouterService.js';
5
+ export { RouteDataProvider, useRouteData, buildRouteKey, } from './router/RouteDataContext.js';
6
+ export { AppRoutes } from './components/AppRoutes.js';
7
+ export { render, } from './ssr/render.js';
@@ -0,0 +1,4 @@
1
+ import type { RouteConfig } from './types.js';
2
+ export declare function registerRoutes(r: RouteConfig[]): void;
3
+ export declare function getRoutes(): RouteConfig[];
4
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,wBAAgB,cAAc,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,IAAI,CAErD;AAED,wBAAgB,SAAS,IAAI,WAAW,EAAE,CAEzC"}
@@ -0,0 +1,7 @@
1
+ let routes = [];
2
+ export function registerRoutes(r) {
3
+ routes = r;
4
+ }
5
+ export function getRoutes() {
6
+ return routes;
7
+ }
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from 'react';
2
+ export type RouteDataState = Record<string, Record<string, unknown>>;
3
+ export declare function buildRouteKey(path: string, params: Record<string, string>): string;
4
+ interface RouteDataContextValue {
5
+ data: RouteDataState;
6
+ setData: (routeKey: string, value: Record<string, unknown>) => void;
7
+ }
8
+ export interface InitialRouteShape {
9
+ path: string;
10
+ }
11
+ interface RouteDataProviderProps {
12
+ children: ReactNode;
13
+ initialData?: Record<string, unknown>;
14
+ initialRoute?: InitialRouteShape;
15
+ initialParams?: Record<string, string>;
16
+ }
17
+ export declare function RouteDataProvider({ children, initialData, initialRoute, initialParams, }: RouteDataProviderProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function useRouteData(): RouteDataContextValue;
19
+ export {};
20
+ //# sourceMappingURL=RouteDataContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouteDataContext.d.ts","sourceRoot":"","sources":["../../src/router/RouteDataContext.tsx"],"names":[],"mappings":"AAAA,OAAc,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAErE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAKlF;AAED,UAAU,qBAAqB;IAC7B,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACrE;AAID,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,aAAkB,GACnB,EAAE,sBAAsB,2CAmBxB;AAED,wBAAgB,YAAY,IAAI,qBAAqB,CAMpD"}
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useMemo, useState, } from 'react';
3
+ export function buildRouteKey(path, params) {
4
+ const sorted = Object.keys(params)
5
+ .sort()
6
+ .reduce((acc, k) => ({ ...acc, [k]: params[k] }), {});
7
+ return `${path}|${JSON.stringify(sorted)}`;
8
+ }
9
+ const RouteDataContext = createContext(null);
10
+ export function RouteDataProvider({ children, initialData, initialRoute, initialParams = {}, }) {
11
+ const initialKey = initialRoute && Object.keys(initialData ?? {}).length > 0
12
+ ? buildRouteKey(initialRoute.path, initialParams)
13
+ : null;
14
+ const [data, setDataState] = useState(() => initialKey && initialData ? { [initialKey]: initialData } : {});
15
+ const setData = useCallback((routeKey, value) => {
16
+ setDataState((prev) => ({ ...prev, [routeKey]: value }));
17
+ }, []);
18
+ const value = useMemo(() => ({ data, setData }), [data, setData]);
19
+ return (_jsx(RouteDataContext.Provider, { value: value, children: children }));
20
+ }
21
+ export function useRouteData() {
22
+ const ctx = useContext(RouteDataContext);
23
+ if (!ctx) {
24
+ throw new Error('useRouteData must be used within RouteDataProvider');
25
+ }
26
+ return ctx;
27
+ }
@@ -0,0 +1,13 @@
1
+ import type { RouteConfig } from '../types.js';
2
+ declare function matchPath(pathPattern: string, pathname: string): boolean;
3
+ declare function isSsrRoute(pathname: string): boolean;
4
+ declare function matchRoute(pathname: string): RouteConfig | undefined;
5
+ declare function routeParams(routePath: string, pathname?: string): Record<string, string>;
6
+ declare const RouterService: {
7
+ isSsrRoute: typeof isSsrRoute;
8
+ matchPath: typeof matchPath;
9
+ matchRoute: typeof matchRoute;
10
+ routeParams: typeof routeParams;
11
+ };
12
+ export default RouterService;
13
+ //# sourceMappingURL=RouterService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouterService.d.ts","sourceRoot":"","sources":["../../src/router/RouterService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,iBAAS,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOjE;AAED,iBAAS,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG7C;AAED,iBAAS,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAM7D;AAED,iBAAS,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUjF;AAED,QAAA,MAAM,aAAa;;;;;CAKlB,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { getRoutes } from '../registry.js';
2
+ function matchPath(pathPattern, pathname) {
3
+ if (pathPattern === '*')
4
+ return true;
5
+ if (pathPattern === pathname)
6
+ return true;
7
+ const segments = pathname.split('/').filter(Boolean);
8
+ const patternSegments = pathPattern.split('/').filter(Boolean);
9
+ if (segments.length !== patternSegments.length)
10
+ return false;
11
+ return patternSegments.every((p, i) => p.startsWith(':') || p === segments[i]);
12
+ }
13
+ function isSsrRoute(pathname) {
14
+ const routes = getRoutes();
15
+ return routes.filter((r) => r.isSSR).some((route) => matchPath(route.path, pathname));
16
+ }
17
+ function matchRoute(pathname) {
18
+ const routes = getRoutes();
19
+ for (const route of routes) {
20
+ if (matchPath(route.path, pathname))
21
+ return route;
22
+ }
23
+ return routes.find((r) => r.path === '*');
24
+ }
25
+ function routeParams(routePath, pathname) {
26
+ const pathSegments = pathname?.split('/').filter(Boolean) || [];
27
+ const patternSegments = routePath.split('/').filter(Boolean);
28
+ const params = {};
29
+ patternSegments.forEach((segment, i) => {
30
+ if (segment.startsWith(':')) {
31
+ params[segment.slice(1)] = pathSegments[i];
32
+ }
33
+ });
34
+ return params;
35
+ }
36
+ const RouterService = {
37
+ isSsrRoute,
38
+ matchPath,
39
+ matchRoute,
40
+ routeParams,
41
+ };
42
+ export default RouterService;
@@ -0,0 +1,3 @@
1
+ export { default as RouterService } from './RouterService.js';
2
+ export { RouteDataProvider, useRouteData, buildRouteKey, type RouteDataState, type InitialRouteShape, } from './RouteDataContext.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default as RouterService } from './RouterService.js';
2
+ export { RouteDataProvider, useRouteData, buildRouteKey, } from './RouteDataContext.js';
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface RenderResult {
3
+ html: string;
4
+ preloadedData: Record<string, unknown>;
5
+ }
6
+ export interface RenderOptions {
7
+ wrap?: (children: ReactNode) => ReactNode;
8
+ }
9
+ export declare function render(url: string, options?: RenderOptions): Promise<RenderResult>;
10
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/ssr/render.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,SAAS,CAAC;CAC3C;AAED,wBAAsB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgCxF"}
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { renderToString } from 'react-dom/server';
3
+ import { StaticRouter } from 'react-router-dom/server';
4
+ import RouterService from '../router/RouterService.js';
5
+ import { RouteDataProvider } from '../router/RouteDataContext.js';
6
+ import { AppRoutes } from '../components/AppRoutes.js';
7
+ export async function render(url, options) {
8
+ const fullUrl = new URL(url, 'http://localhost');
9
+ const pathname = fullUrl.pathname || '/';
10
+ const matchedRoute = RouterService.matchRoute(pathname);
11
+ const params = matchedRoute ? RouterService.routeParams(matchedRoute.path, pathname) : {};
12
+ let preloadedData = { is_success: true };
13
+ const getServerData = matchedRoute?.Component?.getServerData;
14
+ if (typeof getServerData === 'function') {
15
+ try {
16
+ preloadedData = await getServerData(params);
17
+ }
18
+ catch (e) {
19
+ console.error(`SSR getServerData failed for ${matchedRoute?.path}:`, e);
20
+ preloadedData = { ...preloadedData, is_success: false };
21
+ }
22
+ }
23
+ const inner = (_jsx(StaticRouter, { location: url, children: _jsx(RouteDataProvider, { initialData: preloadedData, initialRoute: matchedRoute, initialParams: params, children: _jsx(AppRoutes, {}) }) }));
24
+ const app = options?.wrap ? options.wrap(inner) : inner;
25
+ const html = renderToString(app);
26
+ return { html, preloadedData };
27
+ }
@@ -0,0 +1,21 @@
1
+ import { type Express } from 'express';
2
+ export interface RenderResult {
3
+ html: string;
4
+ preloadedData: Record<string, unknown>;
5
+ }
6
+ export interface CreateServerConfig {
7
+ root: string;
8
+ /** Path to the app entry-server module relative to root (e.g. 'src/entry-server.tsx') */
9
+ entryPath: string;
10
+ port?: number;
11
+ /** Optional link tag to inject in dev for CSS (e.g. '<link rel="stylesheet" href="/src/index.css">') */
12
+ cssLinkInDev?: string;
13
+ }
14
+ export declare function createServer(config: CreateServerConfig): Promise<{
15
+ getApp: () => Express;
16
+ listen: (port?: number, callback?: () => void) => void;
17
+ }>;
18
+ /** Standalone: run server when this file is executed (e.g. tsx packages/lovable-ssr/src/ssr/server.ts).
19
+ * Set env ROOT, ENTRY_PATH, PORT or use defaults. */
20
+ export declare function runServer(config?: Partial<CreateServerConfig>): void;
21
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/ssr/server.ts"],"names":[],"mappings":"AAGA,OAAgB,EAAE,KAAK,OAAO,EAAkD,MAAM,SAAS,CAAC;AAIhG,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wGAAwG;IACxG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAgKD,wBAAsB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC;IACtE,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACxD,CAAC,CAOD;AAED;qDACqD;AACrD,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,QAO7D"}
@@ -0,0 +1,149 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import express from 'express';
5
+ import { createServer as createViteServer } from 'vite';
6
+ import RouterService from '../router/RouterService.js';
7
+ function defaultDistEntryPath(entryPath) {
8
+ return entryPath
9
+ .replace(/^src\//, 'dist/')
10
+ .replace(/\.tsx?$/, '.js');
11
+ }
12
+ class SsrServer {
13
+ app;
14
+ vite;
15
+ config;
16
+ isProd;
17
+ constructor(config) {
18
+ this.config = {
19
+ root: path.resolve(config.root),
20
+ entryPath: config.entryPath,
21
+ port: config.port ?? 5173,
22
+ cssLinkInDev: config.cssLinkInDev ?? '<link rel="stylesheet" href="/src/index.css"></head>',
23
+ };
24
+ this.isProd = process.env.NODE_ENV === 'production';
25
+ this.app = express();
26
+ }
27
+ static async create(config) {
28
+ const server = new SsrServer(config);
29
+ await server.configureVite();
30
+ server.configureStaticAssets();
31
+ server.configureRequestHandler();
32
+ return server;
33
+ }
34
+ async configureVite() {
35
+ if (this.isProd)
36
+ return;
37
+ this.vite = await createViteServer({
38
+ server: { middlewareMode: true },
39
+ appType: 'custom',
40
+ root: this.config.root,
41
+ });
42
+ this.app.use(this.vite.middlewares);
43
+ }
44
+ configureStaticAssets() {
45
+ if (!this.isProd)
46
+ return;
47
+ this.app.use(express.static(path.join(this.config.root, 'dist'), { index: false }));
48
+ }
49
+ configureRequestHandler() {
50
+ this.app.use('*', (req, res, next) => this.handleRequest(req, res, next));
51
+ }
52
+ async handleRequest(req, res, next) {
53
+ const url = req.originalUrl;
54
+ const pathname = url.replace(/\?.*$/, '').replace(/#.*$/, '') || '/';
55
+ try {
56
+ if (!RouterService.isSsrRoute(pathname)) {
57
+ return await this.renderSpa(url, res);
58
+ }
59
+ return await this.renderSsr(url, res);
60
+ }
61
+ catch (e) {
62
+ if (this.vite) {
63
+ this.vite.ssrFixStacktrace?.(e);
64
+ }
65
+ next(e);
66
+ }
67
+ }
68
+ async renderSpa(url, res) {
69
+ if (this.vite) {
70
+ let template = this.readTemplate(path.join(this.config.root, 'index.html'));
71
+ template = this.injectCssInDev(template);
72
+ const html = await this.vite.transformIndexHtml(url, template);
73
+ return res.status(200).set({ 'Content-Type': 'text/html' }).send(html);
74
+ }
75
+ const template = this.readTemplate(path.join(this.config.root, 'dist', 'index.html'));
76
+ return res.status(200).set({ 'Content-Type': 'text/html' }).send(template);
77
+ }
78
+ async renderSsr(url, res) {
79
+ const { template, render } = await this.getSsrRenderer();
80
+ const result = await render(url);
81
+ const appHtml = typeof result.html === 'string' ? result.html : '';
82
+ const preloadedData = result.preloadedData ?? {};
83
+ let html = template.replace('<div id="root"></div>', `<div id="root">${appHtml}</div>`);
84
+ html = this.injectPreloadedData(html, preloadedData);
85
+ if (this.vite) {
86
+ const transformed = await this.vite.transformIndexHtml(url, html);
87
+ return res.status(200).set({ 'Content-Type': 'text/html' }).send(transformed);
88
+ }
89
+ return res.status(200).set({ 'Content-Type': 'text/html' }).send(html);
90
+ }
91
+ async getSsrRenderer() {
92
+ if (this.vite) {
93
+ const template = this.injectCssInDev(this.readTemplate(path.join(this.config.root, 'index.html')));
94
+ const entry = await this.vite.ssrLoadModule(path.join(this.config.root, this.config.entryPath));
95
+ return {
96
+ template,
97
+ render: entry.render,
98
+ };
99
+ }
100
+ const template = this.readTemplate(path.join(this.config.root, 'dist', 'index.html'));
101
+ const distEntryPath = defaultDistEntryPath(this.config.entryPath);
102
+ const entryUrl = pathToFileURL(path.join(this.config.root, distEntryPath)).href;
103
+ const entry = await import(entryUrl);
104
+ return {
105
+ template,
106
+ render: entry.render,
107
+ };
108
+ }
109
+ readTemplate(fullPath) {
110
+ return fs.readFileSync(fullPath, 'utf-8');
111
+ }
112
+ injectCssInDev(html) {
113
+ if (!this.vite)
114
+ return html;
115
+ return html.replace('</head>', this.config.cssLinkInDev);
116
+ }
117
+ injectPreloadedData(html, preloadedData) {
118
+ if (Object.keys(preloadedData).length === 0)
119
+ return html;
120
+ const script = `<script>window.__PRELOADED_DATA__=${JSON.stringify(preloadedData)};</script>`;
121
+ return html.replace('</body>', `${script}</body>`);
122
+ }
123
+ listen(port, callback) {
124
+ const p = port ?? this.config.port;
125
+ this.app.listen(p, callback ?? (() => {
126
+ console.log(`SSR server running at http://localhost:${p}`);
127
+ }));
128
+ }
129
+ getApp() {
130
+ return this.app;
131
+ }
132
+ }
133
+ export async function createServer(config) {
134
+ const server = await SsrServer.create(config);
135
+ return {
136
+ getApp: () => server.getApp(),
137
+ listen: (port, callback) => server.listen(port, callback),
138
+ };
139
+ }
140
+ /** Standalone: run server when this file is executed (e.g. tsx packages/lovable-ssr/src/ssr/server.ts).
141
+ * Set env ROOT, ENTRY_PATH, PORT or use defaults. */
142
+ export function runServer(config) {
143
+ const root = config?.root ?? process.cwd();
144
+ const entryPath = config?.entryPath ?? 'src/entry-server.tsx';
145
+ const port = config?.port ?? (process.env.PORT ? Number(process.env.PORT) : 5173);
146
+ createServer({ root, entryPath, port, ...config })
147
+ .then((s) => s.listen(port))
148
+ .catch(console.error);
149
+ }
@@ -0,0 +1,10 @@
1
+ import type React from 'react';
2
+ export type ComponentWithGetServerData = React.ComponentType<any> & {
3
+ getServerData?: (params?: Record<string, string>) => Promise<Record<string, unknown>>;
4
+ };
5
+ export type RouteConfig = {
6
+ path: string;
7
+ Component: ComponentWithGetServerData;
8
+ isSSR: boolean;
9
+ };
10
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,MAAM,0BAA0B,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG;IAClE,aAAa,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvF,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,0BAA0B,CAAC;IACtC,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "lovable-ssr",
3
+ "version": "0.1.2",
4
+ "description": "SSR and route data engine for Lovable projects",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./server": {
15
+ "import": "./dist/ssr/server.js",
16
+ "types": "./dist/ssr/server.d.ts"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc && npm run build:docs",
24
+ "dev": "tsc --watch",
25
+ "changelog": "standard-version --message \"[skip ci]\"",
26
+ "changelog:rc": "npm run changelog -- --prerelease rc --skip.changelog --skip.tag",
27
+ "changelog:patch": "npm run changelog -- --release-as patch --prerelease rc",
28
+ "changelog:minor": "npm run changelog -- --release-as minor --prerelease rc",
29
+ "changelog:major": "npm run changelog -- --release-as major --prerelease rc",
30
+ "build:docs": "cd doc && npm run docs:build"
31
+ },
32
+ "peerDependencies": {
33
+ "react": "^18.0.0",
34
+ "react-dom": "^18.0.0",
35
+ "react-router-dom": "^6.0.0"
36
+ },
37
+ "dependencies": {
38
+ "express": "^4.21.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/express": "^4.17.21",
42
+ "@types/node": "^22.16.5",
43
+ "@types/react": "^18.3.23",
44
+ "@types/react-dom": "^18.3.7",
45
+ "react": "^18.3.1",
46
+ "react-dom": "^18.3.1",
47
+ "react-router-dom": "^6.30.1",
48
+ "standard-version": "^9.5.0",
49
+ "typescript": "^5.8.3",
50
+ "vite": "^5.4.19"
51
+ },
52
+ "engines": {
53
+ "node": ">=18"
54
+ }
55
+ }