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.
- package/README.md +23 -22
- package/bin/cli.js +1 -6
- package/package.json +2 -2
- package/schema/fixtures/v2/valid/next-app-router-feature-based.json +1 -1
- package/schema/fixtures/v2/valid/next-app-router-none.json +1 -1
- package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +1 -1
- package/schema/fixtures/v2/valid/next-pages-router-none.json +1 -1
- package/schema/lumen.config.v2.json +30 -2
- package/src/agent-context.js +71 -47
- package/src/cleanup.js +34 -1
- package/src/cli-args.js +79 -2
- package/src/configure.js +36 -8
- package/src/css.js +3 -4
- package/src/dependencies.js +42 -15
- package/src/engine/capabilities.js +51 -2
- package/src/engine/composition.js +282 -23
- package/src/env.js +159 -4
- package/src/format.js +14 -6
- package/src/injector.js +66 -14
- package/src/main.js +62 -17
- package/src/manifest/emit.js +35 -6
- package/src/manifest/paths.js +1 -25
- package/src/manifest/schema.js +32 -2
- package/src/prompts.js +101 -11
- package/src/readme.js +0 -16
- package/src/utils/dev-server.js +6 -1
- package/templates/.env.example +1 -1
- package/templates/architectures/feature-based/scripts/create-feature.mjs +2 -2
- 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 +17 -11
- package/templates/conditional/linting/eslint/next/eslint.config.mjs +2 -11
- 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/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
- package/templates/css/tailwind/postcss.config.mjs +3 -1
- package/schema/fixtures/v2/valid/next-app-router-hybrid.json +0 -35
- package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +0 -35
- package/templates/architectures/hybrid/main.jsx +0 -10
- package/templates/architectures/hybrid/main.tsx +0 -10
- package/templates/architectures/hybrid/src/App.jsx +0 -20
- package/templates/architectures/hybrid/src/App.tsx +0 -20
- package/templates/architectures/hybrid/src/features/.gitkeep +0 -1
- package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +0 -31
- package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +0 -36
- package/templates/architectures/hybrid/src/shared/components/ui/index.js +0 -1
- package/templates/architectures/hybrid/src/shared/components/ui/index.ts +0 -1
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +0 -28
- package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +0 -28
- 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 +0 -20
- package/templates/architectures/hybrid/src/shared/types/index.ts +0 -16
- package/templates/architectures/hybrid/src/shared/utils/cn.jsx +0 -3
- package/templates/architectures/hybrid/src/shared/utils/cn.ts +0 -5
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +0 -11
- package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +0 -11
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +0 -10
- package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +0 -10
package/src/manifest/schema.js
CHANGED
|
@@ -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", "
|
|
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", "
|
|
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
|
-
|
|
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:
|
|
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 .
|
|
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 .
|
|
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
|
package/src/utils/dev-server.js
CHANGED
|
@@ -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: [
|
package/templates/.env.example
CHANGED
|
@@ -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,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,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/
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
...
|
|
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 • 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
|
+
}
|