mega-framework 0.1.7 → 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/package.json +3 -3
- package/sample/crud/.env +9 -0
- package/sample/crud/.env.example +9 -0
- 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/mega.config.js +7 -0
- package/sample/crud/package.json +2 -2
- package/sample/crud/scripts/start-ws-hub.sh +18 -4
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/adapters/adapter-options.js +14 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +7 -4
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +4 -1
- package/src/adapters/mongo-adapter.js +21 -7
- package/src/adapters/postgres-adapter.js +8 -4
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +6 -2
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/scaffold.js +38 -2
- package/src/cli/generators/index.js +58 -1
- package/src/cli/index.js +88 -59
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +3 -1
- package/src/core/ctx-builder.js +59 -1
- package/src/core/envelope.js +9 -2
- package/src/core/hub-link.js +24 -14
- package/src/core/index.js +1 -1
- package/src/core/mega-app.js +55 -45
- package/src/core/pipeline.js +8 -6
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +3 -3
- package/src/core/session-store.js +14 -1
- package/src/core/ws-presence.js +17 -5
- package/src/core/ws-roster.js +49 -10
- package/src/core/ws-upgrade.js +105 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +10 -0
- package/src/lib/mega-job-queue.js +53 -13
- package/src/lib/mega-job.js +8 -1
- package/src/lib/mega-metrics.js +28 -1
- package/src/lib/mega-plugin.js +2 -2
- package/src/lib/mega-worker.js +28 -5
- package/src/lib/ws-hub.js +90 -9
- package/templates/adr/code.tpl +23 -0
- package/types/adapters/adapter-options.d.ts +2 -0
- package/types/adapters/file-adapter.d.ts +12 -1
- package/types/adapters/file-session-adapter.d.ts +4 -2
- package/types/adapters/maria-adapter.d.ts +5 -3
- package/types/adapters/mega-cache-adapter.d.ts +27 -1
- package/types/adapters/mega-db-adapter.d.ts +4 -1
- package/types/adapters/mongo-adapter.d.ts +13 -2
- package/types/adapters/postgres-adapter.d.ts +4 -2
- package/types/adapters/redis-adapter.d.ts +8 -0
- package/types/adapters/sqlite-adapter.d.ts +8 -2
- package/types/cli/generators/index.d.ts +11 -1
- package/types/cli/index.d.ts +12 -27
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ctx-builder.d.ts +23 -0
- package/types/core/hub-link.d.ts +3 -1
- package/types/core/index.d.ts +1 -1
- package/types/core/mega-app.d.ts +1 -1
- package/types/core/pipeline.d.ts +2 -1
- package/types/core/security.d.ts +3 -3
- package/types/core/session-store.d.ts +7 -0
- package/types/core/ws-roster.d.ts +13 -1
- package/types/core/ws-upgrade.d.ts +29 -0
- package/types/lib/mega-circuit-breaker.d.ts +4 -2
- package/types/lib/mega-health.d.ts +7 -0
- package/types/lib/mega-job-queue.d.ts +16 -4
- package/types/lib/mega-job.d.ts +8 -1
- package/types/lib/mega-plugin.d.ts +1 -1
- package/types/lib/mega-worker.d.ts +3 -1
- package/types/lib/ws-hub.d.ts +27 -2
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* @module cli/commands/console-cmd
|
|
10
10
|
*/
|
|
11
11
|
import repl from 'node:repl'
|
|
12
|
-
import { prepareRuntime } from '../../core/boot.js'
|
|
13
12
|
import { MegaShutdown } from '../../lib/mega-shutdown.js'
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -38,6 +37,9 @@ export async function startConsole(
|
|
|
38
37
|
projectRoot,
|
|
39
38
|
{ logger, replFactory = defaultReplFactory, out = console.log, shutdown = () => MegaShutdown.now(), setupSignals = (opts) => MegaShutdown.setupSignals(opts) } = {},
|
|
40
39
|
) {
|
|
40
|
+
// boot 그래프(fastify·OTel·pino 등)는 콘솔 기동 시점에만 로드 — scaffold.js 가 본 모듈을 정적
|
|
41
|
+
// import 하므로, 여기서 정적 import 하면 `mega help`/`g` 까지 전체 그래프를 지불하게 된다.
|
|
42
|
+
const { prepareRuntime } = await import('../../core/boot.js')
|
|
41
43
|
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
42
44
|
out('mega: console ready — globals: ctx, config, mega')
|
|
43
45
|
// prepareRuntime 이 어댑터/워커/wsHub 를 connect 하고 각자 MegaShutdown hook 을 자기등록한다. 정리 없이
|
|
@@ -13,7 +13,9 @@ import { Command } from 'commander'
|
|
|
13
13
|
import { existsSync } from 'node:fs'
|
|
14
14
|
import { join } from 'node:path'
|
|
15
15
|
import { pathToFileURL } from 'node:url'
|
|
16
|
-
import {
|
|
16
|
+
import { execFile } from 'node:child_process'
|
|
17
|
+
import { promisify } from 'node:util'
|
|
18
|
+
import { generate, generateFromScaffoldDef, nextAdrNumber, GENERATOR_KINDS } from '../generators/index.js'
|
|
17
19
|
import { scaffoldProject } from './new.js'
|
|
18
20
|
import { runRoutesCommand } from './routes.js'
|
|
19
21
|
import { runTestCommand } from './test-cmd.js'
|
|
@@ -31,6 +33,36 @@ function reportFiles(out, r, root) {
|
|
|
31
33
|
for (const f of r.skipped) out(` skip ${f.startsWith(root) ? f.slice(root.length + 1) : f} (exists — use --force)`)
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
const execFileAsync = promisify(execFile)
|
|
37
|
+
|
|
38
|
+
/**
|
|
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
|
+
|
|
34
66
|
/**
|
|
35
67
|
* `g model --adapter <key>` 의 adapter 키/driver 해석 — mega.config.js 의 services.databases 를
|
|
36
68
|
* best-effort 로 읽는다(스캐폴드는 config 가 아직 없는 초기 프로젝트에서도 돌아야 하므로 로드
|
|
@@ -109,10 +141,14 @@ export function registerScaffoldCommands(program, { out, projectRoot, logger, re
|
|
|
109
141
|
/** @type {{ key: string, driver: string | undefined }} */
|
|
110
142
|
let modelAdapter = { key: 'primary', driver: undefined }
|
|
111
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)
|
|
112
148
|
r = generate(
|
|
113
149
|
kind,
|
|
114
150
|
name,
|
|
115
|
-
{ app: opts.app, version: opts.version, kind: opts.kind, lng: opts.lng, force: opts.force === true, adapter: modelAdapter.key, adapterDriver: modelAdapter.driver },
|
|
151
|
+
{ app: opts.app, version: opts.version, kind: opts.kind, lng: opts.lng, force: opts.force === true, adapter: modelAdapter.key, adapterDriver: modelAdapter.driver, adrNumber },
|
|
116
152
|
projectRoot,
|
|
117
153
|
)
|
|
118
154
|
} else {
|
|
@@ -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,6 +36,7 @@ export const GENERATOR_KINDS = /** @type {const} */ ([
|
|
|
36
36
|
'locale',
|
|
37
37
|
'adapter',
|
|
38
38
|
'migration',
|
|
39
|
+
'adr',
|
|
39
40
|
])
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -185,11 +186,67 @@ export function planArtifacts(kind, rawName, opts, projectRoot) {
|
|
|
185
186
|
case 'app':
|
|
186
187
|
return planApp(v, projectRoot, base)
|
|
187
188
|
|
|
189
|
+
case 'adr':
|
|
190
|
+
return planAdr(v, opts, projectRoot)
|
|
191
|
+
|
|
188
192
|
default:
|
|
189
193
|
throw new Error(`Unknown generator kind '${kind}'. Supported: ${GENERATOR_KINDS.join(', ')}.`)
|
|
190
194
|
}
|
|
191
195
|
}
|
|
192
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
|
+
|
|
193
250
|
/**
|
|
194
251
|
* @typedef {{ kebab: string, pascal: string, camel: string, snake: string, words: string[] }} Variants
|
|
195
252
|
* @typedef {Record<string, string>} BaseVars
|
package/src/cli/index.js
CHANGED
|
@@ -19,26 +19,20 @@
|
|
|
19
19
|
*
|
|
20
20
|
* @module cli/index
|
|
21
21
|
*/
|
|
22
|
+
// 무거운 런타임 그래프(boot→fastify·OTel·pino·ejs·보안 플러그인, 클러스터, 마이그레이션 러너)는
|
|
23
|
+
// 정적 import 하지 않는다 — `mega help`/`g`/`new` 같은 비부팅 명령이 전체 그래프를 평가해 4배
|
|
24
|
+
// 느려지는 것을 막기 위해, 각 런타임 명령의 action/호스트 함수 내부에서 dynamic import 한다.
|
|
25
|
+
// 모듈 레벨 싱글톤(adapter-manager·MegaShutdown 등)은 dynamic import 도 같은 인스턴스를 본다.
|
|
22
26
|
import { existsSync } from 'node:fs'
|
|
23
27
|
import { join } from 'node:path'
|
|
24
28
|
import os from 'node:os'
|
|
25
29
|
import { spawn as nodeSpawn } from 'node:child_process'
|
|
26
|
-
import { bootApp, prepareRuntime } from '../core/boot.js'
|
|
27
|
-
import { MegaCluster } from '../core/mega-cluster.js'
|
|
28
|
-
import { installPrimaryAggregator, installWorkerResponder } from '../core/cluster-metrics.js'
|
|
29
30
|
import { loadAndValidateConfig } from '../core/config-loader.js'
|
|
30
|
-
import { migrateUp, migrateDown, migrateStatus } from '../core/migration-runner.js'
|
|
31
|
-
import { withMigrationLock } from '../core/migration-lock.js'
|
|
32
|
-
import { runMigrateGenerate } from '../core/migration/generate.js'
|
|
33
31
|
import { buildFromGlobalConfig, connectAll, disconnectAll, get as getAdapter } from '../adapters/adapter-manager.js'
|
|
34
32
|
import { MegaDbAdapter } from '../adapters/mega-db-adapter.js'
|
|
35
33
|
import { MegaConfigError } from '../errors/config-error.js'
|
|
36
34
|
import { MegaPluginHost, loadPlugins } from '../lib/mega-plugin.js'
|
|
37
|
-
import { MegaJobWorker } from '../lib/mega-job-worker.js'
|
|
38
|
-
import { MegaScheduler } from '../lib/mega-schedule.js'
|
|
39
35
|
import { MegaShutdown, DEFAULT_HARD_KILL_MS } from '../lib/mega-shutdown.js'
|
|
40
|
-
import { buildLogger } from '../lib/mega-logger.js'
|
|
41
|
-
import * as MegaMetrics from '../lib/mega-metrics.js'
|
|
42
36
|
import { Command } from 'commander'
|
|
43
37
|
import { registerScaffoldCommands, commanderErrorToExitCode } from './commands/scaffold.js'
|
|
44
38
|
|
|
@@ -240,37 +234,35 @@ export function shouldReexecForWatch(flags, env) {
|
|
|
240
234
|
}
|
|
241
235
|
|
|
242
236
|
/**
|
|
243
|
-
* `mega start --watch`
|
|
237
|
+
* `mega start --watch` supervisor 의 **자식 인자**를 빌드한다 (ADR-220, ADR-182 개정).
|
|
244
238
|
*
|
|
245
|
-
* - `--watch`
|
|
239
|
+
* - `--watch`/`--watch-ignore`(값 포함)는 부모(supervisor) 전용 — 자식 인자에서 제거한다
|
|
240
|
+
* (가드 env 와 이중 안전으로 무한재귀 방지).
|
|
246
241
|
* - **단일 프로세스 강제**(`--cluster 1`) — 멀티워커 watch 카오스 방지(기존 `--cluster` 값은 무시).
|
|
247
|
-
* - watchPaths 가 있으면 `--watch-path=…`(이게 모듈 그래프 watch 를 **대체**하므로 앱 소스·전역설정 경로를
|
|
248
|
-
* 명시해야 한다 — 실측 확인). 없으면 plain `--watch`(모듈 그래프 폴백).
|
|
249
242
|
*
|
|
250
|
-
* @param {
|
|
251
|
-
* @
|
|
252
|
-
* @param {string[]} o.watchPaths - 감시할 절대경로(존재하는 것만).
|
|
253
|
-
* @param {string} o.selfPath - mega 진입 스크립트(`process.argv[1]`).
|
|
254
|
-
* @param {string} o.execPath - node 바이너리(`process.execPath`).
|
|
255
|
-
* @returns {{ command: string, args: string[] }}
|
|
243
|
+
* @param {string[]} argv - 원본 mega argv(예: `['start','--watch','--port','3000']`).
|
|
244
|
+
* @returns {string[]} 자식 `mega` 인자(예: `['start','--port','3000','--cluster','1']`).
|
|
256
245
|
*/
|
|
257
|
-
export function
|
|
258
|
-
|
|
259
|
-
Array.isArray(watchPaths) && watchPaths.length > 0 ? watchPaths.map((p) => `--watch-path=${p}`) : ['--watch']
|
|
260
|
-
/** @type {string[]} mega 인자 — `--watch` 제거 + `--cluster`(값 포함) 제거. */
|
|
246
|
+
export function buildWatchChildArgs(argv) {
|
|
247
|
+
/** @type {string[]} */
|
|
261
248
|
const megaArgs = []
|
|
262
249
|
for (let i = 0; i < argv.length; i++) {
|
|
263
250
|
const tok = argv[i]
|
|
264
251
|
if (tok === '--watch' || tok.startsWith('--watch=')) continue
|
|
265
|
-
if (tok === '--
|
|
252
|
+
if (tok === '--watch-ignore') {
|
|
266
253
|
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) i++ // 값 토큰도 건너뜀.
|
|
267
254
|
continue
|
|
268
255
|
}
|
|
256
|
+
if (tok.startsWith('--watch-ignore=')) continue
|
|
257
|
+
if (tok === '--cluster') {
|
|
258
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) i++
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
269
261
|
if (tok.startsWith('--cluster=')) continue
|
|
270
262
|
megaArgs.push(tok)
|
|
271
263
|
}
|
|
272
264
|
megaArgs.push('--cluster', '1') // 단일 프로세스 강제.
|
|
273
|
-
return
|
|
265
|
+
return megaArgs
|
|
274
266
|
}
|
|
275
267
|
|
|
276
268
|
/**
|
|
@@ -338,38 +330,47 @@ export async function runCli(argv, { out = console.log, err = console.error, cwd
|
|
|
338
330
|
|
|
339
331
|
/**
|
|
340
332
|
* `mega start`/`serve` 본체 — dev watch 재실행 또는 부팅+listen(단일/클러스터).
|
|
341
|
-
* @param {{ port?: string, host?: string, cluster?: string | boolean, watch?: boolean }} opts - commander 옵션.
|
|
333
|
+
* @param {{ port?: string, host?: string, cluster?: string | boolean, watch?: boolean, watchIgnore?: string }} opts - commander 옵션.
|
|
342
334
|
* @param {{ argv: string[], out: (msg: string) => void, projectRoot: string, logger?: { debug?: Function, info?: Function, warn?: Function }, spawn: Function, selfPath: string }} ctx
|
|
343
335
|
* @returns {Promise<number>} exit code.
|
|
344
336
|
*/
|
|
345
337
|
async function runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath }) {
|
|
346
338
|
{
|
|
347
|
-
// dev watch (ADR-182) — `--watch` 면
|
|
348
|
-
//
|
|
349
|
-
//
|
|
339
|
+
// dev watch (ADR-220, ADR-182 개정) — `--watch` 면 자체 supervisor 가 자식(`mega start`)을 띄우고
|
|
340
|
+
// fs.watch(recursive) + ignore 패턴 + debounce 로 재시작을 조율한다. node 내장 --watch 는 ignore 가
|
|
341
|
+
// 없어 런타임 산출물(locales saveMissing·업로드·journal)이 재시작 루프를 만들었다. 단일 프로세스
|
|
342
|
+
// 강제 + NODE_ENV 미설정 시 development 기본 + 가드 env 로 무한재귀 방지는 기존과 동일.
|
|
350
343
|
if (shouldReexecForWatch({ watch: opts.watch === true }, process.env)) {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
const { mergeWatchIgnore, startWatchSupervisor, defaultWatchPaths } = await import('./watch.js')
|
|
345
|
+
// config 의 watch.ignore 는 best-effort — dev 루프는 config 가 깨진 동안에도 살아 있어야
|
|
346
|
+
// 하므로(고치면 재시작) 로드 실패 시 디폴트 ignore 로 진행하고 이유를 알린다.
|
|
347
|
+
/** @type {{ ignore?: unknown, ignoreDefaults?: unknown }} */
|
|
348
|
+
let watchConfig = {}
|
|
349
|
+
try {
|
|
350
|
+
const { global } = await loadAndValidateConfig(projectRoot)
|
|
351
|
+
watchConfig = /** @type {any} */ (global).watch ?? {}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
out(`mega: watch — mega.config.js 를 읽지 못해 디폴트 ignore 로 감시합니다(${/** @type {any} */ (e).message?.split('\n')[0] ?? e}).`)
|
|
354
|
+
}
|
|
355
|
+
const ignore = mergeWatchIgnore(watchConfig, typeof opts.watchIgnore === 'string' ? opts.watchIgnore : undefined)
|
|
356
|
+
const watchPaths = defaultWatchPaths(projectRoot)
|
|
357
|
+
const childArgs = buildWatchChildArgs(argv)
|
|
358
|
+
out(`mega: watch mode (single process) — watching [${watchPaths.join(', ')}], ignore ${ignore.length} patterns`)
|
|
359
|
+
if (ignore.length === 0) {
|
|
360
|
+
// 숨은 디폴트가 없으므로(ADR-220) 미설정은 "모든 변경이 재시작" — locales(saveMissing 자동
|
|
361
|
+
// 기입)·업로드 같은 런타임 쓰기 영역이 있으면 루프가 날 수 있어 가시화한다(P4).
|
|
362
|
+
out('mega: watch ignore 가 비어 있습니다 — locales/업로드 등 런타임 쓰기 폴더가 있으면 .env 의 WATCH_IGNORE(또는 config watch.ignore) 설정을 권장합니다.')
|
|
366
363
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
364
|
+
return await startWatchSupervisor({
|
|
365
|
+
projectRoot,
|
|
366
|
+
watchPaths,
|
|
367
|
+
ignore,
|
|
368
|
+
out,
|
|
369
|
+
spawnChild: () =>
|
|
370
|
+
spawn(process.execPath, [selfPath, ...childArgs], {
|
|
371
|
+
stdio: 'inherit',
|
|
372
|
+
env: { ...process.env, MEGA_WATCH_REEXEC: '1', NODE_ENV: process.env.NODE_ENV ?? 'development' },
|
|
373
|
+
}),
|
|
373
374
|
})
|
|
374
375
|
}
|
|
375
376
|
|
|
@@ -394,6 +395,15 @@ async function runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath
|
|
|
394
395
|
else out(`mega: ${msg}`)
|
|
395
396
|
}
|
|
396
397
|
|
|
398
|
+
// 런타임 그래프 lazy 로드 — 부팅 명령에서만 프레임워크 전체를 지불한다(모듈 상단 주석).
|
|
399
|
+
const [{ bootApp }, { MegaCluster }, { installPrimaryAggregator, installWorkerResponder }, { buildLogger }] =
|
|
400
|
+
await Promise.all([
|
|
401
|
+
import('../core/boot.js'),
|
|
402
|
+
import('../core/mega-cluster.js'),
|
|
403
|
+
import('../core/cluster-metrics.js'),
|
|
404
|
+
import('../lib/mega-logger.js'),
|
|
405
|
+
])
|
|
406
|
+
|
|
397
407
|
// 워커 부팅 함수 — 단일/클러스터 모드 공통. 클러스터에선 각 워커 프로세스가 이 함수를 실행한다.
|
|
398
408
|
const bootListen = async () => {
|
|
399
409
|
const { server, appLogger } = await bootApp(projectRoot, { listen: true, port, host, logger })
|
|
@@ -485,7 +495,8 @@ function buildProgram({ argv, out, err, projectRoot, logger, spawn, selfPath, se
|
|
|
485
495
|
'--cluster [n]',
|
|
486
496
|
"워커 프로세스 수 — 정수 N 또는 'max'(CPU 코어 수, 베어 플래그도 max). 우선순위: 플래그 > MEGA_CLUSTER_WORKERS env > server.cluster config. 1/미지정 = 단일 프로세스",
|
|
487
497
|
)
|
|
488
|
-
.option('--watch', 'dev watch — 파일 변경 시 자동 재시작(
|
|
498
|
+
.option('--watch', 'dev watch — 파일 변경 시 자동 재시작(단일 프로세스 강제). locales/views/public/uploads/var/.mega/.env* 등은 디폴트 ignore')
|
|
499
|
+
.option('--watch-ignore <globs>', 'watch 추가 ignore(콤마 구분 glob, 디폴트에 더해짐). config watch.ignore 와 병합')
|
|
489
500
|
.action(async (/** @type {any} */ opts) => {
|
|
490
501
|
setExit(await runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath }))
|
|
491
502
|
})
|
|
@@ -539,6 +550,7 @@ function buildProgram({ argv, out, err, projectRoot, logger, spawn, selfPath, se
|
|
|
539
550
|
.option('--adapter <key>', '특정 adapter(services.databases 키)만 처리(기본: 전부)')
|
|
540
551
|
.option('--app <name>', '마이그레이션 파일을 둘 앱(기본: main)', 'main')
|
|
541
552
|
.action(async (/** @type {string | undefined} */ name, /** @type {any} */ opts) => {
|
|
553
|
+
const { runMigrateGenerate } = await import('../core/migration/generate.js')
|
|
542
554
|
const r = await runMigrateGenerate(projectRoot, {
|
|
543
555
|
name,
|
|
544
556
|
renames: opts.renames,
|
|
@@ -662,10 +674,11 @@ export function collectRegistrations(global, host, kind) {
|
|
|
662
674
|
* 주입(테스트)이 있으면 그쪽을 그대로 쓰고 pino 는 만들지 않는다(side-effect 회피).
|
|
663
675
|
* @param {Object} global - global config(`logger` 보유 가능).
|
|
664
676
|
* @param {{ info?: Function, warn?: Function } | undefined} injectedLogger - 주입 로거(테스트) 또는 undefined.
|
|
665
|
-
* @returns {{ info?: Function, warn?: Function } | null} 호스트 로그에 쓸 로거(없으면 null).
|
|
677
|
+
* @returns {Promise<{ info?: Function, warn?: Function } | null>} 호스트 로그에 쓸 로거(없으면 null).
|
|
666
678
|
*/
|
|
667
|
-
function wireHostLogger(global, injectedLogger) {
|
|
679
|
+
async function wireHostLogger(global, injectedLogger) {
|
|
668
680
|
if (injectedLogger) return injectedLogger
|
|
681
|
+
const { buildLogger } = await import('../lib/mega-logger.js') // pino 그래프 — 호스트 경로에서만 로드.
|
|
669
682
|
const appLogger = buildLogger(/** @type {any} */ (global).logger)
|
|
670
683
|
if (appLogger) {
|
|
671
684
|
// 종료 시퀀스 로그를 pino 로(ADR-178) + graceful 시 로그 버퍼 flush. 'logs' stage 라 종료
|
|
@@ -683,12 +696,17 @@ function wireHostLogger(global, injectedLogger) {
|
|
|
683
696
|
* 등록 소스 = `config.jobs` + 플러그인 `mega.jobs.register` 분(`collectRegistrations`, ADR-123).
|
|
684
697
|
* @param {string} projectRoot
|
|
685
698
|
* @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
|
|
686
|
-
* @returns {Promise<MegaJobWorker>}
|
|
699
|
+
* @returns {Promise<import('../lib/mega-job-worker.js').MegaJobWorker>}
|
|
687
700
|
*/
|
|
688
701
|
export async function runWorkerHost(projectRoot, logger) {
|
|
689
|
-
// bootApp 과 같은 토대(config → 플러그인 install → 어댑터 connect → ctx).
|
|
702
|
+
// bootApp 과 같은 토대(config → 플러그인 install → 어댑터 connect → ctx). 런타임 그래프는 lazy.
|
|
703
|
+
const [{ prepareRuntime }, { MegaJobWorker }, MegaMetrics] = await Promise.all([
|
|
704
|
+
import('../core/boot.js'),
|
|
705
|
+
import('../lib/mega-job-worker.js'),
|
|
706
|
+
import('../lib/mega-metrics.js'),
|
|
707
|
+
])
|
|
690
708
|
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
691
|
-
const log = wireHostLogger(global, logger)
|
|
709
|
+
const log = await wireHostLogger(global, logger)
|
|
692
710
|
const worker = new MegaJobWorker({ ctx })
|
|
693
711
|
// 잡 메트릭 (ADR-132) — prepareRuntime 이 health.exposeMetrics 시 이미 MegaMetrics.init 했고,
|
|
694
712
|
// 옵트인 OFF 면 subscribeJobs 는 no-op() 을 반환한다. start 전에 구독해 enqueue(dispatch)부터 잡는다. 구독은
|
|
@@ -712,11 +730,15 @@ export async function runWorkerHost(projectRoot, logger) {
|
|
|
712
730
|
* `mega scheduler` 호스트 골격 — config + 어댑터 connect + ctx + `MegaScheduler` 인스턴스 + graceful.
|
|
713
731
|
* @param {string} projectRoot
|
|
714
732
|
* @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
|
|
715
|
-
* @returns {Promise<MegaScheduler>}
|
|
733
|
+
* @returns {Promise<import('../lib/mega-schedule.js').MegaScheduler>}
|
|
716
734
|
*/
|
|
717
735
|
export async function runSchedulerHost(projectRoot, logger) {
|
|
736
|
+
const [{ prepareRuntime }, { MegaScheduler }] = await Promise.all([
|
|
737
|
+
import('../core/boot.js'),
|
|
738
|
+
import('../lib/mega-schedule.js'),
|
|
739
|
+
])
|
|
718
740
|
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
719
|
-
const log = wireHostLogger(global, logger)
|
|
741
|
+
const log = await wireHostLogger(global, logger)
|
|
720
742
|
const scheduler = new MegaScheduler({ ctx })
|
|
721
743
|
// 등록 소스(ADR-123) = config.schedules(정적) + 플러그인 host.listSchedules()(동적). register 가
|
|
722
744
|
// cron 미선언·중복을 부팅 시 fail-fast.
|
|
@@ -807,6 +829,13 @@ export async function runMigrateHost(projectRoot, { direction = 'up', dbKey, log
|
|
|
807
829
|
info: (/** @type {any} */ o, /** @type {string} */ msg) => out(`[migrate] ${fmt(o, msg)}`),
|
|
808
830
|
warn: (/** @type {any} */ o, /** @type {string} */ msg) => out(`[migrate] 경고: ${fmt(o, msg)}`),
|
|
809
831
|
}
|
|
832
|
+
// 빌트인 어댑터 배럴을 명시 로드(ADR-150) — migrate 는 boot.js 를 거치지 않고 buildFromGlobalConfig
|
|
833
|
+
// 를 직접 부르므로, boot 정적 import 가 사라진 lazy CLI 에선 여기서 driver 자기등록을 보장해야 한다.
|
|
834
|
+
const [{ migrateUp, migrateDown, migrateStatus }, { withMigrationLock }] = await Promise.all([
|
|
835
|
+
import('../core/migration-runner.js'),
|
|
836
|
+
import('../core/migration-lock.js'),
|
|
837
|
+
import('../adapters/index.js'),
|
|
838
|
+
])
|
|
810
839
|
const { global, apps } = await loadAndValidateConfig(projectRoot)
|
|
811
840
|
// 플러그인 install 을 어댑터 build 보다 먼저(config-driven driver 제약, boot.js 주석 참조).
|
|
812
841
|
const host = new MegaPluginHost({ logger })
|
package/src/cli/watch.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `mega start --watch` 의 dev watch supervisor (ADR-220, ADR-182 개정).
|
|
4
|
+
*
|
|
5
|
+
* Node 내장 `--watch-path` 재실행(구 ADR-182)은 **ignore 패턴이 없어** 런타임이 쓰는 파일까지
|
|
6
|
+
* 재시작을 유발했다 — dev 의 i18n saveMissing 이 `locales/*.json` 자동 기입(재시작 무한 루프),
|
|
7
|
+
* 업로드가 `uploads/`/`var/` 에 파일 저장(사용자 입력이 서버를 재시작), 마이그레이션 generate 가
|
|
8
|
+
* `.mega/` journal 갱신. 본 모듈이 그 재실행을 **자체 supervisor**(fs.watch recursive + 미니 glob
|
|
9
|
+
* ignore + debounce + SIGTERM respawn)로 대체한다 — 신규 dep 0(chokidar 미도입: 필요한 매칭이
|
|
10
|
+
* `**`/`*`/`?` 수준이라 풀스펙 glob 라이브러리가 과함).
|
|
11
|
+
*
|
|
12
|
+
* ignore 합성 = config `watch.ignore` + CLI `--watch-ignore`. **숨은 코드 디폴트는 없다**(사용자
|
|
13
|
+
* 결정 — 개발자가 보지 못하는 목록은 폴더명이 다른 프로젝트에서 오히려 혼란) — 권장 목록은 스캐폴드
|
|
14
|
+
* `.env` 의 `WATCH_IGNORE` 가 실값으로 명시하고 `mega.config.js` 가 읽어 `watch.ignore` 로 넘긴다.
|
|
15
|
+
* ignore 0 이면 supervisor 가 기동 시 경고 1줄을 낸다(locales saveMissing 루프 등 위험 가시화).
|
|
16
|
+
*
|
|
17
|
+
* @module cli/watch
|
|
18
|
+
*/
|
|
19
|
+
import { watch as fsWatch, existsSync, statSync } from 'node:fs'
|
|
20
|
+
import { join, relative, sep } from 'node:path'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 미니 glob → RegExp. 지원: 더블스타+슬래시(0개 이상의 디렉토리), 더블스타(경로 전체), `*`(세그먼트
|
|
24
|
+
* 내), `?`(1글자). 그 외 문자는 리터럴(정규식 특수문자 escape). 매칭 대상은 루트 상대 POSIX 경로
|
|
25
|
+
* 전체(^…$). 패턴 예시는 스캐폴드 `.env` 의 `WATCH_IGNORE` 참조 — JS 블록 주석 안에는
|
|
26
|
+
* 더블스타+슬래시 시퀀스를 쓸 수 없어(주석 조기 종료) 여기 직접 못 적는다.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} pattern - glob 패턴(예 `.env.*`).
|
|
29
|
+
* @returns {RegExp}
|
|
30
|
+
*/
|
|
31
|
+
export function globToRegExp(pattern) {
|
|
32
|
+
let re = ''
|
|
33
|
+
let i = 0
|
|
34
|
+
while (i < pattern.length) {
|
|
35
|
+
const ch = pattern[i]
|
|
36
|
+
if (ch === '*') {
|
|
37
|
+
if (pattern[i + 1] === '*') {
|
|
38
|
+
// `**/` 는 0개 이상의 디렉토리(루트 레벨 포함), 그 외 `**` 는 임의 문자열.
|
|
39
|
+
if (pattern[i + 2] === '/') {
|
|
40
|
+
re += '(?:.*/)?'
|
|
41
|
+
i += 3
|
|
42
|
+
} else {
|
|
43
|
+
re += '.*'
|
|
44
|
+
i += 2
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
re += '[^/]*'
|
|
48
|
+
i += 1
|
|
49
|
+
}
|
|
50
|
+
} else if (ch === '?') {
|
|
51
|
+
re += '[^/]'
|
|
52
|
+
i += 1
|
|
53
|
+
} else {
|
|
54
|
+
re += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
55
|
+
i += 1
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return new RegExp(`^${re}$`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ignore 패턴 합성 — config(`watch.ignore`) + CLI(`--watch-ignore a,b`). 숨은 디폴트 없음(모듈
|
|
63
|
+
* docstring 참조). 잘못된 모양은 fail-fast(silent 무시 금지).
|
|
64
|
+
*
|
|
65
|
+
* @param {{ ignore?: unknown }} [watchConfig] - mega.config.js 의 `watch`.
|
|
66
|
+
* @param {string | undefined} [cliIgnore] - `--watch-ignore` 값(콤마 구분).
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
* @throws {TypeError} config `watch.ignore` 가 문자열 배열이 아닐 때.
|
|
69
|
+
*/
|
|
70
|
+
export function mergeWatchIgnore(watchConfig = {}, cliIgnore = undefined) {
|
|
71
|
+
/** @type {string[]} */
|
|
72
|
+
const merged = []
|
|
73
|
+
if (watchConfig.ignore !== undefined) {
|
|
74
|
+
if (!Array.isArray(watchConfig.ignore) || watchConfig.ignore.some((p) => typeof p !== 'string' || p.length === 0)) {
|
|
75
|
+
throw new TypeError('config watch.ignore 는 비어있지 않은 문자열 배열이어야 합니다.')
|
|
76
|
+
}
|
|
77
|
+
merged.push(...watchConfig.ignore)
|
|
78
|
+
}
|
|
79
|
+
if (typeof cliIgnore === 'string' && cliIgnore.length > 0) {
|
|
80
|
+
merged.push(...cliIgnore.split(',').map((p) => p.trim()).filter((p) => p.length > 0))
|
|
81
|
+
}
|
|
82
|
+
return merged
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 루트 상대 경로가 ignore 에 걸리는지 (Boolean — `is*`).
|
|
87
|
+
* @param {string} rel - 루트 상대 POSIX 경로(예 `apps/main/locales/ko.json`).
|
|
88
|
+
* @param {RegExp[]} regexps - {@link globToRegExp} 컴파일 결과.
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
export function isIgnoredPath(rel, regexps) {
|
|
92
|
+
return regexps.some((r) => r.test(rel))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* watch supervisor — 감시 경로의 변경(ignore 통과분)을 debounce 후 자식(`mega start`)을
|
|
97
|
+
* SIGTERM → respawn 한다. 자식이 스스로 죽으면(crash) 재시작하지 않고 다음 변경을 기다린다
|
|
98
|
+
* (crash-loop 방지 — nodemon 의 "app crashed - waiting for file changes" 와 동형).
|
|
99
|
+
*
|
|
100
|
+
* @param {object} o
|
|
101
|
+
* @param {string} o.projectRoot
|
|
102
|
+
* @param {string[]} o.watchPaths - 감시할 절대경로(존재하는 것만 — 호출측이 필터).
|
|
103
|
+
* @param {string[]} o.ignore - glob 패턴 목록.
|
|
104
|
+
* @param {() => import('node:child_process').ChildProcess} o.spawnChild - 자식 기동 팩토리(주입 — 테스트).
|
|
105
|
+
* @param {(msg: string) => void} o.out
|
|
106
|
+
* @param {number} [o.debounceMs=500] - 연속 변경 디바운스.
|
|
107
|
+
* @param {(dir: string, opts: object, cb: (event: string, filename: string | Buffer | null) => void) => { close(): void }} [o.watchFn] -
|
|
108
|
+
* fs.watch 주입(테스트). 기본 node:fs watch.
|
|
109
|
+
* @returns {Promise<number>} 부모(supervisor)의 exit code — SIGINT/SIGTERM 수신 시 자식 정리 후 0.
|
|
110
|
+
*/
|
|
111
|
+
export function startWatchSupervisor({ projectRoot, watchPaths, ignore, spawnChild, out, debounceMs = 500, watchFn = /** @type {any} */ (fsWatch) }) {
|
|
112
|
+
const regexps = ignore.map(globToRegExp)
|
|
113
|
+
/** @type {import('node:child_process').ChildProcess | null} */
|
|
114
|
+
let child = null
|
|
115
|
+
/** @type {NodeJS.Timeout | null} */
|
|
116
|
+
let pending = null
|
|
117
|
+
let shuttingDown = false
|
|
118
|
+
let restarting = false
|
|
119
|
+
|
|
120
|
+
const start = () => {
|
|
121
|
+
child = spawnChild()
|
|
122
|
+
child.on('exit', (code, signal) => {
|
|
123
|
+
if (shuttingDown) return
|
|
124
|
+
if (restarting) {
|
|
125
|
+
restarting = false
|
|
126
|
+
start()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
// 자식이 스스로 종료(crash/정상) — 재시작 폭주 대신 다음 변경 대기(P4: 이유 출력).
|
|
130
|
+
out(`mega: app exited (code=${code ?? `signal:${signal}`}) — waiting for file changes before restart`)
|
|
131
|
+
child = null
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const scheduleRestart = (/** @type {string} */ rel) => {
|
|
136
|
+
if (pending) clearTimeout(pending)
|
|
137
|
+
pending = setTimeout(() => {
|
|
138
|
+
pending = null
|
|
139
|
+
out(`mega: restarting due to changes (${rel})`)
|
|
140
|
+
if (child && child.exitCode === null && !child.killed) {
|
|
141
|
+
restarting = true
|
|
142
|
+
child.kill('SIGTERM') // graceful — exit 핸들러가 respawn.
|
|
143
|
+
} else {
|
|
144
|
+
start() // crash 후 대기 상태 — 변경으로 재기동.
|
|
145
|
+
}
|
|
146
|
+
}, debounceMs)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @type {Array<{ close(): void }>} */
|
|
150
|
+
const watchers = []
|
|
151
|
+
for (const abs of watchPaths) {
|
|
152
|
+
const isDir = statSync(abs).isDirectory()
|
|
153
|
+
const watcher = watchFn(abs, { recursive: isDir }, (_event, filename) => {
|
|
154
|
+
// filename 은 감시 루트 상대(파일 watch 면 basename) — 루트 상대로 정규화해 ignore 매칭.
|
|
155
|
+
const name = typeof filename === 'string' ? filename : (filename ? String(filename) : '')
|
|
156
|
+
const rel = relative(projectRoot, isDir ? join(abs, name) : abs).split(sep).join('/')
|
|
157
|
+
if (isIgnoredPath(rel, regexps)) return
|
|
158
|
+
scheduleRestart(rel)
|
|
159
|
+
})
|
|
160
|
+
watchers.push(watcher)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
start()
|
|
164
|
+
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
/** @type {NodeJS.Signals[]} */
|
|
167
|
+
const sigs = ['SIGINT', 'SIGTERM']
|
|
168
|
+
const onSignal = () => {
|
|
169
|
+
shuttingDown = true
|
|
170
|
+
if (pending) clearTimeout(pending)
|
|
171
|
+
for (const w of watchers) w.close()
|
|
172
|
+
for (const s of sigs) process.removeListener(s, onSignal)
|
|
173
|
+
if (child && child.exitCode === null && !child.killed) {
|
|
174
|
+
child.once('exit', () => resolve(0))
|
|
175
|
+
child.kill('SIGTERM')
|
|
176
|
+
} else {
|
|
177
|
+
resolve(0)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const s of sigs) process.on(s, onSignal)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** {@link startWatchSupervisor} 의 watchPaths 기본 집합 — 존재하는 것만.
|
|
185
|
+
* @param {string} projectRoot @returns {string[]} */
|
|
186
|
+
export function defaultWatchPaths(projectRoot) {
|
|
187
|
+
return [join(projectRoot, 'apps'), join(projectRoot, 'shared'), join(projectRoot, 'mega.config.js')].filter(existsSync)
|
|
188
|
+
}
|
package/src/core/ajv-mapper.js
CHANGED
|
@@ -74,7 +74,9 @@ function ajvItemToDetail(e) {
|
|
|
74
74
|
if (e.keyword === 'required' && missing) {
|
|
75
75
|
field = field ? `${field}.${missing}` : missing
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
// 정규식 1회 실행 (G1 M-2, ADR-214) — field·missingProperty 를 합쳐 한 번만 검사한다. allErrors
|
|
78
|
+
// (ADR-193) 이후 400 1건당 detail × 2회 실행되던 비용 절반화. 매칭 의미는 동일(부분 일치 OR).
|
|
79
|
+
const isSensitive = SENSITIVE_FIELD_RE.test(missing ? `${field} ${missing}` : field)
|
|
78
80
|
return {
|
|
79
81
|
field,
|
|
80
82
|
rule: e.keyword || 'unknown',
|