olova 2.0.61 → 2.0.63
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/CHANGELOG.md +5 -0
- package/README.md +42 -61
- package/dist/compiler.d.ts +44 -0
- package/dist/compiler.js +2139 -0
- package/dist/compiler.js.map +1 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +859 -0
- package/dist/core.js.map +1 -0
- package/dist/global.d.ts +15 -0
- package/dist/global.js +226 -0
- package/dist/global.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2302 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +89 -0
- package/dist/runtime.js +633 -0
- package/dist/runtime.js.map +1 -0
- package/dist/signals-core-BdfWh1Yt.d.ts +43 -0
- package/dist/vite.d.ts +5 -0
- package/dist/vite.js +2302 -0
- package/dist/vite.js.map +1 -0
- package/package.json +83 -65
- package/dist/chunk-D7SIC5TC.js +0 -367
- package/dist/chunk-D7SIC5TC.js.map +0 -1
- package/dist/entry-server.cjs +0 -120
- package/dist/entry-server.cjs.map +0 -1
- package/dist/entry-server.js +0 -115
- package/dist/entry-server.js.map +0 -1
- package/dist/entry-worker.cjs +0 -133
- package/dist/entry-worker.cjs.map +0 -1
- package/dist/entry-worker.js +0 -127
- package/dist/entry-worker.js.map +0 -1
- package/dist/main.cjs +0 -18
- package/dist/main.cjs.map +0 -1
- package/dist/main.js +0 -16
- package/dist/main.js.map +0 -1
- package/dist/olova.cjs +0 -1680
- package/dist/olova.cjs.map +0 -1
- package/dist/olova.d.cts +0 -72
- package/dist/olova.d.ts +0 -72
- package/dist/olova.js +0 -1321
- package/dist/olova.js.map +0 -1
- package/dist/performance.cjs +0 -386
- package/dist/performance.cjs.map +0 -1
- package/dist/performance.js +0 -3
- package/dist/performance.js.map +0 -1
- package/dist/router.cjs +0 -646
- package/dist/router.cjs.map +0 -1
- package/dist/router.d.cts +0 -113
- package/dist/router.d.ts +0 -113
- package/dist/router.js +0 -632
- package/dist/router.js.map +0 -1
- package/main.tsx +0 -76
- package/olova.ts +0 -619
- package/src/entry-server.tsx +0 -165
- package/src/entry-worker.tsx +0 -201
- package/src/generator/index.ts +0 -409
- package/src/hydration/flight.ts +0 -320
- package/src/hydration/index.ts +0 -12
- package/src/hydration/types.ts +0 -225
- package/src/logger.ts +0 -182
- package/src/main.tsx +0 -24
- package/src/performance.ts +0 -488
- package/src/plugin/index.ts +0 -204
- package/src/router/ErrorBoundary.tsx +0 -145
- package/src/router/Link.tsx +0 -117
- package/src/router/OlovaRouter.tsx +0 -354
- package/src/router/Outlet.tsx +0 -8
- package/src/router/context.ts +0 -117
- package/src/router/index.ts +0 -29
- package/src/router/matching.ts +0 -63
- package/src/router/router.tsx +0 -23
- package/src/router/search-params.ts +0 -29
- package/src/scanner/index.ts +0 -114
- package/src/types/index.ts +0 -190
- package/src/utils/export.ts +0 -85
- package/src/utils/index.ts +0 -4
- package/src/utils/naming.ts +0 -54
- package/src/utils/path.ts +0 -45
- package/tsup.config.ts +0 -35
package/src/plugin/index.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
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/app';
|
|
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(config.root, 'src', '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 isPageFile = (filename.includes('page') && isConfiguredExtension);
|
|
121
|
-
const is404File = (filename.includes('404') && isConfiguredExtension);
|
|
122
|
-
const isNotFoundFile = (filename.includes('not-found') && 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 (isPageFile || is404File || isNotFoundFile || isLayoutFile || isLoadingFile || isErrorFile || isMiddlewareFile || isDirectory || isDynamicSegment || isRenameEvent) {
|
|
132
|
-
if (isPageFile && 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;
|
|
@@ -1,145 +0,0 @@
|
|
|
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
|
-
}
|
package/src/router/Link.tsx
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
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
|
-
}
|