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.
- package/README.md +28 -288
- package/dist/chunk-23UAGQ6N.js +2208 -0
- package/dist/chunk-23UAGQ6N.js.map +1 -0
- package/dist/chunk-D7SIC5TC.js +367 -0
- package/dist/chunk-D7SIC5TC.js.map +1 -0
- package/dist/entry-server.cjs +2341 -0
- package/dist/entry-server.cjs.map +1 -0
- package/dist/entry-server.js +114 -0
- package/dist/entry-server.js.map +1 -0
- package/dist/entry-worker.cjs +2354 -0
- package/dist/entry-worker.cjs.map +1 -0
- package/dist/entry-worker.js +126 -0
- package/dist/entry-worker.js.map +1 -0
- package/dist/main.cjs +18 -0
- package/dist/main.cjs.map +1 -0
- package/dist/main.js +16 -0
- package/dist/main.js.map +1 -0
- package/dist/olova.cjs +1684 -0
- package/dist/olova.cjs.map +1 -0
- package/dist/olova.d.cts +72 -0
- package/dist/olova.d.ts +72 -0
- package/dist/olova.js +1325 -0
- package/dist/olova.js.map +1 -0
- package/dist/performance.cjs +386 -0
- package/dist/performance.cjs.map +1 -0
- package/dist/performance.js +3 -0
- package/dist/performance.js.map +1 -0
- package/dist/router.cjs +646 -0
- package/dist/router.cjs.map +1 -0
- package/dist/router.d.cts +113 -0
- package/dist/router.d.ts +113 -0
- package/dist/router.js +632 -0
- package/dist/router.js.map +1 -0
- package/main.tsx +76 -0
- package/olova.ts +619 -0
- package/package.json +42 -61
- package/src/entry-server.tsx +165 -0
- package/src/entry-worker.tsx +201 -0
- package/src/generator/index.ts +409 -0
- package/src/hydration/flight.ts +320 -0
- package/src/hydration/index.ts +12 -0
- package/src/hydration/types.ts +225 -0
- package/src/logger.ts +182 -0
- package/src/main.tsx +24 -0
- package/src/performance.ts +488 -0
- package/src/plugin/index.ts +204 -0
- package/src/router/ErrorBoundary.tsx +145 -0
- package/src/router/Link.tsx +117 -0
- package/src/router/OlovaRouter.tsx +354 -0
- package/src/router/Outlet.tsx +8 -0
- package/src/router/context.ts +117 -0
- package/src/router/index.ts +29 -0
- package/src/router/matching.ts +63 -0
- package/src/router/router.tsx +23 -0
- package/src/router/search-params.ts +29 -0
- package/src/scanner/index.ts +116 -0
- package/src/types/index.ts +191 -0
- package/src/utils/export.ts +85 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/naming.ts +54 -0
- package/src/utils/path.ts +45 -0
- package/tsup.config.ts +35 -0
- package/CHANGELOG.md +0 -31
- package/LICENSE +0 -21
- package/dist/index.cjs +0 -883
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -138
- package/dist/index.d.ts +0 -138
- package/dist/index.js +0 -832
- package/dist/index.js.map +0 -1
- package/dist/plugin.cjs +0 -927
- package/dist/plugin.cjs.map +0 -1
- package/dist/plugin.d.cts +0 -18
- package/dist/plugin.d.ts +0 -18
- package/dist/plugin.js +0 -894
- package/dist/plugin.js.map +0 -1
- package/dist/ssg.cjs +0 -637
- package/dist/ssg.cjs.map +0 -1
- package/dist/ssg.d.cts +0 -191
- package/dist/ssg.d.ts +0 -191
- package/dist/ssg.js +0 -585
- package/dist/ssg.js.map +0 -1
- package/dist/types-BT6YsBGO.d.cts +0 -143
- package/dist/types-BT6YsBGO.d.ts +0 -143
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { ErrorEntry, LayoutEntry, LoadingEntry, MiddlewareEntry, NotFoundEntry, RouteEntry, ScanResult } from '../types';
|
|
4
|
+
import { isRouteGroup, pathToRoute } from '../utils';
|
|
5
|
+
|
|
6
|
+
const RESERVED_NAMES = new Set([
|
|
7
|
+
'index', 'layout', 'loading', 'error', '404', 'middleware',
|
|
8
|
+
'App', 'main', 'route.tree',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function scanDirectory(
|
|
12
|
+
dir: string,
|
|
13
|
+
rootDir: string,
|
|
14
|
+
extensions: string[],
|
|
15
|
+
routes: RouteEntry[],
|
|
16
|
+
notFoundPages: NotFoundEntry[],
|
|
17
|
+
layouts: LayoutEntry[],
|
|
18
|
+
loadingPages: LoadingEntry[],
|
|
19
|
+
errorPages: ErrorEntry[],
|
|
20
|
+
middlewares: MiddlewareEntry[],
|
|
21
|
+
isRoot = false
|
|
22
|
+
) {
|
|
23
|
+
if (!fs.existsSync(dir)) return;
|
|
24
|
+
|
|
25
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const fullPath = path.join(dir, entry.name);
|
|
29
|
+
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
if (entry.name === 'node_modules' || entry.name === 'assets' || entry.name.startsWith('_')) continue;
|
|
32
|
+
scanDirectory(fullPath, rootDir, extensions, routes, notFoundPages, layouts, loadingPages, errorPages, middlewares, false);
|
|
33
|
+
} else if (entry.isFile()) {
|
|
34
|
+
const ext = path.extname(entry.name);
|
|
35
|
+
const baseName = path.basename(entry.name, ext);
|
|
36
|
+
|
|
37
|
+
if (baseName === 'layout' && extensions.includes(ext)) {
|
|
38
|
+
const relativePath = path.relative(rootDir, dir);
|
|
39
|
+
const { routePath } = pathToRoute(relativePath, path.sep);
|
|
40
|
+
layouts.push({
|
|
41
|
+
path: isRoot ? '/' : routePath,
|
|
42
|
+
filePath: fullPath
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else if (baseName === 'loading' && extensions.includes(ext)) {
|
|
46
|
+
const relativePath = path.relative(rootDir, dir);
|
|
47
|
+
const { routePath } = pathToRoute(relativePath, path.sep);
|
|
48
|
+
loadingPages.push({
|
|
49
|
+
path: isRoot ? '/' : routePath,
|
|
50
|
+
filePath: fullPath
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else if (baseName === 'error' && extensions.includes(ext)) {
|
|
54
|
+
const relativePath = path.relative(rootDir, dir);
|
|
55
|
+
const { routePath } = pathToRoute(relativePath, path.sep);
|
|
56
|
+
errorPages.push({
|
|
57
|
+
path: isRoot ? '/' : routePath,
|
|
58
|
+
filePath: fullPath
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else if (baseName === 'middleware' && extensions.includes(ext)) {
|
|
62
|
+
const relativePath = path.relative(rootDir, dir);
|
|
63
|
+
const { routePath } = pathToRoute(relativePath, path.sep);
|
|
64
|
+
middlewares.push({
|
|
65
|
+
path: isRoot ? '/' : routePath,
|
|
66
|
+
filePath: fullPath
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else if (baseName === '404' && extensions.includes(ext)) {
|
|
70
|
+
const relativeParts = path.relative(rootDir, dir).split(path.sep).filter(Boolean);
|
|
71
|
+
const filteredParts = relativeParts.filter(p => !isRouteGroup(p));
|
|
72
|
+
const pathPrefix = isRoot ? '' : '/' + filteredParts.join('/');
|
|
73
|
+
notFoundPages.push({ pathPrefix: pathPrefix || '', filePath: fullPath });
|
|
74
|
+
} else if (isRoot && baseName === 'App' && extensions.includes(ext)) {
|
|
75
|
+
routes.push({ path: '/', filePath: fullPath, isDynamic: false, params: [] });
|
|
76
|
+
} else if (!isRoot && baseName === 'index' && extensions.includes(ext)) {
|
|
77
|
+
const relativePath = path.relative(rootDir, path.dirname(fullPath));
|
|
78
|
+
const { routePath, params } = pathToRoute(relativePath, path.sep);
|
|
79
|
+
routes.push({ path: routePath, filePath: fullPath, isDynamic: params.length > 0, params });
|
|
80
|
+
} else if (
|
|
81
|
+
!RESERVED_NAMES.has(baseName) &&
|
|
82
|
+
!baseName.endsWith('.d') &&
|
|
83
|
+
!baseName.startsWith('_') &&
|
|
84
|
+
extensions.includes(ext)
|
|
85
|
+
) {
|
|
86
|
+
const relativePath = path.relative(rootDir, fullPath);
|
|
87
|
+
const relativePathNoExt = relativePath.substring(0, relativePath.length - ext.length);
|
|
88
|
+
const { routePath, params } = pathToRoute(relativePathNoExt, path.sep);
|
|
89
|
+
routes.push({ path: routePath, filePath: fullPath, isDynamic: params.length > 0, params });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function scanRoutes(rootDir: string, extensions: string[]): ScanResult {
|
|
96
|
+
const routes: RouteEntry[] = [];
|
|
97
|
+
const notFoundPages: NotFoundEntry[] = [];
|
|
98
|
+
const layouts: LayoutEntry[] = [];
|
|
99
|
+
const loadingPages: LoadingEntry[] = [];
|
|
100
|
+
const errorPages: ErrorEntry[] = [];
|
|
101
|
+
const middlewares: MiddlewareEntry[] = [];
|
|
102
|
+
const absoluteRoot = path.isAbsolute(rootDir) ? rootDir : path.resolve(rootDir);
|
|
103
|
+
|
|
104
|
+
if (!fs.existsSync(absoluteRoot)) {
|
|
105
|
+
throw new Error(`Olova Router: Root directory does not exist: ${absoluteRoot}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
scanDirectory(absoluteRoot, absoluteRoot, extensions, routes, notFoundPages, layouts, loadingPages, errorPages, middlewares, true);
|
|
109
|
+
routes.sort((a, b) => (a.isDynamic !== b.isDynamic ? (a.isDynamic ? 1 : -1) : a.path.localeCompare(b.path)));
|
|
110
|
+
notFoundPages.sort((a, b) => b.pathPrefix.length - a.pathPrefix.length);
|
|
111
|
+
layouts.sort((a, b) => a.path.length - b.path.length);
|
|
112
|
+
loadingPages.sort((a, b) => b.path.length - a.path.length);
|
|
113
|
+
errorPages.sort((a, b) => b.path.length - a.path.length);
|
|
114
|
+
middlewares.sort((a, b) => a.path.length - b.path.length);
|
|
115
|
+
return { routes, notFoundPages, layouts, loadingPages, errorPages, middlewares };
|
|
116
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
export type { PluginOption, ResolvedConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
export interface RouteEntry {
|
|
5
|
+
path: string;
|
|
6
|
+
filePath: string;
|
|
7
|
+
isDynamic: boolean;
|
|
8
|
+
params: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ScannerOptions {
|
|
12
|
+
rootDir: string;
|
|
13
|
+
extensions: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Metadata {
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
keywords?: string[];
|
|
20
|
+
openGraph?: {
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
image?: string;
|
|
24
|
+
url?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
};
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RouteConfig {
|
|
32
|
+
path: string;
|
|
33
|
+
component: string;
|
|
34
|
+
params?: string[];
|
|
35
|
+
metadata?: Metadata;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GeneratorOptions {
|
|
39
|
+
routes: RouteEntry[];
|
|
40
|
+
basePath: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface OlovaRouterOptions {
|
|
44
|
+
rootDir?: string;
|
|
45
|
+
extensions?: string[];
|
|
46
|
+
packageName?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DynamicSegmentResult {
|
|
50
|
+
isDynamic: boolean;
|
|
51
|
+
paramName: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface NotFoundEntry {
|
|
55
|
+
pathPrefix: string;
|
|
56
|
+
filePath: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface NotFoundPageConfig {
|
|
60
|
+
pathPrefix: string;
|
|
61
|
+
component: ComponentType;
|
|
62
|
+
metadata?: Metadata;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LayoutEntry {
|
|
66
|
+
path: string;
|
|
67
|
+
filePath: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface LoadingEntry {
|
|
71
|
+
path: string;
|
|
72
|
+
filePath: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ErrorEntry {
|
|
76
|
+
path: string;
|
|
77
|
+
filePath: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface MiddlewareEntry {
|
|
81
|
+
path: string;
|
|
82
|
+
filePath: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ScanResult {
|
|
86
|
+
routes: RouteEntry[];
|
|
87
|
+
notFoundPages: NotFoundEntry[];
|
|
88
|
+
layouts: LayoutEntry[];
|
|
89
|
+
loadingPages: LoadingEntry[];
|
|
90
|
+
errorPages: ErrorEntry[];
|
|
91
|
+
middlewares: MiddlewareEntry[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RouteWithExport extends RouteConfig {
|
|
95
|
+
hasDefault: boolean;
|
|
96
|
+
namedExport: string | null;
|
|
97
|
+
hasMetadata: boolean;
|
|
98
|
+
metadataSource?: string;
|
|
99
|
+
hasRoute: boolean;
|
|
100
|
+
hasGetStaticPaths: boolean;
|
|
101
|
+
hasLoader: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface NotFoundWithExport extends NotFoundEntry {
|
|
105
|
+
hasDefault: boolean;
|
|
106
|
+
namedExport: string | null;
|
|
107
|
+
hasMetadata: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface LayoutWithExport extends LayoutEntry {
|
|
111
|
+
hasDefault: boolean;
|
|
112
|
+
namedExport: string | null;
|
|
113
|
+
hasMetadata: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface LoadingWithExport extends LoadingEntry {
|
|
117
|
+
hasDefault: boolean;
|
|
118
|
+
namedExport: string | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ErrorWithExport extends ErrorEntry {
|
|
122
|
+
hasDefault: boolean;
|
|
123
|
+
namedExport: string | null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface MiddlewareWithExport extends MiddlewareEntry {
|
|
127
|
+
hasDefault: boolean;
|
|
128
|
+
namedExport: string | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface RouteErrorProps {
|
|
132
|
+
error: Error;
|
|
133
|
+
reset: () => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface LoaderContext {
|
|
137
|
+
request: Request;
|
|
138
|
+
params: Record<string, string>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface Route {
|
|
142
|
+
id?: string;
|
|
143
|
+
path: string;
|
|
144
|
+
component?: ComponentType;
|
|
145
|
+
metadata?: Metadata;
|
|
146
|
+
loading?: ComponentType;
|
|
147
|
+
error?: ComponentType<RouteErrorProps>;
|
|
148
|
+
filePath?: string;
|
|
149
|
+
params?: string[];
|
|
150
|
+
loader?: (ctx: LoaderContext) => Promise<Record<string, unknown>>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface LayoutRoute {
|
|
154
|
+
path: string;
|
|
155
|
+
layout?: ComponentType;
|
|
156
|
+
children: Route[];
|
|
157
|
+
metadata?: Metadata;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type SearchParams = Record<string, string | string[]>;
|
|
161
|
+
|
|
162
|
+
export interface SetSearchParamsOptions {
|
|
163
|
+
replace?: boolean;
|
|
164
|
+
merge?: boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface NavigateOptions {
|
|
168
|
+
scroll?: boolean;
|
|
169
|
+
replace?: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type NavigationListener = (from: string, to: string) => void | boolean;
|
|
173
|
+
|
|
174
|
+
export interface RouterContextType {
|
|
175
|
+
currentPath: string;
|
|
176
|
+
params: Record<string, string>;
|
|
177
|
+
searchParams: SearchParams;
|
|
178
|
+
navigate: (path: string, options?: NavigateOptions) => void;
|
|
179
|
+
push: (path: string, options?: NavigateOptions) => void;
|
|
180
|
+
replace: (path: string, options?: NavigateOptions) => void;
|
|
181
|
+
back: () => void;
|
|
182
|
+
forward: () => void;
|
|
183
|
+
refresh: () => void;
|
|
184
|
+
setSearchParams: (params: Record<string, string | string[] | null>, options?: SetSearchParamsOptions) => void;
|
|
185
|
+
isNavigating: boolean;
|
|
186
|
+
prefetch: (path: string) => void;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface OutletContextType {
|
|
190
|
+
content: ReactNode;
|
|
191
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
function extractBalancedBraces(content: string, startIndex: number): string | null {
|
|
4
|
+
if (content[startIndex] !== '{') return null;
|
|
5
|
+
let depth = 0;
|
|
6
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
7
|
+
if (content[i] === '{') depth++;
|
|
8
|
+
else if (content[i] === '}') {
|
|
9
|
+
depth--;
|
|
10
|
+
if (depth === 0) return content.substring(startIndex, i + 1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractMetadataSource(content: string): string | undefined {
|
|
17
|
+
const match = content.match(/export\s+(?:const|let|var)\s+metadata\s*(?::\s*\w+\s*)?=\s*/);
|
|
18
|
+
if (!match || match.index === undefined) return undefined;
|
|
19
|
+
const afterEquals = match.index + match[0].length;
|
|
20
|
+
return extractBalancedBraces(content, afterEquals) || undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function detectExportType(filePath: string): {
|
|
24
|
+
hasDefault: boolean;
|
|
25
|
+
namedExport: string | null;
|
|
26
|
+
hasMetadata: boolean;
|
|
27
|
+
metadataSource?: string;
|
|
28
|
+
hasRoute: boolean;
|
|
29
|
+
hasGetStaticPaths: boolean;
|
|
30
|
+
hasLoader: boolean;
|
|
31
|
+
} {
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
|
|
35
|
+
const hasMetadata = /export\s+(?:const|let|var)\s+metadata\s*/.test(content) ||
|
|
36
|
+
/export\s*\{\s*metadata\s*\}/.test(content);
|
|
37
|
+
|
|
38
|
+
const hasRoute = /export\s+(?:const|let|var)\s+route\s*=/.test(content) ||
|
|
39
|
+
/export\s*\{\s*route\s*\}/.test(content);
|
|
40
|
+
|
|
41
|
+
const hasGetStaticPaths = /export\s+(?:async\s+)?function\s+getStaticPaths/.test(content) ||
|
|
42
|
+
/export\s+(?:const|let|var)\s+getStaticPaths\s*=/.test(content) ||
|
|
43
|
+
/export\s*\{[^}]*getStaticPaths[^}]*\}/.test(content);
|
|
44
|
+
|
|
45
|
+
const hasLoader = /export\s+(?:async\s+)?function\s+loader/.test(content) ||
|
|
46
|
+
/export\s+(?:const|let|var)\s+loader\s*=/.test(content) ||
|
|
47
|
+
/export\s*\{[^}]*loader[^}]*\}/.test(content);
|
|
48
|
+
|
|
49
|
+
// Extract metadata source for inlining (works for any file type with object literal metadata)
|
|
50
|
+
const metadataSource = extractMetadataSource(content);
|
|
51
|
+
|
|
52
|
+
if (filePath.toLowerCase().endsWith('.mdx')) {
|
|
53
|
+
return {
|
|
54
|
+
hasDefault: true,
|
|
55
|
+
namedExport: null,
|
|
56
|
+
hasMetadata: !!metadataSource,
|
|
57
|
+
hasRoute: false,
|
|
58
|
+
hasGetStaticPaths: false,
|
|
59
|
+
hasLoader: false,
|
|
60
|
+
metadataSource
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (/export\s+default\s+/.test(content)) {
|
|
65
|
+
return { hasDefault: true, namedExport: null, hasMetadata, metadataSource, hasRoute, hasGetStaticPaths, hasLoader };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const namedMatch = content.match(/export\s+(?:const|function|class)\s+(\w+)/);
|
|
69
|
+
if (namedMatch) {
|
|
70
|
+
return { hasDefault: false, namedExport: namedMatch[1], hasMetadata, metadataSource, hasRoute, hasGetStaticPaths, hasLoader };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const exportMatch = content.match(/export\s*\{\s*(\w+)(?:\s+as\s+default)?\s*\}/);
|
|
74
|
+
if (exportMatch) {
|
|
75
|
+
if (content.includes('as default')) {
|
|
76
|
+
return { hasDefault: true, namedExport: null, hasMetadata, metadataSource, hasRoute, hasGetStaticPaths, hasLoader };
|
|
77
|
+
}
|
|
78
|
+
return { hasDefault: false, namedExport: exportMatch[1], hasMetadata, metadataSource, hasRoute, hasGetStaticPaths, hasLoader };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { hasDefault: false, namedExport: null, hasMetadata, metadataSource, hasRoute, hasGetStaticPaths, hasLoader };
|
|
82
|
+
} catch {
|
|
83
|
+
return { hasDefault: true, namedExport: null, hasMetadata: false, hasRoute: false, hasGetStaticPaths: false, hasLoader: false };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function getRouteName(path: string): string {
|
|
2
|
+
if (path === '/' || path === '') return 'Root';
|
|
3
|
+
|
|
4
|
+
// Remove leading/trailing slashes and split
|
|
5
|
+
const segments = path.replace(/^\/|\/$/g, '').split(/[\\/]/);
|
|
6
|
+
|
|
7
|
+
const nameSegments = segments.map(segment => {
|
|
8
|
+
let cleanSegment = segment;
|
|
9
|
+
|
|
10
|
+
// Handle dynamic segments :id -> Id
|
|
11
|
+
if (cleanSegment.startsWith(':')) {
|
|
12
|
+
cleanSegment = cleanSegment.slice(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Handle bracket syntax [id] -> Id, [...slug] -> Slug
|
|
16
|
+
if (cleanSegment.startsWith('[') && cleanSegment.endsWith(']')) {
|
|
17
|
+
cleanSegment = cleanSegment.slice(1, -1);
|
|
18
|
+
if (cleanSegment.startsWith('...')) {
|
|
19
|
+
cleanSegment = cleanSegment.slice(3);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle catch-all * -> All
|
|
24
|
+
if (cleanSegment === '*') {
|
|
25
|
+
return 'All';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Skip 'index' segment for cleaner names
|
|
29
|
+
if (cleanSegment.toLowerCase() === 'index') {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle route groups (auth) -> Auth or ignore? Usually people ignore path but name might want it?
|
|
34
|
+
// User request is about "main folder name". (auth) is usually invisible.
|
|
35
|
+
// Let's strip parens but keep name for uniqueness? Or just strip?
|
|
36
|
+
// scanner often treats (group) as transparent for path, but for Naming it might be good to include or exclude.
|
|
37
|
+
// Let's include it cleaned up to ensure Uniqueness (e.g. /app/(dashboard)/layout vs /app/(marketing)/layout).
|
|
38
|
+
if (cleanSegment.startsWith('(') && cleanSegment.endsWith(')')) {
|
|
39
|
+
cleanSegment = cleanSegment.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return capitalize(cleanSegment);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const finalName = nameSegments.join('');
|
|
46
|
+
if (/^\d/.test(finalName)) {
|
|
47
|
+
return 'Page' + finalName;
|
|
48
|
+
}
|
|
49
|
+
return finalName;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function capitalize(str: string): string {
|
|
53
|
+
return str.charAt(0).toUpperCase() + str.slice(1).replace(/[^a-zA-Z0-9]/g, '');
|
|
54
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DynamicSegmentResult } from '../types';
|
|
2
|
+
|
|
3
|
+
export function parseDynamicSegment(segment: string): DynamicSegmentResult & { isCatchAll: boolean } {
|
|
4
|
+
if (segment.match(/^\[\.\.\.(.+)\]$/)) {
|
|
5
|
+
const paramName = segment.match(/^\[\.\.\.(.+)\]$/)?.[1] || 'slug';
|
|
6
|
+
return { isDynamic: true, paramName, isCatchAll: true };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const bracketMatch = segment.match(/^\[(.+)\]$/);
|
|
10
|
+
if (bracketMatch) {
|
|
11
|
+
return { isDynamic: true, paramName: bracketMatch[1], isCatchAll: false };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
return { isDynamic: false, paramName: null, isCatchAll: false };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isRouteGroup(segment: string): boolean {
|
|
20
|
+
return /^\(.+\)$/.test(segment);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function pathToRoute(relativePath: string, sep: string) {
|
|
24
|
+
const params: string[] = [];
|
|
25
|
+
let hasCatchAll = false;
|
|
26
|
+
const segments = relativePath.split(sep).filter(Boolean);
|
|
27
|
+
|
|
28
|
+
const routeSegments = segments
|
|
29
|
+
.filter(segment => !isRouteGroup(segment))
|
|
30
|
+
.map(segment => {
|
|
31
|
+
const { isDynamic, paramName, isCatchAll } = parseDynamicSegment(segment);
|
|
32
|
+
if (isDynamic && paramName) {
|
|
33
|
+
params.push(paramName);
|
|
34
|
+
if (isCatchAll) {
|
|
35
|
+
hasCatchAll = true;
|
|
36
|
+
return `*`;
|
|
37
|
+
}
|
|
38
|
+
return `:${paramName}`;
|
|
39
|
+
}
|
|
40
|
+
return segment;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const routePath = '/' + routeSegments.join('/');
|
|
44
|
+
return { routePath: routePath === '/' ? '/' : routePath, params, hasCatchAll };
|
|
45
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
'olova': 'olova.ts',
|
|
6
|
+
'main': 'src/main.tsx',
|
|
7
|
+
'entry-server': 'src/entry-server.tsx',
|
|
8
|
+
'entry-worker': 'src/entry-worker.tsx',
|
|
9
|
+
'performance': 'src/performance.ts',
|
|
10
|
+
'router': 'src/router/router.tsx',
|
|
11
|
+
},
|
|
12
|
+
format: ['cjs', 'esm'],
|
|
13
|
+
dts: {
|
|
14
|
+
entry: {
|
|
15
|
+
'olova': 'olova.ts',
|
|
16
|
+
'router': 'src/router/router.tsx',
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
clean: true,
|
|
20
|
+
shims: true,
|
|
21
|
+
platform: 'node',
|
|
22
|
+
external: [
|
|
23
|
+
'vite',
|
|
24
|
+
'react',
|
|
25
|
+
'react-dom',
|
|
26
|
+
'react-dom/client',
|
|
27
|
+
'react-dom/server',
|
|
28
|
+
'virtual:olova-app', // External - resolved at runtime by plugin
|
|
29
|
+
'/src/index.css' // External - resolved by Vite in user project
|
|
30
|
+
],
|
|
31
|
+
treeshake: true,
|
|
32
|
+
sourcemap: true,
|
|
33
|
+
minify: false,
|
|
34
|
+
target: 'node18',
|
|
35
|
+
});
|
package/CHANGELOG.md
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [0.1.0] - 2026-01-08
|
|
9
|
-
|
|
10
|
-
### Added
|
|
11
|
-
|
|
12
|
-
- Initial release of Olova
|
|
13
|
-
- File-based routing with Next.js App Router conventions
|
|
14
|
-
- `OlovaRouter` component for rendering routes
|
|
15
|
-
- Vite plugin for automatic route generation
|
|
16
|
-
- React hooks: `usePathname`, `useParams`, `useSearchParams`, `useRouter`
|
|
17
|
-
- `Link` and `NavLink` components for client-side navigation
|
|
18
|
-
- Error boundaries with `error.tsx` support
|
|
19
|
-
- Loading states with `loading.tsx` support
|
|
20
|
-
- 404 handling with `not-found.tsx` support
|
|
21
|
-
- Dynamic routes with `[slug]` syntax
|
|
22
|
-
- Catch-all routes with `[...slug]` syntax
|
|
23
|
-
- Route groups with `(group)` syntax
|
|
24
|
-
- Nested layouts support
|
|
25
|
-
- TypeScript support with full type definitions
|
|
26
|
-
- CLI for build commands
|
|
27
|
-
|
|
28
|
-
### Known Limitations
|
|
29
|
-
|
|
30
|
-
- SSG pre-rendering is not yet fully implemented
|
|
31
|
-
- Parallel routes are experimental
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 sera
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|