@flexireact/core 1.0.2 → 2.0.0
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 +112 -112
- package/bin/flexireact.js +23 -0
- package/cli/index.ts +9 -21
- package/core/cli/{logger.js → logger.ts} +8 -2
- package/core/client/{hydration.js → hydration.ts} +10 -0
- package/core/client/{islands.js → islands.ts} +6 -1
- package/core/client/{navigation.js → navigation.ts} +10 -2
- package/core/client/{runtime.js → runtime.ts} +16 -0
- package/core/{index.js → index.ts} +2 -1
- package/core/islands/{index.js → index.ts} +16 -4
- package/core/{logger.js → logger.ts} +1 -1
- package/core/middleware/{index.js → index.ts} +32 -9
- package/core/plugins/{index.js → index.ts} +9 -6
- package/core/render/index.ts +1069 -0
- package/core/{render.js → render.ts} +7 -5
- package/core/router/index.ts +543 -0
- package/core/rsc/{index.js → index.ts} +6 -5
- package/core/server/{index.js → index.ts} +25 -6
- package/core/{server.js → server.ts} +8 -2
- package/core/ssg/{index.js → index.ts} +30 -5
- package/core/start-dev.ts +6 -0
- package/core/start-prod.ts +6 -0
- package/core/tsconfig.json +28 -0
- package/core/types.ts +239 -0
- package/package.json +19 -14
- package/cli/index.js +0 -992
- package/core/render/index.js +0 -773
- package/core/router/index.js +0 -296
- /package/core/{api.js → api.ts} +0 -0
- /package/core/build/{index.js → index.ts} +0 -0
- /package/core/client/{index.js → index.ts} +0 -0
- /package/core/{config.js → config.ts} +0 -0
- /package/core/{context.js → context.ts} +0 -0
- /package/core/{dev.js → dev.ts} +0 -0
- /package/core/{loader.js → loader.ts} +0 -0
- /package/core/{router.js → router.ts} +0 -0
- /package/core/{utils.js → utils.ts} +0 -0
package/README.md
CHANGED
|
@@ -2,24 +2,33 @@
|
|
|
2
2
|
<img src="./assets/flexireact.webp" alt="FlexiReact Logo" width="400" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">FlexiReact</h1>
|
|
5
|
+
<h1 align="center">FlexiReact v2</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>The Modern React Framework</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
A blazing-fast React framework with TypeScript, Tailwind CSS, SSR, SSG, Islands architecture, and file-based routing.<br/>
|
|
12
|
+
A blazing-fast React framework with TypeScript, Tailwind CSS v4, SSR, SSG, Islands architecture, and file-based routing.<br/>
|
|
13
13
|
Inspired by Next.js, Remix, Astro, and TanStack Start — but simpler and lighter.
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<a href="https://www.npmjs.com/package/@flexireact/core"><img src="https://img.shields.io/npm/v/@flexireact/core.svg" alt="npm version" /></a>
|
|
18
18
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
19
|
-
<a href="#"><img src="https://img.shields.io/badge/TypeScript-
|
|
20
|
-
<a href="#"><img src="https://img.shields.io/badge/Tailwind-
|
|
19
|
+
<a href="#"><img src="https://img.shields.io/badge/TypeScript-Native-blue.svg" alt="TypeScript Native" /></a>
|
|
20
|
+
<a href="#"><img src="https://img.shields.io/badge/Tailwind-v4-38B2AC.svg" alt="Tailwind CSS v4" /></a>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
|
+
## 🆕 What's New in v2
|
|
24
|
+
|
|
25
|
+
- **TypeScript Native** — Core rewritten in TypeScript for better DX
|
|
26
|
+
- **Tailwind CSS v4** — New `@import "tailwindcss"` and `@theme` syntax
|
|
27
|
+
- **Routes Directory** — New `routes/` directory with route groups, dynamic segments
|
|
28
|
+
- **Modern 404 Page** — Beautiful, interactive error pages
|
|
29
|
+
- **Enhanced DevTools** — Precise error messages with color-coded render times
|
|
30
|
+
- **Improved CLI** — TypeScript-based CLI with better templates
|
|
31
|
+
|
|
23
32
|
## ✨ Features
|
|
24
33
|
|
|
25
34
|
### 🏗️ Core Framework
|
|
@@ -156,51 +165,73 @@ npm run start
|
|
|
156
165
|
|
|
157
166
|
Open http://localhost:3000
|
|
158
167
|
|
|
159
|
-
## 📁 Project Structure
|
|
168
|
+
## 📁 Project Structure (v2)
|
|
169
|
+
|
|
170
|
+
FlexiReact v2 introduces a new `routes/` directory with enhanced routing capabilities:
|
|
160
171
|
|
|
161
172
|
```
|
|
162
173
|
myapp/
|
|
163
|
-
├── app/
|
|
164
|
-
│ ├── components/
|
|
165
|
-
│ │ ├──
|
|
166
|
-
│ │
|
|
167
|
-
│ │ ├── Navbar.tsx # Navigation bar
|
|
168
|
-
│ │ └── index.ts # Component exports
|
|
174
|
+
├── app/ # App directory (layout, components, styles)
|
|
175
|
+
│ ├── components/
|
|
176
|
+
│ │ ├── ui/ # UI components (Button, Card, etc.)
|
|
177
|
+
│ │ └── layout/ # Layout components (Navbar, Footer)
|
|
169
178
|
│ ├── styles/
|
|
170
|
-
│ │ └── globals.css
|
|
171
|
-
│
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
│ ├──
|
|
179
|
+
│ │ └── globals.css # Global styles + Tailwind v4
|
|
180
|
+
│ ├── providers/ # React context providers
|
|
181
|
+
│ └── layout.tsx # Root layout
|
|
182
|
+
├── routes/ # FlexiReact v2 file-based routing
|
|
183
|
+
│ ├── (public)/ # Route groups (don't affect URL)
|
|
184
|
+
│ │ ├── home.tsx # → /
|
|
185
|
+
│ │ └── about.tsx # → /about
|
|
175
186
|
│ ├── blog/
|
|
176
|
-
│ │ ├── index.tsx
|
|
177
|
-
│ │ └── [slug].tsx
|
|
187
|
+
│ │ ├── index.tsx # → /blog
|
|
188
|
+
│ │ └── [slug].tsx # → /blog/:slug
|
|
178
189
|
│ └── api/
|
|
179
|
-
│ └── hello.ts
|
|
180
|
-
├──
|
|
181
|
-
|
|
182
|
-
├──
|
|
183
|
-
├──
|
|
190
|
+
│ └── hello.ts # → /api/hello
|
|
191
|
+
├── lib/ # Utilities
|
|
192
|
+
│ └── utils.ts
|
|
193
|
+
├── public/ # Static assets
|
|
194
|
+
├── tsconfig.json # TypeScript configuration
|
|
195
|
+
├── flexireact.config.ts # FlexiReact configuration
|
|
184
196
|
└── package.json
|
|
185
197
|
```
|
|
186
198
|
|
|
187
|
-
## 🛣️ Routing
|
|
199
|
+
## 🛣️ Routing (v2)
|
|
200
|
+
|
|
201
|
+
FlexiReact v2 supports three routing conventions (in priority order):
|
|
202
|
+
|
|
203
|
+
### 1. Routes Directory (Recommended)
|
|
204
|
+
|
|
205
|
+
| File | Route |
|
|
206
|
+
|------|-------|
|
|
207
|
+
| `routes/(public)/home.tsx` | `/` |
|
|
208
|
+
| `routes/(public)/about.tsx` | `/about` |
|
|
209
|
+
| `routes/blog/index.tsx` | `/blog` |
|
|
210
|
+
| `routes/blog/[slug].tsx` | `/blog/:slug` |
|
|
211
|
+
| `routes/[...path].tsx` | Catch-all route |
|
|
212
|
+
| `routes/api/hello.ts` | `/api/hello` |
|
|
213
|
+
|
|
214
|
+
### 2. App Directory (Next.js style)
|
|
215
|
+
|
|
216
|
+
| File | Route |
|
|
217
|
+
|------|-------|
|
|
218
|
+
| `app/page.tsx` | `/` |
|
|
219
|
+
| `app/about/page.tsx` | `/about` |
|
|
220
|
+
| `app/blog/[slug]/page.tsx` | `/blog/:slug` |
|
|
188
221
|
|
|
189
|
-
###
|
|
222
|
+
### 3. Pages Directory (Legacy)
|
|
190
223
|
|
|
191
224
|
| File | Route |
|
|
192
225
|
|------|-------|
|
|
193
|
-
| `pages/index.
|
|
194
|
-
| `pages/about.
|
|
195
|
-
| `pages/blog/[slug].jsx` | `/blog/:slug` |
|
|
196
|
-
| `pages/[...path].jsx` | Catch-all route |
|
|
226
|
+
| `pages/index.tsx` | `/` |
|
|
227
|
+
| `pages/about.tsx` | `/about` |
|
|
197
228
|
|
|
198
229
|
### Dynamic Routes
|
|
199
230
|
|
|
200
|
-
```
|
|
201
|
-
//
|
|
202
|
-
export default function
|
|
203
|
-
return <h1>
|
|
231
|
+
```tsx
|
|
232
|
+
// routes/blog/[slug].tsx
|
|
233
|
+
export default function BlogPost({ params }: { params: { slug: string } }) {
|
|
234
|
+
return <h1>Post: {params.slug}</h1>;
|
|
204
235
|
}
|
|
205
236
|
```
|
|
206
237
|
|
|
@@ -209,42 +240,62 @@ export default function User({ params }) {
|
|
|
209
240
|
Use parentheses to group routes without affecting the URL:
|
|
210
241
|
|
|
211
242
|
```
|
|
212
|
-
|
|
213
|
-
(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
(
|
|
217
|
-
|
|
243
|
+
routes/
|
|
244
|
+
(public)/
|
|
245
|
+
home.tsx # → /
|
|
246
|
+
about.tsx # → /about
|
|
247
|
+
(dashboard)/
|
|
248
|
+
settings.tsx # → /settings
|
|
218
249
|
```
|
|
219
250
|
|
|
220
251
|
## 📐 Layouts
|
|
221
252
|
|
|
222
|
-
Create
|
|
253
|
+
Create layouts in `app/layout.tsx` or within route directories:
|
|
223
254
|
|
|
224
|
-
```
|
|
225
|
-
//
|
|
226
|
-
|
|
255
|
+
```tsx
|
|
256
|
+
// app/layout.tsx
|
|
257
|
+
import { Navbar } from './components/layout/Navbar';
|
|
258
|
+
import { Footer } from './components/layout/Footer';
|
|
259
|
+
|
|
260
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
227
261
|
return (
|
|
228
|
-
<
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
262
|
+
<html lang="en" className="dark">
|
|
263
|
+
<head>
|
|
264
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
265
|
+
</head>
|
|
266
|
+
<body className="bg-background text-foreground">
|
|
267
|
+
<Navbar />
|
|
268
|
+
<main>{children}</main>
|
|
269
|
+
<Footer />
|
|
270
|
+
</body>
|
|
271
|
+
</html>
|
|
233
272
|
);
|
|
234
273
|
}
|
|
235
274
|
```
|
|
236
275
|
|
|
237
276
|
## ⏳ Loading & Error States
|
|
238
277
|
|
|
239
|
-
```
|
|
240
|
-
//
|
|
278
|
+
```tsx
|
|
279
|
+
// routes/loading.tsx
|
|
241
280
|
export default function Loading() {
|
|
242
|
-
return
|
|
281
|
+
return (
|
|
282
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
283
|
+
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
243
286
|
}
|
|
244
287
|
|
|
245
|
-
//
|
|
246
|
-
export default function Error({ error }) {
|
|
247
|
-
return
|
|
288
|
+
// routes/error.tsx
|
|
289
|
+
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
290
|
+
return (
|
|
291
|
+
<div className="flex flex-col items-center justify-center min-h-screen">
|
|
292
|
+
<h1 className="text-4xl font-bold text-red-500">Something went wrong</h1>
|
|
293
|
+
<p className="text-gray-400 mt-4">{error.message}</p>
|
|
294
|
+
<button onClick={reset} className="mt-8 px-6 py-3 bg-primary text-black rounded-lg">
|
|
295
|
+
Try again
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
248
299
|
}
|
|
249
300
|
```
|
|
250
301
|
|
|
@@ -480,63 +531,6 @@ Islands provide partial hydration:
|
|
|
480
531
|
- **Better performance** — less code to parse/execute
|
|
481
532
|
- **Selective loading** — hydrate on visibility, interaction, etc.
|
|
482
533
|
|
|
483
|
-
## 🎨 FlexiUI - Official UI Library
|
|
484
|
-
|
|
485
|
-
FlexiReact comes with an official UI component library: **@flexireact/flexi-ui**
|
|
486
|
-
|
|
487
|
-
```bash
|
|
488
|
-
npm install @flexireact/flexi-ui
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
### Features
|
|
492
|
-
- 🌙 **Dark-first design** with neon emerald accents
|
|
493
|
-
- ♿ **Fully accessible** (ARIA-compliant, Radix UI primitives)
|
|
494
|
-
- 🎯 **TypeScript native** with full type safety
|
|
495
|
-
- 🌳 **Tree-shakeable** — import only what you need
|
|
496
|
-
- ⚡ **SSR ready** — works with FlexiReact SSR
|
|
497
|
-
|
|
498
|
-
### Quick Setup
|
|
499
|
-
|
|
500
|
-
```js
|
|
501
|
-
// tailwind.config.js
|
|
502
|
-
const { flexiUIPlugin } = require('@flexireact/flexi-ui/tailwind');
|
|
503
|
-
|
|
504
|
-
module.exports = {
|
|
505
|
-
darkMode: 'class',
|
|
506
|
-
content: [
|
|
507
|
-
'./pages/**/*.{js,ts,jsx,tsx}',
|
|
508
|
-
'./node_modules/@flexireact/flexi-ui/dist/**/*.js',
|
|
509
|
-
],
|
|
510
|
-
plugins: [flexiUIPlugin],
|
|
511
|
-
};
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### Usage
|
|
515
|
-
|
|
516
|
-
```jsx
|
|
517
|
-
import { Button, Card, Badge, Input } from '@flexireact/flexi-ui';
|
|
518
|
-
|
|
519
|
-
export default function MyPage() {
|
|
520
|
-
return (
|
|
521
|
-
<Card>
|
|
522
|
-
<Badge variant="success">New</Badge>
|
|
523
|
-
<h2>Welcome!</h2>
|
|
524
|
-
<Input placeholder="Enter your email" />
|
|
525
|
-
<Button>Get Started</Button>
|
|
526
|
-
</Card>
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
### Available Components
|
|
532
|
-
- **Core**: Button, Input, Textarea, Checkbox, Switch, Select
|
|
533
|
-
- **Display**: Card, Badge, Avatar, Tooltip
|
|
534
|
-
- **Feedback**: Alert, Toast, Spinner, Skeleton, Progress
|
|
535
|
-
- **Overlay**: Modal, Drawer, Dropdown
|
|
536
|
-
- **Layout**: Separator, Tabs
|
|
537
|
-
|
|
538
|
-
📖 [FlexiUI Documentation](https://github.com/flexireact/flexi-ui)
|
|
539
|
-
|
|
540
534
|
---
|
|
541
535
|
|
|
542
536
|
## 📋 Requirements
|
|
@@ -544,7 +538,13 @@ export default function MyPage() {
|
|
|
544
538
|
- Node.js 18+
|
|
545
539
|
- React 18+
|
|
546
540
|
|
|
541
|
+
## 🔗 Links
|
|
542
|
+
|
|
543
|
+
- [GitHub Repository](https://github.com/flexireact/flexireact)
|
|
544
|
+
- [npm Package](https://www.npmjs.com/package/@flexireact/core)
|
|
545
|
+
- [Issues](https://github.com/flexireact/flexireact/issues)
|
|
546
|
+
|
|
547
547
|
## 📄 License
|
|
548
548
|
|
|
549
|
-
MIT
|
|
549
|
+
MIT © [FlexiReact Team](https://github.com/flexireact)
|
|
550
550
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FlexiReact CLI Entry Point
|
|
5
|
+
* Uses tsx to run TypeScript CLI directly
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from 'child_process';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const cliPath = join(__dirname, '..', 'cli', 'index.ts');
|
|
16
|
+
|
|
17
|
+
// Run the CLI with tsx
|
|
18
|
+
const result = spawnSync('npx', ['tsx', cliPath, ...process.argv.slice(2)], {
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
shell: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
process.exit(result.status ?? 0);
|
package/cli/index.ts
CHANGED
|
@@ -112,7 +112,7 @@ async function createProject(projectName?: string): Promise<void> {
|
|
|
112
112
|
name: 'projectName',
|
|
113
113
|
message: 'Project name:',
|
|
114
114
|
initial: 'my-flexi-app',
|
|
115
|
-
validate: (value) => value.length > 0 || 'Project name is required'
|
|
115
|
+
validate: (value: string) => value.length > 0 || 'Project name is required'
|
|
116
116
|
});
|
|
117
117
|
name = response.projectName;
|
|
118
118
|
if (!name) process.exit(1);
|
|
@@ -830,21 +830,15 @@ ${pc.cyan(' │')} ${pc.cyan('│')}
|
|
|
830
830
|
${pc.cyan(' ╰─────────────────────────────────────────╯')}
|
|
831
831
|
`);
|
|
832
832
|
|
|
833
|
-
const
|
|
834
|
-
const serverPath = path.join(__dirname, '..', 'core', 'server', 'index.js');
|
|
835
|
-
const loaderUrl = pathToFileURL(loaderPath).href;
|
|
833
|
+
const startDevPath = path.join(__dirname, '..', 'core', 'start-dev.ts');
|
|
836
834
|
|
|
837
835
|
const child = spawn(
|
|
838
|
-
|
|
839
|
-
[
|
|
840
|
-
'--import',
|
|
841
|
-
`data:text/javascript,import { register } from 'node:module'; register('${loaderUrl}', import.meta.url);`,
|
|
842
|
-
'-e',
|
|
843
|
-
`import('${pathToFileURL(serverPath).href}').then(m => m.createServer({ mode: 'development' }))`
|
|
844
|
-
],
|
|
836
|
+
'npx',
|
|
837
|
+
['tsx', startDevPath],
|
|
845
838
|
{
|
|
846
839
|
stdio: 'inherit',
|
|
847
840
|
cwd: process.cwd(),
|
|
841
|
+
shell: true,
|
|
848
842
|
env: { ...process.env, NODE_ENV: 'development', FORCE_COLOR: '1' }
|
|
849
843
|
}
|
|
850
844
|
);
|
|
@@ -909,21 +903,15 @@ async function runStart(): Promise<void> {
|
|
|
909
903
|
log.info('Starting production server...');
|
|
910
904
|
log.blank();
|
|
911
905
|
|
|
912
|
-
const
|
|
913
|
-
const serverPath = path.join(__dirname, '..', 'core', 'server', 'index.js');
|
|
914
|
-
const loaderUrl = pathToFileURL(loaderPath).href;
|
|
906
|
+
const startProdPath = path.join(__dirname, '..', 'core', 'start-prod.ts');
|
|
915
907
|
|
|
916
908
|
const child = spawn(
|
|
917
|
-
|
|
918
|
-
[
|
|
919
|
-
'--import',
|
|
920
|
-
`data:text/javascript,import { register } from 'node:module'; register('${loaderUrl}', import.meta.url);`,
|
|
921
|
-
'-e',
|
|
922
|
-
`import('${pathToFileURL(serverPath).href}').then(m => m.createServer({ mode: 'production' }))`
|
|
923
|
-
],
|
|
909
|
+
'npx',
|
|
910
|
+
['tsx', startProdPath],
|
|
924
911
|
{
|
|
925
912
|
stdio: 'inherit',
|
|
926
913
|
cwd: process.cwd(),
|
|
914
|
+
shell: true,
|
|
927
915
|
env: { ...process.env, NODE_ENV: 'production' }
|
|
928
916
|
}
|
|
929
917
|
);
|
|
@@ -65,7 +65,13 @@ const box = {
|
|
|
65
65
|
verticalLeft: '┤',
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
interface BoxOptions {
|
|
69
|
+
padding?: number;
|
|
70
|
+
borderColor?: (s: string) => string;
|
|
71
|
+
width?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createBox(content: string, options: BoxOptions = {}) {
|
|
69
75
|
const {
|
|
70
76
|
padding = 1,
|
|
71
77
|
borderColor = colors.primary,
|
|
@@ -180,7 +186,7 @@ function pluginLoader(plugins = []) {
|
|
|
180
186
|
// HTTP Request Logger
|
|
181
187
|
// ============================================================================
|
|
182
188
|
|
|
183
|
-
function request(method, path, statusCode, duration, options = {}) {
|
|
189
|
+
function request(method: string, path: string, statusCode: number, duration: number, options: { type?: string } = {}) {
|
|
184
190
|
const { type = 'dynamic' } = options;
|
|
185
191
|
|
|
186
192
|
// Method colors
|
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
8
8
|
|
|
9
|
+
// Extend Window interface for __FLEXI_DATA__
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
__FLEXI_DATA__?: {
|
|
13
|
+
islands?: any[];
|
|
14
|
+
props?: Record<string, any>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
20
|
* Hydrates a specific island component
|
|
11
21
|
*/
|
|
@@ -40,7 +40,12 @@ export function IslandBoundary({ children, fallback = null, name = 'island' }) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Creates a lazy-loaded island
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
interface LazyIslandOptions {
|
|
44
|
+
fallback?: React.ReactNode;
|
|
45
|
+
name?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createClientIsland(loader: () => Promise<any>, options: LazyIslandOptions = {}) {
|
|
44
49
|
const { fallback = null, name = 'lazy-island' } = options;
|
|
45
50
|
|
|
46
51
|
return function LazyIsland(props) {
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
8
|
// Navigation state
|
|
9
|
-
const navigationState
|
|
9
|
+
const navigationState: {
|
|
10
|
+
listeners: Set<(url: string) => void>;
|
|
11
|
+
prefetched: Set<string>;
|
|
12
|
+
} = {
|
|
10
13
|
listeners: new Set(),
|
|
11
14
|
prefetched: new Set()
|
|
12
15
|
};
|
|
@@ -14,7 +17,12 @@ const navigationState = {
|
|
|
14
17
|
/**
|
|
15
18
|
* Navigates to a new URL
|
|
16
19
|
*/
|
|
17
|
-
|
|
20
|
+
interface NavigateOptions {
|
|
21
|
+
replace?: boolean;
|
|
22
|
+
scroll?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function navigate(url: string, options: NavigateOptions = {}) {
|
|
18
26
|
const { replace = false, scroll = true } = options;
|
|
19
27
|
|
|
20
28
|
if (replace) {
|
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
import { hydrateAllIslands, setupProgressiveHydration } from './hydration.js';
|
|
7
7
|
import { navigate, prefetch } from './navigation.js';
|
|
8
8
|
|
|
9
|
+
// Extend Window interface
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
FlexiReact: {
|
|
13
|
+
navigate: typeof navigate;
|
|
14
|
+
prefetch: typeof prefetch;
|
|
15
|
+
hydrateAllIslands: typeof hydrateAllIslands;
|
|
16
|
+
setupProgressiveHydration: typeof setupProgressiveHydration;
|
|
17
|
+
};
|
|
18
|
+
__FLEXI_DATA__?: {
|
|
19
|
+
islands?: any[];
|
|
20
|
+
props?: Record<string, any>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
// Expose to global scope
|
|
10
26
|
window.FlexiReact = {
|
|
11
27
|
navigate,
|
|
@@ -15,7 +15,8 @@ export { buildRouteTree, matchRoute, findRouteLayouts, RouteType } from './route
|
|
|
15
15
|
export { renderPage, renderError, renderLoading } from './render/index.js';
|
|
16
16
|
|
|
17
17
|
// Server
|
|
18
|
-
|
|
18
|
+
import { createServer } from './server/index.js';
|
|
19
|
+
export { createServer };
|
|
19
20
|
|
|
20
21
|
// Build
|
|
21
22
|
export { build, buildDev, BuildMode } from './build/index.js';
|
|
@@ -63,8 +63,13 @@ export function getRegisteredIslands() {
|
|
|
63
63
|
/**
|
|
64
64
|
* Creates an island component wrapper
|
|
65
65
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
interface IslandOptions {
|
|
67
|
+
name?: string;
|
|
68
|
+
clientPath?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createIsland(Component: React.ComponentType<any>, options: IslandOptions = {}) {
|
|
72
|
+
const { name = (Component as any).name || 'Island', clientPath } = options;
|
|
68
73
|
|
|
69
74
|
function IslandWrapper(props) {
|
|
70
75
|
return Island({
|
|
@@ -148,9 +153,16 @@ export const LoadStrategy = {
|
|
|
148
153
|
/**
|
|
149
154
|
* Creates a lazy island that hydrates based on strategy
|
|
150
155
|
*/
|
|
151
|
-
|
|
156
|
+
interface LazyIslandOptions {
|
|
157
|
+
name?: string;
|
|
158
|
+
clientPath?: string;
|
|
159
|
+
strategy?: string;
|
|
160
|
+
media?: string | null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function createLazyIsland(Component: React.ComponentType<any>, options: LazyIslandOptions = {}) {
|
|
152
164
|
const {
|
|
153
|
-
name = Component.name || 'LazyIsland',
|
|
165
|
+
name = (Component as any).name || 'LazyIsland',
|
|
154
166
|
clientPath,
|
|
155
167
|
strategy = LoadStrategy.VISIBLE,
|
|
156
168
|
media = null
|
|
@@ -122,7 +122,7 @@ export const logger = {
|
|
|
122
122
|
},
|
|
123
123
|
|
|
124
124
|
// HTTP request log - Compact single line like Next.js
|
|
125
|
-
request(method, path, status, time, extra = {}) {
|
|
125
|
+
request(method: string, path: string, status: number, time: number, extra: { type?: string } = {}) {
|
|
126
126
|
const methodColor = getMethodColor(method);
|
|
127
127
|
const statusColor = getStatusColor(status);
|
|
128
128
|
const timeStr = formatTime(time);
|
|
@@ -29,8 +29,22 @@ import { pathToFileURL } from 'url';
|
|
|
29
29
|
/**
|
|
30
30
|
* Middleware response helpers
|
|
31
31
|
*/
|
|
32
|
+
interface MiddlewareResponseOptions {
|
|
33
|
+
type?: string;
|
|
34
|
+
status?: number;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
body?: any;
|
|
37
|
+
url?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
export class MiddlewareResponse {
|
|
33
|
-
|
|
41
|
+
type: string;
|
|
42
|
+
status: number;
|
|
43
|
+
headers: Map<string, string>;
|
|
44
|
+
body: any;
|
|
45
|
+
url: string | null;
|
|
46
|
+
|
|
47
|
+
constructor(options: MiddlewareResponseOptions = {}) {
|
|
34
48
|
this.type = options.type || 'next';
|
|
35
49
|
this.status = options.status || 200;
|
|
36
50
|
this.headers = new Map(Object.entries(options.headers || {}));
|
|
@@ -69,7 +83,7 @@ export class MiddlewareResponse {
|
|
|
69
83
|
/**
|
|
70
84
|
* Return a JSON response
|
|
71
85
|
*/
|
|
72
|
-
static json(data, options = {}) {
|
|
86
|
+
static json(data: any, options: { status?: number; headers?: Record<string, string> } = {}) {
|
|
73
87
|
return new MiddlewareResponse({
|
|
74
88
|
type: 'response',
|
|
75
89
|
status: options.status || 200,
|
|
@@ -81,7 +95,7 @@ export class MiddlewareResponse {
|
|
|
81
95
|
/**
|
|
82
96
|
* Return an HTML response
|
|
83
97
|
*/
|
|
84
|
-
static html(content, options = {}) {
|
|
98
|
+
static html(content: string, options: { status?: number; headers?: Record<string, string> } = {}) {
|
|
85
99
|
return new MiddlewareResponse({
|
|
86
100
|
type: 'response',
|
|
87
101
|
status: options.status || 200,
|
|
@@ -95,7 +109,16 @@ export class MiddlewareResponse {
|
|
|
95
109
|
* Middleware request wrapper
|
|
96
110
|
*/
|
|
97
111
|
export class MiddlewareRequest {
|
|
98
|
-
|
|
112
|
+
raw: any;
|
|
113
|
+
method: string;
|
|
114
|
+
url: string;
|
|
115
|
+
headers: Map<string, string>;
|
|
116
|
+
pathname: string;
|
|
117
|
+
searchParams: URLSearchParams;
|
|
118
|
+
query: Record<string, string>;
|
|
119
|
+
cookies: Map<string, string>;
|
|
120
|
+
|
|
121
|
+
constructor(req: any) {
|
|
99
122
|
this.raw = req;
|
|
100
123
|
this.method = req.method;
|
|
101
124
|
this.url = req.url;
|
|
@@ -105,7 +128,7 @@ export class MiddlewareRequest {
|
|
|
105
128
|
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
106
129
|
this.pathname = parsedUrl.pathname;
|
|
107
130
|
this.searchParams = parsedUrl.searchParams;
|
|
108
|
-
this.query = Object.fromEntries(parsedUrl.searchParams)
|
|
131
|
+
this.query = Object.fromEntries(parsedUrl.searchParams) as Record<string, string>;
|
|
109
132
|
|
|
110
133
|
// Parse cookies
|
|
111
134
|
this.cookies = this._parseCookies(req.headers.cookie || '');
|
|
@@ -274,7 +297,7 @@ export const middlewares = {
|
|
|
274
297
|
/**
|
|
275
298
|
* CORS middleware
|
|
276
299
|
*/
|
|
277
|
-
cors(options = {}) {
|
|
300
|
+
cors(options: { origin?: string; methods?: string; headers?: string; credentials?: boolean } = {}) {
|
|
278
301
|
const {
|
|
279
302
|
origin = '*',
|
|
280
303
|
methods = 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
@@ -294,7 +317,7 @@ export const middlewares = {
|
|
|
294
317
|
|
|
295
318
|
// Handle preflight
|
|
296
319
|
if (request.method === 'OPTIONS') {
|
|
297
|
-
return MiddlewareResponse.json({}, { status: 204, headers: response.headers });
|
|
320
|
+
return MiddlewareResponse.json({}, { status: 204, headers: Object.fromEntries(response.headers) });
|
|
298
321
|
}
|
|
299
322
|
|
|
300
323
|
return response;
|
|
@@ -330,7 +353,7 @@ export const middlewares = {
|
|
|
330
353
|
/**
|
|
331
354
|
* Rate limiting middleware
|
|
332
355
|
*/
|
|
333
|
-
rateLimit(options = {}) {
|
|
356
|
+
rateLimit(options: { windowMs?: number; max?: number } = {}) {
|
|
334
357
|
const { windowMs = 60000, max = 100 } = options;
|
|
335
358
|
const requests = new Map();
|
|
336
359
|
|
|
@@ -369,7 +392,7 @@ export const middlewares = {
|
|
|
369
392
|
/**
|
|
370
393
|
* Logging middleware
|
|
371
394
|
*/
|
|
372
|
-
logger(options = {}) {
|
|
395
|
+
logger(options: { format?: string } = {}) {
|
|
373
396
|
const { format = 'combined' } = options;
|
|
374
397
|
|
|
375
398
|
return (request) => {
|