olovaplugin 1.0.13 → 1.0.15

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.
Files changed (62) hide show
  1. package/README.md +26 -17
  2. package/dist/auto-generate.d.ts +3 -0
  3. package/dist/auto-generate.d.ts.map +1 -0
  4. package/dist/auto-generate.js +114 -0
  5. package/dist/auto-generate.js.map +1 -0
  6. package/dist/clean-url.d.ts +3 -0
  7. package/dist/clean-url.d.ts.map +1 -0
  8. package/dist/clean-url.js +40 -0
  9. package/dist/clean-url.js.map +1 -0
  10. package/dist/config.d.ts +3 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +99 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/error-overlay.d.ts +3 -0
  15. package/dist/error-overlay.d.ts.map +1 -0
  16. package/dist/error-overlay.js +59 -0
  17. package/dist/error-overlay.js.map +1 -0
  18. package/dist/framework.d.ts +3 -0
  19. package/dist/framework.d.ts.map +1 -0
  20. package/dist/framework.js +41 -0
  21. package/dist/framework.js.map +1 -0
  22. package/dist/hydration.d.ts +19 -0
  23. package/dist/hydration.d.ts.map +1 -0
  24. package/dist/hydration.js +221 -0
  25. package/dist/hydration.js.map +1 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +44 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/router.d.ts +3 -0
  31. package/dist/router.d.ts.map +1 -0
  32. package/dist/router.js +165 -0
  33. package/dist/router.js.map +1 -0
  34. package/dist/runtime/router.d.ts +81 -0
  35. package/dist/runtime/router.d.ts.map +1 -0
  36. package/dist/runtime/router.js +284 -0
  37. package/dist/runtime/router.js.map +1 -0
  38. package/dist/ssg.d.ts +3 -0
  39. package/dist/ssg.d.ts.map +1 -0
  40. package/dist/ssg.js +376 -0
  41. package/dist/ssg.js.map +1 -0
  42. package/dist/terminal.d.ts +68 -0
  43. package/dist/terminal.d.ts.map +1 -0
  44. package/dist/terminal.js +143 -0
  45. package/dist/terminal.js.map +1 -0
  46. package/dist/utils.d.ts +8 -0
  47. package/dist/utils.d.ts.map +1 -0
  48. package/dist/utils.js +68 -0
  49. package/dist/utils.js.map +1 -0
  50. package/dist/virtual-html.d.ts +3 -0
  51. package/dist/virtual-html.d.ts.map +1 -0
  52. package/dist/virtual-html.js +178 -0
  53. package/dist/virtual-html.js.map +1 -0
  54. package/package.json +33 -30
  55. package/runtime/client.tsx +119 -0
  56. package/runtime/index.ts +18 -0
  57. package/runtime/router.tsx +381 -0
  58. package/runtime/server.tsx +110 -0
  59. package/dist/olova-plugins.cjs +0 -1683
  60. package/dist/olova-plugins.d.cts +0 -123
  61. package/dist/olova-plugins.d.ts +0 -123
  62. package/dist/olova-plugins.js +0 -1622
@@ -0,0 +1,381 @@
1
+ import React, { useState, useEffect, createContext, useContext, type ReactNode } from 'react';
2
+
3
+ // @ts-ignore - Virtual module generated by olovaplugin
4
+ import { routes } from 'olova/routes';
5
+
6
+ // =============================================================================
7
+ // ROUTER CONTEXT
8
+ // =============================================================================
9
+ const RouterContext = createContext<{ params: Record<string, string>, path: string }>({ params: {}, path: '/' });
10
+
11
+ export function useParams() {
12
+ return useContext(RouterContext).params;
13
+ }
14
+
15
+ export function usePath() {
16
+ return useContext(RouterContext).path;
17
+ }
18
+
19
+ /**
20
+ * Returns the current pathname (without query string or hash)
21
+ * Similar to Next.js usePathname hook
22
+ */
23
+ export function usePathname(): string {
24
+ const [pathname, setPathname] = useState(() => {
25
+ if (typeof window === 'undefined') return '/';
26
+ return window.location.pathname;
27
+ });
28
+
29
+ useEffect(() => {
30
+ if (typeof window === 'undefined') return;
31
+
32
+ const handleNavigation = () => {
33
+ setPathname(window.location.pathname);
34
+ };
35
+
36
+ window.addEventListener('popstate', handleNavigation);
37
+ window.addEventListener('pushstate', handleNavigation);
38
+
39
+ return () => {
40
+ window.removeEventListener('popstate', handleNavigation);
41
+ window.removeEventListener('pushstate', handleNavigation);
42
+ };
43
+ }, []);
44
+
45
+ return pathname;
46
+ }
47
+
48
+ /**
49
+ * A read-only interface for URLSearchParams
50
+ * Similar to Next.js useSearchParams hook
51
+ */
52
+ interface ReadonlyURLSearchParams {
53
+ get(name: string): string | null;
54
+ getAll(name: string): string[];
55
+ has(name: string): boolean;
56
+ keys(): IterableIterator<string>;
57
+ values(): IterableIterator<string>;
58
+ entries(): IterableIterator<[string, string]>;
59
+ forEach(callback: (value: string, key: string, parent: URLSearchParams) => void): void;
60
+ toString(): string;
61
+ size: number;
62
+ }
63
+
64
+ /**
65
+ * Returns the current URL search parameters
66
+ * Similar to Next.js useSearchParams hook
67
+ */
68
+ export function useSearchParams(): ReadonlyURLSearchParams {
69
+ const [searchParams, setSearchParams] = useState<URLSearchParams>(() => {
70
+ if (typeof window === 'undefined') return new URLSearchParams();
71
+ return new URLSearchParams(window.location.search);
72
+ });
73
+
74
+ useEffect(() => {
75
+ if (typeof window === 'undefined') return;
76
+
77
+ const handleNavigation = () => {
78
+ setSearchParams(new URLSearchParams(window.location.search));
79
+ };
80
+
81
+ window.addEventListener('popstate', handleNavigation);
82
+ window.addEventListener('pushstate', handleNavigation);
83
+
84
+ return () => {
85
+ window.removeEventListener('popstate', handleNavigation);
86
+ window.removeEventListener('pushstate', handleNavigation);
87
+ };
88
+ }, []);
89
+
90
+ return {
91
+ get: (name: string) => searchParams.get(name),
92
+ getAll: (name: string) => searchParams.getAll(name),
93
+ has: (name: string) => searchParams.has(name),
94
+ keys: () => searchParams.keys(),
95
+ values: () => searchParams.values(),
96
+ entries: () => searchParams.entries(),
97
+ forEach: (callback) => searchParams.forEach(callback),
98
+ toString: () => searchParams.toString(),
99
+ get size() { return Array.from(searchParams.keys()).length; }
100
+ };
101
+ }
102
+
103
+ // =============================================================================
104
+ // ROUTE MATCHING
105
+ // =============================================================================
106
+ const normalizePath = (path: string) => {
107
+ let p = path.split('?')[0].split('#')[0];
108
+ if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
109
+ return p || '/';
110
+ };
111
+
112
+ export function matchRoute(path: string) {
113
+ const normalizedPath = normalizePath(path);
114
+ const routeKeys = Object.keys(routes);
115
+
116
+ for (const route of routeKeys) {
117
+ if (route === normalizedPath) {
118
+ return { loader: routes[route], params: {}, pattern: route };
119
+ }
120
+
121
+ const regexPath = route
122
+ .replace(/:[^\/]+/g, '([^/]+)')
123
+ .replace(/\$[^\/]+/g, '([^/]+)');
124
+
125
+ const regex = new RegExp(`^${regexPath}$`);
126
+ const match = normalizedPath.match(regex);
127
+
128
+ if (match) {
129
+ const params: Record<string, string> = {};
130
+ const paramNames = (route.match(/[:$][^\/]+/g) || []).map(s => s.slice(1));
131
+ paramNames.forEach((name, i) => {
132
+ params[name] = match[i + 1];
133
+ });
134
+ return { loader: routes[route], params, pattern: route };
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+
140
+ // Metadata type - like Next.js
141
+ export interface Metadata {
142
+ title?: string;
143
+ description?: string;
144
+ keywords?: string | string[];
145
+ openGraph?: {
146
+ title?: string;
147
+ description?: string;
148
+ url?: string;
149
+ siteName?: string;
150
+ images?: { url: string; width?: number; height?: number; alt?: string }[];
151
+ type?: string;
152
+ };
153
+ twitter?: {
154
+ card?: 'summary' | 'summary_large_image' | 'app' | 'player';
155
+ site?: string;
156
+ creator?: string;
157
+ title?: string;
158
+ description?: string;
159
+ images?: string[];
160
+ };
161
+ robots?: string;
162
+ canonical?: string;
163
+ jsonLd?: object | object[];
164
+ }
165
+
166
+ // Simple markdown to HTML converter
167
+ function parseMarkdown(md: string): string {
168
+ return md
169
+ .replace(/^### (.*$)/gim, '<h3>$1</h3>')
170
+ .replace(/^## (.*$)/gim, '<h2>$1</h2>')
171
+ .replace(/^# (.*$)/gim, '<h1>$1</h1>')
172
+ .replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
173
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
174
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
175
+ .replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
176
+ .replace(/`(.*?)`/g, '<code>$1</code>')
177
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
178
+ .replace(/\n\n/g, '</p><p>')
179
+ .replace(/\n/g, '<br>')
180
+ .replace(/^(.*)$/, '<p>$1</p>');
181
+ }
182
+
183
+ function HtmlContent({ html }: { html: string }) {
184
+ return React.createElement('div', {
185
+ dangerouslySetInnerHTML: { __html: html },
186
+ className: 'olova-html-content'
187
+ });
188
+ }
189
+
190
+ function MarkdownContent({ markdown }: { markdown: string }) {
191
+ const html = parseMarkdown(markdown);
192
+ return React.createElement('article', {
193
+ dangerouslySetInnerHTML: { __html: html },
194
+ className: 'olova-markdown-content'
195
+ });
196
+ }
197
+
198
+ export async function loadRoute(path: string) {
199
+ const match = matchRoute(path);
200
+ if (match) {
201
+ console.log(`[Router] Loading route: ${path} (pattern: ${match.pattern})`);
202
+ const module = await match.loader();
203
+
204
+ if (module.__isHtml) {
205
+ return {
206
+ module: {
207
+ default: () => HtmlContent({ html: module.__rawHtml as string }),
208
+ },
209
+ params: match.params,
210
+ metadata: undefined
211
+ };
212
+ }
213
+
214
+ if (module.__isMd) {
215
+ return {
216
+ module: {
217
+ default: () => MarkdownContent({ markdown: module.default as unknown as string }),
218
+ },
219
+ params: match.params,
220
+ metadata: undefined
221
+ };
222
+ }
223
+
224
+ return {
225
+ module,
226
+ params: match.params,
227
+ metadata: module.metadata as Metadata | undefined
228
+ };
229
+ }
230
+ console.warn(`[Router] No match for: ${path}`);
231
+ return null;
232
+ }
233
+
234
+ // =============================================================================
235
+ // ROUTER COMPONENT
236
+ // =============================================================================
237
+ interface RouterProps {
238
+ url?: string;
239
+ initialComponent?: React.ComponentType;
240
+ initialParams?: Record<string, string>;
241
+ onRouteChange?: (metadata: Metadata | undefined) => void;
242
+ }
243
+
244
+ export function Router({ url, initialComponent, initialParams, onRouteChange }: RouterProps) {
245
+ const [path, setPath] = useState(() => normalizePath(url || (typeof window !== 'undefined' ? window.location.pathname : '/')));
246
+ const [Component, setComponent] = useState<React.ComponentType | null>(() => initialComponent || null);
247
+ const [params, setParams] = useState<Record<string, string>>(() => initialParams || {});
248
+ const hasHydrated = React.useRef(false);
249
+
250
+ useEffect(() => {
251
+ if (typeof window === 'undefined') return;
252
+
253
+ const handleNavigation = () => {
254
+ const newPath = normalizePath(window.location.pathname);
255
+ console.log(`[Router] Navigation event: ${newPath}`);
256
+ setPath(newPath);
257
+ };
258
+
259
+ window.addEventListener('popstate', handleNavigation);
260
+ window.addEventListener('pushstate', handleNavigation);
261
+
262
+ return () => {
263
+ window.removeEventListener('popstate', handleNavigation);
264
+ window.removeEventListener('pushstate', handleNavigation);
265
+ };
266
+ }, []);
267
+
268
+ useEffect(() => {
269
+ if (!hasHydrated.current && initialComponent && path === normalizePath(url || '')) {
270
+ console.log(`[Router] Hydration skipped for: ${path}`);
271
+ hasHydrated.current = true;
272
+ return;
273
+ }
274
+
275
+ let isCancelled = false;
276
+ const load = async () => {
277
+ const result = await loadRoute(path);
278
+ if (!isCancelled) {
279
+ if (result) {
280
+ setComponent(() => result.module.default);
281
+ setParams(result.params);
282
+ if (onRouteChange) {
283
+ onRouteChange(result.metadata);
284
+ }
285
+ } else {
286
+ const fallbackResult = await loadRoute('/404');
287
+ if (fallbackResult) {
288
+ console.log('[Router] Serving custom 404 page');
289
+ setComponent(() => fallbackResult.module.default);
290
+ setParams(fallbackResult.params);
291
+ if (onRouteChange) {
292
+ onRouteChange(fallbackResult.metadata);
293
+ }
294
+ } else {
295
+ setComponent(() => () => <div>404 Not Found</div>);
296
+ setParams({});
297
+ if (onRouteChange) {
298
+ onRouteChange(undefined);
299
+ }
300
+ }
301
+ }
302
+ }
303
+ };
304
+
305
+ load();
306
+ return () => { isCancelled = true; };
307
+ }, [path, onRouteChange]);
308
+
309
+ if (!Component) return <div>Loading...</div>;
310
+
311
+ return (
312
+ <RouterContext.Provider value={{ params, path }}>
313
+ <Component {...params} />
314
+ </RouterContext.Provider>
315
+ );
316
+ }
317
+
318
+ // =============================================================================
319
+ // LINK COMPONENT
320
+ // =============================================================================
321
+ export function Link({ href, children, ...props }: { href: string, children: ReactNode } & React.AnchorHTMLAttributes<HTMLAnchorElement>): React.ReactElement {
322
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
323
+ if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
324
+ if (href.startsWith('http') || href.startsWith('//')) return;
325
+
326
+ e.preventDefault();
327
+
328
+ const currentUrl = window.location.pathname + window.location.search;
329
+ const targetUrl = href.startsWith('/') ? href : '/' + href;
330
+
331
+ if (currentUrl === targetUrl) return;
332
+
333
+ window.history.pushState({}, '', href);
334
+ window.dispatchEvent(new Event('pushstate'));
335
+ };
336
+
337
+ return (
338
+ <a href={href} onClick={handleClick} {...props}>
339
+ {children}
340
+ </a>
341
+ );
342
+ }
343
+
344
+ // =============================================================================
345
+ // HYDRATION UTILITIES
346
+ // =============================================================================
347
+ export function parseFlightData(): Record<string, unknown> | null {
348
+ if (typeof globalThis === 'undefined' || typeof (globalThis as Record<string, unknown>).document === 'undefined') {
349
+ return null;
350
+ }
351
+
352
+ const flightArray = ((globalThis as Record<string, unknown>).__olova_f) as unknown[] | undefined;
353
+ if (!flightArray) return null;
354
+
355
+ type FlightDataType = 'M' | 'T' | 'R' | 'P' | 'A' | 'S' | 'D' | 'H' | 'E';
356
+
357
+ const result: Record<string, unknown> = {};
358
+ const typeMap: Record<FlightDataType, string> = {
359
+ 'M': '$meta',
360
+ 'T': '$tree',
361
+ 'R': '$route',
362
+ 'P': '$params',
363
+ 'A': '$assets',
364
+ 'S': '$state',
365
+ 'D': '$schema',
366
+ 'H': '$hints',
367
+ 'E': '$end'
368
+ };
369
+
370
+ for (const chunk of flightArray) {
371
+ if (Array.isArray(chunk) && chunk.length >= 3) {
372
+ const [_index, type, data] = chunk;
373
+ const key = typeMap[type as FlightDataType];
374
+ if (key && key !== '$end') {
375
+ result[key] = data;
376
+ }
377
+ }
378
+ }
379
+
380
+ return result;
381
+ }
@@ -0,0 +1,110 @@
1
+ // =============================================================================
2
+ // OLOVA SERVER ENTRY - Handles SSG/SSR rendering
3
+ // Real TSX file served via Vite alias
4
+ // =============================================================================
5
+
6
+ import { renderToString } from 'react-dom/server';
7
+ // @ts-ignore - User's root layout
8
+ import Layout, { metadata as defaultMetadata } from '/src/root.tsx';
9
+ import { Router, loadRoute, type Metadata } from './router';
10
+
11
+ // =============================================================================
12
+ // SEO HEAD GENERATION
13
+ // =============================================================================
14
+ function generateSeoHead(metadata: Metadata | undefined): string {
15
+ const meta = { ...defaultMetadata, ...metadata } as Metadata;
16
+ let head = '';
17
+
18
+ if (meta.title) {
19
+ head += `<title>${meta.title}</title>\n`;
20
+ }
21
+
22
+ if (meta.description) {
23
+ head += `<meta name="description" content="${meta.description}" />\n`;
24
+ }
25
+ if (meta.keywords) {
26
+ const content = Array.isArray(meta.keywords) ? meta.keywords.join(', ') : meta.keywords;
27
+ head += `<meta name="keywords" content="${content}" />\n`;
28
+ }
29
+ if (meta.robots) {
30
+ head += `<meta name="robots" content="${meta.robots}" />\n`;
31
+ }
32
+ if (meta.canonical) {
33
+ head += `<link rel="canonical" href="${meta.canonical}" />\n`;
34
+ }
35
+
36
+ if (meta.openGraph) {
37
+ const og = meta.openGraph;
38
+ head += `<meta property="og:title" content="${og.title || meta.title}" />\n`;
39
+ if (og.description) head += `<meta property="og:description" content="${og.description}" />\n`;
40
+ if (og.url) head += `<meta property="og:url" content="${og.url}" />\n`;
41
+ if (og.siteName) head += `<meta property="og:site_name" content="${og.siteName}" />\n`;
42
+ if (og.type) head += `<meta property="og:type" content="${og.type}" />\n`;
43
+ if (og.images) {
44
+ og.images.forEach(img => {
45
+ head += `<meta property="og:image" content="${img.url}" />\n`;
46
+ });
47
+ }
48
+ }
49
+
50
+ if (meta.twitter) {
51
+ const tw = meta.twitter;
52
+ head += `<meta name="twitter:card" content="${tw.card || 'summary'}" />\n`;
53
+ if (tw.site) head += `<meta name="twitter:site" content="${tw.site}" />\n`;
54
+ if (tw.creator) head += `<meta name="twitter:creator" content="${tw.creator}" />\n`;
55
+ head += `<meta name="twitter:title" content="${tw.title || meta.title}" />\n`;
56
+ if (tw.description) head += `<meta name="twitter:description" content="${tw.description}" />\n`;
57
+ if (tw.images) {
58
+ tw.images.forEach(img => {
59
+ head += `<meta name="twitter:image" content="${img}" />\n`;
60
+ });
61
+ }
62
+ }
63
+
64
+ return head;
65
+ }
66
+
67
+ // =============================================================================
68
+ // SERVER RENDER FUNCTIONS
69
+ // =============================================================================
70
+ export async function render(url: string) {
71
+ const result = await loadRoute(url);
72
+ const Component = result ? result.module.default : () => <div>404 Not Found</div>;
73
+ const params = result ? result.params : {};
74
+ const metadata = result ? result.metadata : undefined;
75
+
76
+ const seoHead = generateSeoHead(metadata);
77
+
78
+ let html = renderToString(
79
+ <Layout>
80
+ <Router url={url} initialComponent={Component} initialParams={params} />
81
+ </Layout>
82
+ );
83
+
84
+ if (html.includes('</head>')) {
85
+ html = html.replace('</head>', seoHead + '</head>');
86
+ }
87
+
88
+ return { html, hydrationData: { params, metadata } };
89
+ }
90
+
91
+ export function renderShell(): string {
92
+ const seoHead = generateSeoHead(undefined);
93
+ let html = renderToString(<Layout>{null}</Layout>);
94
+ if (html.includes('</head>')) {
95
+ html = html.replace('</head>', seoHead + '</head>');
96
+ }
97
+ return html;
98
+ }
99
+
100
+ export function renderShellWithMetadata(metadata: Metadata | undefined): string {
101
+ const seoHead = generateSeoHead(metadata);
102
+ let html = renderToString(<Layout>{null}</Layout>);
103
+ if (html.includes('</head>')) {
104
+ html = html.replace('</head>', seoHead + '</head>');
105
+ }
106
+ return html;
107
+ }
108
+
109
+ // Re-export loadRoute for SSG
110
+ export { loadRoute };