create-lumen 2.0.0-alpha.2 → 2.0.0-beta.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/README.md +24 -6
- package/bin/cli.js +2 -1
- package/package.json +2 -2
- package/schema/lumen.config.v2.json +29 -0
- package/src/agent-context.js +71 -47
- package/src/cleanup.js +19 -1
- package/src/cli-args.js +131 -5
- package/src/configure.js +8 -4
- package/src/dependencies.js +46 -13
- package/src/engine/capabilities.js +71 -1
- package/src/engine/composition.js +334 -17
- package/src/engine/providers/base-provider.js +4 -4
- package/src/env.js +24 -6
- package/src/main.js +38 -15
- package/src/manifest/emit.js +14 -1
- package/src/manifest/schema.js +30 -0
- package/src/prompts.js +227 -23
- package/templates/bases/next/app-router/js/.provenance.json +19 -0
- package/templates/bases/next/app-router/js/README.md +36 -0
- package/templates/bases/next/app-router/js/jsconfig.json +7 -0
- package/templates/bases/next/app-router/js/next.config.mjs +6 -0
- package/templates/bases/next/app-router/js/package.json +15 -0
- package/templates/bases/next/app-router/js/public/file.svg +1 -0
- package/templates/bases/next/app-router/js/public/globe.svg +1 -0
- package/templates/bases/next/app-router/js/public/next.svg +1 -0
- package/templates/bases/next/app-router/js/public/vercel.svg +1 -0
- package/templates/bases/next/app-router/js/public/window.svg +1 -0
- package/templates/bases/next/app-router/js/src/app/api/hello/route.js +5 -0
- package/templates/bases/next/app-router/js/src/app/blog/[slug]/page.js +33 -0
- package/templates/bases/next/app-router/js/src/app/favicon.ico +0 -0
- package/templates/bases/next/app-router/js/src/app/globals.css +49 -0
- package/templates/bases/next/app-router/js/src/app/layout.js +25 -0
- package/templates/bases/next/app-router/js/src/app/loading.js +15 -0
- package/templates/bases/next/app-router/js/src/app/not-found.js +29 -0
- package/templates/bases/next/app-router/js/src/app/page.js +69 -0
- package/templates/bases/next/app-router/js/src/app/page.module.css +150 -0
- package/templates/bases/next/app-router/ts/.provenance.json +19 -0
- package/templates/bases/next/app-router/ts/README.md +36 -0
- package/templates/bases/next/app-router/ts/next.config.ts +7 -0
- package/templates/bases/next/app-router/ts/package.json +21 -0
- package/templates/bases/next/app-router/ts/public/file.svg +1 -0
- package/templates/bases/next/app-router/ts/public/globe.svg +1 -0
- package/templates/bases/next/app-router/ts/public/next.svg +1 -0
- package/templates/bases/next/app-router/ts/public/vercel.svg +1 -0
- package/templates/bases/next/app-router/ts/public/window.svg +1 -0
- package/templates/bases/next/app-router/ts/src/app/api/hello/route.ts +5 -0
- package/templates/bases/next/app-router/ts/src/app/blog/[slug]/page.tsx +38 -0
- package/templates/bases/next/app-router/ts/src/app/favicon.ico +0 -0
- package/templates/bases/next/app-router/ts/src/app/globals.css +49 -0
- package/templates/bases/next/app-router/ts/src/app/layout.tsx +30 -0
- package/templates/bases/next/app-router/ts/src/app/loading.tsx +15 -0
- package/templates/bases/next/app-router/ts/src/app/not-found.tsx +29 -0
- package/templates/bases/next/app-router/ts/src/app/page.module.css +150 -0
- package/templates/bases/next/app-router/ts/src/app/page.tsx +69 -0
- package/templates/bases/next/app-router/ts/tsconfig.json +34 -0
- package/templates/bases/next/pages-router/js/.provenance.json +19 -0
- package/templates/bases/next/pages-router/js/README.md +40 -0
- package/templates/bases/next/pages-router/js/jsconfig.json +7 -0
- package/templates/bases/next/pages-router/js/next.config.mjs +7 -0
- package/templates/bases/next/pages-router/js/package.json +15 -0
- package/templates/bases/next/pages-router/js/public/favicon.ico +0 -0
- package/templates/bases/next/pages-router/js/public/file.svg +1 -0
- package/templates/bases/next/pages-router/js/public/globe.svg +1 -0
- package/templates/bases/next/pages-router/js/public/next.svg +1 -0
- package/templates/bases/next/pages-router/js/public/vercel.svg +1 -0
- package/templates/bases/next/pages-router/js/public/window.svg +1 -0
- package/templates/bases/next/pages-router/js/src/pages/404.js +35 -0
- package/templates/bases/next/pages-router/js/src/pages/500.js +35 -0
- package/templates/bases/next/pages-router/js/src/pages/_app.js +20 -0
- package/templates/bases/next/pages-router/js/src/pages/_document.js +15 -0
- package/templates/bases/next/pages-router/js/src/pages/api/hello.js +5 -0
- package/templates/bases/next/pages-router/js/src/pages/blog/[slug].js +41 -0
- package/templates/bases/next/pages-router/js/src/pages/index.js +77 -0
- package/templates/bases/next/pages-router/js/src/styles/Home.module.css +149 -0
- package/templates/bases/next/pages-router/js/src/styles/globals.css +42 -0
- package/templates/bases/next/pages-router/ts/.provenance.json +19 -0
- package/templates/bases/next/pages-router/ts/README.md +40 -0
- package/templates/bases/next/pages-router/ts/next.config.ts +8 -0
- package/templates/bases/next/pages-router/ts/package.json +21 -0
- package/templates/bases/next/pages-router/ts/public/favicon.ico +0 -0
- package/templates/bases/next/pages-router/ts/public/file.svg +1 -0
- package/templates/bases/next/pages-router/ts/public/globe.svg +1 -0
- package/templates/bases/next/pages-router/ts/public/next.svg +1 -0
- package/templates/bases/next/pages-router/ts/public/vercel.svg +1 -0
- package/templates/bases/next/pages-router/ts/public/window.svg +1 -0
- package/templates/bases/next/pages-router/ts/src/pages/404.tsx +35 -0
- package/templates/bases/next/pages-router/ts/src/pages/500.tsx +35 -0
- package/templates/bases/next/pages-router/ts/src/pages/_app.tsx +21 -0
- package/templates/bases/next/pages-router/ts/src/pages/_document.tsx +15 -0
- package/templates/bases/next/pages-router/ts/src/pages/api/hello.ts +13 -0
- package/templates/bases/next/pages-router/ts/src/pages/blog/[slug].tsx +46 -0
- package/templates/bases/next/pages-router/ts/src/pages/index.tsx +77 -0
- package/templates/bases/next/pages-router/ts/src/styles/Home.module.css +149 -0
- package/templates/bases/next/pages-router/ts/src/styles/globals.css +42 -0
- package/templates/bases/next/pages-router/ts/tsconfig.json +29 -0
- package/templates/conditional/database/prisma/db.js +14 -0
- package/templates/conditional/database/prisma/db.ts +16 -0
- package/templates/conditional/database/prisma/schema.prisma +9 -0
- package/templates/conditional/database/prisma/seed.js +16 -0
- package/templates/conditional/database/prisma/seed.ts +16 -0
- package/templates/conditional/docker/.dockerignore +10 -0
- package/templates/conditional/docker/Dockerfile +57 -0
- package/templates/conditional/docker/docker-compose.yml +35 -0
- package/templates/conditional/linting/biome/biome.json +31 -0
- package/templates/conditional/next/config/cloudflare/next.config.mjs +6 -0
- package/templates/conditional/next/config/cloudflare/next.config.ts +7 -0
- package/templates/conditional/next/config/node/next.config.mjs +6 -0
- package/templates/conditional/next/config/node/next.config.ts +7 -0
- package/templates/conditional/next/config/static/next.config.mjs +6 -0
- package/templates/conditional/next/config/static/next.config.ts +7 -0
- package/templates/conditional/next/config/turbopack/next.config.mjs +7 -0
- package/templates/conditional/next/config/turbopack/next.config.ts +8 -0
- package/templates/conditional/next/config/vercel/next.config.mjs +6 -0
- package/templates/conditional/next/config/vercel/next.config.ts +7 -0
- package/templates/conditional/next/config/webpack/next.config.mjs +9 -0
- package/templates/conditional/next/config/webpack/next.config.ts +10 -0
- package/templates/conditional/next/font/BUILD_SIZE_NOTE.md +17 -0
- package/templates/conditional/next/font/app-router/src/app/layout.js +25 -0
- package/templates/conditional/next/font/app-router/src/app/layout.tsx +30 -0
- package/templates/conditional/next/font/pages-router/src/pages/_app.js +20 -0
- package/templates/conditional/next/font/pages-router/src/pages/_app.tsx +21 -0
- package/templates/conditional/next/font/pages-router/src/pages/_document.js +18 -0
- package/templates/conditional/next/font/pages-router/src/pages/_document.tsx +18 -0
- package/templates/conditional/next/font/src/app/layout.js +25 -0
- package/templates/conditional/next/font/src/app/layout.tsx +30 -0
- package/templates/conditional/next/font/src/lib/fonts.js +11 -0
- package/templates/conditional/next/font/src/lib/fonts.ts +11 -0
- package/templates/conditional/next/font/src/pages/_app.js +20 -0
- package/templates/conditional/next/font/src/pages/_app.tsx +21 -0
- package/templates/conditional/next/font/src/pages/_document.js +18 -0
- package/templates/conditional/next/font/src/pages/_document.tsx +18 -0
- package/templates/conditional/next/image/BUILD_SIZE_NOTE.md +15 -0
- package/templates/conditional/next/image/app-router/src/components/ImageExample.jsx +34 -0
- package/templates/conditional/next/image/app-router/src/components/ImageExample.tsx +44 -0
- package/templates/conditional/next/image/pages-router/src/components/ImageExample.jsx +34 -0
- package/templates/conditional/next/image/pages-router/src/components/ImageExample.tsx +44 -0
- package/templates/conditional/next/image/src/components/ImageExample.jsx +44 -0
- package/templates/conditional/next/image/src/components/ImageExample.tsx +54 -0
- package/templates/conditional/next/image/src/components/image/index.js +1 -0
- package/templates/conditional/next/image/src/components/image/index.ts +2 -0
- package/templates/conditional/next/route-groups/js/src/app/(admin)/layout.jsx +23 -0
- package/templates/conditional/next/route-groups/js/src/app/(auth)/layout.jsx +17 -0
- package/templates/conditional/next/route-groups/js/src/app/(private)/layout.jsx +27 -0
- package/templates/conditional/next/route-groups/js/src/app/(public)/layout.jsx +28 -0
- package/templates/conditional/next/route-groups/js/src/app/(public)/page.jsx +38 -0
- package/templates/conditional/next/route-groups/js/src/middleware.js +48 -0
- package/templates/conditional/next/route-groups/ts/src/app/(admin)/layout.tsx +28 -0
- package/templates/conditional/next/route-groups/ts/src/app/(auth)/layout.tsx +22 -0
- package/templates/conditional/next/route-groups/ts/src/app/(private)/layout.tsx +32 -0
- package/templates/conditional/next/route-groups/ts/src/app/(public)/layout.tsx +33 -0
- package/templates/conditional/next/route-groups/ts/src/app/(public)/page.tsx +38 -0
- package/templates/conditional/next/route-groups/ts/src/middleware.ts +49 -0
- package/templates/conditional/next/server-components/README.md +13 -0
- package/templates/conditional/next/server-components/eslint.config.mjs +47 -0
- package/templates/conditional/next/server-components/rules/no-server-imports-in-client.js +89 -0
- package/templates/conditional/next/server-components/rules/no-server-imports-in-client.mjs +88 -0
- package/templates/conditional/next/server-components/src/components/boundaries/ClientCounter.jsx +48 -0
- package/templates/conditional/next/server-components/src/components/boundaries/ClientCounter.tsx +53 -0
- package/templates/conditional/next/server-components/src/components/boundaries/ServerCard.jsx +41 -0
- package/templates/conditional/next/server-components/src/components/boundaries/ServerCard.tsx +46 -0
- package/templates/conditional/next/server-components/src/components/boundaries/index.js +2 -0
- package/templates/conditional/next/server-components/src/components/boundaries/index.ts +4 -0
- package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.jsx +10 -0
- package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.tsx +10 -0
- package/templates/conditional/testing/jest/next/jest.config.mjs +20 -0
- package/templates/conditional/testing/jest/next/jest.config.ts +20 -0
- package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.jsx +10 -0
- package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.tsx +10 -0
- package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.jsx +11 -0
- package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.tsx +11 -0
- package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.jsx +11 -0
- package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.tsx +11 -0
- package/templates/conditional/testing/vitest/next/vitest.config.js +16 -0
- package/templates/conditional/testing/vitest/next/vitest.config.ts +16 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Image from "next/image";
|
|
3
|
+
|
|
4
|
+
export function ImageExample({
|
|
5
|
+
src = "/next.svg",
|
|
6
|
+
alt = "Next.js logo",
|
|
7
|
+
width = 180,
|
|
8
|
+
height = 37,
|
|
9
|
+
priority = false,
|
|
10
|
+
className = "",
|
|
11
|
+
caption = "Optimized with next/image",
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<figure className={`inline-flex flex-col items-center gap-2 p-4 rounded-xl border border-gray-200 dark:border-gray-800 bg-white/50 dark:bg-black/20 ${className}`}>
|
|
15
|
+
<div className="relative flex items-center justify-center p-2">
|
|
16
|
+
<Image
|
|
17
|
+
src={src}
|
|
18
|
+
alt={alt}
|
|
19
|
+
width={width}
|
|
20
|
+
height={height}
|
|
21
|
+
priority={priority}
|
|
22
|
+
className="dark:invert transition-transform hover:scale-105"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
{caption && (
|
|
26
|
+
<figcaption className="text-xs text-gray-500 dark:text-gray-400 text-center font-mono">
|
|
27
|
+
{caption}
|
|
28
|
+
</figcaption>
|
|
29
|
+
)}
|
|
30
|
+
</figure>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default ImageExample;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Image from "next/image";
|
|
3
|
+
|
|
4
|
+
export interface ImageExampleProps {
|
|
5
|
+
src?: string;
|
|
6
|
+
alt?: string;
|
|
7
|
+
width?: number;
|
|
8
|
+
height?: number;
|
|
9
|
+
priority?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
caption?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ImageExample({
|
|
15
|
+
src = "/next.svg",
|
|
16
|
+
alt = "Next.js logo",
|
|
17
|
+
width = 180,
|
|
18
|
+
height = 37,
|
|
19
|
+
priority = false,
|
|
20
|
+
className = "",
|
|
21
|
+
caption = "Optimized with next/image",
|
|
22
|
+
}: ImageExampleProps) {
|
|
23
|
+
return (
|
|
24
|
+
<figure className={`inline-flex flex-col items-center gap-2 p-4 rounded-xl border border-gray-200 dark:border-gray-800 bg-white/50 dark:bg-black/20 ${className}`}>
|
|
25
|
+
<div className="relative flex items-center justify-center p-2">
|
|
26
|
+
<Image
|
|
27
|
+
src={src}
|
|
28
|
+
alt={alt}
|
|
29
|
+
width={width}
|
|
30
|
+
height={height}
|
|
31
|
+
priority={priority}
|
|
32
|
+
className="dark:invert transition-transform hover:scale-105"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
{caption && (
|
|
36
|
+
<figcaption className="text-xs text-gray-500 dark:text-gray-400 text-center font-mono">
|
|
37
|
+
{caption}
|
|
38
|
+
</figcaption>
|
|
39
|
+
)}
|
|
40
|
+
</figure>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default ImageExample;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Image from "next/image";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Universal Next.js Image component example.
|
|
6
|
+
* Works seamlessly in both App Router (Server and Client components)
|
|
7
|
+
* and Pages Router.
|
|
8
|
+
*
|
|
9
|
+
* Demonstrates:
|
|
10
|
+
* - Built-in automatic format optimization (WebP/AVIF)
|
|
11
|
+
* - Intrinsic sizing to avoid layout shift (CLS)
|
|
12
|
+
* - Priority loading option for LCP optimization
|
|
13
|
+
*/
|
|
14
|
+
export function ImageExample({
|
|
15
|
+
src = "/next.svg",
|
|
16
|
+
alt = "Next.js logo",
|
|
17
|
+
width = 180,
|
|
18
|
+
height = 37,
|
|
19
|
+
priority = false,
|
|
20
|
+
className = "",
|
|
21
|
+
caption = "Optimized with next/image",
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<figure className={`inline-flex flex-col items-center gap-2 p-4 rounded-xl border border-gray-200 dark:border-gray-800 bg-white/50 dark:bg-black/20 ${className}`}>
|
|
25
|
+
<div className="relative flex items-center justify-center p-2">
|
|
26
|
+
<Image
|
|
27
|
+
src={src}
|
|
28
|
+
alt={alt}
|
|
29
|
+
width={width}
|
|
30
|
+
height={height}
|
|
31
|
+
priority={priority}
|
|
32
|
+
className="dark:invert transition-transform hover:scale-105"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
{caption && (
|
|
36
|
+
<figcaption className="text-xs text-gray-500 dark:text-gray-400 text-center font-mono">
|
|
37
|
+
{caption}
|
|
38
|
+
</figcaption>
|
|
39
|
+
)}
|
|
40
|
+
</figure>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default ImageExample;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Image from "next/image";
|
|
3
|
+
|
|
4
|
+
export interface ImageExampleProps {
|
|
5
|
+
src?: string;
|
|
6
|
+
alt?: string;
|
|
7
|
+
width?: number;
|
|
8
|
+
height?: number;
|
|
9
|
+
priority?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
caption?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Universal Next.js Image component example.
|
|
16
|
+
* Works seamlessly in both App Router (Server and Client components)
|
|
17
|
+
* and Pages Router.
|
|
18
|
+
*
|
|
19
|
+
* Demonstrates:
|
|
20
|
+
* - Built-in automatic format optimization (WebP/AVIF)
|
|
21
|
+
* - Intrinsic sizing to avoid layout shift (CLS)
|
|
22
|
+
* - Priority loading option for LCP optimization
|
|
23
|
+
*/
|
|
24
|
+
export function ImageExample({
|
|
25
|
+
src = "/next.svg",
|
|
26
|
+
alt = "Next.js logo",
|
|
27
|
+
width = 180,
|
|
28
|
+
height = 37,
|
|
29
|
+
priority = false,
|
|
30
|
+
className = "",
|
|
31
|
+
caption = "Optimized with next/image",
|
|
32
|
+
}: ImageExampleProps) {
|
|
33
|
+
return (
|
|
34
|
+
<figure className={`inline-flex flex-col items-center gap-2 p-4 rounded-xl border border-gray-200 dark:border-gray-800 bg-white/50 dark:bg-black/20 ${className}`}>
|
|
35
|
+
<div className="relative flex items-center justify-center p-2">
|
|
36
|
+
<Image
|
|
37
|
+
src={src}
|
|
38
|
+
alt={alt}
|
|
39
|
+
width={width}
|
|
40
|
+
height={height}
|
|
41
|
+
priority={priority}
|
|
42
|
+
className="dark:invert transition-transform hover:scale-105"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
{caption && (
|
|
46
|
+
<figcaption className="text-xs text-gray-500 dark:text-gray-400 text-center font-mono">
|
|
47
|
+
{caption}
|
|
48
|
+
</figcaption>
|
|
49
|
+
)}
|
|
50
|
+
</figure>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default ImageExample;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ImageExample } from "../ImageExample";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function AdminLayout({ children }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen flex flex-col">
|
|
6
|
+
<header className="bg-red-700 text-white px-6 py-3 flex items-center justify-between text-sm">
|
|
7
|
+
<div className="font-semibold flex items-center gap-2">
|
|
8
|
+
<span className="inline-block w-2 h-2 rounded-full bg-white animate-pulse" />
|
|
9
|
+
Admin Panel (Restricted Access)
|
|
10
|
+
</div>
|
|
11
|
+
<nav className="flex gap-4">
|
|
12
|
+
<Link href="/admin" className="underline hover:no-underline">
|
|
13
|
+
Management
|
|
14
|
+
</Link>
|
|
15
|
+
<Link href="/dashboard" className="underline hover:no-underline">
|
|
16
|
+
Exit to Dashboard
|
|
17
|
+
</Link>
|
|
18
|
+
</nav>
|
|
19
|
+
</header>
|
|
20
|
+
<main className="flex-1 p-8 bg-gray-100 dark:bg-gray-900">{children}</main>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function AuthLayout({ children }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen flex flex-col justify-center items-center bg-gray-50 dark:bg-gray-950 p-4">
|
|
6
|
+
<div className="w-full max-w-md bg-white dark:bg-gray-900 shadow-md rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
|
7
|
+
<div className="mb-6 text-center">
|
|
8
|
+
<Link href="/" className="text-xl font-bold">
|
|
9
|
+
Lumen Authentication
|
|
10
|
+
</Link>
|
|
11
|
+
<p className="text-sm text-gray-500 mt-1">Sign in to your account</p>
|
|
12
|
+
</div>
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function PrivateLayout({ children }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen flex">
|
|
6
|
+
<aside className="w-64 border-r bg-gray-50 dark:bg-gray-950 p-6 flex flex-col justify-between">
|
|
7
|
+
<div>
|
|
8
|
+
<div className="font-bold text-lg mb-6">
|
|
9
|
+
<Link href="/dashboard">Private Dashboard</Link>
|
|
10
|
+
</div>
|
|
11
|
+
<nav className="flex flex-col gap-3 text-sm">
|
|
12
|
+
<Link href="/dashboard" className="text-gray-700 dark:text-gray-200 hover:text-black">
|
|
13
|
+
Overview
|
|
14
|
+
</Link>
|
|
15
|
+
<Link href="/settings" className="text-gray-700 dark:text-gray-200 hover:text-black">
|
|
16
|
+
Settings
|
|
17
|
+
</Link>
|
|
18
|
+
</nav>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="text-xs text-gray-400">
|
|
21
|
+
Protected Area • Authenticated
|
|
22
|
+
</div>
|
|
23
|
+
</aside>
|
|
24
|
+
<main className="flex-1 p-8">{children}</main>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function PublicLayout({ children }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen flex flex-col">
|
|
6
|
+
<header className="border-b px-6 py-4 flex items-center justify-between">
|
|
7
|
+
<div className="font-bold text-lg">
|
|
8
|
+
<Link href="/">Lumen App</Link>
|
|
9
|
+
</div>
|
|
10
|
+
<nav className="flex gap-4 text-sm">
|
|
11
|
+
<Link href="/login" className="hover:underline">
|
|
12
|
+
Sign In
|
|
13
|
+
</Link>
|
|
14
|
+
<Link href="/dashboard" className="hover:underline">
|
|
15
|
+
Dashboard
|
|
16
|
+
</Link>
|
|
17
|
+
<Link href="/admin" className="hover:underline">
|
|
18
|
+
Admin
|
|
19
|
+
</Link>
|
|
20
|
+
</nav>
|
|
21
|
+
</header>
|
|
22
|
+
<main className="flex-1">{children}</main>
|
|
23
|
+
<footer className="border-t px-6 py-4 text-center text-sm text-gray-500">
|
|
24
|
+
Public Area © {new Date().getFullYear()}
|
|
25
|
+
</footer>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function PublicPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="max-w-4xl mx-auto py-12 px-6">
|
|
6
|
+
<h1 className="text-4xl font-extrabold tracking-tight mb-4">
|
|
7
|
+
Welcome to Route Groups Preset
|
|
8
|
+
</h1>
|
|
9
|
+
<p className="text-lg text-gray-600 mb-8">
|
|
10
|
+
This application is organized using Next.js App Router route groups:
|
|
11
|
+
(public), (auth), (private), and (admin).
|
|
12
|
+
</p>
|
|
13
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
14
|
+
<div className="border rounded-lg p-6 shadow-sm">
|
|
15
|
+
<h2 className="text-xl font-semibold mb-2">Auth Group</h2>
|
|
16
|
+
<p className="text-gray-500 mb-4">Authentication screens and login layouts.</p>
|
|
17
|
+
<Link href="/login" className="text-blue-600 hover:underline">
|
|
18
|
+
Go to Login →
|
|
19
|
+
</Link>
|
|
20
|
+
</div>
|
|
21
|
+
<div className="border rounded-lg p-6 shadow-sm">
|
|
22
|
+
<h2 className="text-xl font-semibold mb-2">Private Group</h2>
|
|
23
|
+
<p className="text-gray-500 mb-4">Protected routes guarded by auth proxy middleware.</p>
|
|
24
|
+
<Link href="/dashboard" className="text-blue-600 hover:underline">
|
|
25
|
+
Go to Dashboard →
|
|
26
|
+
</Link>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="border rounded-lg p-6 shadow-sm">
|
|
29
|
+
<h2 className="text-xl font-semibold mb-2">Admin Group</h2>
|
|
30
|
+
<p className="text-gray-500 mb-4">Elevated management section requiring admin role.</p>
|
|
31
|
+
<Link href="/admin" className="text-blue-600 hover:underline">
|
|
32
|
+
Go to Admin →
|
|
33
|
+
</Link>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auth Proxy / Route Guard Middleware
|
|
5
|
+
*
|
|
6
|
+
* Enforces role-based access control and session verification:
|
|
7
|
+
* - Unauthenticated requests to private or admin routes redirect to /login
|
|
8
|
+
* - Authenticated requests lacking the "admin" role redirect away from /admin
|
|
9
|
+
*/
|
|
10
|
+
export function middleware(request) {
|
|
11
|
+
const { pathname } = request.nextUrl;
|
|
12
|
+
|
|
13
|
+
const authToken = request.cookies.get("auth-token")?.value;
|
|
14
|
+
const userRole = request.cookies.get("user-role")?.value || "user";
|
|
15
|
+
|
|
16
|
+
const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/register");
|
|
17
|
+
const isPrivateRoute = pathname.startsWith("/dashboard") || pathname.startsWith("/settings");
|
|
18
|
+
const isAdminRoute = pathname.startsWith("/admin");
|
|
19
|
+
|
|
20
|
+
// If unauthenticated user tries to access protected areas, redirect to /login
|
|
21
|
+
if ((isPrivateRoute || isAdminRoute) && !authToken) {
|
|
22
|
+
const loginUrl = new URL("/login", request.url);
|
|
23
|
+
loginUrl.searchParams.set("from", pathname);
|
|
24
|
+
return NextResponse.redirect(loginUrl);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If authenticated user without admin role accesses admin area, forbid or redirect to dashboard
|
|
28
|
+
if (isAdminRoute && userRole !== "admin") {
|
|
29
|
+
return NextResponse.redirect(new URL("/dashboard", request.url));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If already authenticated user visits auth routes, redirect to dashboard
|
|
33
|
+
if (isAuthRoute && authToken) {
|
|
34
|
+
return NextResponse.redirect(new URL("/dashboard", request.url));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return NextResponse.next();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const config = {
|
|
41
|
+
matcher: [
|
|
42
|
+
"/dashboard/:path*",
|
|
43
|
+
"/settings/:path*",
|
|
44
|
+
"/admin/:path*",
|
|
45
|
+
"/login",
|
|
46
|
+
"/register",
|
|
47
|
+
],
|
|
48
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export default function AdminLayout({
|
|
5
|
+
children,
|
|
6
|
+
}: Readonly<{
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}>) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="min-h-screen flex flex-col">
|
|
11
|
+
<header className="bg-red-700 text-white px-6 py-3 flex items-center justify-between text-sm">
|
|
12
|
+
<div className="font-semibold flex items-center gap-2">
|
|
13
|
+
<span className="inline-block w-2 h-2 rounded-full bg-white animate-pulse" />
|
|
14
|
+
Admin Panel (Restricted Access)
|
|
15
|
+
</div>
|
|
16
|
+
<nav className="flex gap-4">
|
|
17
|
+
<Link href="/admin" className="underline hover:no-underline">
|
|
18
|
+
Management
|
|
19
|
+
</Link>
|
|
20
|
+
<Link href="/dashboard" className="underline hover:no-underline">
|
|
21
|
+
Exit to Dashboard
|
|
22
|
+
</Link>
|
|
23
|
+
</nav>
|
|
24
|
+
</header>
|
|
25
|
+
<main className="flex-1 p-8 bg-gray-100 dark:bg-gray-900">{children}</main>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export default function AuthLayout({
|
|
5
|
+
children,
|
|
6
|
+
}: Readonly<{
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}>) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="min-h-screen flex flex-col justify-center items-center bg-gray-50 dark:bg-gray-950 p-4">
|
|
11
|
+
<div className="w-full max-w-md bg-white dark:bg-gray-900 shadow-md rounded-lg p-6 border border-gray-200 dark:border-gray-800">
|
|
12
|
+
<div className="mb-6 text-center">
|
|
13
|
+
<Link href="/" className="text-xl font-bold">
|
|
14
|
+
Lumen Authentication
|
|
15
|
+
</Link>
|
|
16
|
+
<p className="text-sm text-gray-500 mt-1">Sign in to your account</p>
|
|
17
|
+
</div>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export default function PrivateLayout({
|
|
5
|
+
children,
|
|
6
|
+
}: Readonly<{
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}>) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="min-h-screen flex">
|
|
11
|
+
<aside className="w-64 border-r bg-gray-50 dark:bg-gray-950 p-6 flex flex-col justify-between">
|
|
12
|
+
<div>
|
|
13
|
+
<div className="font-bold text-lg mb-6">
|
|
14
|
+
<Link href="/dashboard">Private Dashboard</Link>
|
|
15
|
+
</div>
|
|
16
|
+
<nav className="flex flex-col gap-3 text-sm">
|
|
17
|
+
<Link href="/dashboard" className="text-gray-700 dark:text-gray-200 hover:text-black">
|
|
18
|
+
Overview
|
|
19
|
+
</Link>
|
|
20
|
+
<Link href="/settings" className="text-gray-700 dark:text-gray-200 hover:text-black">
|
|
21
|
+
Settings
|
|
22
|
+
</Link>
|
|
23
|
+
</nav>
|
|
24
|
+
</div>
|
|
25
|
+
<div className="text-xs text-gray-400">
|
|
26
|
+
Protected Area • Authenticated
|
|
27
|
+
</div>
|
|
28
|
+
</aside>
|
|
29
|
+
<main className="flex-1 p-8">{children}</main>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export default function PublicLayout({
|
|
5
|
+
children,
|
|
6
|
+
}: Readonly<{
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}>) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="min-h-screen flex flex-col">
|
|
11
|
+
<header className="border-b px-6 py-4 flex items-center justify-between">
|
|
12
|
+
<div className="font-bold text-lg">
|
|
13
|
+
<Link href="/">Lumen App</Link>
|
|
14
|
+
</div>
|
|
15
|
+
<nav className="flex gap-4 text-sm">
|
|
16
|
+
<Link href="/login" className="hover:underline">
|
|
17
|
+
Sign In
|
|
18
|
+
</Link>
|
|
19
|
+
<Link href="/dashboard" className="hover:underline">
|
|
20
|
+
Dashboard
|
|
21
|
+
</Link>
|
|
22
|
+
<Link href="/admin" className="hover:underline">
|
|
23
|
+
Admin
|
|
24
|
+
</Link>
|
|
25
|
+
</nav>
|
|
26
|
+
</header>
|
|
27
|
+
<main className="flex-1">{children}</main>
|
|
28
|
+
<footer className="border-t px-6 py-4 text-center text-sm text-gray-500">
|
|
29
|
+
Public Area © {new Date().getFullYear()}
|
|
30
|
+
</footer>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function PublicPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="max-w-4xl mx-auto py-12 px-6">
|
|
6
|
+
<h1 className="text-4xl font-extrabold tracking-tight mb-4">
|
|
7
|
+
Welcome to Route Groups Preset
|
|
8
|
+
</h1>
|
|
9
|
+
<p className="text-lg text-gray-600 mb-8">
|
|
10
|
+
This application is organized using Next.js App Router route groups:
|
|
11
|
+
(public), (auth), (private), and (admin).
|
|
12
|
+
</p>
|
|
13
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
14
|
+
<div className="border rounded-lg p-6 shadow-sm">
|
|
15
|
+
<h2 className="text-xl font-semibold mb-2">Auth Group</h2>
|
|
16
|
+
<p className="text-gray-500 mb-4">Authentication screens and login layouts.</p>
|
|
17
|
+
<Link href="/login" className="text-blue-600 hover:underline">
|
|
18
|
+
Go to Login →
|
|
19
|
+
</Link>
|
|
20
|
+
</div>
|
|
21
|
+
<div className="border rounded-lg p-6 shadow-sm">
|
|
22
|
+
<h2 className="text-xl font-semibold mb-2">Private Group</h2>
|
|
23
|
+
<p className="text-gray-500 mb-4">Protected routes guarded by auth proxy middleware.</p>
|
|
24
|
+
<Link href="/dashboard" className="text-blue-600 hover:underline">
|
|
25
|
+
Go to Dashboard →
|
|
26
|
+
</Link>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="border rounded-lg p-6 shadow-sm">
|
|
29
|
+
<h2 className="text-xl font-semibold mb-2">Admin Group</h2>
|
|
30
|
+
<p className="text-gray-500 mb-4">Elevated management section requiring admin role.</p>
|
|
31
|
+
<Link href="/admin" className="text-blue-600 hover:underline">
|
|
32
|
+
Go to Admin →
|
|
33
|
+
</Link>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import type { NextRequest } from "next/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Auth Proxy / Route Guard Middleware
|
|
6
|
+
*
|
|
7
|
+
* Enforces role-based access control and session verification:
|
|
8
|
+
* - Unauthenticated requests to private or admin routes redirect to /login
|
|
9
|
+
* - Authenticated requests lacking the "admin" role redirect away from /admin
|
|
10
|
+
*/
|
|
11
|
+
export function middleware(request: NextRequest) {
|
|
12
|
+
const { pathname } = request.nextUrl;
|
|
13
|
+
|
|
14
|
+
const authToken = request.cookies.get("auth-token")?.value;
|
|
15
|
+
const userRole = request.cookies.get("user-role")?.value || "user";
|
|
16
|
+
|
|
17
|
+
const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/register");
|
|
18
|
+
const isPrivateRoute = pathname.startsWith("/dashboard") || pathname.startsWith("/settings");
|
|
19
|
+
const isAdminRoute = pathname.startsWith("/admin");
|
|
20
|
+
|
|
21
|
+
// If unauthenticated user tries to access protected areas, redirect to /login
|
|
22
|
+
if ((isPrivateRoute || isAdminRoute) && !authToken) {
|
|
23
|
+
const loginUrl = new URL("/login", request.url);
|
|
24
|
+
loginUrl.searchParams.set("from", pathname);
|
|
25
|
+
return NextResponse.redirect(loginUrl);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If authenticated user without admin role accesses admin area, forbid or redirect to dashboard
|
|
29
|
+
if (isAdminRoute && userRole !== "admin") {
|
|
30
|
+
return NextResponse.redirect(new URL("/dashboard", request.url));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// If already authenticated user visits auth routes, redirect to dashboard
|
|
34
|
+
if (isAuthRoute && authToken) {
|
|
35
|
+
return NextResponse.redirect(new URL("/dashboard", request.url));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return NextResponse.next();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const config = {
|
|
42
|
+
matcher: [
|
|
43
|
+
"/dashboard/:path*",
|
|
44
|
+
"/settings/:path*",
|
|
45
|
+
"/admin/:path*",
|
|
46
|
+
"/login",
|
|
47
|
+
"/register",
|
|
48
|
+
],
|
|
49
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# React Server Components (RSC) Boundaries & Linting
|
|
2
|
+
|
|
3
|
+
This conditional template provides:
|
|
4
|
+
1. **Server vs Client Boundary Components**: Clean demonstrations of React Server Components composing Client Components with `"use client"`.
|
|
5
|
+
2. **ESLint Rule Wiring (`rsc/no-server-imports-in-client`)**: An AST-based ESLint rule that disallows importing server-only packages (e.g., `server-only`, Node.js built-ins) into Client Components marked with `"use client"`.
|
|
6
|
+
3. **Dual Router Compatibility**: Operates seamlessly in both Next.js App Router and Pages Router.
|
|
7
|
+
|
|
8
|
+
## Components
|
|
9
|
+
- `ClientCounter`: An interactive client component demonstrating React state and event handling.
|
|
10
|
+
- `ServerCard`: A server-rendered component shell passing children and composing `ClientCounter` across the boundary.
|
|
11
|
+
|
|
12
|
+
## ESLint Configuration
|
|
13
|
+
Wires `rsc/no-server-imports-in-client` and `no-restricted-imports` into Next.js flat configuration (`eslint.config.mjs`).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { noServerImportsInClient } from "./rules/no-server-imports-in-client.mjs";
|
|
2
|
+
|
|
3
|
+
let baseConfig = [];
|
|
4
|
+
try {
|
|
5
|
+
const next = await import("eslint-config-next");
|
|
6
|
+
const imported = next.default || next;
|
|
7
|
+
baseConfig = Array.isArray(imported) ? imported : [imported];
|
|
8
|
+
} catch {
|
|
9
|
+
try {
|
|
10
|
+
const { FlatCompat } = await import("@eslint/eslintrc");
|
|
11
|
+
const { fileURLToPath } = await import("url");
|
|
12
|
+
const { dirname } = await import("path");
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const compat = new FlatCompat({ baseDirectory: __dirname });
|
|
16
|
+
baseConfig = compat.extends("next/core-web-vitals");
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const eslintConfig = [
|
|
21
|
+
...baseConfig,
|
|
22
|
+
{
|
|
23
|
+
plugins: {
|
|
24
|
+
rsc: {
|
|
25
|
+
rules: {
|
|
26
|
+
"no-server-imports-in-client": noServerImportsInClient,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
rules: {
|
|
31
|
+
"rsc/no-server-imports-in-client": "error",
|
|
32
|
+
"no-restricted-imports": [
|
|
33
|
+
"error",
|
|
34
|
+
{
|
|
35
|
+
patterns: [
|
|
36
|
+
{
|
|
37
|
+
group: ["server-only", "**/server-only/**"],
|
|
38
|
+
message: "Do not import server-only packages in client modules.",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export default eslintConfig;
|