create-lumen 2.0.0-beta.1 → 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +23 -22
  2. package/bin/cli.js +1 -6
  3. package/package.json +2 -2
  4. package/schema/fixtures/v2/valid/next-app-router-feature-based.json +1 -1
  5. package/schema/fixtures/v2/valid/next-app-router-none.json +1 -1
  6. package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +1 -1
  7. package/schema/fixtures/v2/valid/next-pages-router-none.json +1 -1
  8. package/schema/lumen.config.v2.json +30 -2
  9. package/src/agent-context.js +71 -47
  10. package/src/cleanup.js +34 -1
  11. package/src/cli-args.js +79 -2
  12. package/src/configure.js +36 -8
  13. package/src/css.js +3 -4
  14. package/src/dependencies.js +42 -15
  15. package/src/engine/capabilities.js +51 -2
  16. package/src/engine/composition.js +282 -23
  17. package/src/env.js +159 -4
  18. package/src/format.js +14 -6
  19. package/src/injector.js +66 -14
  20. package/src/main.js +62 -17
  21. package/src/manifest/emit.js +35 -6
  22. package/src/manifest/paths.js +1 -25
  23. package/src/manifest/schema.js +32 -2
  24. package/src/prompts.js +101 -11
  25. package/src/readme.js +0 -16
  26. package/src/utils/dev-server.js +6 -1
  27. package/templates/.env.example +1 -1
  28. package/templates/architectures/feature-based/scripts/create-feature.mjs +2 -2
  29. package/templates/conditional/database/prisma/db.js +14 -0
  30. package/templates/conditional/database/prisma/db.ts +16 -0
  31. package/templates/conditional/database/prisma/schema.prisma +9 -0
  32. package/templates/conditional/database/prisma/seed.js +16 -0
  33. package/templates/conditional/database/prisma/seed.ts +16 -0
  34. package/templates/conditional/docker/.dockerignore +10 -0
  35. package/templates/conditional/docker/Dockerfile +57 -0
  36. package/templates/conditional/docker/docker-compose.yml +35 -0
  37. package/templates/conditional/linting/biome/biome.json +17 -11
  38. package/templates/conditional/linting/eslint/next/eslint.config.mjs +2 -11
  39. package/templates/conditional/next/route-groups/js/src/app/(admin)/layout.jsx +23 -0
  40. package/templates/conditional/next/route-groups/js/src/app/(auth)/layout.jsx +17 -0
  41. package/templates/conditional/next/route-groups/js/src/app/(private)/layout.jsx +27 -0
  42. package/templates/conditional/next/route-groups/js/src/app/(public)/layout.jsx +28 -0
  43. package/templates/conditional/next/route-groups/js/src/app/(public)/page.jsx +38 -0
  44. package/templates/conditional/next/route-groups/js/src/middleware.js +48 -0
  45. package/templates/conditional/next/route-groups/ts/src/app/(admin)/layout.tsx +28 -0
  46. package/templates/conditional/next/route-groups/ts/src/app/(auth)/layout.tsx +22 -0
  47. package/templates/conditional/next/route-groups/ts/src/app/(private)/layout.tsx +32 -0
  48. package/templates/conditional/next/route-groups/ts/src/app/(public)/layout.tsx +33 -0
  49. package/templates/conditional/next/route-groups/ts/src/app/(public)/page.tsx +38 -0
  50. package/templates/conditional/next/route-groups/ts/src/middleware.ts +49 -0
  51. package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.jsx +10 -0
  52. package/templates/conditional/testing/jest/next/app-router/src/__tests__/page.test.tsx +10 -0
  53. package/templates/conditional/testing/jest/next/jest.config.mjs +20 -0
  54. package/templates/conditional/testing/jest/next/jest.config.ts +20 -0
  55. package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.jsx +10 -0
  56. package/templates/conditional/testing/jest/next/pages-router/src/__tests__/index.test.tsx +10 -0
  57. package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.jsx +11 -0
  58. package/templates/conditional/testing/vitest/next/app-router/src/__tests__/page.test.tsx +11 -0
  59. package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.jsx +11 -0
  60. package/templates/conditional/testing/vitest/next/pages-router/src/__tests__/index.test.tsx +11 -0
  61. package/templates/conditional/testing/vitest/next/vitest.config.js +16 -0
  62. package/templates/conditional/testing/vitest/next/vitest.config.ts +16 -0
  63. package/templates/css/tailwind/postcss.config.mjs +3 -1
  64. package/schema/fixtures/v2/valid/next-app-router-hybrid.json +0 -35
  65. package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +0 -35
  66. package/templates/architectures/hybrid/main.jsx +0 -10
  67. package/templates/architectures/hybrid/main.tsx +0 -10
  68. package/templates/architectures/hybrid/src/App.jsx +0 -20
  69. package/templates/architectures/hybrid/src/App.tsx +0 -20
  70. package/templates/architectures/hybrid/src/features/.gitkeep +0 -1
  71. package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +0 -31
  72. package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +0 -36
  73. package/templates/architectures/hybrid/src/shared/components/ui/index.js +0 -1
  74. package/templates/architectures/hybrid/src/shared/components/ui/index.ts +0 -1
  75. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +0 -28
  76. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +0 -28
  77. package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
  78. package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
  79. package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
  80. package/templates/architectures/hybrid/src/shared/types/index.js +0 -20
  81. package/templates/architectures/hybrid/src/shared/types/index.ts +0 -16
  82. package/templates/architectures/hybrid/src/shared/utils/cn.jsx +0 -3
  83. package/templates/architectures/hybrid/src/shared/utils/cn.ts +0 -5
  84. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +0 -11
  85. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +0 -11
  86. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +0 -10
  87. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +0 -10
@@ -13,7 +13,7 @@ export const FRAMEWORK_BUNDLERS = ["turbopack", "webpack"];
13
13
  export const FRAMEWORK_ADAPTERS = ["node", "vercel", "cloudflare", "static"];
14
14
 
15
15
  export const STYLING_ENGINES = ["tailwind", "none"];
16
- export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "hybrid", "none"];
16
+ export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "none"];
17
17
  // Legacy alias: docs used architecture.type, canonical is architecture.preset.
18
18
  // Keep both in sync; parseManifest normalizes to expose both keys.
19
19
  export const ARCHITECTURE_TYPES = ARCHITECTURE_PRESETS;
@@ -23,6 +23,8 @@ export const DOCS_LANGUAGES = ["en", "es"];
23
23
  export const TOOLING_LANGUAGES = ["ts", "js"];
24
24
  export const TOOLING_LINTERS = ["eslint", "oxlint", "biome", "none"];
25
25
  export const TOOLING_FORMATTERS = ["prettier", "oxfmt", "none"];
26
+ export const DATABASE_ORMS = ["prisma", "none"];
27
+ export const DATABASE_PROVIDERS = ["postgresql", "supabase"];
26
28
 
27
29
  // ---------------------------------------------------------------------------
28
30
  // Sub-schemas
@@ -121,6 +123,13 @@ const toolingSchema = z
121
123
  })
122
124
  .strict();
123
125
 
126
+ const databaseSchema = z
127
+ .object({
128
+ orm: z.enum(DATABASE_ORMS),
129
+ provider: z.enum(DATABASE_PROVIDERS).optional().default("postgresql"),
130
+ })
131
+ .strict();
132
+
124
133
  // ---------------------------------------------------------------------------
125
134
  // Top-level manifest v2 - strict, no extra keys except $schema
126
135
  // ---------------------------------------------------------------------------
@@ -136,6 +145,9 @@ export const manifestSchemaV2 = z
136
145
  docs: docsSchema,
137
146
  paths: pathsSchema,
138
147
  tooling: toolingSchema,
148
+ database: databaseSchema.optional(),
149
+ docker: z.boolean().optional(),
150
+ routeGroups: z.boolean().optional(),
139
151
  reactCompiler: z.boolean().optional(),
140
152
  agentDocs: z.boolean().optional(),
141
153
  })
@@ -177,12 +189,30 @@ export const manifestSchemaV2 = z
177
189
  });
178
190
  }
179
191
 
192
+ // database only for Next.js (#62)
193
+ if (val.database !== undefined && val.database.orm !== "none" && framework.name !== "next") {
194
+ ctx.addIssue({
195
+ code: "custom",
196
+ path: ["database", "orm"],
197
+ message: `database is only valid when framework.name is "next"`,
198
+ });
199
+ }
200
+
201
+ // routeGroups only for Next.js App Router (#64)
202
+ if (val.routeGroups === true && (framework.name !== "next" || framework.variant !== "app-router")) {
203
+ ctx.addIssue({
204
+ code: "custom",
205
+ path: ["routeGroups"],
206
+ message: `routeGroups is only valid when framework is "next:app-router"`,
207
+ });
208
+ }
209
+
180
210
  // architecture scoping per framework (#34) — canonical `preset`, legacy `type` fallback
181
211
  const archValue = architecture.preset ?? architecture.type;
182
212
  const allowedByFramework =
183
213
  framework.name === "react"
184
214
  ? ["feature-based", "type-based", "none"]
185
- : ["feature-based", "hybrid", "none"];
215
+ : ["feature-based", "none"];
186
216
  if (!allowedByFramework.includes(archValue)) {
187
217
  ctx.addIssue({
188
218
  code: "custom",
package/src/prompts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isCancel, note, select } from "@clack/prompts";
1
+ import { isCancel, note, select } from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { configExists, loadConfig, saveConfig } from "@/config-cache.js";
4
4
  import {
@@ -24,8 +24,6 @@ export function formatConfig(responses) {
24
24
  ? "Feature-based"
25
25
  : responses.architecture === "type-based" || responses.architecture === "component-based"
26
26
  ? "Type-based"
27
- : responses.architecture === "hybrid"
28
- ? "Hybrid (shared + features)"
29
27
  : responses.architecture === "none"
30
28
  ? "None (flat src)"
31
29
  : responses.architecture;
@@ -71,9 +69,19 @@ export function formatConfig(responses) {
71
69
  if (responses.adapter && responses.adapter !== "none") {
72
70
  lines.push(`• ${chalk.bold("Adapter:")} ${chalk.cyan(responses.adapter)}`);
73
71
  }
72
+ if (responses.database && responses.database !== "none") {
73
+ const providerLabel = responses.databaseProvider === "supabase" ? " (Supabase)" : " (PostgreSQL)";
74
+ lines.push(`• ${chalk.bold("Database:")} ${chalk.cyan(responses.database === "prisma" ? `Prisma ORM${providerLabel}` : responses.database)}`);
75
+ }
74
76
  if (responses.reactCompiler !== undefined) {
75
77
  lines.push(`• ${chalk.bold("React Compiler:")} ${responses.reactCompiler ? chalk.green("Yes") : chalk.red("No")}`);
76
78
  }
79
+ if (responses.routeGroups !== undefined) {
80
+ lines.push(`• ${chalk.bold("Route Groups:")} ${responses.routeGroups ? chalk.green("Yes") : chalk.red("No")}`);
81
+ }
82
+ if (responses.docker !== undefined) {
83
+ lines.push(`• ${chalk.bold("Docker:")} ${responses.docker ? chalk.green("Yes") : chalk.red("No")}`);
84
+ }
77
85
  lines.push(
78
86
  `• ${chalk.bold("State:")} ${chalk.blue(state)}`,
79
87
  `• ${chalk.bold("Icons:")} ${chalk.blue(responses.iconLibrary === "none" ? "None" : responses.iconLibrary === "lucide" ? "Lucide" : "Huge")}`,
@@ -351,11 +359,6 @@ export async function getUserInputs(
351
359
  value: "type-based",
352
360
  hint: "Small apps / component libraries — code grouped by UI type",
353
361
  },
354
- {
355
- label: "Hybrid",
356
- value: "hybrid",
357
- hint: "Next.js: features + shared code",
358
- },
359
362
  {
360
363
  label: "None",
361
364
  value: "none",
@@ -410,7 +413,15 @@ export async function getUserInputs(
410
413
  { label: "Jest", value: "jest" },
411
414
  { label: "None", value: "none" },
412
415
  ],
413
- defaultInitial: "vitest",
416
+ defaultInitial: (ans) => {
417
+ const fw = normalizeFramework(
418
+ ans.framework || {
419
+ name: ans.frameworkName,
420
+ variant: ans.frameworkVariant,
421
+ }
422
+ );
423
+ return fw.name === "next" ? "none" : "vitest";
424
+ },
414
425
  },
415
426
  {
416
427
  id: "router",
@@ -456,6 +467,45 @@ export async function getUserInputs(
456
467
  ]),
457
468
  defaultInitial: "none",
458
469
  },
470
+ {
471
+ id: "database",
472
+ skip: () => !isPromptVisible(framework, "database"),
473
+ onSkip: (ans) => {
474
+ ans.database = "none";
475
+ },
476
+ message: "Which database ORM do you want to use?",
477
+ getOptions: () =>
478
+ filterCompatibleChoices(framework, "database", [
479
+ {
480
+ label: "Prisma ORM",
481
+ value: "prisma",
482
+ hint: "PostgreSQL schema, client singleton, seed script",
483
+ },
484
+ { label: "None", value: "none" },
485
+ ]),
486
+ defaultInitial: "none",
487
+ },
488
+ {
489
+ id: "databaseProvider",
490
+ skip: (ans) => !isPromptVisible(framework, "database") || ans.database !== "prisma",
491
+ onSkip: (ans) => {
492
+ ans.databaseProvider = ans.database === "prisma" ? (ans.databaseProvider || "postgresql") : undefined;
493
+ },
494
+ message: "Which database provider do you want to configure?",
495
+ getOptions: () => [
496
+ {
497
+ label: "PostgreSQL",
498
+ value: "postgresql",
499
+ hint: "Standard PostgreSQL / Docker compose",
500
+ },
501
+ {
502
+ label: "Supabase",
503
+ value: "supabase",
504
+ hint: "PostgreSQL with connection pooling & direct migration URL",
505
+ },
506
+ ],
507
+ defaultInitial: "postgresql",
508
+ },
459
509
  {
460
510
  id: "reactCompiler",
461
511
  skip: () => !isPromptVisible(framework, "reactCompiler"),
@@ -469,6 +519,32 @@ export async function getUserInputs(
469
519
  ],
470
520
  defaultInitial: false,
471
521
  },
522
+ {
523
+ id: "routeGroups",
524
+ skip: () => !isPromptVisible(framework, "routeGroups"),
525
+ onSkip: (ans) => {
526
+ ans.routeGroups = false;
527
+ },
528
+ message: "Include route groups preset (public, auth, private, admin)?",
529
+ getOptions: () => [
530
+ { label: "No (default)", value: false },
531
+ { label: "Yes", value: true },
532
+ ],
533
+ defaultInitial: false,
534
+ },
535
+ {
536
+ id: "docker",
537
+ skip: () => !isPromptVisible(framework, "docker"),
538
+ onSkip: (ans) => {
539
+ ans.docker = false;
540
+ },
541
+ message: "Include Docker setup?",
542
+ getOptions: () => [
543
+ { label: "No (default)", value: false },
544
+ { label: "Yes", value: true, hint: "Multi-stage Dockerfile and docker-compose" },
545
+ ],
546
+ defaultInitial: false,
547
+ },
472
548
  {
473
549
  id: "stateManagement",
474
550
  message: "Which state management library do you want to use?",
@@ -560,10 +636,10 @@ export async function getUserInputs(
560
636
  },
561
637
  {
562
638
  id: "agentDocs",
563
- message: "Generate .lumen/ agent context for AI assistants?",
639
+ message: "Generate AGENTS.md for AI assistants?",
564
640
  getOptions: () => [
565
641
  { label: "No (default)", value: false },
566
- { label: "Yes", value: true, hint: "Emits .lumen/ with project, architecture and conventions" },
642
+ { label: "Yes", value: true, hint: "Emits AGENTS.md referencing lumen.config.json (SSOT)" },
567
643
  ],
568
644
  defaultInitial: false,
569
645
  },
@@ -620,8 +696,12 @@ export async function getUserInputs(
620
696
  quickSetup: _quickSetup,
621
697
  bundler,
622
698
  adapter,
699
+ database,
700
+ databaseProvider,
623
701
  reactCompiler,
702
+ routeGroups,
624
703
  agentDocs,
704
+ docker,
625
705
  ...picked
626
706
  } = responses;
627
707
  // Final safety net: BACK-navigation can reselect the framework after other
@@ -638,12 +718,22 @@ export async function getUserInputs(
638
718
  if (adapter !== undefined && adapter !== "none") {
639
719
  finalResponses.adapter = adapter;
640
720
  }
721
+ if (database !== undefined && database !== "none") {
722
+ finalResponses.database = database;
723
+ finalResponses.databaseProvider = databaseProvider || "postgresql";
724
+ }
641
725
  if (reactCompiler !== undefined) {
642
726
  finalResponses.reactCompiler = reactCompiler;
643
727
  }
728
+ if (routeGroups !== undefined) {
729
+ finalResponses.routeGroups = routeGroups;
730
+ }
644
731
  if (agentDocs !== undefined) {
645
732
  finalResponses.agentDocs = agentDocs;
646
733
  }
734
+ if (docker !== undefined) {
735
+ finalResponses.docker = docker;
736
+ }
647
737
 
648
738
  // No config cache write until final confirm!
649
739
  await saveConfigFn(finalResponses);
package/src/readme.js CHANGED
@@ -117,22 +117,6 @@ ${nestedTree(appChildren)}
117
117
  ${nestedTree(shared)}`;
118
118
  }
119
119
 
120
- if (responses.architecture === "hybrid") {
121
- const shared = [
122
- "components/ # Shared UI components",
123
- "hooks/ # Shared custom hooks",
124
- "layouts/ # High-level layout wrappers",
125
- "stores/ # Global state stores",
126
- `styles/ # ${responses.cssFramework === "none" ? "main.css (reset) + themes.css (CSS variables)" : "globals.css (framework directives) + themes.css"}`,
127
- "types/ # Global type definitions",
128
- "utils/ # Pure utility functions",
129
- ];
130
- return `src/
131
- ├── features/ # Feature modules (domain boundaries)
132
- └── shared/ # Reusable, business-agnostic resources
133
- ${nestedTree(shared)}`;
134
- }
135
-
136
120
  if (responses.architecture === "none") {
137
121
  return `src/
138
122
  ├── App.${responses.language === "ts" ? "tsx" : "jsx"} # Root application component
@@ -2,7 +2,12 @@ import { isCancel, select } from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { execa } from "execa";
4
4
 
5
- export async function handleDevServer(pkg, projectName) {
5
+ export async function handleDevServer(pkg, projectName, { isNonInteractive = false } = {}) {
6
+ if (isNonInteractive || process.env.CI || !process.stdin.isTTY) {
7
+ printNextSteps(projectName, pkg);
8
+ return;
9
+ }
10
+
6
11
  const startDev = await select({
7
12
  message: "Start the development server?",
8
13
  options: [
@@ -1,4 +1,4 @@
1
1
  # Copy this file to `.env.local` (or `.env`) and fill in your values.
2
- # See https://vitejs.dev/guide/env-and-mode for how env vars are exposed.
2
+ # See https://vite.dev/guide/env-and-mode for how env vars are exposed.
3
3
 
4
4
  VITE_API_URL=https://api.example.com
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import fs from "fs";
4
- import path from "path";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
5
 
6
6
  // 1. Get feature name from CLI args
7
7
  const featureName = process.argv[2];
@@ -0,0 +1,14 @@
1
+ import { PrismaClient } from "@prisma/client";
2
+
3
+ const globalForPrisma = globalThis;
4
+
5
+ export const db =
6
+ globalForPrisma.prisma ||
7
+ new PrismaClient({
8
+ log:
9
+ process.env.NODE_ENV === "development"
10
+ ? ["query", "error", "warn"]
11
+ : ["error"],
12
+ });
13
+
14
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
@@ -0,0 +1,16 @@
1
+ import { PrismaClient } from "@prisma/client";
2
+
3
+ const globalForPrisma = globalThis as unknown as {
4
+ prisma: PrismaClient | undefined;
5
+ };
6
+
7
+ export const db =
8
+ globalForPrisma.prisma ??
9
+ new PrismaClient({
10
+ log:
11
+ process.env.NODE_ENV === "development"
12
+ ? ["query", "error", "warn"]
13
+ : ["error"],
14
+ });
15
+
16
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
@@ -0,0 +1,9 @@
1
+ datasource db {
2
+ provider = "postgresql"
3
+ url = env("DATABASE_URL")
4
+ directUrl = env("DIRECT_URL")
5
+ }
6
+
7
+ generator client {
8
+ provider = "prisma-client-js"
9
+ }
@@ -0,0 +1,16 @@
1
+ const { PrismaClient } = require("@prisma/client");
2
+
3
+ const prisma = new PrismaClient();
4
+
5
+ async function main() {
6
+ console.log("Database seeded successfully");
7
+ }
8
+
9
+ main()
10
+ .catch((e) => {
11
+ console.error(e);
12
+ process.exit(1);
13
+ })
14
+ .finally(async () => {
15
+ await prisma.$disconnect();
16
+ });
@@ -0,0 +1,16 @@
1
+ import { PrismaClient } from "@prisma/client";
2
+
3
+ const prisma = new PrismaClient();
4
+
5
+ async function main() {
6
+ console.log("Database seeded successfully");
7
+ }
8
+
9
+ main()
10
+ .catch((e) => {
11
+ console.error(e);
12
+ process.exit(1);
13
+ })
14
+ .finally(async () => {
15
+ await prisma.$disconnect();
16
+ });
@@ -0,0 +1,10 @@
1
+ node_modules
2
+ .next
3
+ .git
4
+ .gitignore
5
+ .env*
6
+ npm-debug.log*
7
+ yarn-debug.log*
8
+ yarn-error.log*
9
+ pnpm-debug.log*
10
+ README.md
@@ -0,0 +1,57 @@
1
+ FROM node:22-alpine AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+ # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
6
+ RUN apk add --no-cache libc6-compat
7
+ WORKDIR /app
8
+
9
+ # Install dependencies based on the preferred package manager
10
+ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* bun.lockb* bun.lock* ./
11
+ RUN \
12
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
13
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
14
+ elif [ -f package-lock.json ]; then npm ci; \
15
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then corepack enable bun && bun install --frozen-lockfile; \
16
+ else npm i; \
17
+ fi
18
+
19
+ # Rebuild the source code only when needed
20
+ FROM base AS builder
21
+ WORKDIR /app
22
+ COPY --from=deps /app/node_modules ./node_modules
23
+ COPY . .
24
+
25
+ # Next.js collects completely anonymous telemetry data about general usage.
26
+ # Learn more here: https://nextjs.org/telemetry
27
+ # Uncomment the following line in case you want to disable telemetry during the build.
28
+ ENV NEXT_TELEMETRY_DISABLED=1
29
+
30
+ RUN npm run build
31
+
32
+ # Production image, copy all the files and run next
33
+ FROM base AS runner
34
+ WORKDIR /app
35
+
36
+ ENV NODE_ENV=production
37
+ # Uncomment the following line in case you want to disable telemetry during runtime.
38
+ ENV NEXT_TELEMETRY_DISABLED=1
39
+
40
+ RUN addgroup --system --gid 1001 nodejs
41
+ RUN adduser --system --uid 1001 nextjs
42
+
43
+ COPY --chown=nextjs:nodejs public ./public
44
+
45
+ # Automatically leverage output traces to reduce image size
46
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
47
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
48
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
49
+
50
+ USER nextjs
51
+
52
+ EXPOSE 3000
53
+
54
+ ENV PORT=3000
55
+ ENV HOSTNAME="0.0.0.0"
56
+
57
+ CMD ["node", "server.js"]
@@ -0,0 +1,35 @@
1
+ services:
2
+ app:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ restart: always
7
+ ports:
8
+ - "3000:3000"
9
+ environment:
10
+ - NODE_ENV=production
11
+ - PORT=3000
12
+ - DATABASE_URL=postgresql://postgres:postgres@db:5432/app?schema=public
13
+ depends_on:
14
+ db:
15
+ condition: service_healthy
16
+
17
+ db:
18
+ image: postgres:16-alpine
19
+ restart: always
20
+ environment:
21
+ POSTGRES_USER: postgres
22
+ POSTGRES_PASSWORD: postgres
23
+ POSTGRES_DB: app
24
+ ports:
25
+ - "5432:5432"
26
+ volumes:
27
+ - postgres_data:/var/lib/postgresql/data
28
+ healthcheck:
29
+ test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
30
+ interval: 5s
31
+ timeout: 5s
32
+ retries: 5
33
+
34
+ volumes:
35
+ postgres_data:
@@ -1,31 +1,37 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
- "vcs": {
4
- "enabled": false,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
2
+ "$schema": "https://biomejs.dev/schemas/2.6.3/schema.json",
8
3
  "files": {
9
4
  "ignoreUnknown": false,
10
- "ignore": []
5
+ "includes": ["**", "!**/.next", "!**/dist", "!**/node_modules"]
11
6
  },
12
7
  "formatter": {
13
8
  "enabled": true,
14
9
  "indentStyle": "space",
15
10
  "indentWidth": 2
16
11
  },
17
- "organizeImports": {
18
- "enabled": true
19
- },
20
12
  "linter": {
21
13
  "enabled": true,
22
14
  "rules": {
23
- "recommended": true
15
+ "preset": "recommended"
24
16
  }
25
17
  },
26
18
  "javascript": {
27
19
  "formatter": {
28
20
  "quoteStyle": "double"
29
21
  }
22
+ },
23
+ "css": {
24
+ "parser": {
25
+ "tailwindDirectives": true
26
+ }
27
+ },
28
+ "assist": {
29
+ "enabled": true,
30
+ "actions": {
31
+ "source": {
32
+ "organizeImports": "on"
33
+ }
34
+ }
30
35
  }
31
36
  }
37
+
@@ -1,16 +1,7 @@
1
- import { dirname } from "path";
2
- import { fileURLToPath } from "url";
3
- import { FlatCompat } from "@eslint/eslintrc";
4
-
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = dirname(__filename);
7
-
8
- const compat = new FlatCompat({
9
- baseDirectory: __dirname,
10
- });
1
+ import nextConfig from "eslint-config-next";
11
2
 
12
3
  const eslintConfig = [
13
- ...compat.extends("next/core-web-vitals"),
4
+ ...nextConfig,
14
5
  ];
15
6
 
16
7
  export default eslintConfig;
@@ -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 &bull; 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 &copy; {new Date().getFullYear()}
25
+ </footer>
26
+ </div>
27
+ );
28
+ }