mega-framework 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/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `mega new <project>` — 빈 폴더에서 멀티앱 hello world 까지 한 명령 스캐폴드 (roadmap §336, ADR-142).
|
|
4
|
+
*
|
|
5
|
+
* 기본 앱 `main` + `*.localhost` 호스트 + README · `.env.example` · `.gitignore` · `package.json`
|
|
6
|
+
* (scripts + concurrently devDep + PM2 `ecosystem.config.cjs`) 를 만든다. `ejsMate` 옵트인이면 EJS +
|
|
7
|
+
* ejs-mate 뷰 골격(ADR-011/136) + i18next 다국어(ADR-037~039) + Bootstrap 5 디자인(ADR-151)을 더한다 —
|
|
8
|
+
* 뷰 렌더 라우트, ko/en 로케일, 그리고 토큰 치환 없이 그대로 복사하는 정적 자산(vendored Bootstrap +
|
|
9
|
+
* 브랜드 CSS/JS). 신규 dep 설치는 하지 않고(네트워크 회피) 안내만 출력한다.
|
|
10
|
+
*
|
|
11
|
+
* @module cli/commands/new
|
|
12
|
+
*/
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { dirname, join, resolve } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { nameVariants, renderTemplate } from '../template-engine.js'
|
|
17
|
+
|
|
18
|
+
/** templates/project 루트. src/cli/commands/new.js 기준 3단계 위 + templates/project. */
|
|
19
|
+
const PROJECT_TEMPLATES = resolve(dirname(fileURLToPath(import.meta.url)), '../../../templates/project')
|
|
20
|
+
|
|
21
|
+
/** 프로젝트 템플릿을 읽어 토큰 치환. @param {string} file @param {Record<string,string>} vars @returns {string} */
|
|
22
|
+
function renderProject(file, vars) {
|
|
23
|
+
return renderTemplate(readFileSync(join(PROJECT_TEMPLATES, file), 'utf8'), vars)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 프로젝트를 스캐폴드한다(순수 — prompts 없이 opts 로만 동작, 테스트 가능).
|
|
28
|
+
* @param {string} targetDir - 프로젝트 루트 절대/상대 경로.
|
|
29
|
+
* @param {object} [opts]
|
|
30
|
+
* @param {string} [opts.name] - 프로젝트 이름(기본: targetDir 의 마지막 세그먼트).
|
|
31
|
+
* @param {boolean} [opts.ejsMate] - EJS + ejs-mate 뷰 골격 포함(기본 false).
|
|
32
|
+
* @param {boolean} [opts.force] - 기존 파일 덮어쓰기(기본 false — 건너뜀).
|
|
33
|
+
* @returns {{ root: string, written: string[], skipped: string[] }}
|
|
34
|
+
*/
|
|
35
|
+
export function scaffoldProject(targetDir, { name, ejsMate = false, force = false } = {}) {
|
|
36
|
+
const root = resolve(targetDir)
|
|
37
|
+
const projectName = nameVariants(name ?? root.split(/[/\\]/).pop() ?? 'mega-app').kebab
|
|
38
|
+
const vars = { name: projectName, Name: nameVariants(projectName).pascal }
|
|
39
|
+
|
|
40
|
+
// 뷰 옵트인이면 GET / 를 뷰 렌더로 바꾸고(JSON hello 대신) 그에 맞는 테스트를 쓴다.
|
|
41
|
+
/** @type {Array<{ rel: string, tpl: string }>} */
|
|
42
|
+
const files = [
|
|
43
|
+
{ rel: 'mega.config.js', tpl: 'mega.config.tpl' },
|
|
44
|
+
{ rel: 'apps/main/app.config.js', tpl: ejsMate ? 'app.config.views.tpl' : 'app.config.tpl' },
|
|
45
|
+
{ rel: 'apps/main/routes/index.js', tpl: ejsMate ? 'route.views.tpl' : 'route.tpl' },
|
|
46
|
+
{ rel: 'test/apps/main/index.test.js', tpl: ejsMate ? 'route.test.views.tpl' : 'route.test.tpl' },
|
|
47
|
+
{ rel: 'package.json', tpl: 'package.tpl' },
|
|
48
|
+
{ rel: 'README.md', tpl: 'readme.tpl' },
|
|
49
|
+
{ rel: '.env.example', tpl: 'env.tpl' },
|
|
50
|
+
{ rel: '.gitignore', tpl: 'gitignore.tpl' },
|
|
51
|
+
{ rel: 'vitest.config.js', tpl: 'vitest.config.tpl' },
|
|
52
|
+
{ rel: 'ecosystem.config.cjs', tpl: 'ecosystem.config.tpl' },
|
|
53
|
+
]
|
|
54
|
+
if (ejsMate) {
|
|
55
|
+
files.push(
|
|
56
|
+
{ rel: 'apps/main/views/layouts/main.ejs', tpl: 'views/layout.ejs.tpl' },
|
|
57
|
+
{ rel: 'apps/main/views/index.ejs', tpl: 'views/index.ejs.tpl' },
|
|
58
|
+
{ rel: 'apps/main/locales/server/ko.json', tpl: 'locales/server/ko.json.tpl' },
|
|
59
|
+
{ rel: 'apps/main/locales/server/en.json', tpl: 'locales/server/en.json.tpl' },
|
|
60
|
+
{ rel: 'apps/main/locales/client/ko.json', tpl: 'locales/client/ko.json.tpl' },
|
|
61
|
+
{ rel: 'apps/main/locales/client/en.json', tpl: 'locales/client/en.json.tpl' },
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 정적 자산 — 토큰 치환 없이 바이트 그대로 복사한다(vendored Bootstrap 5 min 번들 + 브랜드 CSS/JS).
|
|
66
|
+
// 이들은 템플릿이 아니라 정적 파일이므로 renderTemplate(`{{token}}`)을 거치지 않는다.
|
|
67
|
+
/** @type {Array<{ rel: string, src: string }>} */
|
|
68
|
+
const assets = ejsMate
|
|
69
|
+
? [
|
|
70
|
+
{ rel: 'apps/main/public/vendor/bootstrap/bootstrap.min.css', src: 'public/vendor/bootstrap/bootstrap.min.css' },
|
|
71
|
+
{ rel: 'apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js', src: 'public/vendor/bootstrap/bootstrap.bundle.min.js' },
|
|
72
|
+
{ rel: 'apps/main/public/css/app.css', src: 'public/css/app.css' },
|
|
73
|
+
{ rel: 'apps/main/public/js/app.js', src: 'public/js/app.js' },
|
|
74
|
+
{ rel: 'apps/main/public/js/theme-init.js', src: 'public/js/theme-init.js' },
|
|
75
|
+
]
|
|
76
|
+
: []
|
|
77
|
+
|
|
78
|
+
/** @type {string[]} */ const written = []
|
|
79
|
+
/** @type {string[]} */ const skipped = []
|
|
80
|
+
for (const { rel, tpl } of files) {
|
|
81
|
+
const out = join(root, rel)
|
|
82
|
+
if (existsSync(out) && !force) {
|
|
83
|
+
skipped.push(out)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
mkdirSync(dirname(out), { recursive: true })
|
|
87
|
+
writeFileSync(out, renderProject(tpl, vars))
|
|
88
|
+
written.push(out)
|
|
89
|
+
}
|
|
90
|
+
for (const { rel, src } of assets) {
|
|
91
|
+
const out = join(root, rel)
|
|
92
|
+
if (existsSync(out) && !force) {
|
|
93
|
+
skipped.push(out)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
mkdirSync(dirname(out), { recursive: true })
|
|
97
|
+
copyFileSync(join(PROJECT_TEMPLATES, src), out)
|
|
98
|
+
written.push(out)
|
|
99
|
+
}
|
|
100
|
+
return { root, written, skipped }
|
|
101
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `mega routes` — 앱별 등록 라우트 트리 출력 (roadmap §338, ADR-142).
|
|
4
|
+
*
|
|
5
|
+
* 서버를 띄우지 않고(부팅·어댑터 connect·listen 없이) 라우트만 수집한다 — 각 `apps/<name>/routes/*.js`
|
|
6
|
+
* 를 import 해 **기록용 라우터 stub** 으로 `router.http.<method>(path)`/`router.ws(path)` 호출을 잡는다.
|
|
7
|
+
* (loadRoutes 의 자동 스캔과 동형이되 부작용 없음 — 운영 환경에서도 안전.)
|
|
8
|
+
*
|
|
9
|
+
* @module cli/commands/routes
|
|
10
|
+
*/
|
|
11
|
+
import { readdirSync, statSync } from 'node:fs'
|
|
12
|
+
import { join, resolve } from 'node:path'
|
|
13
|
+
import { pathToFileURL } from 'node:url'
|
|
14
|
+
import { loadAndValidateConfig } from '../../core/config-loader.js'
|
|
15
|
+
|
|
16
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head']
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 라우트 등록을 기록하는 라우터 stub.
|
|
20
|
+
* @param {Array<{ method: string, path: string, file: string }>} sink @param {string} file
|
|
21
|
+
* @returns {Record<string, any>}
|
|
22
|
+
*/
|
|
23
|
+
function makeRecordingRouter(sink, file) {
|
|
24
|
+
/** @type {Record<string, any>} */
|
|
25
|
+
const http = {}
|
|
26
|
+
for (const m of HTTP_METHODS) {
|
|
27
|
+
http[m] = (/** @type {string} */ path) => sink.push({ method: m.toUpperCase(), path: String(path), file })
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
http,
|
|
31
|
+
ws: (/** @type {string} */ path) => sink.push({ method: 'WS', path: String(path), file }),
|
|
32
|
+
use: () => {}, // 파일 레벨 미들웨어(router.use) — 수집엔 불필요한 no-op.
|
|
33
|
+
// 실 Router 의 `app` getter 미러(standalone Router 면 null, router.js). 라우트 모듈이 등록 시점에
|
|
34
|
+
// `router.app` 을 읽어도 undefined 가 아니라 null 이라 옵셔널 가드(`router.app?.x`)가 동형 동작(ADR-167).
|
|
35
|
+
app: null,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 프로젝트의 앱별 라우트를 수집한다.
|
|
41
|
+
* @param {string} projectRoot
|
|
42
|
+
* @returns {Promise<Array<{ app: string, hosts: string[], routes: Array<{ method: string, path: string, file: string }> }>>}
|
|
43
|
+
*/
|
|
44
|
+
export async function collectRoutes(projectRoot) {
|
|
45
|
+
const { apps } = await loadAndValidateConfig(projectRoot)
|
|
46
|
+
/** @type {Array<{ app: string, hosts: string[], routes: any[] }>} */
|
|
47
|
+
const result = []
|
|
48
|
+
for (const { name, config } of apps) {
|
|
49
|
+
const routesDir = join(projectRoot, 'apps', name, 'routes')
|
|
50
|
+
/** @type {any[]} */
|
|
51
|
+
const routes = []
|
|
52
|
+
/** @type {string[]} */
|
|
53
|
+
let files = []
|
|
54
|
+
try {
|
|
55
|
+
files = readdirSync(routesDir)
|
|
56
|
+
.filter((f) => f.endsWith('.js') && !f.endsWith('.test.js'))
|
|
57
|
+
.sort()
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (/** @type {any} */ (err).code !== 'ENOENT') throw err
|
|
60
|
+
}
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const abs = resolve(routesDir, file)
|
|
63
|
+
// `.js` 로 끝나는 디렉토리 등 비파일은 스킵한다(import 가 디렉토리에 던짐) — loadRoutes 의 statSync
|
|
64
|
+
// isFile 가드와 동형(routes-loader.js, ADR-167).
|
|
65
|
+
if (!statSync(abs).isFile()) continue
|
|
66
|
+
const mod = await import(pathToFileURL(abs).href)
|
|
67
|
+
if (typeof mod.default !== 'function') continue
|
|
68
|
+
// async 라우트 모듈(`export default async (router) => {...}`)도 끝까지 기다린다 — await 안 하면
|
|
69
|
+
// await 경계 뒤 등록 라우트가 수집 전에 누락된다. loadRoutes 도 `await mod.default(router)`(ADR-167).
|
|
70
|
+
await mod.default(makeRecordingRouter(routes, file))
|
|
71
|
+
}
|
|
72
|
+
result.push({ app: name, hosts: /** @type {any} */ (config).hosts ?? [], routes })
|
|
73
|
+
}
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 수집한 라우트를 사람이 읽는 트리 문자열로 포맷.
|
|
79
|
+
* @param {Array<{ app: string, hosts: string[], routes: Array<{ method: string, path: string }> }>} data
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function formatRoutes(data) {
|
|
83
|
+
/** @type {string[]} */
|
|
84
|
+
const lines = []
|
|
85
|
+
for (const { app, hosts, routes } of data) {
|
|
86
|
+
lines.push(`${app} [${hosts.join(', ') || 'no hosts'}]`)
|
|
87
|
+
if (routes.length === 0) {
|
|
88
|
+
lines.push(' (no routes)')
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
const sorted = [...routes].sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))
|
|
92
|
+
for (const r of sorted) lines.push(` ${r.method.padEnd(7)} ${r.path}`)
|
|
93
|
+
}
|
|
94
|
+
return lines.join('\n')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `mega routes` 명령 본체.
|
|
99
|
+
* @param {string} projectRoot
|
|
100
|
+
* @param {{ out?: (msg: string) => void }} [deps]
|
|
101
|
+
* @returns {Promise<number>} exit code.
|
|
102
|
+
*/
|
|
103
|
+
export async function runRoutesCommand(projectRoot, { out = console.log } = {}) {
|
|
104
|
+
const data = await collectRoutes(projectRoot)
|
|
105
|
+
out(formatRoutes(data))
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* scaffold/dev 명령군 (`new` / `generate`(g) / `routes` / `test` / `console`)을 commander 로 묶는다
|
|
4
|
+
* (ADR-142 — CLI 파서 dep 채택, ADR-123 의 zero-dep 런타임 명령과 공존). 런타임 명령(start/worker/
|
|
5
|
+
* scheduler/plugin)은 기존 zero-dep 디스패치(`src/cli/index.js`)가 그대로 처리한다.
|
|
6
|
+
*
|
|
7
|
+
* @module cli/commands/scaffold
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander'
|
|
10
|
+
import prompts from 'prompts'
|
|
11
|
+
import { generate, GENERATOR_KINDS } from '../generators/index.js'
|
|
12
|
+
import { scaffoldProject } from './new.js'
|
|
13
|
+
import { runRoutesCommand } from './routes.js'
|
|
14
|
+
import { runTestCommand } from './test-cmd.js'
|
|
15
|
+
import { startConsole } from './console-cmd.js'
|
|
16
|
+
|
|
17
|
+
/** scaffold/dev 명령 이름(별칭 포함). runCli 가 이 집합으로 라우팅한다. */
|
|
18
|
+
export const SCAFFOLD_COMMANDS = new Set(['new', 'generate', 'g', 'routes', 'test', 'console'])
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 생성 결과를 사람이 읽게 출력.
|
|
22
|
+
* @param {(msg: string) => void} out @param {{ written: string[], skipped: string[] }} r @param {string} root
|
|
23
|
+
*/
|
|
24
|
+
function reportFiles(out, r, root) {
|
|
25
|
+
for (const f of r.written) out(` create ${f.startsWith(root) ? f.slice(root.length + 1) : f}`)
|
|
26
|
+
for (const f of r.skipped) out(` skip ${f.startsWith(root) ? f.slice(root.length + 1) : f} (exists — use --force)`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* scaffold/dev 명령을 실행한다. commander 로 파싱하되 `process.exit` 를 부르지 않고 exit code 를 반환한다
|
|
31
|
+
* (runCli 계약 정합).
|
|
32
|
+
* @param {string[]} argv - `process.argv.slice(2)` (첫 토큰 = 명령).
|
|
33
|
+
* @param {object} deps
|
|
34
|
+
* @param {(msg: string) => void} deps.out
|
|
35
|
+
* @param {(msg: string) => void} deps.err
|
|
36
|
+
* @param {string} deps.projectRoot - 이미 `--root` 해석된 기준 디렉토리.
|
|
37
|
+
* @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
|
|
38
|
+
* @returns {Promise<number>} exit code.
|
|
39
|
+
*/
|
|
40
|
+
export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }) {
|
|
41
|
+
let exitCode = 0
|
|
42
|
+
const program = new Command()
|
|
43
|
+
program.name('mega').exitOverride()
|
|
44
|
+
program.configureOutput({
|
|
45
|
+
writeOut: (s) => out(s.replace(/\n+$/, '')),
|
|
46
|
+
writeErr: (s) => err(s.replace(/\n+$/, '')),
|
|
47
|
+
})
|
|
48
|
+
// --root 는 runCli 가 이미 해석함 — commander 가 "unknown option" 으로 막지 않도록 받아만 둔다.
|
|
49
|
+
program.option('--root <dir>', '프로젝트 루트(runCli 가 해석)')
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('new <project>')
|
|
53
|
+
.description('빈 폴더에서 멀티앱 hello world 스캐폴드')
|
|
54
|
+
.option('--views', 'EJS + ejs-mate 뷰 골격 포함')
|
|
55
|
+
.option('--force', '기존 파일 덮어쓰기')
|
|
56
|
+
.action(async (/** @type {string} */ project, /** @type {any} */ opts) => {
|
|
57
|
+
// --views 미지정 + 대화형 터미널이면 ejs-mate 옵트인을 묻는다(비-TTY/CI 는 기본 false).
|
|
58
|
+
let ejsMate = opts.views === true
|
|
59
|
+
if (opts.views === undefined && process.stdin.isTTY) {
|
|
60
|
+
const ans = /** @type {any} */ (await prompts({ type: 'confirm', name: 'views', message: 'EJS + ejs-mate 뷰 골격을 포함할까요?', initial: false }))
|
|
61
|
+
ejsMate = ans.views === true
|
|
62
|
+
}
|
|
63
|
+
const target = project === '.' ? projectRoot : `${projectRoot}/${project}`
|
|
64
|
+
const r = scaffoldProject(target, { name: project === '.' ? undefined : project, ejsMate, force: opts.force === true })
|
|
65
|
+
out(`mega: scaffolded project at ${r.root}`)
|
|
66
|
+
reportFiles(out, r, r.root)
|
|
67
|
+
out('\n다음 단계:\n cd ' + project + '\n npm install\n npm run dev')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command('generate <kind> <name>')
|
|
72
|
+
.alias('g')
|
|
73
|
+
.description(`코드+테스트 생성 (kind: ${GENERATOR_KINDS.join('|')})`)
|
|
74
|
+
.option('--app <app>', '대상 앱', 'main')
|
|
75
|
+
.option('--version <v>', '컨트롤러 API 버전(예 v2, ADR-069)')
|
|
76
|
+
.option('--kind <adapterKind>', 'adapter 종류(db|cache|bus|session|log)')
|
|
77
|
+
.option('--lng <lng>', 'locale 언어(기본 en)')
|
|
78
|
+
.option('--force', '기존 파일 덮어쓰기')
|
|
79
|
+
.action((/** @type {string} */ kind, /** @type {string} */ name, /** @type {any} */ opts) => {
|
|
80
|
+
const r = generate(kind, name, { app: opts.app, version: opts.version, kind: opts.kind, lng: opts.lng, force: opts.force === true }, projectRoot)
|
|
81
|
+
out(`mega: generated ${r.kind} '${r.name}'`)
|
|
82
|
+
reportFiles(out, r, projectRoot)
|
|
83
|
+
if (r.written.length === 0) exitCode = 1
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
program
|
|
87
|
+
.command('routes')
|
|
88
|
+
.description('등록된 라우트 트리 출력')
|
|
89
|
+
.action(async () => {
|
|
90
|
+
exitCode = await runRoutesCommand(projectRoot, { out })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command('test')
|
|
95
|
+
.description('vitest 실행(추가 인자 그대로 전달)')
|
|
96
|
+
.allowUnknownOption()
|
|
97
|
+
.argument('[args...]', 'vitest 인자')
|
|
98
|
+
.action(async (/** @type {string[]} */ args) => {
|
|
99
|
+
exitCode = await runTestCommand(projectRoot, args ?? [], { out })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command('console')
|
|
104
|
+
.description('앱 컨텍스트를 로딩한 REPL')
|
|
105
|
+
.action(async () => {
|
|
106
|
+
await startConsole(projectRoot, { logger, out })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await program.parseAsync(argv, { from: 'user' })
|
|
111
|
+
return exitCode
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// commander 의 help/version 출력은 정상 종료(exitOverride 가 던지는 특수 코드).
|
|
114
|
+
const code = /** @type {any} */ (e).exitCode
|
|
115
|
+
if (/** @type {any} */ (e).code === 'commander.helpDisplayed' || /** @type {any} */ (e).code === 'commander.help') return 0
|
|
116
|
+
if (typeof code === 'number') return code
|
|
117
|
+
err(`mega: ${/** @type {any} */ (e).message ?? e}`)
|
|
118
|
+
return 1
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `mega test [...args]` — 프로젝트의 vitest 래퍼 (roadmap §338, ADR-142).
|
|
4
|
+
*
|
|
5
|
+
* 프로젝트 루트에서 `vitest run` 을 실행하고 추가 인자를 그대로 넘긴다. 별도 테스트 러너 dep 없이
|
|
6
|
+
* 프로젝트가 설치한 vitest 를 `npx` 로 호출한다(생성 package.json 의 devDependency).
|
|
7
|
+
*
|
|
8
|
+
* @module cli/commands/test-cmd
|
|
9
|
+
*/
|
|
10
|
+
import { spawn as nodeSpawn } from 'node:child_process'
|
|
11
|
+
import os from 'node:os'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 자식 프로세스 종료를 exit code 로 환산한다. 정상 종료면 그 code(number)를, **시그널로 죽었으면**
|
|
15
|
+
* (code=null·signal=문자열) `128+signo`(bash 관례)를 돌려준다 — `code ?? 0` 으로 0(성공)을 돌려주면
|
|
16
|
+
* 비정상 종료를 통과로 오판하므로 금지(fail-closed, P4, ADR-167).
|
|
17
|
+
* @param {number|null} code - close 이벤트의 종료 code.
|
|
18
|
+
* @param {NodeJS.Signals|null} signal - close 이벤트의 종료 시그널.
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
function exitCodeFromClose(code, signal) {
|
|
22
|
+
if (typeof code === 'number') return code
|
|
23
|
+
const signo = signal ? (os.constants.signals[signal] ?? 0) : 0
|
|
24
|
+
return signo ? 128 + signo : 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* vitest 를 자식 프로세스로 실행한다.
|
|
29
|
+
* @param {string} projectRoot - 실행 cwd.
|
|
30
|
+
* @param {string[]} [args] - vitest 에 넘길 추가 인자.
|
|
31
|
+
* @param {object} [deps]
|
|
32
|
+
* @param {typeof nodeSpawn} [deps.spawn] - 주입용(테스트).
|
|
33
|
+
* @param {(msg: string) => void} [deps.out]
|
|
34
|
+
* @returns {Promise<number>} vitest exit code.
|
|
35
|
+
*/
|
|
36
|
+
export function runTestCommand(projectRoot, args = [], { spawn = nodeSpawn, out = console.log } = {}) {
|
|
37
|
+
out('mega: running vitest...')
|
|
38
|
+
return new Promise((resolvePromise, reject) => {
|
|
39
|
+
const child = spawn('npx', ['vitest', 'run', ...args], { cwd: projectRoot, stdio: 'inherit', shell: false })
|
|
40
|
+
child.on('error', reject)
|
|
41
|
+
child.on('close', (/** @type {number|null} */ code, /** @type {NodeJS.Signals|null} */ signal) =>
|
|
42
|
+
resolvePromise(exitCodeFromClose(code, signal)),
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
}
|