mega-framework 0.1.6 → 0.1.8
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 +9 -0
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +33 -9
- package/sample/crud/.env +10 -1
- package/sample/crud/.env.example +10 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +10 -2
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +20 -6
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +44 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +33 -7
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +10 -1
- package/src/adapters/mongo-adapter.js +40 -8
- package/src/adapters/postgres-adapter.js +33 -6
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +26 -3
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +173 -33
- package/src/cli/generators/index.js +140 -3
- package/src/cli/index.js +437 -155
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +30 -3
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +65 -3
- package/src/core/envelope.js +119 -12
- package/src/core/hub-link.js +89 -18
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +7 -3
- package/src/core/mega-app.js +253 -505
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +131 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +70 -12
- package/src/core/session-store.js +14 -1
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +636 -0
- package/src/core/ws-roster.js +50 -8
- package/src/core/ws-upgrade.js +223 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +35 -4
- package/src/lib/mega-job-queue.js +151 -34
- package/src/lib/mega-job.js +37 -1
- package/src/lib/mega-metrics.js +31 -13
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +33 -6
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +139 -15
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/adr/code.tpl +23 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +93 -0
- package/types/adapters/file-adapter.d.ts +105 -0
- package/types/adapters/file-session-adapter.d.ts +103 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +117 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +73 -0
- package/types/adapters/mega-db-adapter.d.ts +50 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +150 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +141 -0
- package/types/adapters/redis-adapter.d.ts +78 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +112 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +122 -0
- package/types/cli/index.d.ts +234 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +103 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +266 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +93 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +25 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +108 -0
- package/types/core/ws-upgrade.d.ts +260 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +243 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +48 -0
- package/types/lib/mega-job-queue.d.ts +188 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +145 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +129 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +259 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* scaffold/dev 명령군 (`new` / `generate`(g) / `routes` / `test` / `console`)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* scaffold/dev 명령군 (`new` / `generate`(g) / `routes` / `test` / `console`)의 commander 등록.
|
|
4
|
+
*
|
|
5
|
+
* ADR-142 가 commander 를 채택한 명령군이며, ADR-195(commander 전면 일원화) 이후 런타임 명령
|
|
6
|
+
* (start/worker/scheduler/migrate)과 **같은 program 트리**에 등록된다 — `registerScaffoldCommands` 를
|
|
7
|
+
* `cli/index.js` 의 `buildProgram` 이 호출한다. `runScaffoldCommand` 는 scaffold 명령군만 담은 독립
|
|
8
|
+
* program 을 돌리는 기존 진입점으로 유지한다(하위호환·단위 테스트 경계).
|
|
6
9
|
*
|
|
7
10
|
* @module cli/commands/scaffold
|
|
8
11
|
*/
|
|
9
12
|
import { Command } from 'commander'
|
|
10
|
-
import {
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { pathToFileURL } from 'node:url'
|
|
16
|
+
import { execFile } from 'node:child_process'
|
|
17
|
+
import { promisify } from 'node:util'
|
|
18
|
+
import { generate, generateFromScaffoldDef, nextAdrNumber, GENERATOR_KINDS } from '../generators/index.js'
|
|
11
19
|
import { scaffoldProject } from './new.js'
|
|
12
20
|
import { runRoutesCommand } from './routes.js'
|
|
13
21
|
import { runTestCommand } from './test-cmd.js'
|
|
14
22
|
import { startConsole } from './console-cmd.js'
|
|
15
23
|
|
|
16
|
-
/** scaffold/dev 명령 이름(별칭 포함).
|
|
24
|
+
/** scaffold/dev 명령 이름(별칭 포함). */
|
|
17
25
|
export const SCAFFOLD_COMMANDS = new Set(['new', 'generate', 'g', 'routes', 'test', 'console'])
|
|
18
26
|
|
|
19
27
|
/**
|
|
@@ -25,28 +33,83 @@ function reportFiles(out, r, root) {
|
|
|
25
33
|
for (const f of r.skipped) out(` skip ${f.startsWith(root) ? f.slice(root.length + 1) : f} (exists — use --force)`)
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
const execFileAsync = promisify(execFile)
|
|
37
|
+
|
|
28
38
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @
|
|
39
|
+
* `g adr` 의 다음 번호를 **원격 포함**으로 해석한다(ADR-218 — 병렬 task 번호 충돌 회피).
|
|
40
|
+
* `git fetch origin` 후 `origin/main` 의 `docs/adr/` 파일명에서 번호를 모아 로컬 스캔
|
|
41
|
+
* ({@link nextAdrNumber})과 합산한다 — 형제 task 가 방금 push 한 ADR 이 로컬 작업트리에 없어도
|
|
42
|
+
* 번호가 건너뛰어진다. git 부재/오프라인/비-repo 는 로컬 스캔만으로 폴백(경고 1줄 — 스캐폴드는
|
|
43
|
+
* 어디서든 동작해야 하므로 fail 아님).
|
|
44
|
+
*
|
|
45
|
+
* @param {string} projectRoot
|
|
46
|
+
* @param {(msg: string) => void} out
|
|
47
|
+
* @returns {Promise<number>}
|
|
48
|
+
*/
|
|
49
|
+
async function resolveAdrNumberWithRemote(projectRoot, out) {
|
|
50
|
+
/** @type {number[]} */
|
|
51
|
+
const remote = []
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync('git', ['fetch', 'origin', '--quiet'], { cwd: projectRoot })
|
|
54
|
+
const { stdout } = await execFileAsync('git', ['ls-tree', '--name-only', 'origin/main', 'docs/adr/'], { cwd: projectRoot })
|
|
55
|
+
for (const line of stdout.split('\n')) {
|
|
56
|
+
const m = line.match(/(\d{1,4})-[^/]+\.md$/)
|
|
57
|
+
if (m) remote.push(Number(m[1]))
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// 오프라인/비-repo/origin 부재 — 로컬 스캔만으로 진행하되 충돌 가능성을 알린다(silent 금지).
|
|
61
|
+
out(`mega: 원격 ADR 번호 확인 실패(${/** @type {any} */ (err).message?.split('\n')[0] ?? err}) — 로컬 기준으로 번호를 할당합니다.`)
|
|
62
|
+
}
|
|
63
|
+
return nextAdrNumber(projectRoot, remote)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `g model --adapter <key>` 의 adapter 키/driver 해석 — mega.config.js 의 services.databases 를
|
|
68
|
+
* best-effort 로 읽는다(스캐폴드는 config 가 아직 없는 초기 프로젝트에서도 돌아야 하므로 로드
|
|
69
|
+
* 실패는 fail 이 아니라 기본값 + 경고 1줄). 키 미지정 시: 선언 db 가 1개면 그 키, 아니면 'primary'.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} projectRoot @param {string | undefined} explicitKey
|
|
72
|
+
* @param {(msg: string) => void} out
|
|
73
|
+
* @returns {Promise<{ key: string, driver: string | undefined }>}
|
|
74
|
+
*/
|
|
75
|
+
async function resolveModelAdapter(projectRoot, explicitKey, out) {
|
|
76
|
+
/** @type {Record<string, any>} */
|
|
77
|
+
let databases = {}
|
|
78
|
+
const configPath = join(projectRoot, 'mega.config.js')
|
|
79
|
+
if (existsSync(configPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const mod = await import(pathToFileURL(configPath).href)
|
|
82
|
+
databases = mod.default?.services?.databases ?? {}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// config 문법 오류 등 — 스캐폴드는 계속 가능해야 하므로 기본 템플릿으로 폴백하되 명시 안내(P4).
|
|
85
|
+
out(`mega: mega.config.js 를 읽지 못해 adapter driver 를 해석하지 못했습니다(${/** @type {any} */ (err).message}) — SQL 템플릿으로 생성합니다.`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const keys = Object.keys(databases)
|
|
89
|
+
const key = explicitKey ?? (keys.length === 1 ? keys[0] : 'primary')
|
|
90
|
+
if (explicitKey !== undefined && keys.length > 0 && !keys.includes(explicitKey)) {
|
|
91
|
+
out(`mega: --adapter '${explicitKey}' 가 services.databases 에 없습니다(선언: [${keys.join(', ')}]) — 키를 그대로 쓰고 SQL 템플릿으로 생성합니다.`)
|
|
92
|
+
}
|
|
93
|
+
return { key, driver: databases[key]?.driver }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* scaffold/dev 명령 5종을 commander program 에 등록한다 — 명령 정의의 단일 정본(ADR-195).
|
|
98
|
+
* action 은 `process.exit` 를 부르지 않고 `setExit(code)` 로 종료 코드를 보고한다(runCli 계약).
|
|
99
|
+
*
|
|
100
|
+
* @param {import('commander').Command} program - 등록 대상 program.
|
|
32
101
|
* @param {object} deps
|
|
33
102
|
* @param {(msg: string) => void} deps.out
|
|
34
|
-
* @param {(msg: string) => void} deps.err
|
|
35
103
|
* @param {string} deps.projectRoot - 이미 `--root` 해석된 기준 디렉토리.
|
|
36
104
|
* @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
|
|
37
|
-
* @
|
|
105
|
+
* @param {(kind: string) => Promise<{ dir: string, files: Array<{ path: string, template: string }> } | undefined>} [deps.resolvePluginGenerator] -
|
|
106
|
+
* 빌트인이 아닌 kind 의 플러그인 scaffold manifest 조회(`mega.scaffold.register`, 03-api-spec §11).
|
|
107
|
+
* 호출측(runCli)이 config 로드 + 플러그인 install 을 감싼 함수를 주입한다 — 본 모듈이 cli/index.js 를
|
|
108
|
+
* import 하면 순환이라 주입식으로 푼다. 미주입/미발견이면 unknown kind 에러.
|
|
109
|
+
* @param {(code: number) => void} deps.setExit - 명령 종료 코드 보고 콜백.
|
|
110
|
+
* @returns {void}
|
|
38
111
|
*/
|
|
39
|
-
export
|
|
40
|
-
let exitCode = 0
|
|
41
|
-
const program = new Command()
|
|
42
|
-
program.name('mega').exitOverride()
|
|
43
|
-
program.configureOutput({
|
|
44
|
-
writeOut: (s) => out(s.replace(/\n+$/, '')),
|
|
45
|
-
writeErr: (s) => err(s.replace(/\n+$/, '')),
|
|
46
|
-
})
|
|
47
|
-
// --root 는 runCli 가 이미 해석함 — commander 가 "unknown option" 으로 막지 않도록 받아만 둔다.
|
|
48
|
-
program.option('--root <dir>', '프로젝트 루트(runCli 가 해석)')
|
|
49
|
-
|
|
112
|
+
export function registerScaffoldCommands(program, { out, projectRoot, logger, resolvePluginGenerator, setExit }) {
|
|
50
113
|
program
|
|
51
114
|
.command('new <project>')
|
|
52
115
|
.description('sample/crud 데모앱(14기능) 전체를 빈 폴더에 스캐폴드')
|
|
@@ -64,24 +127,58 @@ export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }
|
|
|
64
127
|
program
|
|
65
128
|
.command('generate <kind> <name>')
|
|
66
129
|
.alias('g')
|
|
67
|
-
.description(`코드+테스트 생성 (kind: ${GENERATOR_KINDS.join('|')})`)
|
|
130
|
+
.description(`코드+테스트 생성 (kind: ${GENERATOR_KINDS.join('|')} 또는 플러그인 등록 generator)`)
|
|
68
131
|
.option('--app <app>', '대상 앱', 'main')
|
|
69
132
|
.option('--version <v>', '컨트롤러 API 버전(예 v2, ADR-069)')
|
|
70
133
|
.option('--kind <adapterKind>', 'adapter 종류(db|cache|bus|session|log)')
|
|
134
|
+
.option('--adapter <key>', 'model 전용 — services.databases 키(driver 가 mongodb 면 mongo 템플릿, 기본: 유일 선언 db 또는 primary)')
|
|
71
135
|
.option('--lng <lng>', 'locale 언어(기본 en)')
|
|
72
136
|
.option('--force', '기존 파일 덮어쓰기')
|
|
73
|
-
.action((/** @type {string} */ kind, /** @type {string} */ name, /** @type {any} */ opts) => {
|
|
74
|
-
|
|
137
|
+
.action(async (/** @type {string} */ kind, /** @type {string} */ name, /** @type {any} */ opts) => {
|
|
138
|
+
/** @type {{ kind: string, name: string, written: string[], skipped: string[] }} */
|
|
139
|
+
let r
|
|
140
|
+
if (GENERATOR_KINDS.includes(/** @type {any} */ (kind))) {
|
|
141
|
+
/** @type {{ key: string, driver: string | undefined }} */
|
|
142
|
+
let modelAdapter = { key: 'primary', driver: undefined }
|
|
143
|
+
if (kind === 'model') modelAdapter = await resolveModelAdapter(projectRoot, opts.adapter, out)
|
|
144
|
+
// adr 은 번호를 원격 포함으로 해석한다(병렬 task 충돌 회피, ADR-218).
|
|
145
|
+
/** @type {number | undefined} */
|
|
146
|
+
let adrNumber
|
|
147
|
+
if (kind === 'adr') adrNumber = await resolveAdrNumberWithRemote(projectRoot, out)
|
|
148
|
+
r = generate(
|
|
149
|
+
kind,
|
|
150
|
+
name,
|
|
151
|
+
{ app: opts.app, version: opts.version, kind: opts.kind, lng: opts.lng, force: opts.force === true, adapter: modelAdapter.key, adapterDriver: modelAdapter.driver, adrNumber },
|
|
152
|
+
projectRoot,
|
|
153
|
+
)
|
|
154
|
+
} else {
|
|
155
|
+
// 빌트인이 아니면 플러그인 등록 generator(manifest) 조회(03-api-spec §11 `mega g <name>` 계약).
|
|
156
|
+
// config 로드 실패(프로젝트 밖 등)는 unknown kind 메시지에 원인을 병기해 오도 없이 보고한다.
|
|
157
|
+
/** @type {{ dir: string, files: Array<{ path: string, template: string }> } | undefined} */
|
|
158
|
+
let def
|
|
159
|
+
try {
|
|
160
|
+
def = await resolvePluginGenerator?.(kind)
|
|
161
|
+
} catch (e) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Unknown generator '${kind}'. Supported: ${GENERATOR_KINDS.join(', ')}. ` +
|
|
164
|
+
`(플러그인 generator 확인 실패: ${/** @type {any} */ (e).message ?? e})`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
if (def === undefined) {
|
|
168
|
+
throw new Error(`Unknown generator '${kind}'. Supported: ${GENERATOR_KINDS.join(', ')} (또는 플러그인 등록 generator).`)
|
|
169
|
+
}
|
|
170
|
+
r = generateFromScaffoldDef(kind, def, name, { app: opts.app, force: opts.force === true }, projectRoot)
|
|
171
|
+
}
|
|
75
172
|
out(`mega: generated ${r.kind} '${r.name}'`)
|
|
76
173
|
reportFiles(out, r, projectRoot)
|
|
77
|
-
if (r.written.length === 0)
|
|
174
|
+
if (r.written.length === 0) setExit(1)
|
|
78
175
|
})
|
|
79
176
|
|
|
80
177
|
program
|
|
81
178
|
.command('routes')
|
|
82
179
|
.description('등록된 라우트 트리 출력')
|
|
83
180
|
.action(async () => {
|
|
84
|
-
|
|
181
|
+
setExit(await runRoutesCommand(projectRoot, { out }))
|
|
85
182
|
})
|
|
86
183
|
|
|
87
184
|
program
|
|
@@ -90,7 +187,7 @@ export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }
|
|
|
90
187
|
.allowUnknownOption()
|
|
91
188
|
.argument('[args...]', 'vitest 인자')
|
|
92
189
|
.action(async (/** @type {string[]} */ args) => {
|
|
93
|
-
|
|
190
|
+
setExit(await runTestCommand(projectRoot, args ?? [], { out }))
|
|
94
191
|
})
|
|
95
192
|
|
|
96
193
|
program
|
|
@@ -99,16 +196,59 @@ export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }
|
|
|
99
196
|
.action(async () => {
|
|
100
197
|
await startConsole(projectRoot, { logger, out })
|
|
101
198
|
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* commander 의 exitOverride 예외를 exit code 로 환산한다 — help/version 출력은 정상 종료(0),
|
|
203
|
+
* CommanderError 는 자체 exitCode, 그 외 에러는 메시지 출력 후 1. commander 가 아닌 에러를 그대로
|
|
204
|
+
* 삼키지 않도록 `rethrowUnknown` 옵션을 둔다(runCli 가 부팅·config 에러를 bin 으로 전파할 때 사용).
|
|
205
|
+
*
|
|
206
|
+
* @param {unknown} e - parseAsync 가 던진 예외.
|
|
207
|
+
* @param {(msg: string) => void} err
|
|
208
|
+
* @param {{ rethrowUnknown?: boolean }} [opts] - true 면 CommanderError 가 아닌 예외를 재throw.
|
|
209
|
+
* @returns {number} exit code.
|
|
210
|
+
*/
|
|
211
|
+
export function commanderErrorToExitCode(e, err, { rethrowUnknown = false } = {}) {
|
|
212
|
+
const anyErr = /** @type {any} */ (e)
|
|
213
|
+
if (typeof anyErr?.code === 'string' && anyErr.code.startsWith('commander.')) {
|
|
214
|
+
// help/version 출력은 정상 종료(exitOverride 가 던지는 특수 코드).
|
|
215
|
+
if (anyErr.code === 'commander.helpDisplayed' || anyErr.code === 'commander.help' || anyErr.code === 'commander.version') return 0
|
|
216
|
+
return typeof anyErr.exitCode === 'number' ? anyErr.exitCode : 1
|
|
217
|
+
}
|
|
218
|
+
if (rethrowUnknown) throw e
|
|
219
|
+
err(`mega: ${anyErr?.message ?? e}`)
|
|
220
|
+
return 1
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* scaffold/dev 명령만 담은 독립 program 으로 실행한다(하위호환 진입점 — 단위 테스트 경계 유지).
|
|
225
|
+
* commander 로 파싱하되 `process.exit` 를 부르지 않고 exit code 를 반환한다(runCli 계약 정합).
|
|
226
|
+
*
|
|
227
|
+
* @param {string[]} argv - `process.argv.slice(2)` (첫 토큰 = 명령).
|
|
228
|
+
* @param {object} deps
|
|
229
|
+
* @param {(msg: string) => void} deps.out
|
|
230
|
+
* @param {(msg: string) => void} deps.err
|
|
231
|
+
* @param {string} deps.projectRoot - 이미 `--root` 해석된 기준 디렉토리.
|
|
232
|
+
* @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
|
|
233
|
+
* @param {(kind: string) => Promise<{ dir: string, files: Array<{ path: string, template: string }> } | undefined>} [deps.resolvePluginGenerator]
|
|
234
|
+
* @returns {Promise<number>} exit code.
|
|
235
|
+
*/
|
|
236
|
+
export async function runScaffoldCommand(argv, { out, err, projectRoot, logger, resolvePluginGenerator }) {
|
|
237
|
+
let exitCode = 0
|
|
238
|
+
const program = new Command()
|
|
239
|
+
program.name('mega').exitOverride()
|
|
240
|
+
program.configureOutput({
|
|
241
|
+
writeOut: (s) => out(s.replace(/\n+$/, '')),
|
|
242
|
+
writeErr: (s) => err(s.replace(/\n+$/, '')),
|
|
243
|
+
})
|
|
244
|
+
// --root 는 호출측(runCli)이 이미 해석함 — commander 가 "unknown option" 으로 막지 않도록 받아만 둔다.
|
|
245
|
+
program.option('--root <dir>', '프로젝트 루트(호출측이 해석)')
|
|
246
|
+
registerScaffoldCommands(program, { out, projectRoot, logger, resolvePluginGenerator, setExit: (c) => (exitCode = c) })
|
|
102
247
|
|
|
103
248
|
try {
|
|
104
249
|
await program.parseAsync(argv, { from: 'user' })
|
|
105
250
|
return exitCode
|
|
106
251
|
} catch (e) {
|
|
107
|
-
|
|
108
|
-
const code = /** @type {any} */ (e).exitCode
|
|
109
|
-
if (/** @type {any} */ (e).code === 'commander.helpDisplayed' || /** @type {any} */ (e).code === 'commander.help') return 0
|
|
110
|
-
if (typeof code === 'number') return code
|
|
111
|
-
err(`mega: ${/** @type {any} */ (e).message ?? e}`)
|
|
112
|
-
return 1
|
|
252
|
+
return commanderErrorToExitCode(e, err)
|
|
113
253
|
}
|
|
114
254
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module cli/generators
|
|
15
15
|
*/
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
17
17
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
18
18
|
import { fileURLToPath } from 'node:url'
|
|
19
19
|
import { nameVariants, renderTemplate } from '../template-engine.js'
|
|
@@ -36,8 +36,23 @@ export const GENERATOR_KINDS = /** @type {const} */ ([
|
|
|
36
36
|
'locale',
|
|
37
37
|
'adapter',
|
|
38
38
|
'migration',
|
|
39
|
+
'adr',
|
|
39
40
|
])
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 플러그인 scaffold manifest 의 토큰 계약(ADR-199) — `files[].path`/`files[].template` 의 `{{token}}`
|
|
44
|
+
* 에 쓸 수 있는 이름과 의미. 빌트인 generator 의 base 토큰과 동일 집합이라 템플릿 작성 관례가 하나다.
|
|
45
|
+
* 미정의 토큰은 renderTemplate 가 throw 한다(P4 — silent 치환 누락 방지).
|
|
46
|
+
* @type {Readonly<Record<string, string>>}
|
|
47
|
+
*/
|
|
48
|
+
export const SCAFFOLD_TOKENS = Object.freeze({
|
|
49
|
+
Name: 'PascalCase 이름 (예: userCard → UserCard)',
|
|
50
|
+
name: 'kebab-case 이름 (user-card)',
|
|
51
|
+
camelName: 'camelCase 이름 (userCard)',
|
|
52
|
+
snake: 'snake_case 이름 (user_card)',
|
|
53
|
+
app: '대상 앱 이름 (--app, 기본 main)',
|
|
54
|
+
})
|
|
55
|
+
|
|
41
56
|
/** adapter `--kind` → 베이스 클래스(mega-framework export 명). */
|
|
42
57
|
const ADAPTER_BASES = /** @type {Record<string, string>} */ ({
|
|
43
58
|
db: 'MegaDbAdapter',
|
|
@@ -121,8 +136,19 @@ export function planArtifacts(kind, rawName, opts, projectRoot) {
|
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
switch (kind) {
|
|
124
|
-
case 'model':
|
|
125
|
-
|
|
139
|
+
case 'model': {
|
|
140
|
+
// --adapter <key>: services.databases 키(static adapter 값). driver 가 mongodb 로 해석되면
|
|
141
|
+
// mongo 변형 템플릿(_id 자동·도큐먼트 API — ADR-209)을 쓴다. 해석은 scaffold 명령이
|
|
142
|
+
// best-effort 로 수행해 opts.adapterDriver 로 전달한다(config 부재 시 SQL 템플릿 기본).
|
|
143
|
+
const adapter = typeof opts.adapter === 'string' && opts.adapter.length > 0 ? opts.adapter : 'primary'
|
|
144
|
+
const isMongo = opts.adapterDriver === 'mongodb'
|
|
145
|
+
return pair({
|
|
146
|
+
codeRel: `apps/${app}/models/${v.kebab}.js`,
|
|
147
|
+
vars: { table: v.snake, adapter },
|
|
148
|
+
codeTpl: isMongo ? 'code-mongo.tpl' : 'code.tpl',
|
|
149
|
+
testTpl: isMongo ? 'test-mongo.tpl' : 'test.tpl',
|
|
150
|
+
})
|
|
151
|
+
}
|
|
126
152
|
|
|
127
153
|
case 'service':
|
|
128
154
|
return pair({ codeRel: `apps/${app}/services/${v.kebab}-service.js`, vars: {} })
|
|
@@ -160,11 +186,67 @@ export function planArtifacts(kind, rawName, opts, projectRoot) {
|
|
|
160
186
|
case 'app':
|
|
161
187
|
return planApp(v, projectRoot, base)
|
|
162
188
|
|
|
189
|
+
case 'adr':
|
|
190
|
+
return planAdr(v, opts, projectRoot)
|
|
191
|
+
|
|
163
192
|
default:
|
|
164
193
|
throw new Error(`Unknown generator kind '${kind}'. Supported: ${GENERATOR_KINDS.join(', ')}.`)
|
|
165
194
|
}
|
|
166
195
|
}
|
|
167
196
|
|
|
197
|
+
/**
|
|
198
|
+
* 다음 ADR 번호를 해석한다 — `docs/adr/NNNN-*.md` 파일명 + 레거시 `docs/09` 헤딩(`### ADR-N:`) +
|
|
199
|
+
* 호출측이 모은 추가 번호(예: `git ls-tree origin/main` 의 원격 파일 — 병렬 task 충돌 회피)의
|
|
200
|
+
* 최댓값 + 1. 아무 ADR 도 없으면 1.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} projectRoot
|
|
203
|
+
* @param {number[]} [extraNumbers] - 로컬 밖에서 관측한 번호(원격 스캔 등).
|
|
204
|
+
* @returns {number}
|
|
205
|
+
*/
|
|
206
|
+
export function nextAdrNumber(projectRoot, extraNumbers = []) {
|
|
207
|
+
let max = 0
|
|
208
|
+
const adrDir = join(projectRoot, 'docs/adr')
|
|
209
|
+
if (existsSync(adrDir)) {
|
|
210
|
+
for (const f of readdirSync(adrDir)) {
|
|
211
|
+
const m = f.match(/^(\d{1,4})-.+\.md$/)
|
|
212
|
+
if (m) max = Math.max(max, Number(m[1]))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const legacy = join(projectRoot, 'docs/09-decisions-and-open-questions.md')
|
|
216
|
+
if (existsSync(legacy)) {
|
|
217
|
+
for (const m of readFileSync(legacy, 'utf8').matchAll(/^### ADR-(\d+)/gm)) {
|
|
218
|
+
max = Math.max(max, Number(m[1]))
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const n of extraNumbers) {
|
|
222
|
+
if (Number.isInteger(n)) max = Math.max(max, n)
|
|
223
|
+
}
|
|
224
|
+
return max + 1
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** adr — `docs/adr/NNNN-<name>.md` 1개(코드/테스트 쌍 아님 — 프로젝트 결정 기록 문서, ADR-218).
|
|
228
|
+
* 번호는 opts.adrNumber(호출측이 원격 포함 해석) 우선, 미지정 시 로컬 스캔({@link nextAdrNumber}).
|
|
229
|
+
* @param {Variants} v @param {Record<string, any>} opts @param {string} projectRoot
|
|
230
|
+
* @returns {Artifact[]} */
|
|
231
|
+
function planAdr(v, opts, projectRoot) {
|
|
232
|
+
const number = Number.isInteger(opts.adrNumber) && opts.adrNumber > 0 ? opts.adrNumber : nextAdrNumber(projectRoot)
|
|
233
|
+
const padded = String(number).padStart(4, '0')
|
|
234
|
+
const d = new Date()
|
|
235
|
+
const p = (/** @type {number} */ n) => String(n).padStart(2, '0')
|
|
236
|
+
const vars = {
|
|
237
|
+
number: String(number),
|
|
238
|
+
title: v.words.join(' '),
|
|
239
|
+
date: `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`,
|
|
240
|
+
}
|
|
241
|
+
return [
|
|
242
|
+
{
|
|
243
|
+
outAbs: join(projectRoot, `docs/adr/${padded}-${v.kebab}.md`),
|
|
244
|
+
role: 'code',
|
|
245
|
+
content: renderTemplate(readTpl('adr', 'code.tpl'), vars),
|
|
246
|
+
},
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
|
|
168
250
|
/**
|
|
169
251
|
* @typedef {{ kebab: string, pascal: string, camel: string, snake: string, words: string[] }} Variants
|
|
170
252
|
* @typedef {Record<string, string>} BaseVars
|
|
@@ -347,6 +429,61 @@ export function writeArtifacts(artifacts, { force = false } = {}) {
|
|
|
347
429
|
return { written, skipped }
|
|
348
430
|
}
|
|
349
431
|
|
|
432
|
+
/**
|
|
433
|
+
* 플러그인 scaffold manifest(`mega.scaffold.register(name, { dir, files, description? })`,
|
|
434
|
+
* 03-api-spec §11 / ADR-199)를 artifact 목록으로 계획한다 — 빌트인 13종과 같은 plan→write 2단 분리.
|
|
435
|
+
* 토큰 계약은 {@link SCAFFOLD_TOKENS} 가 정본이며 미정의 토큰은 renderTemplate 가 throw(P4).
|
|
436
|
+
* `files[].path` 에도 토큰을 쓸 수 있다(예 `services/{{name}}-service.js`).
|
|
437
|
+
*
|
|
438
|
+
* @param {{ dir: string, files: Array<{ path: string, template: string }> }} def - 플러그인 등록 정의.
|
|
439
|
+
* @param {string} rawName - `mega g <kind> <name>` 의 name.
|
|
440
|
+
* @param {Record<string, any>} opts - { app }.
|
|
441
|
+
* @param {string} projectRoot - 출력 기준 루트. `def.dir` 는 이 루트 상대.
|
|
442
|
+
* @returns {Artifact[]}
|
|
443
|
+
* @throws {Error} def 파일 항목이 `{ path, template }` 문자열 쌍이 아니거나, 출력 경로가 projectRoot 를
|
|
444
|
+
* 벗어나면(경로 탐색 차단 — template.js resolveViewPath 정합) fail-fast.
|
|
445
|
+
*/
|
|
446
|
+
export function planScaffoldDef(def, rawName, opts, projectRoot) {
|
|
447
|
+
const app = typeof opts.app === 'string' && opts.app.length > 0 ? opts.app : 'main'
|
|
448
|
+
const v = nameVariants(rawName)
|
|
449
|
+
const vars = { Name: v.pascal, name: v.kebab, camelName: v.camel, snake: v.snake, app }
|
|
450
|
+
const rootAbs = resolve(projectRoot)
|
|
451
|
+
const baseDir = resolve(rootAbs, def.dir)
|
|
452
|
+
/** @type {Artifact[]} */
|
|
453
|
+
const out = []
|
|
454
|
+
for (const file of def.files) {
|
|
455
|
+
if (!file || typeof file.path !== 'string' || file.path.length === 0 || typeof file.template !== 'string') {
|
|
456
|
+
throw new Error(`scaffold def: files entries must be { path: string, template: string }. Got ${JSON.stringify(file)}.`)
|
|
457
|
+
}
|
|
458
|
+
const outAbs = resolve(baseDir, renderTemplate(file.path, vars))
|
|
459
|
+
// 출력은 projectRoot 내부로 제한 — def.dir/path 의 `..` 가 프로젝트 밖에 쓰는 걸 차단.
|
|
460
|
+
if (outAbs !== rootAbs && !outAbs.startsWith(rootAbs + sep)) {
|
|
461
|
+
throw new Error(`scaffold def: output '${file.path}' escapes the project root (path traversal blocked).`)
|
|
462
|
+
}
|
|
463
|
+
out.push({ outAbs, role: 'code', content: renderTemplate(file.template, vars) })
|
|
464
|
+
}
|
|
465
|
+
return out
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 플러그인 등록 scaffold generator 실행 — `mega g <plugin-generator> <name>` 의 본체. 빌트인 `generate`
|
|
470
|
+
* 와 같은 계획 → 쓰기 → 결과 계약(존재 파일 skip, `--force` 덮어쓰기).
|
|
471
|
+
* @param {string} kindName - 플러그인이 등록한 generator 이름(결과 보고용).
|
|
472
|
+
* @param {{ dir: string, files: Array<{ path: string, template: string }> }} def
|
|
473
|
+
* @param {string} rawName
|
|
474
|
+
* @param {object} [opts] - { app, force }
|
|
475
|
+
* @param {string} [projectRoot]
|
|
476
|
+
* @returns {{ kind: string, name: string, written: string[], skipped: string[] }}
|
|
477
|
+
*/
|
|
478
|
+
export function generateFromScaffoldDef(kindName, def, rawName, opts = {}, projectRoot = process.cwd()) {
|
|
479
|
+
if (typeof rawName !== 'string' || rawName.trim().length === 0) {
|
|
480
|
+
throw new Error(`mega g ${kindName}: a name is required (e.g. 'mega g ${kindName} users').`)
|
|
481
|
+
}
|
|
482
|
+
const artifacts = planScaffoldDef(def, rawName, /** @type {any} */ (opts), projectRoot)
|
|
483
|
+
const { written, skipped } = writeArtifacts(artifacts, { force: /** @type {any} */ (opts).force === true })
|
|
484
|
+
return { kind: kindName, name: rawName, written, skipped }
|
|
485
|
+
}
|
|
486
|
+
|
|
350
487
|
/**
|
|
351
488
|
* `mega g <kind> <name>` 실행 — 계획 → 쓰기 → 결과 반환.
|
|
352
489
|
* @param {string} kind
|