olova 2.0.55 → 2.0.56

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 (84) hide show
  1. package/README.md +28 -288
  2. package/dist/chunk-23UAGQ6N.js +2208 -0
  3. package/dist/chunk-23UAGQ6N.js.map +1 -0
  4. package/dist/chunk-D7SIC5TC.js +367 -0
  5. package/dist/chunk-D7SIC5TC.js.map +1 -0
  6. package/dist/entry-server.cjs +2341 -0
  7. package/dist/entry-server.cjs.map +1 -0
  8. package/dist/entry-server.js +114 -0
  9. package/dist/entry-server.js.map +1 -0
  10. package/dist/entry-worker.cjs +2354 -0
  11. package/dist/entry-worker.cjs.map +1 -0
  12. package/dist/entry-worker.js +126 -0
  13. package/dist/entry-worker.js.map +1 -0
  14. package/dist/main.cjs +18 -0
  15. package/dist/main.cjs.map +1 -0
  16. package/dist/main.js +16 -0
  17. package/dist/main.js.map +1 -0
  18. package/dist/olova.cjs +1684 -0
  19. package/dist/olova.cjs.map +1 -0
  20. package/dist/olova.d.cts +72 -0
  21. package/dist/olova.d.ts +72 -0
  22. package/dist/olova.js +1325 -0
  23. package/dist/olova.js.map +1 -0
  24. package/dist/performance.cjs +386 -0
  25. package/dist/performance.cjs.map +1 -0
  26. package/dist/performance.js +3 -0
  27. package/dist/performance.js.map +1 -0
  28. package/dist/router.cjs +646 -0
  29. package/dist/router.cjs.map +1 -0
  30. package/dist/router.d.cts +113 -0
  31. package/dist/router.d.ts +113 -0
  32. package/dist/router.js +632 -0
  33. package/dist/router.js.map +1 -0
  34. package/main.tsx +76 -0
  35. package/olova.ts +619 -0
  36. package/package.json +42 -61
  37. package/src/entry-server.tsx +165 -0
  38. package/src/entry-worker.tsx +201 -0
  39. package/src/generator/index.ts +409 -0
  40. package/src/hydration/flight.ts +320 -0
  41. package/src/hydration/index.ts +12 -0
  42. package/src/hydration/types.ts +225 -0
  43. package/src/logger.ts +182 -0
  44. package/src/main.tsx +24 -0
  45. package/src/performance.ts +488 -0
  46. package/src/plugin/index.ts +204 -0
  47. package/src/router/ErrorBoundary.tsx +145 -0
  48. package/src/router/Link.tsx +117 -0
  49. package/src/router/OlovaRouter.tsx +354 -0
  50. package/src/router/Outlet.tsx +8 -0
  51. package/src/router/context.ts +117 -0
  52. package/src/router/index.ts +29 -0
  53. package/src/router/matching.ts +63 -0
  54. package/src/router/router.tsx +23 -0
  55. package/src/router/search-params.ts +29 -0
  56. package/src/scanner/index.ts +116 -0
  57. package/src/types/index.ts +191 -0
  58. package/src/utils/export.ts +85 -0
  59. package/src/utils/index.ts +4 -0
  60. package/src/utils/naming.ts +54 -0
  61. package/src/utils/path.ts +45 -0
  62. package/tsup.config.ts +35 -0
  63. package/CHANGELOG.md +0 -31
  64. package/LICENSE +0 -21
  65. package/dist/index.cjs +0 -883
  66. package/dist/index.cjs.map +0 -1
  67. package/dist/index.d.cts +0 -138
  68. package/dist/index.d.ts +0 -138
  69. package/dist/index.js +0 -832
  70. package/dist/index.js.map +0 -1
  71. package/dist/plugin.cjs +0 -927
  72. package/dist/plugin.cjs.map +0 -1
  73. package/dist/plugin.d.cts +0 -18
  74. package/dist/plugin.d.ts +0 -18
  75. package/dist/plugin.js +0 -894
  76. package/dist/plugin.js.map +0 -1
  77. package/dist/ssg.cjs +0 -637
  78. package/dist/ssg.cjs.map +0 -1
  79. package/dist/ssg.d.cts +0 -191
  80. package/dist/ssg.d.ts +0 -191
  81. package/dist/ssg.js +0 -585
  82. package/dist/ssg.js.map +0 -1
  83. package/dist/types-BT6YsBGO.d.cts +0 -143
  84. package/dist/types-BT6YsBGO.d.ts +0 -143
@@ -0,0 +1,204 @@
1
+ import mdx from '@mdx-js/rollup';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import type { Plugin } from 'vite';
5
+ import { generateRouteTree } from '../generator';
6
+ import { scanRoutes } from '../scanner';
7
+ import type { ErrorWithExport, LayoutWithExport, LoadingWithExport, MiddlewareWithExport, NotFoundWithExport, OlovaRouterOptions, PluginOption, ResolvedConfig, RouteWithExport } from '../types';
8
+ import { detectExportType, getRouteName } from '../utils';
9
+
10
+ export function olovaRouter(options: OlovaRouterOptions = {}): PluginOption[] {
11
+ const rootDir = options.rootDir || 'src';
12
+ const extensions = options.extensions || ['.tsx', '.ts', '.mdx'];
13
+ const packageName = options.packageName || 'olovastart';
14
+
15
+ let config: ResolvedConfig;
16
+ let absoluteRootDir: string;
17
+ let watcher: fs.FSWatcher | null = null;
18
+ let timer: NodeJS.Timeout | null = null;
19
+
20
+ function generateRouteTreeFile() {
21
+ const { routes, notFoundPages, layouts, loadingPages, errorPages, middlewares } = scanRoutes(absoluteRootDir, extensions);
22
+
23
+ const routeConfigs: RouteWithExport[] = routes.map(r => {
24
+ let exportInfo = detectExportType(r.filePath);
25
+
26
+ if (r.filePath.toLowerCase().endsWith('.mdx')) {
27
+ exportInfo = {
28
+ ...exportInfo,
29
+ hasDefault: true,
30
+ namedExport: null
31
+ };
32
+ }
33
+
34
+ return {
35
+ path: r.path,
36
+ component: r.filePath.replace(/\\/g, '/'),
37
+ params: r.params.length > 0 ? r.params : undefined,
38
+ hasDefault: exportInfo.hasDefault,
39
+ namedExport: exportInfo.namedExport,
40
+ hasMetadata: exportInfo.hasMetadata,
41
+ metadataSource: exportInfo.metadataSource,
42
+ hasRoute: exportInfo.hasRoute,
43
+ hasGetStaticPaths: exportInfo.hasGetStaticPaths,
44
+ hasLoader: exportInfo.hasLoader
45
+ };
46
+ });
47
+
48
+ const notFoundConfigs: NotFoundWithExport[] = notFoundPages.map(nf => {
49
+ const exportInfo = detectExportType(nf.filePath);
50
+ return {
51
+ pathPrefix: nf.pathPrefix,
52
+ filePath: nf.filePath.replace(/\\/g, '/'),
53
+ hasDefault: exportInfo.hasDefault,
54
+ namedExport: exportInfo.namedExport,
55
+ hasMetadata: exportInfo.hasMetadata
56
+ };
57
+ });
58
+
59
+ const layoutConfigs: LayoutWithExport[] = layouts.map(l => {
60
+ const exportInfo = detectExportType(l.filePath);
61
+ return {
62
+ path: l.path,
63
+ filePath: l.filePath.replace(/\\/g, '/'),
64
+ hasDefault: exportInfo.hasDefault,
65
+ namedExport: exportInfo.namedExport,
66
+ hasMetadata: exportInfo.hasMetadata
67
+ };
68
+ });
69
+
70
+ const loadingConfigs: LoadingWithExport[] = loadingPages.map(lp => {
71
+ const exportInfo = detectExportType(lp.filePath);
72
+ return {
73
+ path: lp.path,
74
+ filePath: lp.filePath.replace(/\\/g, '/'),
75
+ hasDefault: exportInfo.hasDefault,
76
+ namedExport: exportInfo.namedExport,
77
+ };
78
+ });
79
+
80
+ const errorConfigs: ErrorWithExport[] = errorPages.map(ep => {
81
+ const exportInfo = detectExportType(ep.filePath);
82
+ return {
83
+ path: ep.path,
84
+ filePath: ep.filePath.replace(/\\/g, '/'),
85
+ hasDefault: exportInfo.hasDefault,
86
+ namedExport: exportInfo.namedExport,
87
+ };
88
+ });
89
+
90
+ const middlewareConfigs: MiddlewareWithExport[] = middlewares.map(mw => {
91
+ const exportInfo = detectExportType(mw.filePath);
92
+ return {
93
+ path: mw.path,
94
+ filePath: mw.filePath.replace(/\\/g, '/'),
95
+ hasDefault: exportInfo.hasDefault,
96
+ namedExport: exportInfo.namedExport,
97
+ };
98
+ });
99
+
100
+ const content = generateRouteTree(routeConfigs, notFoundConfigs, layoutConfigs, loadingConfigs, errorConfigs, middlewareConfigs, absoluteRootDir, packageName);
101
+ const treePath = path.resolve(absoluteRootDir, 'route.tree.ts');
102
+
103
+ const existing = fs.existsSync(treePath) ? fs.readFileSync(treePath, 'utf-8') : '';
104
+ if (existing !== content) {
105
+ fs.writeFileSync(treePath, content);
106
+ console.log('\x1b[32m[olova]\x1b[0m Route tree updated');
107
+ }
108
+ }
109
+
110
+ function startWatcher() {
111
+ if (watcher) return;
112
+
113
+ watcher = fs.watch(absoluteRootDir, { recursive: true }, (eventType, filename) => {
114
+ if (!filename) return;
115
+ if (filename.includes('route.tree.ts')) return;
116
+
117
+ const ext = path.extname(filename);
118
+ const isConfiguredExtension = extensions.includes(ext);
119
+
120
+ const isIndexFile = (filename.includes('index') && isConfiguredExtension);
121
+ const isAppFile = (filename.includes('App') && isConfiguredExtension);
122
+ const is404File = (filename.includes('404') && isConfiguredExtension);
123
+ const isLayoutFile = (filename.includes('layout') && isConfiguredExtension);
124
+ const isLoadingFile = (filename.includes('loading') && isConfiguredExtension);
125
+ const isErrorFile = (filename.includes('error') && isConfiguredExtension);
126
+ const isMiddlewareFile = (filename.includes('middleware') && isConfiguredExtension);
127
+ const isDirectory = !filename.includes('.');
128
+ const isDynamicSegment = filename.includes('[');
129
+ const isRenameEvent = eventType === 'rename';
130
+
131
+ if (isIndexFile || isAppFile || is404File || isLayoutFile || isLoadingFile || isErrorFile || isMiddlewareFile || isDirectory || isDynamicSegment || isRenameEvent) {
132
+ if (isIndexFile && filename) {
133
+ const fullPath = path.join(absoluteRootDir, filename);
134
+ if (fs.existsSync(fullPath)) {
135
+ const stat = fs.statSync(fullPath);
136
+ if (stat.size === 0 && !filename.endsWith('.mdx')) {
137
+ const relativeDir = path.relative(absoluteRootDir, path.dirname(fullPath));
138
+ const pascalCaseName = getRouteName(relativeDir);
139
+
140
+ const boilerplate = `
141
+ export const metadata = {
142
+ title: "${pascalCaseName}",
143
+ description: "${pascalCaseName} page",
144
+ }
145
+
146
+ export default function ${pascalCaseName}() {
147
+ return (
148
+ <div>
149
+ <h1>${pascalCaseName}</h1>
150
+ </div>
151
+ );
152
+ }
153
+ `;
154
+ fs.writeFileSync(fullPath, boilerplate);
155
+ console.log(`\x1b[32m[olova]\x1b[0m Generated boilerplate for ${filename}`);
156
+ }
157
+ }
158
+ }
159
+
160
+ if (timer) clearTimeout(timer);
161
+ timer = setTimeout(() => {
162
+ try {
163
+ generateRouteTreeFile();
164
+ } catch (error) {
165
+ console.error('\x1b[31m[olova]\x1b[0m Error generating route tree:', error);
166
+ }
167
+ }, 100);
168
+ }
169
+ });
170
+
171
+ console.log('\x1b[32m[olova]\x1b[0m Watching for route changes...');
172
+ }
173
+
174
+ const routerPlugin: Plugin = {
175
+ name: 'olova-router',
176
+
177
+ configResolved(resolvedConfig: ResolvedConfig) {
178
+ config = resolvedConfig;
179
+ absoluteRootDir = path.resolve(config.root, rootDir);
180
+ },
181
+
182
+ buildStart() {
183
+ generateRouteTreeFile();
184
+
185
+ if (config.command === 'serve') {
186
+ startWatcher();
187
+ }
188
+ },
189
+
190
+ buildEnd() {
191
+ if (watcher) {
192
+ watcher.close();
193
+ watcher = null;
194
+ }
195
+ },
196
+ };
197
+
198
+ return [
199
+ { enforce: 'pre', ...mdx() } as Plugin,
200
+ routerPlugin
201
+ ];
202
+ }
203
+
204
+ export default olovaRouter;
@@ -0,0 +1,145 @@
1
+ import { Component, type ComponentType, type ErrorInfo, type ReactNode } from 'react';
2
+
3
+ interface ErrorBoundaryProps {
4
+ children: ReactNode;
5
+ fallback?: ReactNode;
6
+ }
7
+
8
+ interface ErrorBoundaryState {
9
+ hasError: boolean;
10
+ error: Error | null;
11
+ }
12
+
13
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
14
+ constructor(props: ErrorBoundaryProps) {
15
+ super(props);
16
+ this.state = { hasError: false, error: null };
17
+ }
18
+
19
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
20
+ return { hasError: true, error };
21
+ }
22
+
23
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
24
+ console.error('[olova] Uncaught error:', error, errorInfo);
25
+ }
26
+
27
+ handleReset = () => {
28
+ this.setState({ hasError: false, error: null });
29
+ };
30
+
31
+ render() {
32
+ if (this.state.hasError) {
33
+ if (this.props.fallback) {
34
+ return this.props.fallback;
35
+ }
36
+
37
+ return (
38
+ <div style={{
39
+ padding: '2rem',
40
+ maxWidth: '600px',
41
+ margin: '4rem auto',
42
+ fontFamily: 'system-ui, -apple-system, sans-serif',
43
+ textAlign: 'center',
44
+ }}>
45
+ <h1 style={{ fontSize: '2rem', marginBottom: '1rem', color: '#e11d48' }}>
46
+ Something went wrong
47
+ </h1>
48
+ <p style={{ color: '#6b7280', marginBottom: '1.5rem' }}>
49
+ An unexpected error occurred while rendering this page.
50
+ </p>
51
+ {this.state.error && (
52
+ <pre style={{
53
+ padding: '1rem',
54
+ backgroundColor: '#f3f4f6',
55
+ borderRadius: '0.5rem',
56
+ fontSize: '0.875rem',
57
+ textAlign: 'left',
58
+ overflow: 'auto',
59
+ marginBottom: '1.5rem',
60
+ color: '#374151',
61
+ }}>
62
+ {this.state.error.message}
63
+ </pre>
64
+ )}
65
+ <div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
66
+ <button
67
+ onClick={this.handleReset}
68
+ style={{
69
+ padding: '0.5rem 1.5rem',
70
+ backgroundColor: '#3b82f6',
71
+ color: 'white',
72
+ border: 'none',
73
+ borderRadius: '0.375rem',
74
+ cursor: 'pointer',
75
+ fontSize: '0.875rem',
76
+ }}
77
+ >
78
+ Try Again
79
+ </button>
80
+ <button
81
+ onClick={() => window.location.reload()}
82
+ style={{
83
+ padding: '0.5rem 1.5rem',
84
+ backgroundColor: 'transparent',
85
+ color: '#3b82f6',
86
+ border: '1px solid #3b82f6',
87
+ borderRadius: '0.375rem',
88
+ cursor: 'pointer',
89
+ fontSize: '0.875rem',
90
+ }}
91
+ >
92
+ Reload Page
93
+ </button>
94
+ </div>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ return this.props.children;
100
+ }
101
+ }
102
+
103
+ interface RouteErrorBoundaryProps {
104
+ children: ReactNode;
105
+ fallbackComponent: ComponentType<{ error: Error; reset: () => void }>;
106
+ routePath: string;
107
+ }
108
+
109
+ interface RouteErrorBoundaryState {
110
+ hasError: boolean;
111
+ error: Error | null;
112
+ }
113
+
114
+ export class RouteErrorBoundary extends Component<RouteErrorBoundaryProps, RouteErrorBoundaryState> {
115
+ constructor(props: RouteErrorBoundaryProps) {
116
+ super(props);
117
+ this.state = { hasError: false, error: null };
118
+ }
119
+
120
+ static getDerivedStateFromError(error: Error): RouteErrorBoundaryState {
121
+ return { hasError: true, error };
122
+ }
123
+
124
+ componentDidUpdate(prevProps: RouteErrorBoundaryProps) {
125
+ if (prevProps.routePath !== this.props.routePath && this.state.hasError) {
126
+ this.setState({ hasError: false, error: null });
127
+ }
128
+ }
129
+
130
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
131
+ console.error('[olova] Route error:', error, errorInfo);
132
+ }
133
+
134
+ handleReset = () => {
135
+ this.setState({ hasError: false, error: null });
136
+ };
137
+
138
+ render() {
139
+ if (this.state.hasError && this.state.error) {
140
+ const FallbackComponent = this.props.fallbackComponent;
141
+ return <FallbackComponent error={this.state.error} reset={this.handleReset} />;
142
+ }
143
+ return this.props.children;
144
+ }
145
+ }
@@ -0,0 +1,117 @@
1
+ import { useCallback, useEffect, useRef, type AnchorHTMLAttributes, type ReactNode } from 'react';
2
+ import { useRouter } from './context';
3
+
4
+ type ResolveSegment<S extends string> =
5
+ S extends `:${string}` ? string :
6
+ S extends '*' ? string :
7
+ S;
8
+
9
+ type ResolvePathSegments<Path extends string> =
10
+ Path extends `${infer Segment}/${infer Rest}`
11
+ ? `${ResolveSegment<Segment>}/${ResolvePathSegments<Rest>}`
12
+ : ResolveSegment<Path>;
13
+
14
+ export type ResolveRoutePath<Path extends string> =
15
+ Path extends `${infer Base}/*`
16
+ ? `${ResolvePathSegments<Base>}/${string}`
17
+ : ResolvePathSegments<Path>;
18
+
19
+ type ResolveRoutes<T extends string> = T extends string ? ResolveRoutePath<T> : never;
20
+
21
+ interface LinkProps<T extends string> extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
22
+ href: ResolveRoutes<T>;
23
+ children: ReactNode;
24
+ className?: string;
25
+ activeClassName?: string;
26
+ exactActiveClassName?: string;
27
+ prefetch?: boolean | 'hover' | 'viewport';
28
+ replace?: boolean;
29
+ scroll?: boolean;
30
+ target?: string;
31
+ }
32
+
33
+ function isActive(currentPath: string, href: string, exact: boolean): boolean {
34
+ const normalizedCurrent = currentPath.replace(/\/$/, '') || '/';
35
+ const normalizedHref = (href.split('?')[0]).replace(/\/$/, '') || '/';
36
+ if (exact) return normalizedCurrent === normalizedHref;
37
+ if (normalizedHref === '/') return normalizedCurrent === '/';
38
+ return normalizedCurrent === normalizedHref || normalizedCurrent.startsWith(normalizedHref + '/');
39
+ }
40
+
41
+ export function createLink<T extends string>() {
42
+ const Link = ({
43
+ href,
44
+ children,
45
+ className,
46
+ activeClassName,
47
+ exactActiveClassName,
48
+ prefetch = 'hover',
49
+ replace: shouldReplace = false,
50
+ scroll = true,
51
+ target,
52
+ ...rest
53
+ }: LinkProps<T>) => {
54
+ const { push, replace, currentPath, prefetch: prefetchRoute } = useRouter();
55
+ const isSSR = typeof window === 'undefined';
56
+ const linkRef = useRef<HTMLAnchorElement>(null);
57
+ const prefetched = useRef(false);
58
+
59
+ const isExactActive = isActive(currentPath, href, true);
60
+ const isPartialActive = isActive(currentPath, href, false);
61
+
62
+ const computedClassName = [
63
+ className,
64
+ isPartialActive && activeClassName,
65
+ isExactActive && exactActiveClassName,
66
+ ].filter(Boolean).join(' ') || undefined;
67
+
68
+ const handlePrefetch = useCallback(() => {
69
+ if (!prefetched.current && prefetch !== false) {
70
+ prefetched.current = true;
71
+ prefetchRoute(href);
72
+ }
73
+ }, [href, prefetch, prefetchRoute]);
74
+
75
+ useEffect(() => {
76
+ if (isSSR || prefetch !== 'viewport' || !linkRef.current) return;
77
+ const observer = new IntersectionObserver(
78
+ (entries) => {
79
+ if (entries[0]?.isIntersecting) {
80
+ handlePrefetch();
81
+ observer.disconnect();
82
+ }
83
+ },
84
+ { rootMargin: '200px' }
85
+ );
86
+ observer.observe(linkRef.current);
87
+ return () => observer.disconnect();
88
+ }, [isSSR, prefetch, handlePrefetch]);
89
+
90
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
91
+ if (rest.onClick) rest.onClick(e);
92
+ if (e.defaultPrevented) return;
93
+ if (target === '_blank' || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
94
+ e.preventDefault();
95
+ const navFn = shouldReplace ? replace : push;
96
+ navFn(href, { scroll });
97
+ };
98
+
99
+ return (
100
+ <a
101
+ ref={linkRef}
102
+ href={href}
103
+ className={computedClassName}
104
+ data-active={isPartialActive || undefined}
105
+ data-exact-active={isExactActive || undefined}
106
+ onClick={isSSR ? undefined : handleClick}
107
+ onMouseEnter={isSSR || prefetch !== 'hover' ? undefined : handlePrefetch}
108
+ onFocus={isSSR || prefetch !== 'hover' ? undefined : handlePrefetch}
109
+ target={target}
110
+ {...rest}
111
+ >
112
+ {children}
113
+ </a>
114
+ );
115
+ };
116
+ return Link;
117
+ }