nexu-app 2.0.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/README.md +149 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1192 -0
- package/package.json +43 -0
- package/templates/default/.changeset/config.json +11 -0
- package/templates/default/.eslintignore +16 -0
- package/templates/default/.eslintrc.js +67 -0
- package/templates/default/.github/actions/build/action.yml +35 -0
- package/templates/default/.github/actions/quality/action.yml +53 -0
- package/templates/default/.github/dependabot.yml +51 -0
- package/templates/default/.github/workflows/deploy-dev.yml +83 -0
- package/templates/default/.github/workflows/deploy-prod.yml +83 -0
- package/templates/default/.github/workflows/deploy-rec.yml +83 -0
- package/templates/default/.husky/commit-msg +1 -0
- package/templates/default/.husky/pre-commit +1 -0
- package/templates/default/.nexu-version +1 -0
- package/templates/default/.prettierignore +7 -0
- package/templates/default/.prettierrc +19 -0
- package/templates/default/.vscode/extensions.json +14 -0
- package/templates/default/.vscode/settings.json +36 -0
- package/templates/default/apps/gitkeep +0 -0
- package/templates/default/commitlint.config.js +26 -0
- package/templates/default/docker/docker-compose.dev.yml +49 -0
- package/templates/default/docker/docker-compose.prod.yml +64 -0
- package/templates/default/docker/docker-compose.yml +6 -0
- package/templates/default/docs/architecture.md +452 -0
- package/templates/default/docs/cli.md +330 -0
- package/templates/default/docs/contributing.md +462 -0
- package/templates/default/docs/scripts.md +460 -0
- package/templates/default/gitignore +44 -0
- package/templates/default/lintstagedrc.cjs +4 -0
- package/templates/default/package.json +51 -0
- package/templates/default/packages/auth/package.json +61 -0
- package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
- package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
- package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
- package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
- package/templates/default/packages/auth/src/components/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
- package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
- package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
- package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
- package/templates/default/packages/auth/src/index.ts +45 -0
- package/templates/default/packages/auth/src/next/index.ts +18 -0
- package/templates/default/packages/auth/src/next/middleware.ts +183 -0
- package/templates/default/packages/auth/src/next/server.ts +219 -0
- package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
- package/templates/default/packages/auth/src/providers/index.ts +1 -0
- package/templates/default/packages/auth/src/types/index.ts +284 -0
- package/templates/default/packages/auth/src/utils/api.ts +228 -0
- package/templates/default/packages/auth/src/utils/index.ts +3 -0
- package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
- package/templates/default/packages/auth/src/utils/token.ts +204 -0
- package/templates/default/packages/auth/tsconfig.json +14 -0
- package/templates/default/packages/auth/tsup.config.ts +18 -0
- package/templates/default/packages/cache/package.json +26 -0
- package/templates/default/packages/cache/src/index.ts +137 -0
- package/templates/default/packages/cache/tsconfig.json +9 -0
- package/templates/default/packages/cache/tsup.config.ts +9 -0
- package/templates/default/packages/config/eslint/index.js +20 -0
- package/templates/default/packages/config/package.json +9 -0
- package/templates/default/packages/config/typescript/base.json +26 -0
- package/templates/default/packages/constants/package.json +26 -0
- package/templates/default/packages/constants/src/index.ts +121 -0
- package/templates/default/packages/constants/tsconfig.json +9 -0
- package/templates/default/packages/constants/tsup.config.ts +9 -0
- package/templates/default/packages/logger/package.json +27 -0
- package/templates/default/packages/logger/src/index.ts +197 -0
- package/templates/default/packages/logger/tsconfig.json +11 -0
- package/templates/default/packages/logger/tsup.config.ts +9 -0
- package/templates/default/packages/result/package.json +26 -0
- package/templates/default/packages/result/src/index.ts +142 -0
- package/templates/default/packages/result/tsconfig.json +9 -0
- package/templates/default/packages/result/tsup.config.ts +9 -0
- package/templates/default/packages/types/package.json +26 -0
- package/templates/default/packages/types/src/index.ts +78 -0
- package/templates/default/packages/types/tsconfig.json +9 -0
- package/templates/default/packages/types/tsup.config.ts +10 -0
- package/templates/default/packages/ui/package.json +38 -0
- package/templates/default/packages/ui/src/components/Button.tsx +58 -0
- package/templates/default/packages/ui/src/components/Card.tsx +85 -0
- package/templates/default/packages/ui/src/components/Input.tsx +45 -0
- package/templates/default/packages/ui/src/index.ts +15 -0
- package/templates/default/packages/ui/tsconfig.json +11 -0
- package/templates/default/packages/ui/tsup.config.ts +11 -0
- package/templates/default/packages/utils/package.json +30 -0
- package/templates/default/packages/utils/src/index.test.ts +130 -0
- package/templates/default/packages/utils/src/index.ts +154 -0
- package/templates/default/packages/utils/tsconfig.json +10 -0
- package/templates/default/packages/utils/tsup.config.ts +10 -0
- package/templates/default/pnpm-workspace.yaml +3 -0
- package/templates/default/scripts/audit.mjs +700 -0
- package/templates/default/scripts/deploy.mjs +40 -0
- package/templates/default/scripts/generate-app.mjs +808 -0
- package/templates/default/scripts/lib/package-manager.mjs +186 -0
- package/templates/default/scripts/setup.mjs +102 -0
- package/templates/default/services/.env.example +16 -0
- package/templates/default/services/docker-compose.yml +207 -0
- package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
- package/templates/default/services/postgres/init/gitkeep +2 -0
- package/templates/default/services/prometheus/prometheus.yml +13 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/turbo.json +40 -0
- package/templates/default/vitest.config.ts +15 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import readline from 'readline';
|
|
8
|
+
import { detectPackageManager, getRunCommand, getInstallCommand } from './lib/package-manager.mjs';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
// Colors for console output
|
|
14
|
+
const colors = {
|
|
15
|
+
red: '\x1b[31m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
blue: '\x1b[34m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
cyan: '\x1b[36m',
|
|
20
|
+
bold: '\x1b[1m',
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const log = {
|
|
25
|
+
info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
|
|
26
|
+
success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
|
|
27
|
+
warn: (msg) => console.log(`${colors.yellow}!${colors.reset} ${msg}`),
|
|
28
|
+
error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
|
|
29
|
+
title: (msg) => console.log(`\n${colors.bold}${colors.cyan}${msg}${colors.reset}\n`),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Get directories
|
|
33
|
+
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
34
|
+
const APPS_DIR = path.join(ROOT_DIR, 'apps');
|
|
35
|
+
const DOCKER_DIR = path.join(ROOT_DIR, 'docker');
|
|
36
|
+
|
|
37
|
+
// Detect package manager
|
|
38
|
+
const pm = detectPackageManager(ROOT_DIR);
|
|
39
|
+
const runCmd = getRunCommand(pm);
|
|
40
|
+
|
|
41
|
+
// Docker-specific helpers based on package manager
|
|
42
|
+
function getDockerSetup(pm) {
|
|
43
|
+
switch (pm) {
|
|
44
|
+
case 'pnpm':
|
|
45
|
+
return {
|
|
46
|
+
enableCmd: 'RUN corepack enable && corepack prepare pnpm@9 --activate',
|
|
47
|
+
lockFile: 'pnpm-lock.yaml pnpm-workspace.yaml',
|
|
48
|
+
installCmd: 'RUN pnpm install --frozen-lockfile',
|
|
49
|
+
installProdCmd: 'RUN pnpm install --prod --frozen-lockfile',
|
|
50
|
+
runPrefix: 'pnpm',
|
|
51
|
+
buildCmd: 'pnpm turbo build',
|
|
52
|
+
};
|
|
53
|
+
case 'yarn':
|
|
54
|
+
return {
|
|
55
|
+
enableCmd: 'RUN corepack enable && corepack prepare yarn@4 --activate',
|
|
56
|
+
lockFile: 'yarn.lock',
|
|
57
|
+
installCmd: 'RUN yarn install --frozen-lockfile',
|
|
58
|
+
installProdCmd: 'RUN yarn install --production --frozen-lockfile',
|
|
59
|
+
runPrefix: 'yarn',
|
|
60
|
+
buildCmd: 'yarn turbo build',
|
|
61
|
+
};
|
|
62
|
+
case 'npm':
|
|
63
|
+
default:
|
|
64
|
+
return {
|
|
65
|
+
enableCmd: '# Using npm (default)',
|
|
66
|
+
lockFile: 'package-lock.json',
|
|
67
|
+
installCmd: 'RUN npm ci',
|
|
68
|
+
installProdCmd: 'RUN npm ci --omit=dev',
|
|
69
|
+
runPrefix: 'npm run',
|
|
70
|
+
buildCmd: 'npm run turbo build',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const docker = getDockerSetup(pm);
|
|
76
|
+
|
|
77
|
+
// Framework configurations
|
|
78
|
+
const FRAMEWORKS = {
|
|
79
|
+
// Frontend frameworks
|
|
80
|
+
'next': {
|
|
81
|
+
name: 'Next.js',
|
|
82
|
+
type: 'frontend',
|
|
83
|
+
defaultPort: 3000,
|
|
84
|
+
createCommand: (name) => `npx create-next-app@latest ${name} --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${pm}`,
|
|
85
|
+
devCommand: 'dev',
|
|
86
|
+
buildCommand: 'build',
|
|
87
|
+
startCommand: 'start',
|
|
88
|
+
outputDir: '.next',
|
|
89
|
+
dockerfile: 'nextjs',
|
|
90
|
+
},
|
|
91
|
+
'vite-react': {
|
|
92
|
+
name: 'Vite + React',
|
|
93
|
+
type: 'frontend',
|
|
94
|
+
defaultPort: 5173,
|
|
95
|
+
createCommand: (name) => `npm create vite@latest ${name} -- --template react-ts`,
|
|
96
|
+
devCommand: 'dev --host',
|
|
97
|
+
buildCommand: 'build',
|
|
98
|
+
startCommand: 'preview --host',
|
|
99
|
+
outputDir: 'dist',
|
|
100
|
+
dockerfile: 'vite',
|
|
101
|
+
},
|
|
102
|
+
'vite-vue': {
|
|
103
|
+
name: 'Vite + Vue',
|
|
104
|
+
type: 'frontend',
|
|
105
|
+
defaultPort: 5173,
|
|
106
|
+
createCommand: (name) => `npm create vite@latest ${name} -- --template vue-ts`,
|
|
107
|
+
devCommand: 'dev --host',
|
|
108
|
+
buildCommand: 'build',
|
|
109
|
+
startCommand: 'preview --host',
|
|
110
|
+
outputDir: 'dist',
|
|
111
|
+
dockerfile: 'vite',
|
|
112
|
+
},
|
|
113
|
+
'vite-svelte': {
|
|
114
|
+
name: 'Vite + Svelte',
|
|
115
|
+
type: 'frontend',
|
|
116
|
+
defaultPort: 5173,
|
|
117
|
+
createCommand: (name) => `npm create vite@latest ${name} -- --template svelte-ts`,
|
|
118
|
+
devCommand: 'dev --host',
|
|
119
|
+
buildCommand: 'build',
|
|
120
|
+
startCommand: 'preview --host',
|
|
121
|
+
outputDir: 'dist',
|
|
122
|
+
dockerfile: 'vite',
|
|
123
|
+
},
|
|
124
|
+
'nuxt': {
|
|
125
|
+
name: 'Nuxt',
|
|
126
|
+
type: 'frontend',
|
|
127
|
+
defaultPort: 3000,
|
|
128
|
+
createCommand: (name) => `npx nuxi@latest init ${name}`,
|
|
129
|
+
devCommand: 'dev',
|
|
130
|
+
buildCommand: 'build',
|
|
131
|
+
startCommand: 'preview',
|
|
132
|
+
outputDir: '.output',
|
|
133
|
+
dockerfile: 'nuxt',
|
|
134
|
+
},
|
|
135
|
+
// Backend frameworks
|
|
136
|
+
'express': {
|
|
137
|
+
name: 'Express.js',
|
|
138
|
+
type: 'backend',
|
|
139
|
+
defaultPort: 4000,
|
|
140
|
+
createCommand: null, // Manual setup
|
|
141
|
+
devCommand: 'dev',
|
|
142
|
+
buildCommand: 'build',
|
|
143
|
+
startCommand: 'start',
|
|
144
|
+
outputDir: 'dist',
|
|
145
|
+
dockerfile: 'node',
|
|
146
|
+
dependencies: ['express', 'cors', 'helmet'],
|
|
147
|
+
devDependencies: ['@types/express', '@types/cors', 'tsx', 'typescript'],
|
|
148
|
+
},
|
|
149
|
+
'fastify': {
|
|
150
|
+
name: 'Fastify',
|
|
151
|
+
type: 'backend',
|
|
152
|
+
defaultPort: 4000,
|
|
153
|
+
createCommand: null,
|
|
154
|
+
devCommand: 'dev',
|
|
155
|
+
buildCommand: 'build',
|
|
156
|
+
startCommand: 'start',
|
|
157
|
+
outputDir: 'dist',
|
|
158
|
+
dockerfile: 'node',
|
|
159
|
+
dependencies: ['fastify', '@fastify/cors'],
|
|
160
|
+
devDependencies: ['tsx', 'typescript'],
|
|
161
|
+
},
|
|
162
|
+
'hono': {
|
|
163
|
+
name: 'Hono',
|
|
164
|
+
type: 'backend',
|
|
165
|
+
defaultPort: 4000,
|
|
166
|
+
createCommand: null,
|
|
167
|
+
devCommand: 'dev',
|
|
168
|
+
buildCommand: 'build',
|
|
169
|
+
startCommand: 'start',
|
|
170
|
+
outputDir: 'dist',
|
|
171
|
+
dockerfile: 'node',
|
|
172
|
+
dependencies: ['hono', '@hono/node-server'],
|
|
173
|
+
devDependencies: ['tsx', 'typescript'],
|
|
174
|
+
},
|
|
175
|
+
'nestjs': {
|
|
176
|
+
name: 'NestJS',
|
|
177
|
+
type: 'backend',
|
|
178
|
+
defaultPort: 4000,
|
|
179
|
+
createCommand: (name) => `npx @nestjs/cli@latest new ${name} --package-manager ${pm} --skip-git`,
|
|
180
|
+
devCommand: 'start:dev',
|
|
181
|
+
buildCommand: 'build',
|
|
182
|
+
startCommand: 'start:prod',
|
|
183
|
+
outputDir: 'dist',
|
|
184
|
+
dockerfile: 'node',
|
|
185
|
+
},
|
|
186
|
+
'empty': {
|
|
187
|
+
name: 'Empty (Node.js)',
|
|
188
|
+
type: 'backend',
|
|
189
|
+
defaultPort: 3000,
|
|
190
|
+
createCommand: null,
|
|
191
|
+
devCommand: 'dev',
|
|
192
|
+
buildCommand: 'build',
|
|
193
|
+
startCommand: 'start',
|
|
194
|
+
outputDir: 'dist',
|
|
195
|
+
dockerfile: 'node',
|
|
196
|
+
dependencies: [],
|
|
197
|
+
devDependencies: ['tsx', 'typescript'],
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Dockerfile templates
|
|
202
|
+
const DOCKERFILES = {
|
|
203
|
+
nextjs: (appName, port) => `# ====== Base ======
|
|
204
|
+
FROM node:20-alpine AS base
|
|
205
|
+
${docker.enableCmd}
|
|
206
|
+
WORKDIR /app
|
|
207
|
+
|
|
208
|
+
# ====== Dependencies ======
|
|
209
|
+
FROM base AS deps
|
|
210
|
+
COPY ${docker.lockFile} package.json ./
|
|
211
|
+
COPY apps/${appName}/package.json ./apps/${appName}/
|
|
212
|
+
COPY packages/*/package.json ./packages/
|
|
213
|
+
${docker.installCmd}
|
|
214
|
+
|
|
215
|
+
# ====== Development ======
|
|
216
|
+
FROM base AS development
|
|
217
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
218
|
+
COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
|
|
219
|
+
COPY . .
|
|
220
|
+
WORKDIR /app/apps/${appName}
|
|
221
|
+
ENV PORT=${port}
|
|
222
|
+
EXPOSE ${port}
|
|
223
|
+
CMD ["${docker.runPrefix}", "dev"]
|
|
224
|
+
|
|
225
|
+
# ====== Builder ======
|
|
226
|
+
FROM base AS builder
|
|
227
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
228
|
+
COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
|
|
229
|
+
COPY . .
|
|
230
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
231
|
+
RUN ${docker.buildCmd} --filter=@repo/${appName}
|
|
232
|
+
|
|
233
|
+
# ====== Production ======
|
|
234
|
+
FROM node:20-alpine AS production
|
|
235
|
+
WORKDIR /app
|
|
236
|
+
ENV NODE_ENV=production
|
|
237
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
238
|
+
ENV PORT=${port}
|
|
239
|
+
|
|
240
|
+
RUN addgroup --system --gid 1001 nodejs
|
|
241
|
+
RUN adduser --system --uid 1001 nextjs
|
|
242
|
+
|
|
243
|
+
COPY --from=builder /app/apps/${appName}/public ./public
|
|
244
|
+
COPY --from=builder --chown=nextjs:nodejs /app/apps/${appName}/.next/standalone ./
|
|
245
|
+
COPY --from=builder --chown=nextjs:nodejs /app/apps/${appName}/.next/static ./.next/static
|
|
246
|
+
|
|
247
|
+
USER nextjs
|
|
248
|
+
EXPOSE ${port}
|
|
249
|
+
CMD ["node", "server.js"]
|
|
250
|
+
`,
|
|
251
|
+
|
|
252
|
+
vite: (appName, port) => `# ====== Base ======
|
|
253
|
+
FROM node:20-alpine AS base
|
|
254
|
+
${docker.enableCmd}
|
|
255
|
+
WORKDIR /app
|
|
256
|
+
|
|
257
|
+
# ====== Dependencies ======
|
|
258
|
+
FROM base AS deps
|
|
259
|
+
COPY ${docker.lockFile} package.json ./
|
|
260
|
+
COPY apps/${appName}/package.json ./apps/${appName}/
|
|
261
|
+
COPY packages/*/package.json ./packages/
|
|
262
|
+
${docker.installCmd}
|
|
263
|
+
|
|
264
|
+
# ====== Development ======
|
|
265
|
+
FROM base AS development
|
|
266
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
267
|
+
COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
|
|
268
|
+
COPY . .
|
|
269
|
+
WORKDIR /app/apps/${appName}
|
|
270
|
+
EXPOSE ${port}
|
|
271
|
+
CMD ["${docker.runPrefix}", "dev", "--host"]
|
|
272
|
+
|
|
273
|
+
# ====== Builder ======
|
|
274
|
+
FROM base AS builder
|
|
275
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
276
|
+
COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
|
|
277
|
+
COPY . .
|
|
278
|
+
RUN ${docker.buildCmd} --filter=@repo/${appName}
|
|
279
|
+
|
|
280
|
+
# ====== Production ======
|
|
281
|
+
FROM nginx:alpine AS production
|
|
282
|
+
COPY --from=builder /app/apps/${appName}/dist /usr/share/nginx/html
|
|
283
|
+
COPY apps/${appName}/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
|
284
|
+
EXPOSE ${port}
|
|
285
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
286
|
+
`,
|
|
287
|
+
|
|
288
|
+
nuxt: (appName, port) => `# ====== Base ======
|
|
289
|
+
FROM node:20-alpine AS base
|
|
290
|
+
${docker.enableCmd}
|
|
291
|
+
WORKDIR /app
|
|
292
|
+
|
|
293
|
+
# ====== Dependencies ======
|
|
294
|
+
FROM base AS deps
|
|
295
|
+
COPY ${docker.lockFile} package.json ./
|
|
296
|
+
COPY apps/${appName}/package.json ./apps/${appName}/
|
|
297
|
+
COPY packages/*/package.json ./packages/
|
|
298
|
+
${docker.installCmd}
|
|
299
|
+
|
|
300
|
+
# ====== Development ======
|
|
301
|
+
FROM base AS development
|
|
302
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
303
|
+
COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
|
|
304
|
+
COPY . .
|
|
305
|
+
WORKDIR /app/apps/${appName}
|
|
306
|
+
ENV PORT=${port}
|
|
307
|
+
EXPOSE ${port}
|
|
308
|
+
CMD ["${docker.runPrefix}", "dev"]
|
|
309
|
+
|
|
310
|
+
# ====== Builder ======
|
|
311
|
+
FROM base AS builder
|
|
312
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
313
|
+
COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
|
|
314
|
+
COPY . .
|
|
315
|
+
RUN ${docker.buildCmd} --filter=@repo/${appName}
|
|
316
|
+
|
|
317
|
+
# ====== Production ======
|
|
318
|
+
FROM node:20-alpine AS production
|
|
319
|
+
WORKDIR /app
|
|
320
|
+
ENV NODE_ENV=production
|
|
321
|
+
ENV PORT=${port}
|
|
322
|
+
|
|
323
|
+
COPY --from=builder /app/apps/${appName}/.output ./
|
|
324
|
+
|
|
325
|
+
EXPOSE ${port}
|
|
326
|
+
CMD ["node", "server/index.mjs"]
|
|
327
|
+
`,
|
|
328
|
+
|
|
329
|
+
node: (appName, port) => `# ====== Base ======
|
|
330
|
+
FROM node:20-alpine AS base
|
|
331
|
+
${docker.enableCmd}
|
|
332
|
+
WORKDIR /app
|
|
333
|
+
|
|
334
|
+
# ====== Dependencies ======
|
|
335
|
+
FROM base AS deps
|
|
336
|
+
COPY ${docker.lockFile} package.json ./
|
|
337
|
+
COPY apps/${appName}/package.json ./apps/${appName}/
|
|
338
|
+
COPY packages/*/package.json ./packages/
|
|
339
|
+
${docker.installCmd}
|
|
340
|
+
|
|
341
|
+
# ====== Development ======
|
|
342
|
+
FROM base AS development
|
|
343
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
344
|
+
COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
|
|
345
|
+
COPY . .
|
|
346
|
+
WORKDIR /app/apps/${appName}
|
|
347
|
+
ENV PORT=${port}
|
|
348
|
+
EXPOSE ${port}
|
|
349
|
+
CMD ["${docker.runPrefix}", "dev"]
|
|
350
|
+
|
|
351
|
+
# ====== Builder ======
|
|
352
|
+
FROM base AS builder
|
|
353
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
354
|
+
COPY --from=deps /app/apps/${appName}/node_modules ./apps/${appName}/node_modules
|
|
355
|
+
COPY . .
|
|
356
|
+
RUN ${docker.buildCmd} --filter=@repo/${appName}
|
|
357
|
+
|
|
358
|
+
# ====== Production ======
|
|
359
|
+
FROM node:20-alpine AS production
|
|
360
|
+
${docker.enableCmd}
|
|
361
|
+
WORKDIR /app
|
|
362
|
+
ENV NODE_ENV=production
|
|
363
|
+
ENV PORT=${port}
|
|
364
|
+
|
|
365
|
+
COPY --from=builder /app/apps/${appName}/dist ./dist
|
|
366
|
+
COPY --from=builder /app/apps/${appName}/package.json ./
|
|
367
|
+
|
|
368
|
+
${docker.installProdCmd}
|
|
369
|
+
|
|
370
|
+
EXPOSE ${port}
|
|
371
|
+
CMD ["node", "dist/index.js"]
|
|
372
|
+
`,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Helper to ask questions
|
|
376
|
+
function question(prompt, defaultValue = '') {
|
|
377
|
+
const rl = readline.createInterface({
|
|
378
|
+
input: process.stdin,
|
|
379
|
+
output: process.stdout,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return new Promise((resolve) => {
|
|
383
|
+
const defaultText = defaultValue ? ` (${defaultValue})` : '';
|
|
384
|
+
rl.question(`${colors.cyan}?${colors.reset} ${prompt}${defaultText}: `, (answer) => {
|
|
385
|
+
rl.close();
|
|
386
|
+
resolve(answer || defaultValue);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Helper to select from list
|
|
392
|
+
async function select(prompt, choices) {
|
|
393
|
+
console.log(`\n${colors.cyan}?${colors.reset} ${prompt}\n`);
|
|
394
|
+
|
|
395
|
+
choices.forEach((choice, index) => {
|
|
396
|
+
console.log(` ${colors.cyan}${index + 1}${colors.reset}) ${choice.name}`);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
console.log('');
|
|
400
|
+
const answer = await question('Enter number', '1');
|
|
401
|
+
const index = parseInt(answer, 10) - 1;
|
|
402
|
+
|
|
403
|
+
if (index >= 0 && index < choices.length) {
|
|
404
|
+
return choices[index].value;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return choices[0].value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Create backend app manually
|
|
411
|
+
function createBackendApp(appDir, appName, framework, port) {
|
|
412
|
+
const config = FRAMEWORKS[framework];
|
|
413
|
+
|
|
414
|
+
// Create directory structure
|
|
415
|
+
fs.mkdirSync(path.join(appDir, 'src'), { recursive: true });
|
|
416
|
+
|
|
417
|
+
// Create package.json
|
|
418
|
+
const packageJson = {
|
|
419
|
+
name: `@repo/${appName}`,
|
|
420
|
+
version: '0.0.1',
|
|
421
|
+
private: true,
|
|
422
|
+
type: 'module',
|
|
423
|
+
scripts: {
|
|
424
|
+
dev: 'tsx watch src/index.ts',
|
|
425
|
+
build: 'tsc',
|
|
426
|
+
start: 'node dist/index.js',
|
|
427
|
+
lint: 'eslint src/',
|
|
428
|
+
typecheck: 'tsc --noEmit',
|
|
429
|
+
},
|
|
430
|
+
dependencies: {},
|
|
431
|
+
devDependencies: {
|
|
432
|
+
'@repo/config': 'workspace:*',
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Add framework-specific dependencies
|
|
437
|
+
if (config.dependencies) {
|
|
438
|
+
for (const dep of config.dependencies) {
|
|
439
|
+
packageJson.dependencies[dep] = 'latest';
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (config.devDependencies) {
|
|
443
|
+
for (const dep of config.devDependencies) {
|
|
444
|
+
packageJson.devDependencies[dep] = 'latest';
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
fs.writeFileSync(
|
|
449
|
+
path.join(appDir, 'package.json'),
|
|
450
|
+
JSON.stringify(packageJson, null, 2)
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// Create tsconfig.json
|
|
454
|
+
const tsconfig = {
|
|
455
|
+
extends: '@repo/config/typescript/base',
|
|
456
|
+
compilerOptions: {
|
|
457
|
+
outDir: 'dist',
|
|
458
|
+
rootDir: 'src',
|
|
459
|
+
},
|
|
460
|
+
include: ['src/**/*'],
|
|
461
|
+
exclude: ['node_modules', 'dist'],
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
fs.writeFileSync(
|
|
465
|
+
path.join(appDir, 'tsconfig.json'),
|
|
466
|
+
JSON.stringify(tsconfig, null, 2)
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Create main file based on framework
|
|
470
|
+
let mainFile = '';
|
|
471
|
+
|
|
472
|
+
if (framework === 'express') {
|
|
473
|
+
mainFile = `import express from 'express';
|
|
474
|
+
import cors from 'cors';
|
|
475
|
+
import helmet from 'helmet';
|
|
476
|
+
|
|
477
|
+
const app = express();
|
|
478
|
+
const port = process.env.PORT || ${port};
|
|
479
|
+
|
|
480
|
+
app.use(helmet());
|
|
481
|
+
app.use(cors());
|
|
482
|
+
app.use(express.json());
|
|
483
|
+
|
|
484
|
+
app.get('/', (req, res) => {
|
|
485
|
+
res.json({ message: 'Hello from ${appName}!' });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
app.get('/health', (req, res) => {
|
|
489
|
+
res.json({ status: 'ok' });
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
app.listen(port, () => {
|
|
493
|
+
console.log(\`🚀 Server running on http://localhost:\${port}\`);
|
|
494
|
+
});
|
|
495
|
+
`;
|
|
496
|
+
} else if (framework === 'fastify') {
|
|
497
|
+
mainFile = `import Fastify from 'fastify';
|
|
498
|
+
import cors from '@fastify/cors';
|
|
499
|
+
|
|
500
|
+
const fastify = Fastify({ logger: true });
|
|
501
|
+
const port = parseInt(process.env.PORT || '${port}', 10);
|
|
502
|
+
|
|
503
|
+
await fastify.register(cors);
|
|
504
|
+
|
|
505
|
+
fastify.get('/', async () => {
|
|
506
|
+
return { message: 'Hello from ${appName}!' };
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
fastify.get('/health', async () => {
|
|
510
|
+
return { status: 'ok' };
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
await fastify.listen({ port, host: '0.0.0.0' });
|
|
515
|
+
} catch (err) {
|
|
516
|
+
fastify.log.error(err);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
`;
|
|
520
|
+
} else if (framework === 'hono') {
|
|
521
|
+
mainFile = `import { serve } from '@hono/node-server';
|
|
522
|
+
import { Hono } from 'hono';
|
|
523
|
+
import { cors } from 'hono/cors';
|
|
524
|
+
|
|
525
|
+
const app = new Hono();
|
|
526
|
+
const port = parseInt(process.env.PORT || '${port}', 10);
|
|
527
|
+
|
|
528
|
+
app.use('*', cors());
|
|
529
|
+
|
|
530
|
+
app.get('/', (c) => {
|
|
531
|
+
return c.json({ message: 'Hello from ${appName}!' });
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
app.get('/health', (c) => {
|
|
535
|
+
return c.json({ status: 'ok' });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
console.log(\`🚀 Server running on http://localhost:\${port}\`);
|
|
539
|
+
|
|
540
|
+
serve({
|
|
541
|
+
fetch: app.fetch,
|
|
542
|
+
port,
|
|
543
|
+
});
|
|
544
|
+
`;
|
|
545
|
+
} else {
|
|
546
|
+
// Empty template
|
|
547
|
+
mainFile = `const port = process.env.PORT || ${port};
|
|
548
|
+
|
|
549
|
+
console.log(\`🚀 App ${appName} starting on port \${port}\`);
|
|
550
|
+
|
|
551
|
+
// Add your code here
|
|
552
|
+
`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
fs.writeFileSync(path.join(appDir, 'src', 'index.ts'), mainFile);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Create nginx config for Vite apps
|
|
559
|
+
function createNginxConfig(appDir, port) {
|
|
560
|
+
const nginxConfig = `server {
|
|
561
|
+
listen ${port};
|
|
562
|
+
server_name localhost;
|
|
563
|
+
root /usr/share/nginx/html;
|
|
564
|
+
index index.html;
|
|
565
|
+
|
|
566
|
+
location / {
|
|
567
|
+
try_files $uri $uri/ /index.html;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
location /assets {
|
|
571
|
+
expires 1y;
|
|
572
|
+
add_header Cache-Control "public, immutable";
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
gzip on;
|
|
576
|
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
|
|
577
|
+
}
|
|
578
|
+
`;
|
|
579
|
+
|
|
580
|
+
fs.mkdirSync(path.join(appDir, 'docker'), { recursive: true });
|
|
581
|
+
fs.writeFileSync(path.join(appDir, 'docker', 'nginx.conf'), nginxConfig);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Create docker-compose files
|
|
585
|
+
function createDockerComposeFiles(appDir, appName, port, framework) {
|
|
586
|
+
const config = FRAMEWORKS[framework];
|
|
587
|
+
|
|
588
|
+
// docker-compose.yml (dev)
|
|
589
|
+
const dockerCompose = `services:
|
|
590
|
+
${appName}:
|
|
591
|
+
build:
|
|
592
|
+
context: ../..
|
|
593
|
+
dockerfile: apps/${appName}/docker/Dockerfile
|
|
594
|
+
target: development
|
|
595
|
+
ports:
|
|
596
|
+
- "${port}:${port}"
|
|
597
|
+
environment:
|
|
598
|
+
- NODE_ENV=development
|
|
599
|
+
- PORT=${port}
|
|
600
|
+
volumes:
|
|
601
|
+
- ../../apps/${appName}:/app/apps/${appName}
|
|
602
|
+
- ../../packages:/app/packages
|
|
603
|
+
- /app/node_modules
|
|
604
|
+
- /app/apps/${appName}/node_modules
|
|
605
|
+
command: pnpm ${config.devCommand}
|
|
606
|
+
`;
|
|
607
|
+
|
|
608
|
+
fs.writeFileSync(path.join(appDir, 'docker-compose.yml'), dockerCompose);
|
|
609
|
+
|
|
610
|
+
// docker-compose.prod.yml
|
|
611
|
+
const dockerComposeProd = `services:
|
|
612
|
+
${appName}:
|
|
613
|
+
build:
|
|
614
|
+
context: ../..
|
|
615
|
+
dockerfile: apps/${appName}/docker/Dockerfile
|
|
616
|
+
target: production
|
|
617
|
+
ports:
|
|
618
|
+
- "${port}:${port}"
|
|
619
|
+
environment:
|
|
620
|
+
- NODE_ENV=production
|
|
621
|
+
- PORT=${port}
|
|
622
|
+
restart: unless-stopped
|
|
623
|
+
`;
|
|
624
|
+
|
|
625
|
+
fs.writeFileSync(path.join(appDir, 'docker-compose.prod.yml'), dockerComposeProd);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Update main docker-compose.yml
|
|
629
|
+
function updateMainCompose(appName) {
|
|
630
|
+
const mainComposePath = path.join(DOCKER_DIR, 'docker-compose.yml');
|
|
631
|
+
const includePath = `../apps/${appName}/docker-compose.yml`;
|
|
632
|
+
|
|
633
|
+
if (!fs.existsSync(DOCKER_DIR)) {
|
|
634
|
+
fs.mkdirSync(DOCKER_DIR, { recursive: true });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!fs.existsSync(mainComposePath)) {
|
|
638
|
+
const content = `# Main docker-compose - includes all apps
|
|
639
|
+
# Each app has its own docker-compose.yml in apps/<app-name>/
|
|
640
|
+
|
|
641
|
+
include:
|
|
642
|
+
- path: ${includePath}
|
|
643
|
+
`;
|
|
644
|
+
fs.writeFileSync(mainComposePath, content);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let content = fs.readFileSync(mainComposePath, 'utf-8');
|
|
649
|
+
|
|
650
|
+
if (content.includes(`apps/${appName}/docker-compose.yml`)) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (content.includes('include: []')) {
|
|
655
|
+
content = content.replace('include: []', `include:\n - path: ${includePath}`);
|
|
656
|
+
fs.writeFileSync(mainComposePath, content);
|
|
657
|
+
} else {
|
|
658
|
+
content = content.trimEnd() + `\n - path: ${includePath}\n`;
|
|
659
|
+
fs.writeFileSync(mainComposePath, content);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Run command
|
|
664
|
+
function run(cmd, cwd) {
|
|
665
|
+
console.log(`${colors.blue}>${colors.reset} ${cmd}`);
|
|
666
|
+
execSync(cmd, { stdio: 'inherit', cwd });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Main function
|
|
670
|
+
async function main() {
|
|
671
|
+
log.title('🚀 Generate New App');
|
|
672
|
+
|
|
673
|
+
// Get arguments
|
|
674
|
+
const args = process.argv.slice(2);
|
|
675
|
+
let appName = args[0];
|
|
676
|
+
let framework = args[1];
|
|
677
|
+
let port = args[2];
|
|
678
|
+
|
|
679
|
+
// If no app name provided, ask for it
|
|
680
|
+
if (!appName) {
|
|
681
|
+
appName = await question('App name');
|
|
682
|
+
if (!appName) {
|
|
683
|
+
log.error('App name is required');
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const APP_DIR = path.join(APPS_DIR, appName);
|
|
689
|
+
|
|
690
|
+
// Check if app already exists
|
|
691
|
+
if (fs.existsSync(APP_DIR)) {
|
|
692
|
+
log.error(`App '${appName}' already exists in apps/`);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// If no framework provided, show selection
|
|
697
|
+
if (!framework) {
|
|
698
|
+
const choices = Object.entries(FRAMEWORKS).map(([key, value]) => ({
|
|
699
|
+
name: `${value.name} (${value.type})`,
|
|
700
|
+
value: key,
|
|
701
|
+
}));
|
|
702
|
+
|
|
703
|
+
framework = await select('Select framework', choices);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Validate framework
|
|
707
|
+
if (!FRAMEWORKS[framework]) {
|
|
708
|
+
log.error(`Unknown framework: ${framework}`);
|
|
709
|
+
log.info('Available frameworks: ' + Object.keys(FRAMEWORKS).join(', '));
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const config = FRAMEWORKS[framework];
|
|
714
|
+
|
|
715
|
+
// Get port
|
|
716
|
+
if (!port) {
|
|
717
|
+
port = await question('Port', config.defaultPort.toString());
|
|
718
|
+
}
|
|
719
|
+
port = parseInt(port, 10);
|
|
720
|
+
|
|
721
|
+
log.info(`Creating ${config.name} app: ${appName} (port: ${port})`);
|
|
722
|
+
console.log('');
|
|
723
|
+
|
|
724
|
+
// Create app
|
|
725
|
+
if (config.createCommand) {
|
|
726
|
+
// Use framework's CLI
|
|
727
|
+
log.info(`Running ${config.name} installer...`);
|
|
728
|
+
|
|
729
|
+
// Create apps directory if needed
|
|
730
|
+
if (!fs.existsSync(APPS_DIR)) {
|
|
731
|
+
fs.mkdirSync(APPS_DIR, { recursive: true });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
run(config.createCommand(appName), APPS_DIR);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
log.error(`Failed to create app with ${config.name} CLI`);
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Update package.json name to use @repo/ prefix
|
|
742
|
+
const pkgPath = path.join(APP_DIR, 'package.json');
|
|
743
|
+
if (fs.existsSync(pkgPath)) {
|
|
744
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
745
|
+
pkg.name = `@repo/${appName}`;
|
|
746
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
// Manual setup for backend frameworks
|
|
750
|
+
log.info(`Setting up ${config.name} app manually...`);
|
|
751
|
+
createBackendApp(APP_DIR, appName, framework, port);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Create docker directory
|
|
755
|
+
fs.mkdirSync(path.join(APP_DIR, 'docker'), { recursive: true });
|
|
756
|
+
|
|
757
|
+
// Create Dockerfile
|
|
758
|
+
const dockerfileContent = DOCKERFILES[config.dockerfile](appName, port);
|
|
759
|
+
fs.writeFileSync(path.join(APP_DIR, 'docker', 'Dockerfile'), dockerfileContent);
|
|
760
|
+
|
|
761
|
+
// Create nginx config for Vite apps
|
|
762
|
+
if (config.dockerfile === 'vite') {
|
|
763
|
+
createNginxConfig(APP_DIR, port);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Create docker-compose files
|
|
767
|
+
createDockerComposeFiles(APP_DIR, appName, port, framework);
|
|
768
|
+
|
|
769
|
+
// Update main docker-compose
|
|
770
|
+
updateMainCompose(appName);
|
|
771
|
+
|
|
772
|
+
// Install dependencies if it was a manual setup
|
|
773
|
+
if (!config.createCommand) {
|
|
774
|
+
log.info('Installing dependencies...');
|
|
775
|
+
try {
|
|
776
|
+
const installCmd = getInstallCommand(pm);
|
|
777
|
+
log.info(`> ${installCmd}`);
|
|
778
|
+
run(installCmd, ROOT_DIR);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
log.warn(`Failed to install dependencies. Run "${getInstallCommand(pm)}" manually.`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Success message
|
|
785
|
+
console.log('');
|
|
786
|
+
log.success(`Created app: apps/${appName}`);
|
|
787
|
+
console.log('');
|
|
788
|
+
console.log(`${colors.cyan}Files created:${colors.reset}`);
|
|
789
|
+
console.log(` - apps/${appName}/`);
|
|
790
|
+
console.log(` - apps/${appName}/docker/Dockerfile`);
|
|
791
|
+
console.log(` - apps/${appName}/docker-compose.yml`);
|
|
792
|
+
console.log(` - apps/${appName}/docker-compose.prod.yml`);
|
|
793
|
+
if (config.dockerfile === 'vite') {
|
|
794
|
+
console.log(` - apps/${appName}/docker/nginx.conf`);
|
|
795
|
+
}
|
|
796
|
+
console.log('');
|
|
797
|
+
console.log(`${colors.cyan}Commands:${colors.reset}`);
|
|
798
|
+
console.log(` Dev (local): cd apps/${appName} && ${runCmd} dev`);
|
|
799
|
+
console.log(` Dev (docker): cd apps/${appName} && docker compose up`);
|
|
800
|
+
console.log(` Dev (all apps): ${runCmd} docker:dev`);
|
|
801
|
+
console.log(` Build: ${runCmd} build --filter=@repo/${appName}`);
|
|
802
|
+
console.log('');
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
main().catch((error) => {
|
|
806
|
+
log.error(error.message);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
});
|