@lolyjs/core 0.2.0-alpha.27 → 0.2.0-alpha.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.
- package/README.md +321 -0
- package/dist/cli.cjs +904 -315
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +898 -309
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +916 -316
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +38 -3
- package/dist/index.d.ts +38 -3
- package/dist/index.js +910 -310
- package/dist/index.js.map +1 -1
- package/dist/{index.types-DMOO-uvF.d.mts → index.types-B9j4OQft.d.mts} +1 -0
- package/dist/{index.types-DMOO-uvF.d.ts → index.types-B9j4OQft.d.ts} +1 -0
- package/dist/react/cache.cjs.map +1 -1
- package/dist/react/cache.d.mts +3 -1
- package/dist/react/cache.d.ts +3 -1
- package/dist/react/cache.js.map +1 -1
- package/dist/react/components.cjs +1 -4
- package/dist/react/components.cjs.map +1 -1
- package/dist/react/components.js +1 -4
- package/dist/react/components.js.map +1 -1
- package/dist/runtime.cjs +16 -5
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +16 -5
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -342,6 +342,327 @@ Layouts can have their own server hooks that provide stable data across all page
|
|
|
342
342
|
- Layout server hooks: `app/layout.server.hook.ts` (same directory as `layout.tsx`)
|
|
343
343
|
- Page server hooks: `app/page.server.hook.ts` (preferred) or `app/server.hook.ts` (legacy, backward compatible)
|
|
344
344
|
|
|
345
|
+
### 📦 Route Groups
|
|
346
|
+
|
|
347
|
+
Route groups allow you to organize routes without affecting the URL structure. Directories wrapped in parentheses like `(dashboard)` or `(landing)` are treated as route groups and don't appear in the URL.
|
|
348
|
+
|
|
349
|
+
**Key Features:**
|
|
350
|
+
- Route groups don't appear in URLs
|
|
351
|
+
- Each route group can have its own layout
|
|
352
|
+
- Route groups help organize large applications
|
|
353
|
+
- Layouts are applied in order: root → route group → nested → page
|
|
354
|
+
|
|
355
|
+
**Example Structure:**
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
app/
|
|
359
|
+
├── layout.tsx # Root layout (applies to all routes)
|
|
360
|
+
├── layout.server.hook.ts # Root layout server hook
|
|
361
|
+
├── (dashboard)/
|
|
362
|
+
│ ├── layout.tsx # Dashboard layout (applies to /settings, /profile)
|
|
363
|
+
│ ├── layout.server.hook.ts # Dashboard layout server hook
|
|
364
|
+
│ ├── settings/
|
|
365
|
+
│ │ └── page.tsx # → /settings (NOT /dashboard/settings)
|
|
366
|
+
│ └── profile/
|
|
367
|
+
│ └── page.tsx # → /profile (NOT /dashboard/profile)
|
|
368
|
+
├── (landing)/
|
|
369
|
+
│ ├── layout.tsx # Landing layout (applies to /about, /contact)
|
|
370
|
+
│ ├── layout.server.hook.ts # Landing layout server hook
|
|
371
|
+
│ ├── about/
|
|
372
|
+
│ │ └── page.tsx # → /about (NOT /landing/about)
|
|
373
|
+
│ └── contact/
|
|
374
|
+
│ └── page.tsx # → /contact (NOT /landing/contact)
|
|
375
|
+
└── page.tsx # → / (root page)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Layout Order:**
|
|
379
|
+
|
|
380
|
+
For a page at `app/(dashboard)/settings/page.tsx`, layouts are applied in this order:
|
|
381
|
+
|
|
382
|
+
1. `app/layout.tsx` (root layout)
|
|
383
|
+
2. `app/(dashboard)/layout.tsx` (route group layout)
|
|
384
|
+
3. `app/(dashboard)/settings/layout.tsx` (if exists, nested layout)
|
|
385
|
+
|
|
386
|
+
**Server Hooks:**
|
|
387
|
+
|
|
388
|
+
Server hooks work the same way with route groups. The execution order is:
|
|
389
|
+
|
|
390
|
+
1. Root layout hook (`app/layout.server.hook.ts`)
|
|
391
|
+
2. Route group layout hook (`app/(dashboard)/layout.server.hook.ts`)
|
|
392
|
+
3. Nested layout hooks (if any)
|
|
393
|
+
4. Page hook (`app/(dashboard)/settings/page.server.hook.ts`)
|
|
394
|
+
|
|
395
|
+
**Example:**
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
// app/(dashboard)/layout.tsx
|
|
399
|
+
export default function DashboardLayout({ children, user }) {
|
|
400
|
+
return (
|
|
401
|
+
<div className="dashboard">
|
|
402
|
+
<nav>Dashboard Navigation</nav>
|
|
403
|
+
{children}
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
```tsx
|
|
410
|
+
// app/(dashboard)/layout.server.hook.ts
|
|
411
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
412
|
+
|
|
413
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
414
|
+
const user = await getCurrentUser(ctx.req);
|
|
415
|
+
return {
|
|
416
|
+
props: {
|
|
417
|
+
user, // Available to all pages in (dashboard) group
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
// app/(dashboard)/settings/page.tsx
|
|
425
|
+
export default function SettingsPage({ user, settings }) {
|
|
426
|
+
// user comes from (dashboard)/layout.server.hook.ts
|
|
427
|
+
// settings comes from page.server.hook.ts
|
|
428
|
+
return <div>Settings for {user.name}</div>;
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Important Notes:**
|
|
433
|
+
- Route groups are purely organizational - they don't affect URLs
|
|
434
|
+
- You cannot have duplicate routes that would result from ignoring route groups
|
|
435
|
+
- ❌ Invalid: `app/(dashboard)/settings/page.tsx` and `app/settings/page.tsx` (both map to `/settings`)
|
|
436
|
+
- ✅ Valid: `app/(dashboard)/settings/page.tsx` and `app/(landing)/settings/page.tsx` (both map to `/settings` - conflict!)
|
|
437
|
+
- Route groups work seamlessly with SPA navigation and preloading
|
|
438
|
+
|
|
439
|
+
**Future: Parallel Routes**
|
|
440
|
+
|
|
441
|
+
The architecture is prepared for future parallel routes support (e.g., `(modal)`). Route groups can be extended to support special types that render in parallel slots.
|
|
442
|
+
|
|
443
|
+
### 🔄 URL Rewrites
|
|
444
|
+
|
|
445
|
+
URL rewrites allow you to rewrite incoming request paths to different destination paths internally, without changing the URL visible in the browser. This is especially useful for multitenancy, API proxying, and other advanced routing scenarios.
|
|
446
|
+
|
|
447
|
+
**Key Features:**
|
|
448
|
+
- Rewrites happen internally - the URL in the browser doesn't change
|
|
449
|
+
- Support for dynamic parameters (`:param`, `*` catch-all)
|
|
450
|
+
- Conditional rewrites based on host, headers, cookies, or query parameters
|
|
451
|
+
- Async destination functions for dynamic rewrites
|
|
452
|
+
- High performance with pre-compiled regex patterns and caching
|
|
453
|
+
|
|
454
|
+
**Configuration:**
|
|
455
|
+
|
|
456
|
+
Create `rewrites.config.ts` in your project root:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import type { RewriteConfig } from "@lolyjs/core";
|
|
460
|
+
|
|
461
|
+
export default async function rewrites(): Promise<RewriteConfig> {
|
|
462
|
+
return [
|
|
463
|
+
// Static rewrite
|
|
464
|
+
{
|
|
465
|
+
source: "/old-path",
|
|
466
|
+
destination: "/new-path",
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
// Rewrite with parameters
|
|
470
|
+
{
|
|
471
|
+
source: "/tenant/:tenant*",
|
|
472
|
+
destination: "/app/:tenant*",
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
// Rewrite with async function (for dynamic logic)
|
|
476
|
+
{
|
|
477
|
+
source: "/api/proxy/:path*",
|
|
478
|
+
destination: async (params, req) => {
|
|
479
|
+
const tenant = extractTenantFromRequest(req);
|
|
480
|
+
return `/api/${tenant}/:path*`;
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
// Conditional rewrite based on host (multitenant by subdomain)
|
|
485
|
+
{
|
|
486
|
+
source: "/:path*",
|
|
487
|
+
has: [
|
|
488
|
+
{ type: "host", value: ":tenant.example.com" },
|
|
489
|
+
],
|
|
490
|
+
destination: "/project/:tenant/:path*",
|
|
491
|
+
},
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Multitenant by Subdomain (Main Use Case):**
|
|
497
|
+
|
|
498
|
+
The most common use case is multitenancy where each tenant has its own subdomain:
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// rewrites.config.ts
|
|
502
|
+
import type { RewriteConfig } from "@lolyjs/core";
|
|
503
|
+
|
|
504
|
+
export default async function rewrites(): Promise<RewriteConfig> {
|
|
505
|
+
return [
|
|
506
|
+
// Multitenant by subdomain - catch-all pattern
|
|
507
|
+
// tenant1.example.com/* → /project/tenant1/*
|
|
508
|
+
// tenant2.example.com/* → /project/tenant2/*
|
|
509
|
+
// All routes under the tenant subdomain will be rewritten
|
|
510
|
+
// If a route doesn't exist in /project/[tenantId]/*, it will return 404
|
|
511
|
+
{
|
|
512
|
+
source: "/:path*",
|
|
513
|
+
has: [
|
|
514
|
+
{
|
|
515
|
+
type: "host",
|
|
516
|
+
value: ":tenant.example.com" // Captures tenant from subdomain
|
|
517
|
+
}
|
|
518
|
+
],
|
|
519
|
+
destination: "/project/:tenant/:path*",
|
|
520
|
+
},
|
|
521
|
+
];
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**How It Works:**
|
|
526
|
+
- User visits: `tenant1.example.com/dashboard`
|
|
527
|
+
- Internally rewrites to: `/project/tenant1/dashboard`
|
|
528
|
+
- URL visible in browser: `tenant1.example.com/dashboard` (unchanged)
|
|
529
|
+
- Route `/project/[tenantId]/dashboard` receives `params.tenantId = "tenant1"`
|
|
530
|
+
|
|
531
|
+
**Multitenant by Path:**
|
|
532
|
+
|
|
533
|
+
Alternatively, you can use path-based multitenancy:
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
// rewrites.config.ts
|
|
537
|
+
export default async function rewrites(): Promise<RewriteConfig> {
|
|
538
|
+
return [
|
|
539
|
+
// /tenant1/dashboard → /project/tenant1/dashboard
|
|
540
|
+
{
|
|
541
|
+
source: "/:tenant/:path*",
|
|
542
|
+
destination: "/project/:tenant/:path*",
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**API Proxy Example:**
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
export default async function rewrites(): Promise<RewriteConfig> {
|
|
552
|
+
return [
|
|
553
|
+
// Proxy all /api/proxy/* requests to external API
|
|
554
|
+
{
|
|
555
|
+
source: "/api/proxy/:path*",
|
|
556
|
+
destination: async (params, req) => {
|
|
557
|
+
const externalApi = process.env.EXTERNAL_API_URL;
|
|
558
|
+
return `${externalApi}/${params.path}`;
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
];
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Conditional Rewrites:**
|
|
566
|
+
|
|
567
|
+
Rewrites can be conditional based on request properties:
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
export default async function rewrites(): Promise<RewriteConfig> {
|
|
571
|
+
return [
|
|
572
|
+
// Rewrite based on host
|
|
573
|
+
{
|
|
574
|
+
source: "/:path*",
|
|
575
|
+
has: [
|
|
576
|
+
{ type: "host", value: "api.example.com" },
|
|
577
|
+
],
|
|
578
|
+
destination: "/api/:path*",
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
// Rewrite based on header
|
|
582
|
+
{
|
|
583
|
+
source: "/admin/:path*",
|
|
584
|
+
has: [
|
|
585
|
+
{ type: "header", key: "X-Admin-Key", value: "secret" },
|
|
586
|
+
],
|
|
587
|
+
destination: "/admin-panel/:path*",
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
// Rewrite based on cookie
|
|
591
|
+
{
|
|
592
|
+
source: "/premium/:path*",
|
|
593
|
+
has: [
|
|
594
|
+
{ type: "cookie", key: "premium", value: "true" },
|
|
595
|
+
],
|
|
596
|
+
destination: "/premium-content/:path*",
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
// Rewrite based on query parameter
|
|
600
|
+
{
|
|
601
|
+
source: "/:path*",
|
|
602
|
+
has: [
|
|
603
|
+
{ type: "query", key: "version", value: "v2" },
|
|
604
|
+
],
|
|
605
|
+
destination: "/v2/:path*",
|
|
606
|
+
},
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Pattern Syntax:**
|
|
612
|
+
|
|
613
|
+
- `:param` - Named parameter (matches one segment)
|
|
614
|
+
- `:param*` - Named catch-all (matches remaining path)
|
|
615
|
+
- `*` - Anonymous catch-all (matches remaining path)
|
|
616
|
+
|
|
617
|
+
**Accessing Extracted Parameters:**
|
|
618
|
+
|
|
619
|
+
Parameters extracted from rewrites (including from host conditions) are automatically available in:
|
|
620
|
+
- `req.query` - Query parameters
|
|
621
|
+
- `req.locals` - Request locals (for server hooks)
|
|
622
|
+
- `ctx.params` - Route parameters (if the rewritten path matches a dynamic route)
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
// app/project/[tenantId]/dashboard/page.server.hook.ts
|
|
626
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
627
|
+
// tenantId comes from the rewrite: /project/:tenant/:path*
|
|
628
|
+
const tenantId = ctx.params.tenantId;
|
|
629
|
+
|
|
630
|
+
// Also available in req.query and req.locals
|
|
631
|
+
const tenantFromQuery = ctx.req.query.tenant;
|
|
632
|
+
const tenantFromLocals = ctx.req.locals?.tenant;
|
|
633
|
+
|
|
634
|
+
return { props: { tenantId } };
|
|
635
|
+
};
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
**Performance & Caching:**
|
|
639
|
+
|
|
640
|
+
- Rewrites config is loaded once and cached
|
|
641
|
+
- Regex patterns are pre-compiled for performance
|
|
642
|
+
- In development: File tracking invalidates cache only when `rewrites.config.ts` changes
|
|
643
|
+
- In production: Rewrites are loaded from manifest (faster, no async functions)
|
|
644
|
+
|
|
645
|
+
**Important Notes:**
|
|
646
|
+
|
|
647
|
+
- Rewrites are applied **before** route matching
|
|
648
|
+
- The original URL is preserved in the browser (not a redirect)
|
|
649
|
+
- Query parameters are preserved and can be extended
|
|
650
|
+
- Rewrites work for both pages and API routes
|
|
651
|
+
- Functions in rewrite destinations cannot be serialized in production builds (only static rewrites are included in manifest)
|
|
652
|
+
- Rewrites are evaluated in order - the first match wins
|
|
653
|
+
- **Behavior**: Rewrites are applied ALWAYS if the source pattern matches, regardless of whether the destination route exists
|
|
654
|
+
- If a rewritten route doesn't exist, a 404 will be returned (strict behavior, no fallback to original route)
|
|
655
|
+
- Catch-all patterns (`/:path*`) are fully supported and recommended for multitenancy scenarios
|
|
656
|
+
- **API Routes**: Can be rewritten. If rewritten route starts with `/api/`, it's handled as API route. Otherwise, it's handled as a page route
|
|
657
|
+
- **WSS Routes**: Automatically excluded from rewrites (WebSocket handled separately by Socket.IO)
|
|
658
|
+
- System routes (`/static/*`, `/__fw/*`, `/favicon.ico`) are automatically excluded from rewrites
|
|
659
|
+
|
|
660
|
+
**Validation:**
|
|
661
|
+
|
|
662
|
+
The framework automatically validates rewrites to prevent:
|
|
663
|
+
- Infinite loops (warns if source and destination are identical)
|
|
664
|
+
- Duplicate source patterns (warns if multiple rewrites have the same source)
|
|
665
|
+
|
|
345
666
|
### 🚀 Hybrid Rendering
|
|
346
667
|
|
|
347
668
|
Choose the best rendering strategy for each page:
|