doo-boilerplate 0.1.0
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/dist/index.js +382 -0
- package/package.json +31 -0
- package/templates/template-nextjs/Dockerfile +29 -0
- package/templates/template-nextjs/README.md +215 -0
- package/templates/template-nextjs/_env.example +7 -0
- package/templates/template-nextjs/_gitignore +8 -0
- package/templates/template-nextjs/_prettierignore +4 -0
- package/templates/template-nextjs/_prettierrc +12 -0
- package/templates/template-nextjs/components.json +19 -0
- package/templates/template-nextjs/docker-compose.yml +12 -0
- package/templates/template-nextjs/docs/swagger/api.json +77 -0
- package/templates/template-nextjs/eslint.config.ts +25 -0
- package/templates/template-nextjs/generate/.gitkeep +0 -0
- package/templates/template-nextjs/knip.config.ts +8 -0
- package/templates/template-nextjs/messages/en.json +14 -0
- package/templates/template-nextjs/messages/vi.json +14 -0
- package/templates/template-nextjs/next.config.ts +13 -0
- package/templates/template-nextjs/optional/charts/deps.json +7 -0
- package/templates/template-nextjs/optional/dark-mode/deps.json +5 -0
- package/templates/template-nextjs/optional/dark-mode/files/providers/theme-provider.tsx +17 -0
- package/templates/template-nextjs/optional/dnd/deps.json +8 -0
- package/templates/template-nextjs/optional/editor/deps.json +10 -0
- package/templates/template-nextjs/optional/i18n/deps.json +5 -0
- package/templates/template-nextjs/optional/sentry/deps.json +5 -0
- package/templates/template-nextjs/package.json +82 -0
- package/templates/template-nextjs/postcss.config.js +1 -0
- package/templates/template-nextjs/public/images/.gitkeep +0 -0
- package/templates/template-nextjs/scripts/build-and-scan.sh +13 -0
- package/templates/template-nextjs/scripts/trivy-scan.sh +24 -0
- package/templates/template-nextjs/src/app/(auth)/layout.tsx +10 -0
- package/templates/template-nextjs/src/app/(auth)/sign-in/page.tsx +9 -0
- package/templates/template-nextjs/src/app/(dashboard)/layout.tsx +17 -0
- package/templates/template-nextjs/src/app/(dashboard)/page.tsx +27 -0
- package/templates/template-nextjs/src/app/(dashboard)/profile/page.tsx +39 -0
- package/templates/template-nextjs/src/app/(dashboard)/settings/page.tsx +15 -0
- package/templates/template-nextjs/src/app/api/health/route.ts +3 -0
- package/templates/template-nextjs/src/app/layout.tsx +22 -0
- package/templates/template-nextjs/src/app/not-found.tsx +18 -0
- package/templates/template-nextjs/src/app/page.tsx +5 -0
- package/templates/template-nextjs/src/components/common/error-boundary.tsx +56 -0
- package/templates/template-nextjs/src/components/common/loading-spinner.tsx +28 -0
- package/templates/template-nextjs/src/components/common/theme-toggle.tsx +21 -0
- package/templates/template-nextjs/src/components/layout/header.tsx +91 -0
- package/templates/template-nextjs/src/components/layout/page-layout.tsx +23 -0
- package/templates/template-nextjs/src/components/layout/sidebar.tsx +126 -0
- package/templates/template-nextjs/src/components/ui/avatar.tsx +45 -0
- package/templates/template-nextjs/src/components/ui/badge.tsx +31 -0
- package/templates/template-nextjs/src/components/ui/button.tsx +44 -0
- package/templates/template-nextjs/src/components/ui/card.tsx +55 -0
- package/templates/template-nextjs/src/components/ui/checkbox.tsx +26 -0
- package/templates/template-nextjs/src/components/ui/dialog.tsx +99 -0
- package/templates/template-nextjs/src/components/ui/dropdown-menu.tsx +180 -0
- package/templates/template-nextjs/src/components/ui/form.tsx +158 -0
- package/templates/template-nextjs/src/components/ui/input.tsx +24 -0
- package/templates/template-nextjs/src/components/ui/label.tsx +19 -0
- package/templates/template-nextjs/src/components/ui/scroll-area.tsx +40 -0
- package/templates/template-nextjs/src/components/ui/select.tsx +148 -0
- package/templates/template-nextjs/src/components/ui/separator.tsx +24 -0
- package/templates/template-nextjs/src/components/ui/sheet.tsx +96 -0
- package/templates/template-nextjs/src/components/ui/skeleton.tsx +7 -0
- package/templates/template-nextjs/src/components/ui/switch.tsx +27 -0
- package/templates/template-nextjs/src/components/ui/tabs.tsx +53 -0
- package/templates/template-nextjs/src/components/ui/tooltip.tsx +28 -0
- package/templates/template-nextjs/src/config/site.ts +5 -0
- package/templates/template-nextjs/src/features/auth/components/sign-in-form.tsx +93 -0
- package/templates/template-nextjs/src/features/auth/hooks/use-auth.ts +47 -0
- package/templates/template-nextjs/src/features/auth/schemas/auth-schema.ts +21 -0
- package/templates/template-nextjs/src/features/auth/services/auth-api.ts +30 -0
- package/templates/template-nextjs/src/hooks/use-media-query.ts +15 -0
- package/templates/template-nextjs/src/i18n/config.ts +3 -0
- package/templates/template-nextjs/src/lib/api-client.ts +43 -0
- package/templates/template-nextjs/src/lib/query-client.ts +17 -0
- package/templates/template-nextjs/src/lib/utils.ts +19 -0
- package/templates/template-nextjs/src/middleware.ts +23 -0
- package/templates/template-nextjs/src/providers/app-providers.tsx +18 -0
- package/templates/template-nextjs/src/providers/query-provider.tsx +16 -0
- package/templates/template-nextjs/src/providers/theme-provider.tsx +17 -0
- package/templates/template-nextjs/src/stores/auth-store.ts +82 -0
- package/templates/template-nextjs/src/styles/globals.css +65 -0
- package/templates/template-nextjs/src/types/index.ts +13 -0
- package/templates/template-nextjs/tsconfig.json +23 -0
- package/templates/template-vite/Dockerfile +20 -0
- package/templates/template-vite/README.md +241 -0
- package/templates/template-vite/_env.example +8 -0
- package/templates/template-vite/_gitignore +7 -0
- package/templates/template-vite/_prettierignore +4 -0
- package/templates/template-vite/_prettierrc +13 -0
- package/templates/template-vite/components.json +19 -0
- package/templates/template-vite/docker-compose.yml +11 -0
- package/templates/template-vite/docs/swagger/api.json +77 -0
- package/templates/template-vite/eslint.config.ts +30 -0
- package/templates/template-vite/generate/.gitkeep +0 -0
- package/templates/template-vite/index.html +13 -0
- package/templates/template-vite/knip.config.ts +8 -0
- package/templates/template-vite/nginx.conf +37 -0
- package/templates/template-vite/optional/charts/deps.json +7 -0
- package/templates/template-vite/optional/dark-mode/deps.json +5 -0
- package/templates/template-vite/optional/dnd/deps.json +8 -0
- package/templates/template-vite/optional/editor/deps.json +10 -0
- package/templates/template-vite/optional/i18n/deps.json +7 -0
- package/templates/template-vite/optional/sentry/deps.json +6 -0
- package/templates/template-vite/package.json +91 -0
- package/templates/template-vite/public/favicon.svg +5 -0
- package/templates/template-vite/scripts/build-and-scan.sh +13 -0
- package/templates/template-vite/scripts/trivy-scan.sh +24 -0
- package/templates/template-vite/src/components/common/error-boundary.tsx +51 -0
- package/templates/template-vite/src/components/common/loading-spinner.tsx +38 -0
- package/templates/template-vite/src/components/common/theme-toggle.tsx +19 -0
- package/templates/template-vite/src/components/layout/header.tsx +58 -0
- package/templates/template-vite/src/components/layout/page-layout.tsx +31 -0
- package/templates/template-vite/src/components/layout/sidebar.tsx +74 -0
- package/templates/template-vite/src/components/ui/avatar.tsx +45 -0
- package/templates/template-vite/src/components/ui/badge.tsx +31 -0
- package/templates/template-vite/src/components/ui/button.tsx +49 -0
- package/templates/template-vite/src/components/ui/card.tsx +56 -0
- package/templates/template-vite/src/components/ui/checkbox.tsx +26 -0
- package/templates/template-vite/src/components/ui/dialog.tsx +99 -0
- package/templates/template-vite/src/components/ui/dropdown-menu.tsx +174 -0
- package/templates/template-vite/src/components/ui/form.tsx +161 -0
- package/templates/template-vite/src/components/ui/input.tsx +24 -0
- package/templates/template-vite/src/components/ui/label.tsx +19 -0
- package/templates/template-vite/src/components/ui/scroll-area.tsx +44 -0
- package/templates/template-vite/src/components/ui/select.tsx +148 -0
- package/templates/template-vite/src/components/ui/separator.tsx +24 -0
- package/templates/template-vite/src/components/ui/sheet.tsx +118 -0
- package/templates/template-vite/src/components/ui/skeleton.tsx +7 -0
- package/templates/template-vite/src/components/ui/switch.tsx +27 -0
- package/templates/template-vite/src/components/ui/tabs.tsx +53 -0
- package/templates/template-vite/src/components/ui/tooltip.tsx +26 -0
- package/templates/template-vite/src/config/site.ts +5 -0
- package/templates/template-vite/src/context/theme-provider.tsx +10 -0
- package/templates/template-vite/src/features/auth/components/sign-in-form.tsx +86 -0
- package/templates/template-vite/src/features/auth/hooks/use-auth.ts +46 -0
- package/templates/template-vite/src/features/auth/schemas/auth-schema.ts +8 -0
- package/templates/template-vite/src/features/auth/services/auth-api.ts +24 -0
- package/templates/template-vite/src/hooks/use-media-query.ts +26 -0
- package/templates/template-vite/src/lib/api-client.ts +38 -0
- package/templates/template-vite/src/lib/i18n.ts +34 -0
- package/templates/template-vite/src/lib/query-client.ts +14 -0
- package/templates/template-vite/src/lib/utils.ts +28 -0
- package/templates/template-vite/src/main.tsx +37 -0
- package/templates/template-vite/src/routeTree.gen.ts +6 -0
- package/templates/template-vite/src/routes/(auth)/sign-in.tsx +17 -0
- package/templates/template-vite/src/routes/(errors)/404.tsx +19 -0
- package/templates/template-vite/src/routes/__root.tsx +17 -0
- package/templates/template-vite/src/routes/_authenticated/dashboard.tsx +22 -0
- package/templates/template-vite/src/routes/_authenticated/profile.tsx +47 -0
- package/templates/template-vite/src/routes/_authenticated/settings.tsx +17 -0
- package/templates/template-vite/src/routes/_authenticated.tsx +32 -0
- package/templates/template-vite/src/stores/auth-store.ts +56 -0
- package/templates/template-vite/src/styles/globals.css +81 -0
- package/templates/template-vite/src/types/index.ts +39 -0
- package/templates/template-vite/tsconfig.app.json +24 -0
- package/templates/template-vite/tsconfig.json +7 -0
- package/templates/template-vite/tsconfig.node.json +16 -0
- package/templates/template-vite/vite.config.ts +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import * as p4 from "@clack/prompts";
|
|
5
|
+
import pc3 from "picocolors";
|
|
6
|
+
import path5 from "path";
|
|
7
|
+
import fse4 from "fs-extra";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
|
|
10
|
+
// src/prompts.ts
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
|
|
14
|
+
// src/utils/validate.ts
|
|
15
|
+
import validatePkgName from "validate-npm-package-name";
|
|
16
|
+
function validateProjectName(name) {
|
|
17
|
+
if (name.includes("/") || name.includes("\\") || name.includes("..")) {
|
|
18
|
+
return "Project name cannot contain path separators";
|
|
19
|
+
}
|
|
20
|
+
const result = validatePkgName(name);
|
|
21
|
+
if (!result.validForNewPackages) {
|
|
22
|
+
return result.errors?.[0] ?? result.warnings?.[0] ?? "Invalid package name";
|
|
23
|
+
}
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/prompts.ts
|
|
28
|
+
async function collectOptions(defaults, isTTY2 = true) {
|
|
29
|
+
if (!isTTY2) {
|
|
30
|
+
const valid = ["editor", "charts", "dnd", "sentry"];
|
|
31
|
+
const validPMs2 = ["npm", "yarn", "pnpm", "bun", "deno"];
|
|
32
|
+
const projectName2 = defaults.projectName ?? "my-portal";
|
|
33
|
+
const framework2 = defaults.framework === "nextjs" || defaults.framework === "vite" ? defaults.framework : "vite";
|
|
34
|
+
const packageManager2 = validPMs2.includes(defaults.pm) ? defaults.pm : "pnpm";
|
|
35
|
+
const features2 = (defaults.features ?? []).filter((f) => valid.includes(f));
|
|
36
|
+
const auth2 = defaults.auth === "jwt" || defaults.auth === "oauth" || defaults.auth === "none" ? defaults.auth : "jwt";
|
|
37
|
+
return { projectName: projectName2, framework: framework2, packageManager: packageManager2, features: features2, auth: auth2, install: true };
|
|
38
|
+
}
|
|
39
|
+
let projectName;
|
|
40
|
+
if (defaults.projectName) {
|
|
41
|
+
const validation = validateProjectName(defaults.projectName);
|
|
42
|
+
if (validation !== void 0) {
|
|
43
|
+
p.cancel(`Invalid project name: ${validation}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
projectName = defaults.projectName;
|
|
47
|
+
} else {
|
|
48
|
+
const answer = await p.text({
|
|
49
|
+
message: "Project name",
|
|
50
|
+
placeholder: "my-portal",
|
|
51
|
+
defaultValue: "my-portal",
|
|
52
|
+
validate: validateProjectName
|
|
53
|
+
});
|
|
54
|
+
if (p.isCancel(answer)) {
|
|
55
|
+
p.cancel("Operation cancelled.");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
projectName = answer;
|
|
59
|
+
}
|
|
60
|
+
let framework;
|
|
61
|
+
if (defaults.framework === "nextjs" || defaults.framework === "vite") {
|
|
62
|
+
framework = defaults.framework;
|
|
63
|
+
} else {
|
|
64
|
+
const answer = await p.select({
|
|
65
|
+
message: "Framework",
|
|
66
|
+
options: [
|
|
67
|
+
{ value: "nextjs", label: "Next.js 16", hint: "SSR \xB7 App Router \xB7 i18n-ready" },
|
|
68
|
+
{ value: "vite", label: "Vite 7", hint: "SPA \xB7 faster builds \xB7 TanStack Router" }
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
if (p.isCancel(answer)) {
|
|
72
|
+
p.cancel("Operation cancelled.");
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
framework = answer;
|
|
76
|
+
}
|
|
77
|
+
const validPMs = ["npm", "yarn", "pnpm", "bun", "deno"];
|
|
78
|
+
let packageManager;
|
|
79
|
+
if (validPMs.includes(defaults.pm)) {
|
|
80
|
+
packageManager = defaults.pm;
|
|
81
|
+
} else {
|
|
82
|
+
const answer = await p.select({
|
|
83
|
+
message: "Package manager",
|
|
84
|
+
options: [
|
|
85
|
+
{ value: "npm", label: "npm", hint: "Node package manager" },
|
|
86
|
+
{ value: "yarn", label: "yarn", hint: "Fast, reliable" },
|
|
87
|
+
{ value: "pnpm", label: "pnpm", hint: "Efficient disk usage" },
|
|
88
|
+
{ value: "bun", label: "bun", hint: "All-in-one runtime" },
|
|
89
|
+
{ value: "deno", label: "deno", hint: "Secure by default" }
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
if (p.isCancel(answer)) {
|
|
93
|
+
p.cancel("Operation cancelled.");
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
packageManager = answer;
|
|
97
|
+
}
|
|
98
|
+
let features;
|
|
99
|
+
if (defaults.features && defaults.features.length > 0) {
|
|
100
|
+
const valid = ["editor", "charts", "dnd", "sentry"];
|
|
101
|
+
features = defaults.features.filter((f) => valid.includes(f));
|
|
102
|
+
} else {
|
|
103
|
+
const answer = await p.multiselect({
|
|
104
|
+
message: "Features " + pc.dim("(space to toggle, enter to confirm)"),
|
|
105
|
+
options: [
|
|
106
|
+
{ value: "editor", label: "Rich text editor", hint: "Tiptap" },
|
|
107
|
+
{ value: "charts", label: "Charts", hint: "ECharts" },
|
|
108
|
+
{ value: "dnd", label: "Drag & drop", hint: "@dnd-kit" },
|
|
109
|
+
{ value: "sentry", label: "Error tracking", hint: "Sentry" }
|
|
110
|
+
],
|
|
111
|
+
required: false
|
|
112
|
+
});
|
|
113
|
+
if (p.isCancel(answer)) {
|
|
114
|
+
p.cancel("Operation cancelled.");
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
features = answer;
|
|
118
|
+
}
|
|
119
|
+
let auth;
|
|
120
|
+
if (defaults.auth === "jwt" || defaults.auth === "oauth" || defaults.auth === "none") {
|
|
121
|
+
auth = defaults.auth;
|
|
122
|
+
} else {
|
|
123
|
+
const answer = await p.select({
|
|
124
|
+
message: "Auth pattern",
|
|
125
|
+
options: [
|
|
126
|
+
{ value: "jwt", label: "JWT tokens", hint: "access + refresh token flow" },
|
|
127
|
+
{ value: "oauth", label: "OAuth", hint: "placeholder, configure later" },
|
|
128
|
+
{ value: "none", label: "None", hint: "skip auth setup" }
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
if (p.isCancel(answer)) {
|
|
132
|
+
p.cancel("Operation cancelled.");
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
auth = answer;
|
|
136
|
+
}
|
|
137
|
+
const installAnswer = await p.confirm({
|
|
138
|
+
message: "Install dependencies now?",
|
|
139
|
+
initialValue: true
|
|
140
|
+
});
|
|
141
|
+
if (p.isCancel(installAnswer)) {
|
|
142
|
+
p.cancel("Operation cancelled.");
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
const install2 = installAnswer;
|
|
146
|
+
return { projectName, framework, packageManager, features, auth, install: install2 };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/scaffold.ts
|
|
150
|
+
import fse3 from "fs-extra";
|
|
151
|
+
import path3 from "path";
|
|
152
|
+
import { fileURLToPath } from "url";
|
|
153
|
+
|
|
154
|
+
// src/utils/package-json.ts
|
|
155
|
+
import fse from "fs-extra";
|
|
156
|
+
import path from "path";
|
|
157
|
+
async function mergeDepsIntoPackageJson(destDir, deps) {
|
|
158
|
+
const pkgPath = path.join(destDir, "package.json");
|
|
159
|
+
const pkg = await fse.readJson(pkgPath);
|
|
160
|
+
if (deps.dependencies) {
|
|
161
|
+
pkg.dependencies = { ...pkg.dependencies, ...deps.dependencies };
|
|
162
|
+
}
|
|
163
|
+
if (deps.devDependencies) {
|
|
164
|
+
pkg.devDependencies = { ...pkg.devDependencies, ...deps.devDependencies };
|
|
165
|
+
}
|
|
166
|
+
await fse.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/utils/template.ts
|
|
170
|
+
import fse2 from "fs-extra";
|
|
171
|
+
import path2 from "path";
|
|
172
|
+
var TEXT_EXTENSIONS = [
|
|
173
|
+
".ts",
|
|
174
|
+
".tsx",
|
|
175
|
+
".js",
|
|
176
|
+
".jsx",
|
|
177
|
+
".json",
|
|
178
|
+
".md",
|
|
179
|
+
".yml",
|
|
180
|
+
".yaml",
|
|
181
|
+
".env",
|
|
182
|
+
".css",
|
|
183
|
+
".html",
|
|
184
|
+
".txt"
|
|
185
|
+
];
|
|
186
|
+
async function getAllTextFiles(dir) {
|
|
187
|
+
const entries = await fse2.readdir(dir, { withFileTypes: true, recursive: true });
|
|
188
|
+
return entries.filter((e) => {
|
|
189
|
+
if (!e.isFile()) return false;
|
|
190
|
+
if (!TEXT_EXTENSIONS.some((ext) => e.name.endsWith(ext))) return false;
|
|
191
|
+
const parent = e.parentPath ?? e.path ?? dir;
|
|
192
|
+
const fullPath = path2.join(parent, e.name);
|
|
193
|
+
return !fullPath.includes("node_modules") && !fullPath.includes(`${path2.sep}optional${path2.sep}`);
|
|
194
|
+
}).map((e) => {
|
|
195
|
+
const parent = e.parentPath ?? e.path ?? dir;
|
|
196
|
+
return path2.join(parent, e.name);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function replaceInFiles(dir, replacements) {
|
|
200
|
+
const files = await getAllTextFiles(dir);
|
|
201
|
+
await Promise.all(
|
|
202
|
+
files.map(async (file) => {
|
|
203
|
+
let content = await fse2.readFile(file, "utf-8");
|
|
204
|
+
let changed = false;
|
|
205
|
+
for (const [from, to] of Object.entries(replacements)) {
|
|
206
|
+
if (content.includes(from)) {
|
|
207
|
+
content = content.replaceAll(from, to);
|
|
208
|
+
changed = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (changed) {
|
|
212
|
+
await fse2.writeFile(file, content, "utf-8");
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/scaffold.ts
|
|
219
|
+
var __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
|
|
220
|
+
var FILE_RENAMES = [
|
|
221
|
+
["_gitignore", ".gitignore"],
|
|
222
|
+
["_env.example", ".env.example"],
|
|
223
|
+
["_prettierrc", ".prettierrc"],
|
|
224
|
+
["_prettierignore", ".prettierignore"]
|
|
225
|
+
];
|
|
226
|
+
async function scaffold(options, destDir) {
|
|
227
|
+
const templateName = options.framework === "nextjs" ? "template-nextjs" : "template-vite";
|
|
228
|
+
const templateSymlink = path3.join(__dirname2, "..", "templates", templateName);
|
|
229
|
+
const templateDir = await fse3.realpath(templateSymlink);
|
|
230
|
+
await fse3.copy(templateDir, destDir, {
|
|
231
|
+
overwrite: true,
|
|
232
|
+
filter: (src) => !src.includes(`${path3.sep}optional${path3.sep}`) && src !== path3.join(templateDir, "optional")
|
|
233
|
+
});
|
|
234
|
+
for (const [from, to] of FILE_RENAMES) {
|
|
235
|
+
const fromPath = path3.join(destDir, from);
|
|
236
|
+
const toPath = path3.join(destDir, to);
|
|
237
|
+
if (await fse3.pathExists(fromPath)) {
|
|
238
|
+
await fse3.move(fromPath, toPath);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const feature of options.features) {
|
|
242
|
+
await applyFeature(feature, templateDir, destDir);
|
|
243
|
+
}
|
|
244
|
+
await replaceInFiles(destDir, {
|
|
245
|
+
"{{PROJECT_NAME}}": options.projectName,
|
|
246
|
+
"{{YEAR}}": (/* @__PURE__ */ new Date()).getFullYear().toString()
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async function applyFeature(feature, templateDir, destDir) {
|
|
250
|
+
const featureDir = path3.join(templateDir, "optional", feature);
|
|
251
|
+
if (!await fse3.pathExists(featureDir)) return;
|
|
252
|
+
const filesDir = path3.join(featureDir, "files");
|
|
253
|
+
if (await fse3.pathExists(filesDir)) {
|
|
254
|
+
await fse3.copy(filesDir, path3.join(destDir, "src"), { overwrite: true });
|
|
255
|
+
}
|
|
256
|
+
const depsFile = path3.join(featureDir, "deps.json");
|
|
257
|
+
if (await fse3.pathExists(depsFile)) {
|
|
258
|
+
const deps = await fse3.readJson(depsFile);
|
|
259
|
+
await mergeDepsIntoPackageJson(destDir, deps);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/install.ts
|
|
264
|
+
import { execa } from "execa";
|
|
265
|
+
import * as p2 from "@clack/prompts";
|
|
266
|
+
function getInstallArgs(pm) {
|
|
267
|
+
if (pm === "deno") return { cmd: "deno", args: ["install", "--node-modules-dir"] };
|
|
268
|
+
return { cmd: pm, args: ["install"] };
|
|
269
|
+
}
|
|
270
|
+
async function install(destDir, packageManager) {
|
|
271
|
+
const spinner3 = p2.spinner();
|
|
272
|
+
spinner3.start("Installing dependencies...");
|
|
273
|
+
try {
|
|
274
|
+
const { cmd, args } = getInstallArgs(packageManager);
|
|
275
|
+
await execa(cmd, args, { cwd: destDir });
|
|
276
|
+
spinner3.stop("Dependencies installed");
|
|
277
|
+
} catch (err) {
|
|
278
|
+
spinner3.stop("Installation failed");
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/git.ts
|
|
284
|
+
import { execa as execa2 } from "execa";
|
|
285
|
+
async function initGit(destDir) {
|
|
286
|
+
await execa2("git", ["init"], { cwd: destDir });
|
|
287
|
+
await execa2("git", ["add", "-A"], { cwd: destDir });
|
|
288
|
+
await execa2("git", ["commit", "-m", "chore: initial scaffold from create-pila-app"], {
|
|
289
|
+
cwd: destDir
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/post-setup.ts
|
|
294
|
+
import * as p3 from "@clack/prompts";
|
|
295
|
+
import pc2 from "picocolors";
|
|
296
|
+
import path4 from "path";
|
|
297
|
+
function printSuccess(options, destDir, isTTY2 = true) {
|
|
298
|
+
const relPath = path4.relative(process.cwd(), destDir);
|
|
299
|
+
const cdTarget = relPath || options.projectName;
|
|
300
|
+
const runCmd = `${options.packageManager} ${options.packageManager === "npm" ? "run dev" : "dev"}`;
|
|
301
|
+
const installLine = options.install ? "" : ` ${pc2.cyan(`${options.packageManager} install`)}
|
|
302
|
+
`;
|
|
303
|
+
const msg = `${pc2.green("\u2713")} Project created! Get started:
|
|
304
|
+
|
|
305
|
+
${pc2.cyan(`cd ${cdTarget}`)}
|
|
306
|
+
` + installLine + ` ${pc2.cyan(runCmd)}
|
|
307
|
+
|
|
308
|
+
${pc2.dim("Copy .env.example \u2192 .env and fill in your API URLs.")}`;
|
|
309
|
+
if (isTTY2) {
|
|
310
|
+
p3.outro(msg);
|
|
311
|
+
} else {
|
|
312
|
+
console.log("\n" + msg + "\n");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/cli.ts
|
|
317
|
+
var isTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
318
|
+
function log(msg) {
|
|
319
|
+
console.log(msg);
|
|
320
|
+
}
|
|
321
|
+
async function run() {
|
|
322
|
+
const program = new Command().name("create-pila-app").description("Scaffold a Pila portal frontend project").version("0.1.0").argument("[project-name]", "Name of the project").option("--framework <framework>", "nextjs or vite").option("--pm <pm>", "Package manager: npm, yarn, pnpm, bun, or deno").option("--features <features>", "Comma-separated: editor,charts,dnd,sentry").option("--auth <auth>", "jwt, oauth, or none").option("--no-install", "Skip dependency installation").option("--no-git", "Skip git initialization").parse(process.argv);
|
|
323
|
+
log("");
|
|
324
|
+
if (isTTY) {
|
|
325
|
+
p4.intro(pc3.bgCyan(pc3.black(" create-pila-app ")));
|
|
326
|
+
} else {
|
|
327
|
+
log(pc3.bgCyan(pc3.black(" create-pila-app ")));
|
|
328
|
+
}
|
|
329
|
+
const args = program.args;
|
|
330
|
+
const opts = program.opts();
|
|
331
|
+
const options = await collectOptions({
|
|
332
|
+
projectName: args[0],
|
|
333
|
+
framework: opts.framework,
|
|
334
|
+
pm: opts.pm,
|
|
335
|
+
features: opts.features?.split(","),
|
|
336
|
+
auth: opts.auth
|
|
337
|
+
}, isTTY);
|
|
338
|
+
if (!opts.install) {
|
|
339
|
+
options.install = false;
|
|
340
|
+
}
|
|
341
|
+
const destDir = path5.resolve(process.cwd(), options.projectName);
|
|
342
|
+
if (await fse4.pathExists(destDir)) {
|
|
343
|
+
const files = await fse4.readdir(destDir);
|
|
344
|
+
if (files.length > 0) {
|
|
345
|
+
const msg = `Directory "${options.projectName}" already exists and is not empty.`;
|
|
346
|
+
if (isTTY) p4.cancel(msg);
|
|
347
|
+
else log(pc3.red("\u2716 " + msg));
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
await fse4.ensureDir(destDir);
|
|
352
|
+
log(pc3.cyan("\u2192 Scaffolding project..."));
|
|
353
|
+
if (isTTY) {
|
|
354
|
+
const scaffoldSpinner = p4.spinner();
|
|
355
|
+
scaffoldSpinner.start("Scaffolding project...");
|
|
356
|
+
try {
|
|
357
|
+
await scaffold(options, destDir);
|
|
358
|
+
scaffoldSpinner.stop("Project scaffolded");
|
|
359
|
+
} catch (err) {
|
|
360
|
+
scaffoldSpinner.stop("Scaffolding failed");
|
|
361
|
+
throw err;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
await scaffold(options, destDir);
|
|
365
|
+
log(pc3.green("\u2713 Project scaffolded"));
|
|
366
|
+
}
|
|
367
|
+
if (options.install) {
|
|
368
|
+
await install(destDir, options.packageManager);
|
|
369
|
+
}
|
|
370
|
+
if (opts.git !== false) {
|
|
371
|
+
try {
|
|
372
|
+
await initGit(destDir);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
printSuccess(options, destDir, isTTY);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/index.ts
|
|
380
|
+
(async () => {
|
|
381
|
+
await run();
|
|
382
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "doo-boilerplate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to scaffold Pila portal frontend projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "create-pila-app": "./dist/index.js" },
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"files": ["dist", "templates"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"type-check": "tsc --noEmit",
|
|
13
|
+
"copy-templates": "node scripts/copy-templates.mjs",
|
|
14
|
+
"prepublishOnly": "node scripts/copy-templates.mjs && tsup"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@clack/prompts": "^0.9.1",
|
|
18
|
+
"commander": "^13.0.0",
|
|
19
|
+
"execa": "^9.5.2",
|
|
20
|
+
"fs-extra": "^11.2.0",
|
|
21
|
+
"picocolors": "^1.1.1",
|
|
22
|
+
"validate-npm-package-name": "^6.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/fs-extra": "^11.0.4",
|
|
26
|
+
"@types/node": "^22",
|
|
27
|
+
"@types/validate-npm-package-name": "^4.0.2",
|
|
28
|
+
"tsup": "^8.3.5",
|
|
29
|
+
"typescript": "^5.7.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Stage 1: Dependencies
|
|
2
|
+
FROM node:22-alpine AS deps
|
|
3
|
+
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY package.json pnpm-lock.yaml ./
|
|
6
|
+
RUN pnpm install --frozen-lockfile
|
|
7
|
+
|
|
8
|
+
# Stage 2: Builder
|
|
9
|
+
FROM node:22-alpine AS builder
|
|
10
|
+
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
13
|
+
COPY . .
|
|
14
|
+
ARG NEXT_PUBLIC_API_URL
|
|
15
|
+
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
|
16
|
+
RUN pnpm build
|
|
17
|
+
|
|
18
|
+
# Stage 3: Runner
|
|
19
|
+
FROM node:22-alpine AS runner
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
ENV NODE_ENV=production
|
|
22
|
+
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
|
23
|
+
COPY --from=builder /app/public ./public
|
|
24
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
25
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
26
|
+
USER nextjs
|
|
27
|
+
EXPOSE 3000
|
|
28
|
+
ENV PORT=3000 HOSTNAME="0.0.0.0"
|
|
29
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
> Next.js 16 server-side rendered portal frontend
|
|
4
|
+
|
|
5
|
+
Production-ready Next.js 16 application with App Router, i18n, dark mode, authentication, and DevOps integration.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
|
14
|
+
|
|
15
|
+
## Available Scripts
|
|
16
|
+
|
|
17
|
+
| Command | Purpose |
|
|
18
|
+
|---------|---------|
|
|
19
|
+
| `pnpm dev` | Start dev server with Turbopack |
|
|
20
|
+
| `pnpm build` | Build for production |
|
|
21
|
+
| `pnpm start` | Run production server |
|
|
22
|
+
| `pnpm lint` | Run ESLint |
|
|
23
|
+
| `pnpm lint:fix` | Fix linting issues |
|
|
24
|
+
| `pnpm type-check` | TypeScript type checking |
|
|
25
|
+
| `pnpm format` | Format code with Prettier |
|
|
26
|
+
| `pnpm format:check` | Check formatting without changes |
|
|
27
|
+
| `pnpm knip` | Find unused files/exports |
|
|
28
|
+
| `pnpm gen:api` | Generate types from Swagger spec |
|
|
29
|
+
| `pnpm gen:api:watch` | Watch mode for API generation |
|
|
30
|
+
| `pnpm docker:build` | Build & scan Docker image |
|
|
31
|
+
| `pnpm docker:scan` | Security scan image with Trivy |
|
|
32
|
+
|
|
33
|
+
## Tech Stack
|
|
34
|
+
|
|
35
|
+
- **Framework:** Next.js 16 with App Router
|
|
36
|
+
- **Styling:** Tailwind CSS 4 + Shadcn/ui
|
|
37
|
+
- **i18n:** next-intl (EN/VI built-in)
|
|
38
|
+
- **State:** Zustand 5 (global) + TanStack Query 5 (server)
|
|
39
|
+
- **Forms:** React Hook Form + Zod
|
|
40
|
+
- **HTTP:** Axios with JWT interceptors
|
|
41
|
+
- **Auth:** JWT store with refresh token persistence
|
|
42
|
+
- **DevOps:** Docker (multi-stage) + Trivy security scan
|
|
43
|
+
|
|
44
|
+
## Environment Variables
|
|
45
|
+
|
|
46
|
+
Copy `.env.example` to `.env`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cp .env.example .env
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Key variables:
|
|
53
|
+
|
|
54
|
+
```env
|
|
55
|
+
# API Configuration
|
|
56
|
+
NEXT_PUBLIC_API_URL=http://localhost:8000
|
|
57
|
+
NEXT_PUBLIC_API_AUTH_URL=http://localhost:8001
|
|
58
|
+
|
|
59
|
+
# App
|
|
60
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
61
|
+
NEXT_PUBLIC_APP_NAME={{PROJECT_NAME}}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Project Structure
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
src/
|
|
68
|
+
├── app/
|
|
69
|
+
│ ├── (auth)/ # Auth layout group (login, register)
|
|
70
|
+
│ ├── (dashboard)/ # Protected layout group
|
|
71
|
+
│ ├── layout.tsx # Root layout
|
|
72
|
+
│ └── page.tsx # Homepage
|
|
73
|
+
├── components/
|
|
74
|
+
│ ├── ui/ # Shadcn/ui components
|
|
75
|
+
│ └── **/ # Feature-specific components
|
|
76
|
+
├── features/
|
|
77
|
+
│ ├── auth/
|
|
78
|
+
│ │ ├── hooks/
|
|
79
|
+
│ │ ├── stores/ # Zustand auth store
|
|
80
|
+
│ │ ├── services/
|
|
81
|
+
│ │ │ └── gen/ # Generated API types (gen:api)
|
|
82
|
+
│ │ └── types/
|
|
83
|
+
│ └── **/
|
|
84
|
+
├── hooks/ # Custom React hooks
|
|
85
|
+
├── i18n/
|
|
86
|
+
│ ├── request.ts # Server-side i18n setup
|
|
87
|
+
│ ├── routing.ts # Route configuration
|
|
88
|
+
│ └── translations/ # Message files
|
|
89
|
+
├── lib/
|
|
90
|
+
│ ├── cn.ts # Tailwind merge utility
|
|
91
|
+
│ ├── http.ts # Axios instance
|
|
92
|
+
│ └── **/
|
|
93
|
+
├── middleware.ts # next-intl middleware
|
|
94
|
+
├── providers/
|
|
95
|
+
│ ├── ClientProvider.tsx # Client-side providers
|
|
96
|
+
│ └── ThemeProvider.tsx # next-themes setup
|
|
97
|
+
├── stores/ # Zustand stores
|
|
98
|
+
├── styles/
|
|
99
|
+
│ ├── globals.css
|
|
100
|
+
│ └── variables.css # CSS variables
|
|
101
|
+
└── types/
|
|
102
|
+
└── index.ts
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## API Code Generation
|
|
106
|
+
|
|
107
|
+
Generate TypeScript types from backend Swagger spec:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Place spec at docs/swagger/api.json
|
|
111
|
+
curl https://api.example.com/swagger.json > docs/swagger/api.json
|
|
112
|
+
|
|
113
|
+
# Generate types
|
|
114
|
+
pnpm gen:api
|
|
115
|
+
|
|
116
|
+
# Watch mode during API development
|
|
117
|
+
pnpm gen:api:watch
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Generated types appear in `src/features/auth/services/gen/`.
|
|
121
|
+
|
|
122
|
+
## Docker Deployment
|
|
123
|
+
|
|
124
|
+
### Build & Scan
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm docker:build
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Builds optimized multi-stage image and scans for vulnerabilities with Trivy.
|
|
131
|
+
|
|
132
|
+
### Local Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
docker-compose up
|
|
136
|
+
# App runs on http://localhost:3000
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Environment variables read from `.env` file.
|
|
140
|
+
|
|
141
|
+
### Production Build
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
docker build -t my-app:latest \
|
|
145
|
+
--build-arg NEXT_PUBLIC_API_URL=https://api.example.com .
|
|
146
|
+
|
|
147
|
+
docker run -p 3000:3000 \
|
|
148
|
+
-e NEXT_PUBLIC_API_URL=https://api.example.com \
|
|
149
|
+
my-app:latest
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Next.js App Router Basics
|
|
153
|
+
|
|
154
|
+
### Routing
|
|
155
|
+
|
|
156
|
+
Routes defined by file structure in `src/app/`:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
app/
|
|
160
|
+
├── page.tsx → /
|
|
161
|
+
├── about/
|
|
162
|
+
│ └── page.tsx → /about
|
|
163
|
+
└── (auth)/
|
|
164
|
+
├── login/
|
|
165
|
+
│ └── page.tsx → /login
|
|
166
|
+
└── register/
|
|
167
|
+
└── page.tsx → /register
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Parentheses `(auth)` create layout groups without affecting URL.
|
|
171
|
+
|
|
172
|
+
### Server & Client Components
|
|
173
|
+
|
|
174
|
+
By default, all components are **Server Components** (run on server):
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// src/app/page.tsx (Server Component)
|
|
178
|
+
export default function Home() {
|
|
179
|
+
return <h1>SSR rendered on server</h1>
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
For client-side logic, add `'use client'`:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// src/components/Counter.tsx (Client Component)
|
|
187
|
+
'use client'
|
|
188
|
+
|
|
189
|
+
import { useState } from 'react'
|
|
190
|
+
|
|
191
|
+
export function Counter() {
|
|
192
|
+
const [count, setCount] = useState(0)
|
|
193
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Layouts
|
|
198
|
+
|
|
199
|
+
Create shared layouts for route groups:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
// src/app/(dashboard)/layout.tsx
|
|
203
|
+
export default function DashboardLayout({ children }) {
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
<nav>Sidebar</nav>
|
|
207
|
+
<main>{children}</main>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
**Learn more:** [Next.js Docs](https://nextjs.org/docs)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"semi": false,
|
|
3
|
+
"singleQuote": true,
|
|
4
|
+
"trailingComma": "none",
|
|
5
|
+
"printWidth": 80,
|
|
6
|
+
"tabWidth": 2,
|
|
7
|
+
"endOfLine": "lf",
|
|
8
|
+
"plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
|
|
9
|
+
"importOrder": ["^(react|next)(.*)", "^@/", "^\\."],
|
|
10
|
+
"importOrderSeparation": true,
|
|
11
|
+
"importOrderSortSpecifiers": true
|
|
12
|
+
}
|