create-lumen 2.0.0-alpha.1 → 2.0.0-beta.1
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 +38 -12
- package/bin/cli.js +3 -1
- package/package.json +3 -3
- package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +1 -1
- package/schema/fixtures/v2/valid/react-vite-type-based.json +1 -1
- package/schema/lumen.config.v2.json +0 -1
- package/src/cli-args.js +169 -7
- package/src/configure.js +12 -4
- package/src/css.js +14 -30
- package/src/dependencies.js +36 -20
- package/src/engine/capabilities.js +195 -4
- package/src/engine/composition.js +736 -0
- package/src/engine/index.js +1 -0
- package/src/engine/providers/base-provider.js +4 -4
- package/src/injector.js +294 -446
- package/src/main.js +38 -16
- package/src/manifest/emit.js +1 -1
- package/src/manifest/schema.js +1 -1
- package/src/prompts.js +581 -238
- package/src/readme.js +23 -2
- package/src/scaffold.js +17 -0
- package/templates/architectures/hybrid/main.jsx +10 -0
- package/templates/architectures/hybrid/main.tsx +10 -0
- package/templates/architectures/hybrid/src/App.jsx +20 -0
- package/templates/architectures/hybrid/src/App.tsx +20 -0
- package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +31 -0
- package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +36 -0
- package/templates/architectures/hybrid/src/shared/components/ui/index.js +1 -0
- package/templates/architectures/hybrid/src/shared/components/ui/index.ts +1 -0
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +28 -0
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +28 -0
- package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
- package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
- package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
- package/templates/architectures/hybrid/src/shared/types/index.js +20 -0
- package/templates/architectures/hybrid/src/shared/types/index.ts +16 -0
- package/templates/architectures/hybrid/src/shared/utils/cn.jsx +3 -0
- package/templates/architectures/hybrid/src/shared/utils/cn.ts +5 -0
- package/templates/architectures/none/main.jsx +10 -0
- package/templates/architectures/none/main.tsx +10 -0
- package/templates/architectures/none/src/App.jsx +15 -0
- package/templates/architectures/none/src/App.tsx +15 -0
- package/templates/architectures/none/src/styles/.gitkeep +1 -0
- package/templates/architectures/none/src/types/index.js +20 -0
- package/templates/architectures/none/src/types/index.ts +16 -0
- 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/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/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/hybrid/src/__tests__/App.test.jsx +11 -0
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +11 -0
- package/templates/conditional/testing/jest/none/src/__tests__/App.test.jsx +11 -0
- package/templates/conditional/testing/jest/none/src/__tests__/App.test.tsx +11 -0
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +10 -0
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +10 -0
- package/templates/conditional/testing/vitest/none/src/__tests__/App.test.jsx +10 -0
- package/templates/conditional/testing/vitest/none/src/__tests__/App.test.tsx +10 -0
- package/templates/css/tailwind/src/globals.css +10 -0
- package/templates/css/tailwind/src/themes.css +86 -4
- package/templates/ui/shadcn/lib/utils.js +6 -0
- package/templates/ui/shadcn/lib/utils.ts +6 -0
- package/templates/ui/shadcn/primitives/Button.jsx +48 -0
- package/templates/ui/shadcn/primitives/Button.tsx +59 -0
- package/templates/ui/shadcn/primitives/Card.jsx +55 -0
- package/templates/ui/shadcn/primitives/Card.tsx +73 -0
- package/templates/ui/shadcn/primitives/index.js +9 -0
- package/templates/ui/shadcn/primitives/index.ts +9 -0
- package/templates/css/bootstrap/src/themes.css +0 -14
- /package/templates/{css/bootstrap/src/globals.css → architectures/hybrid/src/features/.gitkeep} +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Html, Head, Main, NextScript } from "next/document";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pages Router Custom Document.
|
|
5
|
+
* Note: Per Next.js compiler specifications, next/font loaders are configured
|
|
6
|
+
* in pages/_app to avoid the 'Cannot be used within pages/_document' compile error.
|
|
7
|
+
*/
|
|
8
|
+
export default function Document() {
|
|
9
|
+
return (
|
|
10
|
+
<Html lang="en">
|
|
11
|
+
<Head />
|
|
12
|
+
<body className="antialiased">
|
|
13
|
+
<Main />
|
|
14
|
+
<NextScript />
|
|
15
|
+
</body>
|
|
16
|
+
</Html>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# next/image Build Size and Performance Impact
|
|
2
|
+
|
|
3
|
+
## Built-In Optimization
|
|
4
|
+
`next/image` extends standard HTML `<img>` elements with automatic visual stability and performance optimizations:
|
|
5
|
+
1. **Zero Cumulative Layout Shift (CLS)**: Enforces explicit `width` and `height` dimensions or fills the parent container with `fill`, reserving geometric space before the bitmap payload loads.
|
|
6
|
+
2. **Modern Format Negotiation**: Serves WebP and AVIF modern formats dynamically when supported by the requesting browser, yielding 30–50% bandwidth reduction compared to legacy JPEG/PNG.
|
|
7
|
+
3. **Native Lazy Loading**: Viewport-aware on-demand loading so off-screen images do not delay initial page interactivity. The `priority` property allows opting into preloading for Largest Contentful Paint (LCP) elements.
|
|
8
|
+
|
|
9
|
+
## Bundle Footprint
|
|
10
|
+
- **Runtime JavaScript Overhead**: ~5 KB gzipped client-side runtime handling IntersectionObserver and responsive `srcSet` generation.
|
|
11
|
+
- **Build Impact**: Zero additional build time overhead during static compilation when referencing local SVGs or remote URLs; image transformations are handled on-demand by the Next.js server image optimizer.
|
|
12
|
+
|
|
13
|
+
## Router Parity
|
|
14
|
+
- **App Router**: Fully compatible with both Server Components (static rendering) and Client Components (`"use client"`).
|
|
15
|
+
- **Pages Router**: Fully compatible with all page and component hierarchies.
|
|
@@ -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,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,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;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: rsc/no-server-imports-in-client
|
|
3
|
+
*
|
|
4
|
+
* Enforces server/client boundaries in React Server Components (RSC).
|
|
5
|
+
* When a file is marked with the "use client" directive, it is prohibited
|
|
6
|
+
* from importing server-only packages (e.g. 'server-only', Node.js builtins,
|
|
7
|
+
* or modules from server-designated paths).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const noServerImportsInClient = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Disallow server-only imports in client components marked with 'use client'",
|
|
15
|
+
category: "Possible Errors",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
schema: [
|
|
19
|
+
{
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
serverModules: {
|
|
23
|
+
type: "array",
|
|
24
|
+
items: { type: "string" },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
messages: {
|
|
31
|
+
noServerImport:
|
|
32
|
+
"Server-only module '{{name}}' cannot be imported into a Client Component ('use client'). Keep server logic on the server side of the RSC boundary.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
create(context) {
|
|
36
|
+
const options = context.options[0] || {};
|
|
37
|
+
const restrictedSet = new Set(
|
|
38
|
+
options.serverModules || [
|
|
39
|
+
"server-only",
|
|
40
|
+
"node:fs",
|
|
41
|
+
"node:fs/promises",
|
|
42
|
+
"fs",
|
|
43
|
+
"fs/promises",
|
|
44
|
+
"node:child_process",
|
|
45
|
+
"child_process",
|
|
46
|
+
"node:net",
|
|
47
|
+
"net",
|
|
48
|
+
"node:tls",
|
|
49
|
+
"tls",
|
|
50
|
+
]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let isClientComponent = false;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
Program(node) {
|
|
57
|
+
// Inspect top-level statements for "use client" directive
|
|
58
|
+
for (const stmt of node.body) {
|
|
59
|
+
if (
|
|
60
|
+
stmt.type === "ExpressionStatement" &&
|
|
61
|
+
stmt.expression.type === "Literal" &&
|
|
62
|
+
stmt.expression.value === "use client"
|
|
63
|
+
) {
|
|
64
|
+
isClientComponent = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
ImportDeclaration(node) {
|
|
70
|
+
if (!isClientComponent) return;
|
|
71
|
+
const source = node.source.value;
|
|
72
|
+
if (
|
|
73
|
+
restrictedSet.has(source) ||
|
|
74
|
+
source.startsWith("server-only") ||
|
|
75
|
+
source.includes("/server/") ||
|
|
76
|
+
source.endsWith("/server")
|
|
77
|
+
) {
|
|
78
|
+
context.report({
|
|
79
|
+
node,
|
|
80
|
+
messageId: "noServerImport",
|
|
81
|
+
data: { name: source },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default noServerImportsInClient;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: rsc/no-server-imports-in-client
|
|
3
|
+
*
|
|
4
|
+
* Enforces server/client boundaries in React Server Components (RSC).
|
|
5
|
+
* When a file is marked with the "use client" directive, it is prohibited
|
|
6
|
+
* from importing server-only packages (e.g. 'server-only', Node.js builtins,
|
|
7
|
+
* or modules from server-designated paths).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const noServerImportsInClient = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Disallow server-only imports in client components marked with 'use client'",
|
|
15
|
+
category: "Possible Errors",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
schema: [
|
|
19
|
+
{
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
serverModules: {
|
|
23
|
+
type: "array",
|
|
24
|
+
items: { type: "string" },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
messages: {
|
|
31
|
+
noServerImport:
|
|
32
|
+
"Server-only module '{{name}}' cannot be imported into a Client Component ('use client'). Keep server logic on the server side of the RSC boundary.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
create(context) {
|
|
36
|
+
const options = context.options[0] || {};
|
|
37
|
+
const restrictedSet = new Set(
|
|
38
|
+
options.serverModules || [
|
|
39
|
+
"server-only",
|
|
40
|
+
"node:fs",
|
|
41
|
+
"node:fs/promises",
|
|
42
|
+
"fs",
|
|
43
|
+
"fs/promises",
|
|
44
|
+
"node:child_process",
|
|
45
|
+
"child_process",
|
|
46
|
+
"node:net",
|
|
47
|
+
"net",
|
|
48
|
+
"node:tls",
|
|
49
|
+
"tls",
|
|
50
|
+
]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let isClientComponent = false;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
Program(node) {
|
|
57
|
+
for (const stmt of node.body) {
|
|
58
|
+
if (
|
|
59
|
+
stmt.type === "ExpressionStatement" &&
|
|
60
|
+
stmt.expression.type === "Literal" &&
|
|
61
|
+
stmt.expression.value === "use client"
|
|
62
|
+
) {
|
|
63
|
+
isClientComponent = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
ImportDeclaration(node) {
|
|
69
|
+
if (!isClientComponent) return;
|
|
70
|
+
const source = node.source.value;
|
|
71
|
+
if (
|
|
72
|
+
restrictedSet.has(source) ||
|
|
73
|
+
source.startsWith("server-only") ||
|
|
74
|
+
source.includes("/server/") ||
|
|
75
|
+
source.endsWith("/server")
|
|
76
|
+
) {
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: "noServerImport",
|
|
80
|
+
data: { name: source },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default noServerImportsInClient;
|
package/templates/conditional/next/server-components/src/components/boundaries/ClientCounter.jsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Client Component example for RSC boundary.
|
|
7
|
+
* Marked with "use client" directive — renders on client with full interactivity,
|
|
8
|
+
* React hooks, and state management.
|
|
9
|
+
*/
|
|
10
|
+
export function ClientCounter({ initialCount = 0, children }) {
|
|
11
|
+
const [count, setCount] = useState(initialCount);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="client-boundary p-4 rounded-lg border border-blue-500/20 bg-blue-500/5 my-4">
|
|
15
|
+
<div className="flex items-center gap-2 mb-2">
|
|
16
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800 dark:bg-blue-900/60 dark:text-blue-300">
|
|
17
|
+
Client Component ("use client")
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
|
21
|
+
Interactive client boundary component. Manages state and handles user interaction.
|
|
22
|
+
</p>
|
|
23
|
+
<div className="flex items-center gap-3">
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={() => setCount((c) => c + 1)}
|
|
27
|
+
className="px-3 py-1.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors cursor-pointer"
|
|
28
|
+
>
|
|
29
|
+
Count: {count}
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={() => setCount(0)}
|
|
34
|
+
className="px-2 py-1.5 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 cursor-pointer"
|
|
35
|
+
>
|
|
36
|
+
Reset
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
{children && (
|
|
40
|
+
<div className="mt-4 pt-3 border-t border-blue-500/10">
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default ClientCounter;
|
package/templates/conditional/next/server-components/src/components/boundaries/ClientCounter.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
|
|
5
|
+
export interface ClientCounterProps {
|
|
6
|
+
initialCount?: number;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Client Component example for RSC boundary.
|
|
12
|
+
* Marked with "use client" directive — renders on client with full interactivity,
|
|
13
|
+
* React hooks, and state management.
|
|
14
|
+
*/
|
|
15
|
+
export function ClientCounter({ initialCount = 0, children }: ClientCounterProps) {
|
|
16
|
+
const [count, setCount] = useState(initialCount);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="client-boundary p-4 rounded-lg border border-blue-500/20 bg-blue-500/5 my-4">
|
|
20
|
+
<div className="flex items-center gap-2 mb-2">
|
|
21
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800 dark:bg-blue-900/60 dark:text-blue-300">
|
|
22
|
+
Client Component ("use client")
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
|
26
|
+
Interactive client boundary component. Manages state and handles user interaction.
|
|
27
|
+
</p>
|
|
28
|
+
<div className="flex items-center gap-3">
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={() => setCount((c) => c + 1)}
|
|
32
|
+
className="px-3 py-1.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors cursor-pointer"
|
|
33
|
+
>
|
|
34
|
+
Count: {count}
|
|
35
|
+
</button>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => setCount(0)}
|
|
39
|
+
className="px-2 py-1.5 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 cursor-pointer"
|
|
40
|
+
>
|
|
41
|
+
Reset
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
{children && (
|
|
45
|
+
<div className="mt-4 pt-3 border-t border-blue-500/10">
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default ClientCounter;
|