vista-core-js 0.0.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/dist/ErrorOverlay.d.ts +7 -0
- package/dist/ErrorOverlay.js +68 -0
- package/dist/app.d.ts +21 -0
- package/dist/app.js +119 -0
- package/dist/client/link.d.ts +23 -0
- package/dist/client/link.js +42 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +290 -0
- package/dist/components/PixelBlast.d.ts +28 -0
- package/dist/components/PixelBlast.js +584 -0
- package/dist/entry-client.d.ts +1 -0
- package/dist/entry-client.js +56 -0
- package/dist/entry-server.d.ts +9 -0
- package/dist/entry-server.js +33 -0
- package/dist/error-overlay.d.ts +1 -0
- package/dist/error-overlay.js +166 -0
- package/dist/font/google/index.d.ts +1923 -0
- package/dist/font/google/index.js +1948 -0
- package/dist/image/get-img-props.d.ts +20 -0
- package/dist/image/get-img-props.js +46 -0
- package/dist/image/image-config.d.ts +20 -0
- package/dist/image/image-config.js +17 -0
- package/dist/image/image-loader.d.ts +7 -0
- package/dist/image/image-loader.js +10 -0
- package/dist/image/index.d.ts +12 -0
- package/dist/image/index.js +12 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +88 -0
- package/dist/router.d.ts +18 -0
- package/dist/router.js +55 -0
- package/package.json +47 -0
- package/src/ErrorOverlay.tsx +194 -0
- package/src/app.tsx +138 -0
- package/src/assets/vista.gif +0 -0
- package/src/client/link.tsx +85 -0
- package/src/client.tsx +368 -0
- package/src/entry-client.tsx +70 -0
- package/src/entry-server.tsx +58 -0
- package/src/error-overlay.ts +187 -0
- package/src/font/google/index.d.ts +19011 -0
- package/src/font/google/index.ts +1968 -0
- package/src/font/types.d.ts +13 -0
- package/src/image/get-img-props.ts +100 -0
- package/src/image/image-config.ts +22 -0
- package/src/image/image-loader.ts +23 -0
- package/src/image/index.tsx +21 -0
- package/src/index.ts +7 -0
- package/src/plugin.ts +100 -0
- package/src/router-loader.ts +51 -0
- package/src/router.tsx +80 -0
- package/tsconfig.json +23 -0
package/src/app.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { Suspense } from 'react';
|
|
2
|
+
import { useRouter } from './router.js';
|
|
3
|
+
import { pages, rootLayout, getPageKey } from './router-loader.js';
|
|
4
|
+
|
|
5
|
+
/* NOTE: Keeping VistaHead as is (or assuming it is preserved by replace_file_content Context) */
|
|
6
|
+
// I will rely on the tool to keep lines I didn't verify in 'Context' but I need to be careful.
|
|
7
|
+
// Actually, I must ensure I don't delete lines 51-110. The prompt for replace_file_content requires TargetContent.
|
|
8
|
+
// I will target the top section and App component separately or use multi_replace if needed?
|
|
9
|
+
// The tool documentation says "Use this tool ONLY when you are making a SINGLE CONTIGUOUS block of edits".
|
|
10
|
+
// I need to change lines 11-50 (Loaders) AND lines 136-142 (App component logic).
|
|
11
|
+
// These are non-contiguous.
|
|
12
|
+
// I MUST use multi_replace_file_content.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// Define Asset Types
|
|
16
|
+
export type VistaAsset = {
|
|
17
|
+
type: 'style' | 'script' | 'preamble';
|
|
18
|
+
src?: string;
|
|
19
|
+
content?: string;
|
|
20
|
+
key?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
function VistaHead({ assets, metadata }: { assets?: VistaAsset[], metadata?: any }) {
|
|
25
|
+
// Serialize assets for client hydration
|
|
26
|
+
const serializedAssets = JSON.stringify(assets || []);
|
|
27
|
+
|
|
28
|
+
// With hydrateRoot(document) and React 19, we can just render the tags.
|
|
29
|
+
// React handles the hoisting for stylesheets if we hint them, or just standard placement.
|
|
30
|
+
// Since we are hydrating the full document, and RootLayout contains <head>,
|
|
31
|
+
// these tags will be rendered where they are (in body/root children).
|
|
32
|
+
// React 19 "Hoistable" Resources: <title>, <meta>, <link rel=stylesheet>.
|
|
33
|
+
// They will be hoisted to <head> automatically.
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
{/* Metadata */}
|
|
38
|
+
{metadata?.title && <title>{metadata.title}</title>}
|
|
39
|
+
{metadata?.description && <meta name="description" content={metadata.description} />}
|
|
40
|
+
|
|
41
|
+
{/* Assets */}
|
|
42
|
+
{assets?.map((asset, index) => {
|
|
43
|
+
if (asset.type === 'style' && asset.src) {
|
|
44
|
+
// Precedence="default" opts into React 19 Resource Loading/Hoisting
|
|
45
|
+
/* @ts-ignore: React 19 prop */
|
|
46
|
+
return <link key={asset.key || asset.src} rel="stylesheet" href={asset.src} precedence="default" />;
|
|
47
|
+
}
|
|
48
|
+
if (asset.type === 'script' && asset.src) {
|
|
49
|
+
return <script key={asset.key || asset.src} type="module" src={asset.src} />;
|
|
50
|
+
}
|
|
51
|
+
if (asset.type === 'preamble' && asset.content) {
|
|
52
|
+
return (
|
|
53
|
+
<script
|
|
54
|
+
key="preamble"
|
|
55
|
+
type="module"
|
|
56
|
+
dangerouslySetInnerHTML={{ __html: asset.content }}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
})}
|
|
62
|
+
|
|
63
|
+
{/* Asset Serialization Script */}
|
|
64
|
+
<script
|
|
65
|
+
id="vista-assets"
|
|
66
|
+
dangerouslySetInnerHTML={{
|
|
67
|
+
__html: `window.__VISTA_ASSETS__ = ${serializedAssets};`
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default function App({
|
|
75
|
+
initialComponent,
|
|
76
|
+
initialRootLayout,
|
|
77
|
+
metadata,
|
|
78
|
+
assets
|
|
79
|
+
}: {
|
|
80
|
+
initialComponent?: React.ComponentType<any>,
|
|
81
|
+
initialRootLayout?: React.ComponentType<any>,
|
|
82
|
+
metadata?: any,
|
|
83
|
+
assets?: VistaAsset[]
|
|
84
|
+
}) {
|
|
85
|
+
const { path } = useRouter();
|
|
86
|
+
const pageKey = getPageKey(path);
|
|
87
|
+
|
|
88
|
+
if (!pages[pageKey]) {
|
|
89
|
+
return <div>404 Not Found: {pageKey}</div>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Page Logic ---
|
|
93
|
+
// --- Page Logic ---
|
|
94
|
+
const isInitialRender = React.useRef(!!initialComponent);
|
|
95
|
+
const [useInitial, setUseInitial] = React.useState(!!initialComponent);
|
|
96
|
+
const [initialPath] = React.useState(path); // FIX: Hook must be at top level
|
|
97
|
+
|
|
98
|
+
React.useEffect(() => {
|
|
99
|
+
// Switch to dynamic component (HMR-capable) after hydration
|
|
100
|
+
if (useInitial) {
|
|
101
|
+
setUseInitial(false);
|
|
102
|
+
isInitialRender.current = false;
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
let PageComponent: React.ComponentType<any>;
|
|
107
|
+
|
|
108
|
+
// Use initial component only if we are on the same path and fresh from hydration
|
|
109
|
+
if (useInitial && initialComponent && path === initialPath) {
|
|
110
|
+
PageComponent = initialComponent;
|
|
111
|
+
} else {
|
|
112
|
+
// Eager: Direct access
|
|
113
|
+
PageComponent = (pages[pageKey] as any).default;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Layout Logic ---
|
|
117
|
+
let RootLayoutComponent: any = initialRootLayout;
|
|
118
|
+
|
|
119
|
+
if (!RootLayoutComponent) {
|
|
120
|
+
const rootLayoutKey = '/app/layout.tsx';
|
|
121
|
+
if (rootLayout[rootLayoutKey]) {
|
|
122
|
+
// Eager
|
|
123
|
+
RootLayoutComponent = (rootLayout[rootLayoutKey] as any).default;
|
|
124
|
+
} else {
|
|
125
|
+
RootLayoutComponent = React.Fragment;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
/* @ts-ignore */
|
|
131
|
+
<RootLayoutComponent>
|
|
132
|
+
<VistaHead metadata={metadata} assets={assets} />
|
|
133
|
+
<Suspense fallback={null}>
|
|
134
|
+
<PageComponent key={path} />
|
|
135
|
+
</Suspense>
|
|
136
|
+
</RootLayoutComponent>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
import React, { AnchorHTMLAttributes } from 'react';
|
|
3
|
+
import { useRouter } from '../router';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Replicating Next.js Link Props Interface
|
|
7
|
+
* based on the provided link.d.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Url = string | { href: string; query?: any; hash?: string }; // Simplified UrlObject
|
|
11
|
+
|
|
12
|
+
export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
13
|
+
href: Url;
|
|
14
|
+
as?: Url;
|
|
15
|
+
replace?: boolean;
|
|
16
|
+
scroll?: boolean;
|
|
17
|
+
shallow?: boolean;
|
|
18
|
+
passHref?: boolean;
|
|
19
|
+
prefetch?: boolean | 'auto' | null;
|
|
20
|
+
locale?: string | false;
|
|
21
|
+
legacyBehavior?: boolean;
|
|
22
|
+
onNavigate?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Simple normalization for href (string or object)
|
|
26
|
+
const formatUrl = (url: Url): string => {
|
|
27
|
+
if (typeof url === 'string') return url;
|
|
28
|
+
if (typeof url === 'object' && url !== null) {
|
|
29
|
+
// Very basic support just for demo
|
|
30
|
+
return (url.href || '') + (url.hash || '');
|
|
31
|
+
}
|
|
32
|
+
return '';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(({
|
|
36
|
+
href,
|
|
37
|
+
as,
|
|
38
|
+
replace,
|
|
39
|
+
scroll,
|
|
40
|
+
shallow,
|
|
41
|
+
passHref,
|
|
42
|
+
prefetch,
|
|
43
|
+
legacyBehavior,
|
|
44
|
+
children,
|
|
45
|
+
onClick,
|
|
46
|
+
...props
|
|
47
|
+
}, ref) => {
|
|
48
|
+
const { push, replace: replaceRoute } = useRouter();
|
|
49
|
+
const targetPath = formatUrl(as || href);
|
|
50
|
+
|
|
51
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
52
|
+
if (onClick) onClick(e);
|
|
53
|
+
|
|
54
|
+
// Standard link behavior checks
|
|
55
|
+
if (e.defaultPrevented) return;
|
|
56
|
+
if (e.button !== 0) return; // ignore right clicks
|
|
57
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // ignore open in new tab
|
|
58
|
+
if (!href) return;
|
|
59
|
+
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
|
|
62
|
+
if (replace) {
|
|
63
|
+
replaceRoute(targetPath);
|
|
64
|
+
} else {
|
|
65
|
+
push(targetPath);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<a
|
|
71
|
+
href={targetPath}
|
|
72
|
+
onClick={handleClick}
|
|
73
|
+
ref={ref}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
</a>
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const useLinkStatus = () => {
|
|
82
|
+
return { pending: false }; // Mock implementation
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default Link;
|
package/src/client.tsx
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
|
|
2
|
+
import React, { useState, useEffect, ErrorInfo } from 'react';
|
|
3
|
+
import ReactDOM from 'react-dom/client';
|
|
4
|
+
import { RouterProvider, useRouter } from './router.js';
|
|
5
|
+
import { ErrorOverlay } from './ErrorOverlay';
|
|
6
|
+
|
|
7
|
+
// --- Error Management ---
|
|
8
|
+
|
|
9
|
+
function VistaErrorManager({ children, initialError }: { children: React.ReactNode, initialError?: any }) {
|
|
10
|
+
const [error, setError] = useState<any>(initialError || null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// 1. Listen for Vite HMR/Build Errors
|
|
14
|
+
if (import.meta.hot) {
|
|
15
|
+
import.meta.hot.on('vite:error', (payloade: any) => {
|
|
16
|
+
// Vite sends an object like { err: { message, stack, plugin, id, frame, loc } }
|
|
17
|
+
// OR sometimes just the error object directly depending on version/context.
|
|
18
|
+
console.log('[Vista] Received vite:error payload:', payloade);
|
|
19
|
+
|
|
20
|
+
const err = payloade.err || payloade;
|
|
21
|
+
|
|
22
|
+
setError({
|
|
23
|
+
message: err.message,
|
|
24
|
+
stack: err.stack,
|
|
25
|
+
id: err.id, // Vite uses 'id' for filename often
|
|
26
|
+
frame: err.frame,
|
|
27
|
+
plugin: err.plugin,
|
|
28
|
+
loc: err.loc,
|
|
29
|
+
type: 'build',
|
|
30
|
+
// Fallbacks for Overlay keys
|
|
31
|
+
filename: err.id || err.file || 'Build Error',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
import.meta.hot.on('vite:beforeUpdate', () => {
|
|
35
|
+
setError(null); // Clear error on new update
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Listen for Unhandled Promise Rejections
|
|
40
|
+
const handleRejection = (event: PromiseRejectionEvent) => {
|
|
41
|
+
// Ignore dynamic import errors if we likely have a build error
|
|
42
|
+
if (event.reason?.message?.includes('Failed to fetch dynamically imported module')) {
|
|
43
|
+
console.log('[Vista] Ignoring dynamic import error (likely causing a build error)');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setError((prev: any) => {
|
|
48
|
+
// specific check: don't overwrite build errors with runtime noise
|
|
49
|
+
if (prev && prev.type === 'build') return prev;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
message: event.reason?.message || 'Unhandled Promise Rejection',
|
|
53
|
+
stack: event.reason?.stack || '',
|
|
54
|
+
filename: 'Runtime Error',
|
|
55
|
+
loc: { line: 0, column: 0 },
|
|
56
|
+
type: 'runtime'
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 3. Listen for Global Errors
|
|
62
|
+
const handleError = (event: ErrorEvent) => {
|
|
63
|
+
setError((prev: any) => {
|
|
64
|
+
if (prev && prev.type === 'build') return prev;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
message: event.error?.message || event.message || 'Unknown Error',
|
|
68
|
+
stack: event.error?.stack || '',
|
|
69
|
+
filename: 'Runtime Error',
|
|
70
|
+
loc: { line: event.lineno || 0, column: event.colno || 0 },
|
|
71
|
+
type: 'runtime'
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
window.addEventListener('unhandledrejection', handleRejection);
|
|
77
|
+
window.addEventListener('error', handleError);
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
window.removeEventListener('unhandledrejection', handleRejection);
|
|
81
|
+
window.removeEventListener('error', handleError);
|
|
82
|
+
};
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const handleBoundaryError = (err: any) => {
|
|
86
|
+
// Logic to ignore cascade errors
|
|
87
|
+
if (error && error.type === 'build') return;
|
|
88
|
+
if (err.message && err.message.includes('Failed to fetch dynamically imported module')) return;
|
|
89
|
+
|
|
90
|
+
setError({
|
|
91
|
+
message: err.message,
|
|
92
|
+
stack: err.stack,
|
|
93
|
+
loc: { line: 0, column: 0 },
|
|
94
|
+
filename: 'Runtime Error',
|
|
95
|
+
type: 'runtime'
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (error) {
|
|
100
|
+
return <ErrorOverlay error={error} onClose={() => setError(null)} />;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ErrorBoundary onError={handleBoundaryError}>
|
|
105
|
+
{children}
|
|
106
|
+
</ErrorBoundary>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ... existing ErrorBoundary ...
|
|
111
|
+
|
|
112
|
+
// ... existing AppContent ...
|
|
113
|
+
|
|
114
|
+
// ... existing App ...
|
|
115
|
+
|
|
116
|
+
// Export a mount function that accepts the glob results
|
|
117
|
+
export function mount(modules: Record<string, any>) {
|
|
118
|
+
console.log('[Vista] Mount called with modules:', Object.keys(modules));
|
|
119
|
+
const root = document.getElementById('root');
|
|
120
|
+
if (root) {
|
|
121
|
+
console.log('[Vista] Root element found, rendering...');
|
|
122
|
+
ReactDOM.createRoot(root).render(
|
|
123
|
+
<VistaErrorManager>
|
|
124
|
+
<App modules={modules} />
|
|
125
|
+
</VistaErrorManager>
|
|
126
|
+
);
|
|
127
|
+
} else {
|
|
128
|
+
console.error('[Vista] FATAL: Root element #root not found in DOM');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Export a mountError function for when module loading fails
|
|
133
|
+
export function mountError(error: any) {
|
|
134
|
+
const root = document.getElementById('root');
|
|
135
|
+
if (root) {
|
|
136
|
+
// Mount just the ErrorManager to catch and display the Vite error
|
|
137
|
+
ReactDOM.createRoot(root).render(
|
|
138
|
+
<VistaErrorManager initialError={error}>
|
|
139
|
+
<div style={{ padding: '20px', fontFamily: 'sans-serif', color: '#666' }}>
|
|
140
|
+
{/* Placeholder content while waiting for error overlay */}
|
|
141
|
+
Initializing error overlay...
|
|
142
|
+
</div>
|
|
143
|
+
</VistaErrorManager>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class ErrorBoundary extends React.Component<{ children: React.ReactNode, onError: (error: any) => void }, { hasError: boolean }> {
|
|
149
|
+
constructor(props: any) {
|
|
150
|
+
super(props);
|
|
151
|
+
this.state = { hasError: false };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static getDerivedStateFromError(error: any) {
|
|
155
|
+
return { hasError: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
componentDidCatch(error: any, errorInfo: ErrorInfo) {
|
|
159
|
+
this.props.onError({
|
|
160
|
+
message: error.message,
|
|
161
|
+
stack: error.stack,
|
|
162
|
+
loc: { line: 0, column: 0 },
|
|
163
|
+
filename: 'Runtime Error'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
render() {
|
|
168
|
+
if (this.state.hasError) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return this.props.children;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- App Logic ---
|
|
176
|
+
|
|
177
|
+
function AppContent({ modules }: { modules: Record<string, any> }) {
|
|
178
|
+
const { path: currentPath } = useRouter();
|
|
179
|
+
|
|
180
|
+
// 1. Find the matching page component
|
|
181
|
+
let PageComponent: any = () => <div className="p-10 text-xl">404 Not Found</div>;
|
|
182
|
+
|
|
183
|
+
// 2. Build the Layout Tree
|
|
184
|
+
// ... (Logic to build layoutPaths is same) ...
|
|
185
|
+
|
|
186
|
+
// Refactored Async Logic
|
|
187
|
+
const [componentTree, setComponentTree] = React.useState<React.ReactNode | null>(null);
|
|
188
|
+
const [pageError, setPageError] = React.useState<any>(null);
|
|
189
|
+
|
|
190
|
+
React.useEffect(() => {
|
|
191
|
+
let isMounted = true;
|
|
192
|
+
setPageError(null);
|
|
193
|
+
|
|
194
|
+
const loadRoute = async () => {
|
|
195
|
+
try {
|
|
196
|
+
// --- 1. Route Matching Engine ---
|
|
197
|
+
let pageLoader: (() => Promise<any>) | null = null;
|
|
198
|
+
let matchedRoutePath = '';
|
|
199
|
+
let routeParams: Record<string, string> = {};
|
|
200
|
+
|
|
201
|
+
// Normalize current path: remove trailing slash (unless root)
|
|
202
|
+
let normalizedPath = currentPath;
|
|
203
|
+
if (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
|
|
204
|
+
normalizedPath = normalizedPath.slice(0, -1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Helper: Convert file path to route regex
|
|
208
|
+
// e.g. "/app/users/[id]/index.tsx" -> "^/users/([^/]+)$"
|
|
209
|
+
// e.g. "/app/index.tsx" -> "^/$"
|
|
210
|
+
const createRouteMatcher = (filePath: string) => {
|
|
211
|
+
// Remove prefix: handle "/", "./", or no slash before "app"
|
|
212
|
+
let cleanPath = filePath
|
|
213
|
+
.replace(/^(\.?\/)?app\//, '') // Remove app/ prefix (robust)
|
|
214
|
+
.replace(/\/index\.tsx$/, '') // Remove /index.tsx (nested)
|
|
215
|
+
.replace(/^index\.tsx$/, '') // Remove index.tsx (root)
|
|
216
|
+
.replace(/\.tsx$/, ''); // Remove .tsx (if any)
|
|
217
|
+
|
|
218
|
+
if (cleanPath === '') cleanPath = '/';
|
|
219
|
+
|
|
220
|
+
// Split into segments
|
|
221
|
+
const segments = cleanPath.split('/').filter(Boolean);
|
|
222
|
+
const paramNames: string[] = [];
|
|
223
|
+
|
|
224
|
+
// Console log for debug
|
|
225
|
+
// console.log(`[Router Debug] File: ${filePath} -> Clean: ${cleanPath}`);
|
|
226
|
+
|
|
227
|
+
const pathPattern = segments.map(seg => {
|
|
228
|
+
if (seg.startsWith('[') && seg.endsWith(']')) {
|
|
229
|
+
const paramName = seg.slice(1, -1);
|
|
230
|
+
paramNames.push(paramName);
|
|
231
|
+
return '([^/]+)'; // Match any non-slash char
|
|
232
|
+
}
|
|
233
|
+
return seg; // Static segment
|
|
234
|
+
}).join('/');
|
|
235
|
+
|
|
236
|
+
// Generate Regex: Ensure it starts with ^/ and ends with $
|
|
237
|
+
// cleanPath modules like 'about' become '^/about$'
|
|
238
|
+
// root '' becomes '^/$'
|
|
239
|
+
const regexString = pathPattern ? `^/${pathPattern}$` : '^/$';
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
regex: new RegExp(regexString),
|
|
243
|
+
paramNames,
|
|
244
|
+
originalPath: cleanPath
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Iterate all modules to find a match
|
|
249
|
+
// Note: In a real framework, you'd cache the route table.
|
|
250
|
+
// console.log('[Vista Router] Current Path (Normalized):', normalizedPath);
|
|
251
|
+
// console.log('[Vista Router] Modules Found:', Object.keys(modules));
|
|
252
|
+
|
|
253
|
+
for (const path in modules) {
|
|
254
|
+
// Only care about pages (index.tsx)
|
|
255
|
+
if (!path.endsWith('/index.tsx')) continue;
|
|
256
|
+
|
|
257
|
+
const { regex, paramNames } = createRouteMatcher(path);
|
|
258
|
+
const match = normalizedPath.match(regex);
|
|
259
|
+
|
|
260
|
+
// console.log(`[Vista Router] Checking ${path} -> Regex: ${regex} -> Match: ${match ? 'YES' : 'NO'}`);
|
|
261
|
+
|
|
262
|
+
if (match) {
|
|
263
|
+
pageLoader = modules[path];
|
|
264
|
+
matchedRoutePath = path;
|
|
265
|
+
|
|
266
|
+
// Extract params
|
|
267
|
+
match.slice(1).forEach((val, index) => {
|
|
268
|
+
routeParams[paramNames[index]] = decodeURIComponent(val);
|
|
269
|
+
});
|
|
270
|
+
break; // Found first match (TODO: strict sorting Order)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!pageLoader) {
|
|
275
|
+
if (isMounted) setComponentTree(<div className="p-10 text-xl">404 Not Found</div>);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// --- 2. Load Page & Layouts ---
|
|
280
|
+
const pageModule = await pageLoader();
|
|
281
|
+
let PageComponent = pageModule.default;
|
|
282
|
+
|
|
283
|
+
// Load Layouts: Climb up the directory tree from the page path
|
|
284
|
+
// e.g. /app/users/[id]/index.tsx
|
|
285
|
+
// Check: /app/users/[id]/layout.tsx
|
|
286
|
+
// Check: /app/users/layout.tsx
|
|
287
|
+
// Check: /app/layout.tsx
|
|
288
|
+
|
|
289
|
+
const layoutLoaders: Promise<any>[] = [];
|
|
290
|
+
const dirSegments = matchedRoutePath.split('/').slice(0, -1); // remove index.tsx
|
|
291
|
+
|
|
292
|
+
// Construct potential layout paths from root down to leaf
|
|
293
|
+
// But we want to load them all.
|
|
294
|
+
// Actually, let's just create the list of all parent directories and check for layout.tsx
|
|
295
|
+
|
|
296
|
+
let currentDir = ''; // starts empty
|
|
297
|
+
const possibleLayouts: string[] = [];
|
|
298
|
+
|
|
299
|
+
// We know everything starts with /app
|
|
300
|
+
// dirSegments looks like ['', 'app', 'users', '[id]']
|
|
301
|
+
|
|
302
|
+
for (const segment of dirSegments) {
|
|
303
|
+
if (!segment) continue;
|
|
304
|
+
currentDir += `/${segment}`;
|
|
305
|
+
const layoutPath = `${currentDir}/layout.tsx`;
|
|
306
|
+
if (modules[layoutPath]) {
|
|
307
|
+
possibleLayouts.push(layoutPath);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Execute layout loaders
|
|
312
|
+
const layoutModules = await Promise.all(
|
|
313
|
+
possibleLayouts.map(path => modules[path]())
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
if (!isMounted) return;
|
|
317
|
+
|
|
318
|
+
// Handle Metadata
|
|
319
|
+
let meta = pageModule.metadata || {};
|
|
320
|
+
layoutModules.forEach(mod => {
|
|
321
|
+
if (mod.metadata) meta = { ...meta, ...mod.metadata };
|
|
322
|
+
});
|
|
323
|
+
if (meta.title) document.title = meta.title;
|
|
324
|
+
|
|
325
|
+
// Build Tree: Wrap Page with Layouts (Inner -> Outer? NO. Outer wraps Inner.)
|
|
326
|
+
// layoutModules is [AppLayout, UsersLayout, UserIdLayout]
|
|
327
|
+
// We want <AppLayout><UsersLayout><UserIdLayout><Page /></...></...></...>
|
|
328
|
+
|
|
329
|
+
let Content = <PageComponent params={routeParams} />; // Pass params to page!
|
|
330
|
+
|
|
331
|
+
// Reverse iterate to wrap from inside out
|
|
332
|
+
for (let i = layoutModules.length - 1; i >= 0; i--) {
|
|
333
|
+
const LayoutComponent = layoutModules[i].default;
|
|
334
|
+
Content = <LayoutComponent params={routeParams}>{Content}</LayoutComponent>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
setComponentTree(Content);
|
|
338
|
+
|
|
339
|
+
} catch (err) {
|
|
340
|
+
console.error('[Vista] Failed to load route:', err);
|
|
341
|
+
if (isMounted) setPageError(err);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
loadRoute();
|
|
346
|
+
|
|
347
|
+
return () => { isMounted = false; };
|
|
348
|
+
}, [currentPath, modules]);
|
|
349
|
+
|
|
350
|
+
if (pageError) {
|
|
351
|
+
// Propagate to ErrorManager
|
|
352
|
+
throw pageError;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return componentTree;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function App({ modules }: { modules: Record<string, any> }) {
|
|
359
|
+
return (
|
|
360
|
+
<React.StrictMode>
|
|
361
|
+
<RouterProvider>
|
|
362
|
+
<AppContent modules={modules} />
|
|
363
|
+
</RouterProvider>
|
|
364
|
+
</React.StrictMode>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { RouterProvider } from './router';
|
|
4
|
+
import App from './app.js';
|
|
5
|
+
|
|
6
|
+
import { showErrorOverlay } from './error-overlay';
|
|
7
|
+
import { loadRoute, loadRootLayout } from './router-loader.js';
|
|
8
|
+
|
|
9
|
+
export async function mount() {
|
|
10
|
+
// For Full Document Hydration (RootLayout provides html/body)
|
|
11
|
+
// we hydrate 'document' directly.
|
|
12
|
+
const root = document;
|
|
13
|
+
|
|
14
|
+
let initialComponent: React.ComponentType<any> | undefined;
|
|
15
|
+
let initialRootLayout: React.ComponentType<any> | undefined;
|
|
16
|
+
let initialMetadata = {};
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const [component, layoutResult] = await Promise.all([
|
|
20
|
+
loadRoute(window.location.pathname),
|
|
21
|
+
loadRootLayout() as Promise<any>
|
|
22
|
+
]);
|
|
23
|
+
initialComponent = component || undefined;
|
|
24
|
+
initialRootLayout = layoutResult?.Component || undefined;
|
|
25
|
+
initialMetadata = layoutResult?.metadata || {};
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// We hydrate the full document because we are doing full SSR with HTML shell.
|
|
28
|
+
}
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
ReactDOM.hydrateRoot(window.document,
|
|
31
|
+
// <React.StrictMode>
|
|
32
|
+
<RouterProvider>
|
|
33
|
+
{/* @ts-ignore */}
|
|
34
|
+
<App
|
|
35
|
+
initialComponent={initialComponent}
|
|
36
|
+
initialRootLayout={initialRootLayout}
|
|
37
|
+
metadata={initialMetadata}
|
|
38
|
+
assets={(window as any).__VISTA_ASSETS__ || []}
|
|
39
|
+
/>
|
|
40
|
+
</RouterProvider>,
|
|
41
|
+
// </React.StrictMode>
|
|
42
|
+
{
|
|
43
|
+
onRecoverableError: (error) => {
|
|
44
|
+
console.error('Hydration failed:', error);
|
|
45
|
+
// Only show overlay if it's a real error, not just a cancelled transition
|
|
46
|
+
// showErrorOverlay(error as Error, 'Hydration Mismatch');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Auto-mount if in browser environment and not already mounted
|
|
53
|
+
if (typeof window !== 'undefined') {
|
|
54
|
+
window.addEventListener('error', (event) => {
|
|
55
|
+
console.error('Global Error Caught:', event.error);
|
|
56
|
+
showErrorOverlay(event.error, 'Runtime Error');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Wrapped in async IIFE to handle await
|
|
60
|
+
(async () => {
|
|
61
|
+
try {
|
|
62
|
+
console.log('Mounting Vista App...');
|
|
63
|
+
await mount();
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Mounting failed:', e);
|
|
66
|
+
showErrorOverlay(e as Error, 'Mount Error');
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
}
|
|
70
|
+
|