frontend-hamroun 1.2.27 → 1.2.29

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 (41) hide show
  1. package/README.md +7 -0
  2. package/bin/banner.js +0 -0
  3. package/bin/cli-utils.js +0 -0
  4. package/bin/cli.js +536 -598
  5. package/package.json +1 -1
  6. package/templates/fullstack-app/README.md +37 -0
  7. package/templates/fullstack-app/build/main.css +664 -0
  8. package/templates/fullstack-app/build/main.css.map +7 -0
  9. package/templates/fullstack-app/build/main.js +682 -0
  10. package/templates/fullstack-app/build/main.js.map +7 -0
  11. package/templates/fullstack-app/build.ts +211 -0
  12. package/templates/fullstack-app/index.html +26 -3
  13. package/templates/fullstack-app/package-lock.json +2402 -438
  14. package/templates/fullstack-app/package.json +24 -9
  15. package/templates/fullstack-app/postcss.config.js +6 -0
  16. package/templates/fullstack-app/process-tailwind.js +45 -0
  17. package/templates/fullstack-app/public/_redirects +1 -0
  18. package/templates/fullstack-app/public/route-handler.js +47 -0
  19. package/templates/fullstack-app/public/spa-fix.html +17 -0
  20. package/templates/fullstack-app/public/styles.css +768 -0
  21. package/templates/fullstack-app/public/tailwind.css +15 -0
  22. package/templates/fullstack-app/server.js +101 -44
  23. package/templates/fullstack-app/server.ts +402 -39
  24. package/templates/fullstack-app/src/README.md +55 -0
  25. package/templates/fullstack-app/src/client.js +83 -16
  26. package/templates/fullstack-app/src/components/Layout.tsx +45 -0
  27. package/templates/fullstack-app/src/components/UserList.tsx +27 -0
  28. package/templates/fullstack-app/src/config.ts +42 -0
  29. package/templates/fullstack-app/src/data/api.ts +71 -0
  30. package/templates/fullstack-app/src/main.tsx +219 -7
  31. package/templates/fullstack-app/src/pages/about/index.tsx +67 -0
  32. package/templates/fullstack-app/src/pages/index.tsx +30 -60
  33. package/templates/fullstack-app/src/pages/users.tsx +60 -0
  34. package/templates/fullstack-app/src/router.ts +255 -0
  35. package/templates/fullstack-app/src/styles.css +5 -0
  36. package/templates/fullstack-app/tailwind.config.js +11 -0
  37. package/templates/fullstack-app/tsconfig.json +18 -0
  38. package/templates/fullstack-app/vite.config.js +53 -6
  39. package/templates/ssr-template/readme.md +50 -0
  40. package/templates/ssr-template/src/client.ts +46 -14
  41. package/templates/ssr-template/src/server.ts +190 -18
@@ -1,63 +1,33 @@
1
- import { jsx, useState, useEffect } from 'frontend-hamroun';
1
+ import { jsx } from 'frontend-hamroun';
2
+ import Layout from '../components/Layout';
3
+ import UserList from '../components/UserList';
4
+ import { UserApi } from '../data/api';
2
5
 
3
- export default function HomePage() {
4
- const [apiData, setApiData] = useState(null);
5
- const [loading, setLoading] = useState(true);
6
- const [error, setError] = useState(null);
6
+ const HomePage = ({ initialState }) => (
7
+ <Layout title="Home">
8
+ <div className="max-w-4xl mx-auto py-8">
9
+ <h1 className="text-3xl font-bold text-blue-600 mb-6">Welcome to your Frontend Hamroun application!</h1>
10
+
11
+ <div className="bg-white shadow-lg rounded-lg p-6 mb-8">
12
+ <h2 className="text-xl font-semibold text-gray-800 mb-4">User List</h2>
13
+ <UserList users={initialState.data?.users || []} />
14
+ </div>
15
+
16
+ <div className="bg-gray-50 rounded-lg p-6 border border-gray-200">
17
+ <h3 className="text-lg font-medium text-gray-700 mb-3">Application State</h3>
18
+ <pre className="overflow-auto p-4 bg-gray-100 rounded-md text-sm text-gray-800">
19
+ {JSON.stringify(initialState, null, 2)}
20
+ </pre>
21
+ </div>
22
+ </div>
23
+ </Layout>
24
+ );
7
25
 
8
- useEffect(() => {
9
- async function fetchData() {
10
- try {
11
- const response = await fetch('/api/hello');
12
- const data = await response.json();
13
- setApiData(data);
14
- setLoading(false);
15
- } catch (err) {
16
- setError(err.message);
17
- setLoading(false);
18
- }
19
- }
20
-
21
- fetchData();
22
- }, []);
26
+ // Static method to fetch initial data for this page
27
+ HomePage.getInitialData = async () => {
28
+ return {
29
+ users: await UserApi.getAll()
30
+ };
31
+ };
23
32
 
24
- return jsx('div', { className: "container mx-auto p-4" }, [
25
- jsx('h1', { className: "text-3xl font-bold mb-4" }, "Frontend Hamroun Full Stack"),
26
-
27
- jsx('div', { className: "mb-6" }, [
28
- jsx('h2', { className: "text-xl font-semibold mb-2" }, "API Response:"),
29
- loading
30
- ? jsx('p', {}, "Loading...")
31
- : error
32
- ? jsx('p', { className: "text-red-500" }, `Error: ${error}`)
33
- : jsx('pre', { className: "bg-gray-100 p-4 rounded" },
34
- JSON.stringify(apiData, null, 2)
35
- )
36
- ]),
37
-
38
- jsx('div', { className: "mb-6" }, [
39
- jsx('h2', { className: "text-xl font-semibold mb-2" }, "Features:"),
40
- jsx('ul', { className: "list-disc pl-6" }, [
41
- jsx('li', {}, "Server-side rendering"),
42
- jsx('li', {}, "API routes with Express"),
43
- jsx('li', {}, "Database integration"),
44
- jsx('li', {}, "Authentication and authorization"),
45
- jsx('li', {}, "File-based routing")
46
- ])
47
- ]),
48
-
49
- jsx('div', {}, [
50
- jsx('h2', { className: "text-xl font-semibold mb-2" }, "Get Started:"),
51
- jsx('p', {}, [
52
- "Edit ",
53
- jsx('code', { className: "bg-gray-100 px-2 py-1 rounded" }, "src/pages/index.tsx"),
54
- " to customize this page"
55
- ]),
56
- jsx('p', {}, [
57
- "API routes are in the ",
58
- jsx('code', { className: "bg-gray-100 px-2 py-1 rounded" }, "api"),
59
- " directory"
60
- ])
61
- ])
62
- ]);
63
- }
33
+ export default HomePage;
@@ -0,0 +1,60 @@
1
+ import { jsx } from 'frontend-hamroun';
2
+ import Layout from '../components/Layout';
3
+ import { UserApi } from '../data/api';
4
+
5
+ const UsersPage = ({ initialState }) => {
6
+ const users = initialState.data?.users || [];
7
+
8
+ return (
9
+ <Layout title="User Management">
10
+ <div className="max-w-4xl mx-auto">
11
+ <div className="bg-blue-50 p-6 rounded-lg mb-8 border border-blue-100">
12
+ <h2 className="text-xl font-semibold text-blue-800 mb-2">Data Fetching Demo</h2>
13
+ <p className="text-blue-700">This page demonstrates dynamic data fetching with the Users API.</p>
14
+ </div>
15
+
16
+ <div className="bg-white shadow-md rounded-lg overflow-hidden">
17
+ <div className="px-6 py-4 border-b border-gray-200">
18
+ <h2 className="text-xl font-semibold text-gray-800">User List</h2>
19
+ </div>
20
+
21
+ {users.length === 0 ? (
22
+ <div className="p-6 text-center text-gray-500">
23
+ <p>No users found.</p>
24
+ </div>
25
+ ) : (
26
+ <div className="overflow-x-auto">
27
+ <table className="w-full">
28
+ <thead>
29
+ <tr className="bg-gray-50">
30
+ <th className="text-left py-3 px-6 font-medium text-gray-600 text-sm uppercase tracking-wider border-b">ID</th>
31
+ <th className="text-left py-3 px-6 font-medium text-gray-600 text-sm uppercase tracking-wider border-b">Name</th>
32
+ <th className="text-left py-3 px-6 font-medium text-gray-600 text-sm uppercase tracking-wider border-b">Email</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody className="divide-y divide-gray-200">
36
+ {users.map(user => (
37
+ <tr key={user.id} className="hover:bg-gray-50">
38
+ <td className="py-4 px-6 text-sm text-gray-900">{user.id}</td>
39
+ <td className="py-4 px-6 text-sm font-medium text-gray-900">{user.name}</td>
40
+ <td className="py-4 px-6 text-sm text-gray-500">{user.email}</td>
41
+ </tr>
42
+ ))}
43
+ </tbody>
44
+ </table>
45
+ </div>
46
+ )}
47
+ </div>
48
+ </div>
49
+ </Layout>
50
+ );
51
+ };
52
+
53
+ // Static method to fetch initial data for this page
54
+ UsersPage.getInitialData = async () => {
55
+ return {
56
+ users: await UserApi.getAll()
57
+ };
58
+ };
59
+
60
+ export default UsersPage;
@@ -0,0 +1,255 @@
1
+ import { jsx } from 'frontend-hamroun';
2
+ // Use dynamic import to ensure it's available
3
+ // import UsersPage from './pages/users';
4
+
5
+ // Type definitions for page components
6
+ export interface PageProps {
7
+ initialState: any;
8
+ }
9
+
10
+ export interface PageComponent {
11
+ (props: PageProps): any;
12
+ getInitialData?: (path: string) => Promise<any>;
13
+ }
14
+
15
+ // Define router interface for type safety
16
+ interface RouteParams {
17
+ [key: string]: string;
18
+ }
19
+
20
+ // Define router class for handling routes
21
+ export class Router {
22
+ private routes: Record<string, PageComponent> = {};
23
+ private notFoundComponent: PageComponent | null = null;
24
+
25
+ // Register a component for a specific route
26
+ register(path: string, component: PageComponent): Router {
27
+ const normalizedPath = path === '/' ? 'index' : path.replace(/^\//, '');
28
+ this.routes[normalizedPath] = component;
29
+ console.log(`[Router] Registered component for path: ${normalizedPath}`);
30
+ return this;
31
+ }
32
+
33
+ // Set the not found component
34
+ setNotFound(component: PageComponent): Router {
35
+ this.notFoundComponent = component;
36
+ return this;
37
+ }
38
+
39
+ // Get the not found component
40
+ getNotFound(): PageComponent | null {
41
+ return this.notFoundComponent;
42
+ }
43
+
44
+ // Get all registered routes
45
+ getAllRoutes(): Record<string, PageComponent> {
46
+ return this.routes;
47
+ }
48
+
49
+ // Find component for a given path
50
+ async resolve(path: string): Promise<PageComponent | null> {
51
+ const normalizedPath = path === '/' ? 'index' : path.replace(/^\//, '');
52
+
53
+ console.log(`[Router] Resolving component for path: ${normalizedPath}`);
54
+
55
+ // Check for exact match first
56
+ if (this.routes[normalizedPath]) {
57
+ return this.routes[normalizedPath];
58
+ }
59
+
60
+ // Check for nested routes (e.g., "users/123" should match a "users/[id]" route)
61
+ const pathSegments = normalizedPath.split('/');
62
+ const registeredRoutes = Object.keys(this.routes);
63
+
64
+ // Try to find dynamic route matches
65
+ for (const route of registeredRoutes) {
66
+ const routeSegments = route.split('/');
67
+
68
+ // Skip routes with different segment count
69
+ if (routeSegments.length !== pathSegments.length) continue;
70
+
71
+ let isMatch = true;
72
+ const params: RouteParams = {};
73
+
74
+ // Compare each segment
75
+ for (let i = 0; i < routeSegments.length; i++) {
76
+ const routeSegment = routeSegments[i];
77
+ const pathSegment = pathSegments[i];
78
+
79
+ // Handle dynamic segments (e.g., [id])
80
+ if (routeSegment.startsWith('[') && routeSegment.endsWith(']')) {
81
+ const paramName = routeSegment.slice(1, -1);
82
+ params[paramName] = pathSegment;
83
+ }
84
+ // Regular segment, must match exactly
85
+ else if (routeSegment !== pathSegment) {
86
+ isMatch = false;
87
+ break;
88
+ }
89
+ }
90
+
91
+ if (isMatch) {
92
+ console.log(`[Router] Found dynamic route match: ${route}`);
93
+ // Return the component with params
94
+ return this.routes[route];
95
+ }
96
+ }
97
+
98
+ // If no match found yet, try to dynamically import the component
99
+ try {
100
+ let component = null;
101
+ let resolvedPath = normalizedPath;
102
+
103
+ // Next.js-style dynamic route resolution
104
+ try {
105
+ // First, try direct file match (e.g., ./pages/users.tsx)
106
+ // Focus on .tsx since that's what this project uses
107
+ try {
108
+ console.log(`[Router] Trying direct import: ./pages/${resolvedPath}.tsx`);
109
+ const directModule = await import(/* @vite-ignore */ `./pages/${resolvedPath}.tsx`)
110
+ .catch(() => null);
111
+
112
+ if (directModule) {
113
+ component = directModule.default || directModule;
114
+ }
115
+ } catch (e) {
116
+ console.warn(`[Router] Error importing ./pages/${resolvedPath}.tsx:`, e);
117
+ }
118
+
119
+ // Next, try index file in directory (e.g., ./pages/about/index.tsx)
120
+ if (!component && !resolvedPath.endsWith('index')) {
121
+ try {
122
+ console.log(`[Router] Trying index file: ./pages/${resolvedPath}/index.tsx`);
123
+ const indexModule = await import(/* @vite-ignore */ `./pages/${resolvedPath}/index.tsx`)
124
+ .catch(() => null);
125
+
126
+ if (indexModule) {
127
+ component = indexModule.default || indexModule;
128
+ }
129
+ } catch (e) {
130
+ console.warn(`[Router] Error importing ./pages/${resolvedPath}/index.tsx:`, e);
131
+ }
132
+ }
133
+ } catch (routeError) {
134
+ console.warn(`[Router] Error resolving Next.js style route:`, routeError);
135
+ }
136
+
137
+ // Register and return component if found
138
+ if (component) {
139
+ this.routes[normalizedPath] = component;
140
+ return component;
141
+ }
142
+ } catch (error) {
143
+ console.warn(`[Router] Error importing component for ${normalizedPath}:`, error);
144
+ }
145
+
146
+ // If we reach here, no component was found
147
+ console.warn(`[Router] No component found for path: ${normalizedPath}`);
148
+ return this.notFoundComponent;
149
+ }
150
+
151
+ // Auto-discover components in the pages directory
152
+ async discoverRoutes(): Promise<Record<string, PageComponent>> {
153
+ console.log('[Router] Auto-discovering routes from pages directory...');
154
+
155
+ try {
156
+ // Use a more conventional approach instead of relying on import.meta.glob
157
+ await this.tryLoadCoreRoutes();
158
+
159
+ console.log('[Router] Route discovery complete. Available routes:', Object.keys(this.routes));
160
+ return this.routes;
161
+ } catch (error) {
162
+ console.error('[Router] Error discovering routes:', error);
163
+ await this.tryLoadCoreRoutes();
164
+ return this.routes;
165
+ }
166
+ }
167
+
168
+ // Fallback method to load core routes
169
+ async tryLoadCoreRoutes(): Promise<void> {
170
+ // First try the auto-discovery approach with dynamic imports
171
+ const pageModules = [
172
+ { path: './pages/index', route: 'index' },
173
+ { path: './pages/about/index', route: 'about' },
174
+ { path: './pages/users', route: 'users' }
175
+ ];
176
+
177
+ // Try importing each module dynamically - focus on .tsx files
178
+ for (const { path, route } of pageModules) {
179
+ if (!this.routes[route]) {
180
+ try {
181
+ console.log(`[Router] Attempting to load route: ${route}`);
182
+
183
+ // Only try .tsx imports since that's what the project uses
184
+ const module = await import(/* @vite-ignore */ `${path}.tsx`)
185
+ .catch(() => null);
186
+
187
+ if (module && module.default) {
188
+ this.routes[route] = module.default;
189
+ console.log(`[Router] Registered route: ${route}`);
190
+ }
191
+ } catch (error) {
192
+ console.warn(`[Router] Could not load route: ${route}`, error);
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ // Create and export a router instance
200
+ export const router = new Router();
201
+
202
+ // NotFound component as fallback (using jsx function instead of JSX syntax)
203
+ export const NotFound: PageComponent = ({ initialState }) => {
204
+ return jsx('div', { className: 'container mx-auto px-4 py-12 max-w-4xl' }, [
205
+ jsx('div', { className: 'bg-white shadow-lg rounded-lg overflow-hidden p-8' }, [
206
+ jsx('h1', { className: 'text-3xl font-bold text-gray-800 mb-4' }, ['Page Not Found']),
207
+ jsx('p', { className: 'text-gray-600 mb-6' }, [`No component found for path: ${initialState?.route || 'unknown'}`]),
208
+ jsx('a', { href: '/', className: 'text-blue-600 hover:text-blue-800 hover:underline transition-colors' }, ['Go to Home']),
209
+ jsx('div', { className: 'mt-8 p-6 bg-gray-50 rounded-lg border border-gray-200' }, [
210
+ jsx('h3', { className: 'text-lg font-medium text-gray-700 mb-3' }, ['Available Routes']),
211
+ jsx('ul', { className: 'space-y-2' }, [
212
+ jsx('li', {}, [jsx('a', { href: '/', className: 'text-blue-600 hover:text-blue-800 hover:underline' }, ['Home'])]),
213
+ jsx('li', {}, [jsx('a', { href: '/about', className: 'text-blue-600 hover:text-blue-800 hover:underline' }, ['About'])]),
214
+ jsx('li', {}, [jsx('a', { href: '/users', className: 'text-blue-600 hover:text-blue-800 hover:underline' }, ['Users'])])
215
+ ])
216
+ ])
217
+ ])
218
+ ]);
219
+ };
220
+
221
+ // Set NotFound as the default fallback
222
+ router.setNotFound(NotFound);
223
+
224
+ // Improved router initialization with auto-discovery
225
+ export async function initializeRouter(): Promise<Router> {
226
+ try {
227
+ console.log('[Router] Initializing router with auto-discovery...');
228
+
229
+ // Auto-discover routes
230
+ await router.discoverRoutes();
231
+
232
+ // Register fallback pages for key routes if they weren't discovered
233
+ const routes = router.getAllRoutes();
234
+
235
+ if (!routes['index']) {
236
+ router.register('index', ({ initialState }: PageProps) => jsx('div', {}, [
237
+ jsx('h1', {}, ['Welcome']),
238
+ jsx('p', {}, ['This is the home page.'])
239
+ ]));
240
+ }
241
+
242
+ if (!routes['about']) {
243
+ router.register('about', ({ initialState }: PageProps) => jsx('div', {}, [
244
+ jsx('h1', {}, ['About']),
245
+ jsx('p', {}, ['This is the about page.'])
246
+ ]));
247
+ }
248
+
249
+ console.log('[Router] Router initialized with routes:', Object.keys(router.getAllRoutes()));
250
+ return router;
251
+ } catch (error) {
252
+ console.error('[Router] Error initializing router:', error);
253
+ return router;
254
+ }
255
+ }
@@ -0,0 +1,5 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Your custom styles can go here */
@@ -0,0 +1,11 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "esModuleInterop": true,
7
+ "sourceMap": true,
8
+ "outDir": "dist",
9
+ "strict": true,
10
+ "lib": ["ES2020"],
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "allowSyntheticDefaultImports": true,
14
+ "resolveJsonModule": true
15
+ },
16
+ "include": ["**/*.ts"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
@@ -6,35 +6,82 @@ export default defineConfig({
6
6
  server: {
7
7
  port: 5173,
8
8
  proxy: {
9
- '/api': 'http://localhost:3000'
9
+ '/api': 'http://localhost:3000',
10
+ '/socket.io': {
11
+ target: 'http://localhost:3000',
12
+ ws: true
13
+ }
10
14
  }
11
15
  },
12
16
 
13
- // Using jsx transform with a different configuration
17
+ // Using jsx transform with frontend-hamroun's jsx function
14
18
  esbuild: {
15
19
  jsxFactory: 'jsx',
16
- jsxFragment: 'Fragment'
20
+ jsxFragment: 'Fragment',
21
+ jsxInject: `import { jsx, Fragment } from 'frontend-hamroun'`
17
22
  },
18
23
 
19
24
  // Build configuration
20
25
  build: {
21
26
  outDir: 'dist',
27
+ assetsDir: 'assets',
28
+ // Generate SPA-friendly build that works with client-side routing
22
29
  rollupOptions: {
23
30
  input: {
24
31
  main: resolve(__dirname, 'index.html')
32
+ },
33
+ output: {
34
+ manualChunks: {
35
+ vendor: ['frontend-hamroun']
36
+ }
25
37
  }
38
+ },
39
+ // Make sure CSS is properly processed
40
+ cssCodeSplit: true
41
+ },
42
+
43
+ // CSS preprocessing
44
+ css: {
45
+ postcss: './postcss.config.js',
46
+ // Include module CSS as well as global CSS
47
+ modules: {
48
+ scopeBehaviour: 'local',
49
+ localsConvention: 'camelCaseOnly',
26
50
  }
27
51
  },
28
52
 
29
53
  // Resolve aliases
30
54
  resolve: {
31
55
  alias: {
32
- '@': resolve(__dirname, 'src')
33
- }
56
+ '@': resolve(__dirname, 'src'),
57
+ '@components': resolve(__dirname, 'src/components'),
58
+ '@pages': resolve(__dirname, 'src/pages'),
59
+ '@utils': resolve(__dirname, 'src/utils')
60
+ },
61
+ extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
34
62
  },
35
63
 
36
64
  // Optimize dependencies
37
65
  optimizeDeps: {
38
66
  include: ['frontend-hamroun']
39
- }
67
+ },
68
+
69
+ // This is a critical plugin for SPA routing - much simpler than before
70
+ plugins: [{
71
+ name: 'spa-fallback',
72
+ configureServer(server) {
73
+ return () => {
74
+ server.middlewares.use((req, res, next) => {
75
+ if (req.url.includes('.') || req.url.startsWith('/api/') || req.url.startsWith('/socket.io/')) {
76
+ next();
77
+ return;
78
+ }
79
+
80
+ console.log(`[SPA] Handling route: ${req.url}`);
81
+ req.url = '/'; // Rewrite all routes to root
82
+ next();
83
+ });
84
+ };
85
+ }
86
+ }]
40
87
  });
@@ -26,6 +26,14 @@ This template demonstrates:
26
26
  - Data fetching during server rendering
27
27
  - AI-powered meta tag generation
28
28
 
29
+ ### Automatic File-Based Routing
30
+ - Pages are automatically rendered based on their file path in the `pages` directory
31
+ - For example:
32
+ - `/pages/index.js` → `/` route
33
+ - `/pages/about.js` → `/about` route
34
+ - `/pages/blog/index.js` → `/blog` route
35
+ - `/pages/users/[id].js` → `/users/:id` dynamic route
36
+
29
37
  ### API Integration
30
38
  - RESTful API endpoints
31
39
  - Dynamic API routing
@@ -133,6 +141,48 @@ app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
133
141
  app.use(errorHandler);
134
142
  ```
135
143
 
144
+ ## File-Based Routing Examples
145
+
146
+ ### Static Routes
147
+ Create files in the `pages` directory:
148
+
149
+ ```jsx
150
+ // pages/index.js - Maps to "/"
151
+ export default function HomePage() {
152
+ return <h1>Home Page</h1>;
153
+ }
154
+
155
+ // pages/about.js - Maps to "/about"
156
+ export default function AboutPage() {
157
+ return <h1>About Us</h1>;
158
+ }
159
+
160
+ // pages/contact/index.js - Maps to "/contact"
161
+ export default function ContactPage() {
162
+ return <h1>Contact Us</h1>;
163
+ }
164
+ ```
165
+
166
+ ### Dynamic Routes
167
+ Use brackets in filenames to define dynamic parameters:
168
+
169
+ ```jsx
170
+ // pages/users/[id].js - Maps to "/users/:id"
171
+ export default function UserPage({ params }) {
172
+ return <h1>User Profile: {params.id}</h1>;
173
+ }
174
+
175
+ // pages/blog/[category]/[slug].js - Maps to "/blog/:category/:slug"
176
+ export default function BlogPost({ params }) {
177
+ return (
178
+ <div>
179
+ <h1>Blog Post: {params.slug}</h1>
180
+ <p>Category: {params.category}</p>
181
+ </div>
182
+ );
183
+ }
184
+ ```
185
+
136
186
  ## Next Steps
137
187
 
138
188
  Explore the full API documentation for more advanced features and customization options.
@@ -1,29 +1,61 @@
1
- import { hydrate } from 'frontend-hamroun';
1
+ import { hydrate, jsx } from 'frontend-hamroun';
2
2
 
3
3
  // Dynamically import the appropriate page component
4
4
  async function hydratePage() {
5
5
  try {
6
- // Get current path
7
- const path = window.location.pathname === '/' ? '/index' : window.location.pathname;
6
+ // Get initial state from server
7
+ const initialState = window.__INITIAL_STATE__ || {};
8
8
 
9
- // Dynamically import the component
10
- const module = await import(`./pages${path}.js`);
11
- const PageComponent = module.default;
9
+ // Get current path
10
+ const path = initialState.route || window.location.pathname;
11
+ const normalizedPath = path === '/' ? '/index' : path;
12
12
 
13
- // Find the root element
14
- const rootElement = document.getElementById('app');
13
+ // Create path to module
14
+ const modulePath = `.${normalizedPath.replace(/\/$/, '')}.js`;
15
15
 
16
- if (rootElement && PageComponent) {
17
- // Hydrate the application
18
- hydrate(PageComponent(), rootElement);
19
- console.log('Hydration complete');
20
- } else {
21
- console.error('Could not find root element or page component');
16
+ try {
17
+ // Dynamically import the component
18
+ const module = await import(`./pages${normalizedPath}.js`).catch(() =>
19
+ import(`./pages${normalizedPath}/index.js`));
20
+
21
+ const PageComponent = module.default;
22
+
23
+ // Find the root element
24
+ const rootElement = document.getElementById('root');
25
+
26
+ if (rootElement && PageComponent) {
27
+ // Hydrate the application with the same params from the server
28
+ hydrate(jsx(PageComponent, { params: initialState.params || {} }), rootElement);
29
+ console.log('Hydration complete');
30
+ } else {
31
+ console.error('Could not find root element or page component');
32
+ }
33
+ } catch (importError) {
34
+ console.error('Error importing page component:', importError);
35
+
36
+ // Fallback to App component if available
37
+ try {
38
+ const { App } = await import('./App.js');
39
+ const rootElement = document.getElementById('root');
40
+
41
+ if (rootElement && App) {
42
+ hydrate(jsx(App, {}), rootElement);
43
+ console.log('Fallback hydration complete');
44
+ }
45
+ } catch (fallbackError) {
46
+ console.error('Fallback hydration failed:', fallbackError);
47
+ }
22
48
  }
23
49
  } catch (error) {
24
50
  console.error('Hydration error:', error);
25
51
  }
26
52
  }
27
53
 
54
+ // Add global variable for JSX
55
+ window.jsx = jsx;
56
+
28
57
  // Hydrate when DOM is ready
29
58
  document.addEventListener('DOMContentLoaded', hydratePage);
59
+
60
+ // Handle client-side navigation (if implemented)
61
+ window.addEventListener('popstate', hydratePage);