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.
Files changed (156) hide show
  1. package/dist/index.js +382 -0
  2. package/package.json +31 -0
  3. package/templates/template-nextjs/Dockerfile +29 -0
  4. package/templates/template-nextjs/README.md +215 -0
  5. package/templates/template-nextjs/_env.example +7 -0
  6. package/templates/template-nextjs/_gitignore +8 -0
  7. package/templates/template-nextjs/_prettierignore +4 -0
  8. package/templates/template-nextjs/_prettierrc +12 -0
  9. package/templates/template-nextjs/components.json +19 -0
  10. package/templates/template-nextjs/docker-compose.yml +12 -0
  11. package/templates/template-nextjs/docs/swagger/api.json +77 -0
  12. package/templates/template-nextjs/eslint.config.ts +25 -0
  13. package/templates/template-nextjs/generate/.gitkeep +0 -0
  14. package/templates/template-nextjs/knip.config.ts +8 -0
  15. package/templates/template-nextjs/messages/en.json +14 -0
  16. package/templates/template-nextjs/messages/vi.json +14 -0
  17. package/templates/template-nextjs/next.config.ts +13 -0
  18. package/templates/template-nextjs/optional/charts/deps.json +7 -0
  19. package/templates/template-nextjs/optional/dark-mode/deps.json +5 -0
  20. package/templates/template-nextjs/optional/dark-mode/files/providers/theme-provider.tsx +17 -0
  21. package/templates/template-nextjs/optional/dnd/deps.json +8 -0
  22. package/templates/template-nextjs/optional/editor/deps.json +10 -0
  23. package/templates/template-nextjs/optional/i18n/deps.json +5 -0
  24. package/templates/template-nextjs/optional/sentry/deps.json +5 -0
  25. package/templates/template-nextjs/package.json +82 -0
  26. package/templates/template-nextjs/postcss.config.js +1 -0
  27. package/templates/template-nextjs/public/images/.gitkeep +0 -0
  28. package/templates/template-nextjs/scripts/build-and-scan.sh +13 -0
  29. package/templates/template-nextjs/scripts/trivy-scan.sh +24 -0
  30. package/templates/template-nextjs/src/app/(auth)/layout.tsx +10 -0
  31. package/templates/template-nextjs/src/app/(auth)/sign-in/page.tsx +9 -0
  32. package/templates/template-nextjs/src/app/(dashboard)/layout.tsx +17 -0
  33. package/templates/template-nextjs/src/app/(dashboard)/page.tsx +27 -0
  34. package/templates/template-nextjs/src/app/(dashboard)/profile/page.tsx +39 -0
  35. package/templates/template-nextjs/src/app/(dashboard)/settings/page.tsx +15 -0
  36. package/templates/template-nextjs/src/app/api/health/route.ts +3 -0
  37. package/templates/template-nextjs/src/app/layout.tsx +22 -0
  38. package/templates/template-nextjs/src/app/not-found.tsx +18 -0
  39. package/templates/template-nextjs/src/app/page.tsx +5 -0
  40. package/templates/template-nextjs/src/components/common/error-boundary.tsx +56 -0
  41. package/templates/template-nextjs/src/components/common/loading-spinner.tsx +28 -0
  42. package/templates/template-nextjs/src/components/common/theme-toggle.tsx +21 -0
  43. package/templates/template-nextjs/src/components/layout/header.tsx +91 -0
  44. package/templates/template-nextjs/src/components/layout/page-layout.tsx +23 -0
  45. package/templates/template-nextjs/src/components/layout/sidebar.tsx +126 -0
  46. package/templates/template-nextjs/src/components/ui/avatar.tsx +45 -0
  47. package/templates/template-nextjs/src/components/ui/badge.tsx +31 -0
  48. package/templates/template-nextjs/src/components/ui/button.tsx +44 -0
  49. package/templates/template-nextjs/src/components/ui/card.tsx +55 -0
  50. package/templates/template-nextjs/src/components/ui/checkbox.tsx +26 -0
  51. package/templates/template-nextjs/src/components/ui/dialog.tsx +99 -0
  52. package/templates/template-nextjs/src/components/ui/dropdown-menu.tsx +180 -0
  53. package/templates/template-nextjs/src/components/ui/form.tsx +158 -0
  54. package/templates/template-nextjs/src/components/ui/input.tsx +24 -0
  55. package/templates/template-nextjs/src/components/ui/label.tsx +19 -0
  56. package/templates/template-nextjs/src/components/ui/scroll-area.tsx +40 -0
  57. package/templates/template-nextjs/src/components/ui/select.tsx +148 -0
  58. package/templates/template-nextjs/src/components/ui/separator.tsx +24 -0
  59. package/templates/template-nextjs/src/components/ui/sheet.tsx +96 -0
  60. package/templates/template-nextjs/src/components/ui/skeleton.tsx +7 -0
  61. package/templates/template-nextjs/src/components/ui/switch.tsx +27 -0
  62. package/templates/template-nextjs/src/components/ui/tabs.tsx +53 -0
  63. package/templates/template-nextjs/src/components/ui/tooltip.tsx +28 -0
  64. package/templates/template-nextjs/src/config/site.ts +5 -0
  65. package/templates/template-nextjs/src/features/auth/components/sign-in-form.tsx +93 -0
  66. package/templates/template-nextjs/src/features/auth/hooks/use-auth.ts +47 -0
  67. package/templates/template-nextjs/src/features/auth/schemas/auth-schema.ts +21 -0
  68. package/templates/template-nextjs/src/features/auth/services/auth-api.ts +30 -0
  69. package/templates/template-nextjs/src/hooks/use-media-query.ts +15 -0
  70. package/templates/template-nextjs/src/i18n/config.ts +3 -0
  71. package/templates/template-nextjs/src/lib/api-client.ts +43 -0
  72. package/templates/template-nextjs/src/lib/query-client.ts +17 -0
  73. package/templates/template-nextjs/src/lib/utils.ts +19 -0
  74. package/templates/template-nextjs/src/middleware.ts +23 -0
  75. package/templates/template-nextjs/src/providers/app-providers.tsx +18 -0
  76. package/templates/template-nextjs/src/providers/query-provider.tsx +16 -0
  77. package/templates/template-nextjs/src/providers/theme-provider.tsx +17 -0
  78. package/templates/template-nextjs/src/stores/auth-store.ts +82 -0
  79. package/templates/template-nextjs/src/styles/globals.css +65 -0
  80. package/templates/template-nextjs/src/types/index.ts +13 -0
  81. package/templates/template-nextjs/tsconfig.json +23 -0
  82. package/templates/template-vite/Dockerfile +20 -0
  83. package/templates/template-vite/README.md +241 -0
  84. package/templates/template-vite/_env.example +8 -0
  85. package/templates/template-vite/_gitignore +7 -0
  86. package/templates/template-vite/_prettierignore +4 -0
  87. package/templates/template-vite/_prettierrc +13 -0
  88. package/templates/template-vite/components.json +19 -0
  89. package/templates/template-vite/docker-compose.yml +11 -0
  90. package/templates/template-vite/docs/swagger/api.json +77 -0
  91. package/templates/template-vite/eslint.config.ts +30 -0
  92. package/templates/template-vite/generate/.gitkeep +0 -0
  93. package/templates/template-vite/index.html +13 -0
  94. package/templates/template-vite/knip.config.ts +8 -0
  95. package/templates/template-vite/nginx.conf +37 -0
  96. package/templates/template-vite/optional/charts/deps.json +7 -0
  97. package/templates/template-vite/optional/dark-mode/deps.json +5 -0
  98. package/templates/template-vite/optional/dnd/deps.json +8 -0
  99. package/templates/template-vite/optional/editor/deps.json +10 -0
  100. package/templates/template-vite/optional/i18n/deps.json +7 -0
  101. package/templates/template-vite/optional/sentry/deps.json +6 -0
  102. package/templates/template-vite/package.json +91 -0
  103. package/templates/template-vite/public/favicon.svg +5 -0
  104. package/templates/template-vite/scripts/build-and-scan.sh +13 -0
  105. package/templates/template-vite/scripts/trivy-scan.sh +24 -0
  106. package/templates/template-vite/src/components/common/error-boundary.tsx +51 -0
  107. package/templates/template-vite/src/components/common/loading-spinner.tsx +38 -0
  108. package/templates/template-vite/src/components/common/theme-toggle.tsx +19 -0
  109. package/templates/template-vite/src/components/layout/header.tsx +58 -0
  110. package/templates/template-vite/src/components/layout/page-layout.tsx +31 -0
  111. package/templates/template-vite/src/components/layout/sidebar.tsx +74 -0
  112. package/templates/template-vite/src/components/ui/avatar.tsx +45 -0
  113. package/templates/template-vite/src/components/ui/badge.tsx +31 -0
  114. package/templates/template-vite/src/components/ui/button.tsx +49 -0
  115. package/templates/template-vite/src/components/ui/card.tsx +56 -0
  116. package/templates/template-vite/src/components/ui/checkbox.tsx +26 -0
  117. package/templates/template-vite/src/components/ui/dialog.tsx +99 -0
  118. package/templates/template-vite/src/components/ui/dropdown-menu.tsx +174 -0
  119. package/templates/template-vite/src/components/ui/form.tsx +161 -0
  120. package/templates/template-vite/src/components/ui/input.tsx +24 -0
  121. package/templates/template-vite/src/components/ui/label.tsx +19 -0
  122. package/templates/template-vite/src/components/ui/scroll-area.tsx +44 -0
  123. package/templates/template-vite/src/components/ui/select.tsx +148 -0
  124. package/templates/template-vite/src/components/ui/separator.tsx +24 -0
  125. package/templates/template-vite/src/components/ui/sheet.tsx +118 -0
  126. package/templates/template-vite/src/components/ui/skeleton.tsx +7 -0
  127. package/templates/template-vite/src/components/ui/switch.tsx +27 -0
  128. package/templates/template-vite/src/components/ui/tabs.tsx +53 -0
  129. package/templates/template-vite/src/components/ui/tooltip.tsx +26 -0
  130. package/templates/template-vite/src/config/site.ts +5 -0
  131. package/templates/template-vite/src/context/theme-provider.tsx +10 -0
  132. package/templates/template-vite/src/features/auth/components/sign-in-form.tsx +86 -0
  133. package/templates/template-vite/src/features/auth/hooks/use-auth.ts +46 -0
  134. package/templates/template-vite/src/features/auth/schemas/auth-schema.ts +8 -0
  135. package/templates/template-vite/src/features/auth/services/auth-api.ts +24 -0
  136. package/templates/template-vite/src/hooks/use-media-query.ts +26 -0
  137. package/templates/template-vite/src/lib/api-client.ts +38 -0
  138. package/templates/template-vite/src/lib/i18n.ts +34 -0
  139. package/templates/template-vite/src/lib/query-client.ts +14 -0
  140. package/templates/template-vite/src/lib/utils.ts +28 -0
  141. package/templates/template-vite/src/main.tsx +37 -0
  142. package/templates/template-vite/src/routeTree.gen.ts +6 -0
  143. package/templates/template-vite/src/routes/(auth)/sign-in.tsx +17 -0
  144. package/templates/template-vite/src/routes/(errors)/404.tsx +19 -0
  145. package/templates/template-vite/src/routes/__root.tsx +17 -0
  146. package/templates/template-vite/src/routes/_authenticated/dashboard.tsx +22 -0
  147. package/templates/template-vite/src/routes/_authenticated/profile.tsx +47 -0
  148. package/templates/template-vite/src/routes/_authenticated/settings.tsx +17 -0
  149. package/templates/template-vite/src/routes/_authenticated.tsx +32 -0
  150. package/templates/template-vite/src/stores/auth-store.ts +56 -0
  151. package/templates/template-vite/src/styles/globals.css +81 -0
  152. package/templates/template-vite/src/types/index.ts +39 -0
  153. package/templates/template-vite/tsconfig.app.json +24 -0
  154. package/templates/template-vite/tsconfig.json +7 -0
  155. package/templates/template-vite/tsconfig.node.json +16 -0
  156. 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,7 @@
1
+ # API Configuration
2
+ NEXT_PUBLIC_API_URL=http://localhost:8000
3
+ NEXT_PUBLIC_API_AUTH_URL=http://localhost:8001
4
+
5
+ # App
6
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
7
+ NEXT_PUBLIC_APP_NAME={{PROJECT_NAME}}
@@ -0,0 +1,8 @@
1
+ .env
2
+ .env.local
3
+ .env.production
4
+ node_modules
5
+ .next
6
+ dist
7
+ *.log
8
+ .turbo
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ .next
3
+ dist
4
+ src/features/*/services/gen
@@ -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
+ }