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.
Files changed (322) hide show
  1. package/.env +127 -0
  2. package/.env.example +186 -0
  3. package/.prettierrc.json +8 -0
  4. package/CHANGELOG.md +259 -0
  5. package/LICENSE +21 -0
  6. package/README.md +153 -0
  7. package/bin/mega-ws-hub.js +15 -0
  8. package/bin/mega.js +38 -0
  9. package/docker-compose.yml +201 -0
  10. package/eslint.config.js +57 -0
  11. package/infra/otel-collector-config.yaml +43 -0
  12. package/jsconfig.json +18 -0
  13. package/package.json +121 -0
  14. package/sample/crud/.env +18 -0
  15. package/sample/crud/.env.example +50 -0
  16. package/sample/crud/README.md +85 -0
  17. package/sample/crud/apps/main/app.config.js +114 -0
  18. package/sample/crud/apps/main/channels/chat-bus.js +115 -0
  19. package/sample/crud/apps/main/channels/chat-channel.js +145 -0
  20. package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
  21. package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
  22. package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
  23. package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
  24. package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
  25. package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
  26. package/sample/crud/apps/main/controllers/note-controller.js +116 -0
  27. package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
  28. package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
  29. package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
  30. package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
  31. package/sample/crud/apps/main/controllers/user-controller.js +34 -0
  32. package/sample/crud/apps/main/controllers/web-controller.js +137 -0
  33. package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
  34. package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
  35. package/sample/crud/apps/main/jobs/email-job.js +72 -0
  36. package/sample/crud/apps/main/locales/client/en.json +3 -0
  37. package/sample/crud/apps/main/locales/client/ko.json +3 -0
  38. package/sample/crud/apps/main/locales/server/en.json +316 -0
  39. package/sample/crud/apps/main/locales/server/ko.json +316 -0
  40. package/sample/crud/apps/main/middleware/web-auth.js +40 -0
  41. package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
  42. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
  43. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
  44. package/sample/crud/apps/main/models/note.js +71 -0
  45. package/sample/crud/apps/main/models/user.js +86 -0
  46. package/sample/crud/apps/main/public/css/app.css +101 -0
  47. package/sample/crud/apps/main/public/css/guide.css +137 -0
  48. package/sample/crud/apps/main/public/js/app.js +54 -0
  49. package/sample/crud/apps/main/public/js/perf.js +129 -0
  50. package/sample/crud/apps/main/public/js/theme-init.js +12 -0
  51. package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
  52. package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
  53. package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
  54. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  55. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  56. package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
  57. package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
  58. package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
  59. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
  60. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
  61. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
  62. package/sample/crud/apps/main/routes/auth.js +15 -0
  63. package/sample/crud/apps/main/routes/cron.js +14 -0
  64. package/sample/crud/apps/main/routes/guide.js +25 -0
  65. package/sample/crud/apps/main/routes/jobs.js +14 -0
  66. package/sample/crud/apps/main/routes/logs.js +28 -0
  67. package/sample/crud/apps/main/routes/metrics.js +13 -0
  68. package/sample/crud/apps/main/routes/notes.js +19 -0
  69. package/sample/crud/apps/main/routes/perf.js +47 -0
  70. package/sample/crud/apps/main/routes/redis.js +14 -0
  71. package/sample/crud/apps/main/routes/tracing.js +14 -0
  72. package/sample/crud/apps/main/routes/upload.js +16 -0
  73. package/sample/crud/apps/main/routes/users.js +54 -0
  74. package/sample/crud/apps/main/routes/web.js +23 -0
  75. package/sample/crud/apps/main/routes/worker.js +15 -0
  76. package/sample/crud/apps/main/routes/ws.js +30 -0
  77. package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
  78. package/sample/crud/apps/main/services/auth-service.js +74 -0
  79. package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
  80. package/sample/crud/apps/main/services/guide-service.js +145 -0
  81. package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
  82. package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
  83. package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
  84. package/sample/crud/apps/main/services/note-service.js +75 -0
  85. package/sample/crud/apps/main/services/perf-service.js +302 -0
  86. package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
  87. package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
  88. package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
  89. package/sample/crud/apps/main/services/user-service.js +65 -0
  90. package/sample/crud/apps/main/views/auth/login.ejs +57 -0
  91. package/sample/crud/apps/main/views/auth/register.ejs +71 -0
  92. package/sample/crud/apps/main/views/cron/index.ejs +92 -0
  93. package/sample/crud/apps/main/views/guide/index.ejs +24 -0
  94. package/sample/crud/apps/main/views/guide/page.ejs +64 -0
  95. package/sample/crud/apps/main/views/home.ejs +82 -0
  96. package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
  97. package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
  98. package/sample/crud/apps/main/views/logs/index.ejs +80 -0
  99. package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
  100. package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
  101. package/sample/crud/apps/main/views/notes/list.ejs +74 -0
  102. package/sample/crud/apps/main/views/notes/new.ejs +45 -0
  103. package/sample/crud/apps/main/views/perf/index.ejs +90 -0
  104. package/sample/crud/apps/main/views/redis/index.ejs +65 -0
  105. package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
  106. package/sample/crud/apps/main/views/upload/index.ejs +79 -0
  107. package/sample/crud/apps/main/views/users/edit.ejs +48 -0
  108. package/sample/crud/apps/main/views/users/list.ejs +81 -0
  109. package/sample/crud/apps/main/views/users/new.ejs +48 -0
  110. package/sample/crud/apps/main/views/worker/index.ejs +70 -0
  111. package/sample/crud/apps/main/views/ws/index.ejs +62 -0
  112. package/sample/crud/apps/main/workers/hash-worker.js +17 -0
  113. package/sample/crud/apps/main/workers/hash.task.js +22 -0
  114. package/sample/crud/ecosystem.config.cjs +9 -0
  115. package/sample/crud/mega.config.js +105 -0
  116. package/sample/crud/package-lock.json +5665 -0
  117. package/sample/crud/package.json +28 -0
  118. package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
  119. package/sample/crud/test/apps/main/auth-service.test.js +93 -0
  120. package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
  121. package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
  122. package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
  123. package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
  124. package/sample/crud/test/apps/main/email-job.test.js +76 -0
  125. package/sample/crud/test/apps/main/guide-service.test.js +68 -0
  126. package/sample/crud/test/apps/main/hash-task.test.js +30 -0
  127. package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
  128. package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
  129. package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
  130. package/sample/crud/test/apps/main/note-service.test.js +68 -0
  131. package/sample/crud/test/apps/main/perf-service.test.js +121 -0
  132. package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
  133. package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
  134. package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
  135. package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
  136. package/sample/crud/test/apps/main/user-service.test.js +65 -0
  137. package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
  138. package/sample/crud/vitest.config.js +8 -0
  139. package/sample/crud/yarn.lock +2142 -0
  140. package/sample/simple/.env.example +15 -0
  141. package/sample/simple/README.md +52 -0
  142. package/sample/simple/apps/main/app.config.js +35 -0
  143. package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
  144. package/sample/simple/apps/main/locales/client/en.json +3 -0
  145. package/sample/simple/apps/main/locales/client/ko.json +3 -0
  146. package/sample/simple/apps/main/locales/server/en.json +23 -0
  147. package/sample/simple/apps/main/locales/server/ko.json +23 -0
  148. package/sample/simple/apps/main/public/css/app.css +101 -0
  149. package/sample/simple/apps/main/public/hello.txt +1 -0
  150. package/sample/simple/apps/main/public/js/app.js +54 -0
  151. package/sample/simple/apps/main/public/js/theme-init.js +12 -0
  152. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  153. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  154. package/sample/simple/apps/main/routes/index.js +9 -0
  155. package/sample/simple/apps/main/routes/pages.js +12 -0
  156. package/sample/simple/apps/main/views/index.ejs +56 -0
  157. package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
  158. package/sample/simple/ecosystem.config.cjs +10 -0
  159. package/sample/simple/mega.config.js +27 -0
  160. package/sample/simple/package-lock.json +1851 -0
  161. package/sample/simple/package.json +25 -0
  162. package/sample/simple/test/apps/main/index.test.js +13 -0
  163. package/sample/simple/vitest.config.js +8 -0
  164. package/src/adapters/adapter-manager.js +305 -0
  165. package/src/adapters/adapter-options.js +208 -0
  166. package/src/adapters/file-adapter.js +350 -0
  167. package/src/adapters/file-session-adapter.js +363 -0
  168. package/src/adapters/index.js +38 -0
  169. package/src/adapters/maria-adapter.js +425 -0
  170. package/src/adapters/mega-adapter.js +511 -0
  171. package/src/adapters/mega-bus-adapter.js +81 -0
  172. package/src/adapters/mega-cache-adapter.js +94 -0
  173. package/src/adapters/mega-db-adapter.js +72 -0
  174. package/src/adapters/mega-lock-adapter.js +118 -0
  175. package/src/adapters/mega-log-sink-adapter.js +46 -0
  176. package/src/adapters/mega-session-adapter.js +72 -0
  177. package/src/adapters/mongo-adapter.js +396 -0
  178. package/src/adapters/nats-adapter.js +370 -0
  179. package/src/adapters/postgres-adapter.js +341 -0
  180. package/src/adapters/redis-adapter.js +331 -0
  181. package/src/adapters/redis-session-adapter.js +261 -0
  182. package/src/adapters/redlock-adapter.js +385 -0
  183. package/src/adapters/registry.js +157 -0
  184. package/src/adapters/sqlite-adapter.js +309 -0
  185. package/src/auth/index.js +103 -0
  186. package/src/cli/commands/console-cmd.js +56 -0
  187. package/src/cli/commands/new.js +101 -0
  188. package/src/cli/commands/routes.js +107 -0
  189. package/src/cli/commands/scaffold.js +120 -0
  190. package/src/cli/commands/test-cmd.js +45 -0
  191. package/src/cli/generators/index.js +368 -0
  192. package/src/cli/index.js +472 -0
  193. package/src/cli/template-engine.js +72 -0
  194. package/src/cli/ws-hub.js +582 -0
  195. package/src/core/ajv-mapper.js +80 -0
  196. package/src/core/boot.js +323 -0
  197. package/src/core/cluster-metrics.js +278 -0
  198. package/src/core/config-loader.js +115 -0
  199. package/src/core/config-validator.js +322 -0
  200. package/src/core/ctx-builder.js +253 -0
  201. package/src/core/envelope.js +88 -0
  202. package/src/core/error-mapper.js +116 -0
  203. package/src/core/formbody.js +69 -0
  204. package/src/core/hub-link.js +552 -0
  205. package/src/core/i18n.js +525 -0
  206. package/src/core/index.js +63 -0
  207. package/src/core/mega-app.js +1138 -0
  208. package/src/core/mega-cluster.js +232 -0
  209. package/src/core/mega-server.js +176 -0
  210. package/src/core/mega-service.js +41 -0
  211. package/src/core/migration-runner.js +196 -0
  212. package/src/core/multipart.js +282 -0
  213. package/src/core/openapi.js +114 -0
  214. package/src/core/router.js +388 -0
  215. package/src/core/routes-loader.js +57 -0
  216. package/src/core/scope-registry.js +53 -0
  217. package/src/core/security.js +275 -0
  218. package/src/core/services-loader.js +98 -0
  219. package/src/core/session-cleanup-schedule.js +57 -0
  220. package/src/core/session-store.js +55 -0
  221. package/src/core/session.js +414 -0
  222. package/src/core/static-assets.js +126 -0
  223. package/src/core/template.js +294 -0
  224. package/src/core/workers-manager.js +193 -0
  225. package/src/core/ws-compression.js +112 -0
  226. package/src/core/ws-controller.js +109 -0
  227. package/src/core/ws-message.js +176 -0
  228. package/src/core/ws-upgrade.js +445 -0
  229. package/src/errors/config-error.js +16 -0
  230. package/src/errors/http-errors.js +130 -0
  231. package/src/errors/index.js +19 -0
  232. package/src/errors/mega-error.js +34 -0
  233. package/src/eslint-plugin/index.js +15 -0
  234. package/src/eslint-plugin/no-direct-model-import.js +113 -0
  235. package/src/index.js +131 -0
  236. package/src/lib/asp/config.js +83 -0
  237. package/src/lib/asp/crypto.js +145 -0
  238. package/src/lib/asp/errors.js +49 -0
  239. package/src/lib/asp/nonce-cache.js +94 -0
  240. package/src/lib/asp/plugin.js +263 -0
  241. package/src/lib/asp/ws-terminator.js +101 -0
  242. package/src/lib/env-mapper.js +222 -0
  243. package/src/lib/hub-protocol.js +322 -0
  244. package/src/lib/index.js +42 -0
  245. package/src/lib/logger/telegram-core.js +150 -0
  246. package/src/lib/logger/telegram-transport.js +126 -0
  247. package/src/lib/mega-brute-force.js +225 -0
  248. package/src/lib/mega-circuit-breaker.js +412 -0
  249. package/src/lib/mega-cron.js +169 -0
  250. package/src/lib/mega-hash.js +179 -0
  251. package/src/lib/mega-health.js +91 -0
  252. package/src/lib/mega-job-queue.js +600 -0
  253. package/src/lib/mega-job-worker.js +295 -0
  254. package/src/lib/mega-job.js +140 -0
  255. package/src/lib/mega-logger.js +128 -0
  256. package/src/lib/mega-metrics.js +661 -0
  257. package/src/lib/mega-plugin.js +650 -0
  258. package/src/lib/mega-retry.js +95 -0
  259. package/src/lib/mega-schedule.js +507 -0
  260. package/src/lib/mega-shutdown.js +176 -0
  261. package/src/lib/mega-tracing.js +715 -0
  262. package/src/lib/mega-worker.js +653 -0
  263. package/src/lib/worker-runner/process-entry.js +30 -0
  264. package/src/lib/worker-runner/task-dispatch.js +72 -0
  265. package/src/lib/worker-runner/thread-entry.js +26 -0
  266. package/src/models/index.js +7 -0
  267. package/src/models/mega-model.js +151 -0
  268. package/src/test/index.js +288 -0
  269. package/templates/adapter/code.tpl +40 -0
  270. package/templates/adapter/test.tpl +13 -0
  271. package/templates/app/app.config.tpl +10 -0
  272. package/templates/app/route.tpl +10 -0
  273. package/templates/app/test.tpl +13 -0
  274. package/templates/channel/code.tpl +38 -0
  275. package/templates/channel/test.tpl +19 -0
  276. package/templates/controller/code.tpl +16 -0
  277. package/templates/controller/route.tpl +9 -0
  278. package/templates/controller/test.tpl +14 -0
  279. package/templates/job/code.tpl +23 -0
  280. package/templates/job/test.tpl +17 -0
  281. package/templates/locale/code.tpl +3 -0
  282. package/templates/locale/test.tpl +13 -0
  283. package/templates/middleware/code.tpl +13 -0
  284. package/templates/middleware/test.tpl +11 -0
  285. package/templates/migration/code.tpl +20 -0
  286. package/templates/migration/test.tpl +14 -0
  287. package/templates/model/code.tpl +21 -0
  288. package/templates/model/test.tpl +29 -0
  289. package/templates/project/app.config.tpl +8 -0
  290. package/templates/project/app.config.views.tpl +37 -0
  291. package/templates/project/ecosystem.config.tpl +10 -0
  292. package/templates/project/env.tpl +12 -0
  293. package/templates/project/gitignore.tpl +8 -0
  294. package/templates/project/locales/client/en.json.tpl +3 -0
  295. package/templates/project/locales/client/ko.json.tpl +3 -0
  296. package/templates/project/locales/server/en.json.tpl +17 -0
  297. package/templates/project/locales/server/ko.json.tpl +17 -0
  298. package/templates/project/mega.config.tpl +11 -0
  299. package/templates/project/package.tpl +25 -0
  300. package/templates/project/public/css/app.css +101 -0
  301. package/templates/project/public/js/app.js +54 -0
  302. package/templates/project/public/js/theme-init.js +12 -0
  303. package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  304. package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
  305. package/templates/project/readme.tpl +48 -0
  306. package/templates/project/route.test.tpl +13 -0
  307. package/templates/project/route.test.views.tpl +15 -0
  308. package/templates/project/route.tpl +10 -0
  309. package/templates/project/route.views.tpl +10 -0
  310. package/templates/project/views/index.ejs.tpl +58 -0
  311. package/templates/project/views/layout.ejs.tpl +73 -0
  312. package/templates/project/vitest.config.tpl +8 -0
  313. package/templates/route/code.tpl +11 -0
  314. package/templates/route/test.tpl +26 -0
  315. package/templates/schedule/code.tpl +19 -0
  316. package/templates/schedule/test.tpl +17 -0
  317. package/templates/service/code.tpl +18 -0
  318. package/templates/service/test.tpl +17 -0
  319. package/templates/worker/code.tpl +14 -0
  320. package/templates/worker/task.tpl +13 -0
  321. package/templates/worker/test.tpl +18 -0
  322. package/vitest.config.js +33 -0
@@ -0,0 +1,650 @@
1
+ // @ts-check
2
+ /**
3
+ * 플러그인 시스템 — 명시 등록 + `install(mega)` 패턴 + apiVersion 호환 강제.
4
+ *
5
+ * 정본: [ADR-079](../../docs/09-decisions-and-open-questions.md), [ADR-122] / 03-api-spec §11 /
6
+ * 02-architecture §14. **자족형 플러그인 호스트(ADR-122 옵션 A + ADR-123 jobs/schedules 확장 +
7
+ * ADR-134 workers 확장)** — 코어 hook 의 *등록 표면*을 제공한다:
8
+ * - `adapters.register` 는 실 어댑터 드라이버 레지스트리(ADR-044)에 **즉시 위임**(live consumer).
9
+ * - `cli.command`/`app.use`/`middlewares.global`/`scaffold.register`/`lib.register`/`jobs.register`/
10
+ * `schedules.register`/`workers.register` 는 호스트 내부의 **실제 queryable 레지스트리**(Map/배열)에
11
+ * 수집 → 후속 CLI/호스트가 소비(`jobs`/`schedules` 는 `mega worker`/`mega scheduler` 가
12
+ * `config.jobs`/`config.schedules` 정적 등록분과 함께 흡수, ADR-123; `workers` 는 `bootApp`/CLI 호스트의
13
+ * `buildWorkers` 가 `config.workers` 정적 등록분과 함께 흡수, ADR-134).
14
+ * - `on('beforeBoot'|'afterBoot'|'beforeShutdown')` 은 lifecycle hook 배열에 수집 → 부팅/종료
15
+ * orchestrator(`bootApp`, ADR-123)가 {@link MegaPluginHost#runLifecycle} 로 구동.
16
+ *
17
+ * **자족적**이라 중앙 부팅 orchestrator 없이도 단위/통합 테스트가 완결된다(`install(host)` 한 줄).
18
+ * 실 부팅 시퀀스에 {@link loadPlugins} 를 끼우는 배선은 중앙 orchestrator
19
+ * 영역(ADR-122).
20
+ *
21
+ * @module lib/mega-plugin
22
+ */
23
+ import { resolve as pathResolve } from 'node:path'
24
+ import { pathToFileURL } from 'node:url'
25
+ import { MegaConfigError } from '../errors/config-error.js'
26
+ import { register as registerAdapterDriver, BUILTIN_DRIVERS } from '../adapters/registry.js'
27
+
28
+ /**
29
+ * 코어 플러그인 API 계약 메이저 버전 (ADR-122). npm 패키지 버전(`package.json`, 현재 pre-1.0 `0.x`)과
30
+ * **분리**한다 — 패키지 0.x 는 자주 바뀌어 거기 묶으면 0.x bump 마다 모든 플러그인이 깨진다. 플러그인은
31
+ * 코어 *내부 구조*가 아니라 *공개 hook 계약*에 의존하므로, 계약이 바뀔 때만 올리는 독립 버전이 맞다.
32
+ * 03-api-spec/02-architecture 의 예시(`apiVersion: '1'`)와도 정합.
33
+ * @type {string}
34
+ */
35
+ export const CORE_API_VERSION = '1'
36
+
37
+ /** lifecycle 이벤트 화이트리스트 (오타 차단 — 형제 MegaScheduler/MegaCircuitBreaker `on` 정책). */
38
+ const LIFECYCLE_EVENTS = /** @type {const} */ (['beforeBoot', 'afterBoot', 'beforeShutdown'])
39
+
40
+ /**
41
+ * @typedef {Object} MegaPlugin
42
+ * @property {string} name - npm 패키지명 또는 식별자.
43
+ * @property {string} version - semver (예: '1.0.0').
44
+ * @property {string} apiVersion - 코어 API 계약 메이저 버전. mismatch 시 부팅 throw.
45
+ * @property {(mega: MegaPluginHost, options?: object) => void | Promise<void>} install
46
+ */
47
+
48
+ /**
49
+ * @typedef {Object} MegaCliCommandDef
50
+ * @property {string} description
51
+ * @property {(args: object) => unknown} handler - 03-api-spec 은 `Promise<void>|void` 로 적었으나,
52
+ * 핸들러가 결과(예: exit code·출력)를 반환할 수 있게 `unknown` 으로 넓힘(CLI 가 소비).
53
+ */
54
+
55
+ /**
56
+ * @typedef {Object} MegaScaffoldDef
57
+ * @property {string} dir
58
+ * @property {Array<{ path: string, template: string }>} files
59
+ */
60
+
61
+ /**
62
+ * @typedef {Object} LoadedPluginMeta
63
+ * @property {string} name
64
+ * @property {string} version
65
+ * @property {string} apiVersion
66
+ */
67
+
68
+ /**
69
+ * `mega.config.js` 의 `plugins` 배열 (04-data-models §1.1 MegaPluginsConfig).
70
+ * @typedef {Array<string | { name: string, options?: object }>} MegaPluginsConfig
71
+ */
72
+
73
+ /**
74
+ * `MegaPluginContext` — `install(mega)` 가 받는 `mega` 인자(03-api-spec §11).
75
+ *
76
+ * 코어 hook 의 등록 표면을 dot-notation(`mega.adapters.register(...)`)으로 노출하고, 등록 결과를
77
+ * 내부 레지스트리에 수집한다. 수집물은 getter/list 메서드로 queryable 하다(후속 Step 이 소비).
78
+ */
79
+ export class MegaPluginHost {
80
+ /** @type {{ debug?: Function } | null} */
81
+ #logger
82
+ /** @type {Map<string, MegaCliCommandDef>} */
83
+ #commands = new Map()
84
+ /** @type {Map<string, MegaScaffoldDef>} */
85
+ #generators = new Map()
86
+ /** @type {Map<string, Function>} */
87
+ #libs = new Map()
88
+ /** @type {Array<{ plugin: Function, opts: object | undefined }>} */
89
+ #fastifyPlugins = []
90
+ /** @type {Function[]} */
91
+ #globalMiddlewares = []
92
+ /** @type {{ beforeBoot: Function[], afterBoot: Function[], beforeShutdown: Function[] }} */
93
+ #lifecycle = { beforeBoot: [], afterBoot: [], beforeShutdown: [] }
94
+ /** @type {Function[]} 플러그인이 등록한 MegaJob 서브클래스(등록 순서). */
95
+ #jobs = []
96
+ /** @type {Set<Function>} 잡 중복 등록 차단(같은 클래스 2회). */
97
+ #jobSet = new Set()
98
+ /** @type {Function[]} 플러그인이 등록한 MegaSchedule 서브클래스(등록 순서). */
99
+ #schedules = []
100
+ /** @type {Set<Function>} 스케줄 중복 등록 차단. */
101
+ #scheduleSet = new Set()
102
+ /** @type {Function[]} 플러그인이 등록한 MegaWorker 서브클래스(등록 순서). */
103
+ #workers = []
104
+ /** @type {Set<Function>} 워커 중복 등록 차단(같은 클래스 2회). */
105
+ #workerSet = new Set()
106
+ /** @type {LoadedPluginMeta[]} */
107
+ #loaded = []
108
+
109
+ /**
110
+ * @param {{ logger?: { debug?: Function } }} [opts] - 길목 debug 로그용(선택). 순수 등록 유틸이라
111
+ * 미주입이 기본 — 미주입 시 no-op.
112
+ */
113
+ constructor({ logger } = {}) {
114
+ this.#logger = logger ?? null
115
+
116
+ // dot-notation hook 표면 (03-api-spec §11). 각 hook 은 private 등록 메서드로 위임.
117
+ /** 3rd party 어댑터 등록 (ADR-044) — 실 드라이버 레지스트리에 즉시 위임. */
118
+ this.adapters = {
119
+ /** @param {string} driver @param {Function} AdapterClass */
120
+ register: (driver, AdapterClass) => this.#registerAdapter(driver, AdapterClass),
121
+ }
122
+ /** CLI 명령 등록 — `mega <name>`(소비는 CLI). */
123
+ this.cli = {
124
+ /** @param {string} name @param {MegaCliCommandDef} opts */
125
+ command: (name, opts) => this.#registerCommand(name, opts),
126
+ }
127
+ /** Fastify 플러그인을 모든 앱에 등록(소비는 부팅 orchestrator). */
128
+ this.app = {
129
+ /** @param {Function} fastifyPlugin @param {object} [opts] */
130
+ use: (fastifyPlugin, opts) => this.#useFastifyPlugin(fastifyPlugin, opts),
131
+ }
132
+ /** 글로벌 미들웨어 등록(소비는 라우트 파이프라인). */
133
+ this.middlewares = {
134
+ /** @param {Function} mw */
135
+ global: (mw) => this.#registerGlobalMiddleware(mw),
136
+ }
137
+ /** 스캐폴드 generator 등록 — `mega g <name>`(소비는 CLI). */
138
+ this.scaffold = {
139
+ /** @param {string} name @param {MegaScaffoldDef} def */
140
+ register: (name, def) => this.#registerGenerator(name, def),
141
+ }
142
+ /** 코어 lib 노출 — `ctx.lib.<name>`(소비는 ctx 빌더). */
143
+ this.lib = {
144
+ /** @param {string} name @param {Function} Class */
145
+ register: (name, Class) => this.#registerLib(name, Class),
146
+ }
147
+ /** 잡 등록 — `mega worker` 가 `config.jobs` 정적 등록분과 함께 흡수(ADR-123). */
148
+ this.jobs = {
149
+ /** @param {Function} JobClass - MegaJob 서브클래스. */
150
+ register: (JobClass) => this.#registerJob(JobClass),
151
+ }
152
+ /** 스케줄 등록 — `mega scheduler` 가 `config.schedules` 정적 등록분과 함께 흡수(ADR-123). */
153
+ this.schedules = {
154
+ /** @param {Function} TaskClass - MegaSchedule 서브클래스. */
155
+ register: (TaskClass) => this.#registerSchedule(TaskClass),
156
+ }
157
+ /** 워커 등록 — `buildWorkers` 가 `config.workers` 정적 등록분과 함께 흡수(ADR-134). */
158
+ this.workers = {
159
+ /** @param {Function} WorkerClass - MegaWorker 서브클래스. */
160
+ register: (WorkerClass) => this.#registerWorker(WorkerClass),
161
+ }
162
+ }
163
+
164
+ /**
165
+ * 3rd party 어댑터 드라이버 등록(ADR-044). 빌트인 8종 이름은 점유 금지(registerFromConfig 와 동일
166
+ * 보호) — 실 클래스 검증·중복 충돌은 어댑터 레지스트리가 throw.
167
+ * @param {string} driver
168
+ * @param {Function} AdapterClass - MegaAdapter 서브클래스.
169
+ * @returns {void}
170
+ */
171
+ #registerAdapter(driver, AdapterClass) {
172
+ if (typeof driver === 'string' && BUILTIN_DRIVERS.has(driver)) {
173
+ throw new MegaConfigError(
174
+ 'plugin.builtin_driver_conflict',
175
+ `Plugin cannot register builtin driver "${driver}" (reserved).`,
176
+ { details: { driver } },
177
+ )
178
+ }
179
+ // 어댑터 레지스트리에 위임 — 비-클래스/충돌은 거기서 fail-fast(MegaValidationError/MegaInternalError).
180
+ registerAdapterDriver(driver, /** @type {any} */ (AdapterClass))
181
+ }
182
+
183
+ /**
184
+ * CLI 명령 등록. 같은 이름 재등록은 fail-fast(silent override 금지 — 형제 register 패턴).
185
+ * @param {string} name
186
+ * @param {MegaCliCommandDef} opts
187
+ * @returns {void}
188
+ */
189
+ #registerCommand(name, opts) {
190
+ if (typeof name !== 'string' || name.length === 0) {
191
+ throw new TypeError('mega.cli.command: name must be a non-empty string.')
192
+ }
193
+ if (!opts || typeof opts.handler !== 'function') {
194
+ throw new TypeError(`mega.cli.command('${name}'): opts.handler must be a function.`)
195
+ }
196
+ if (this.#commands.has(name)) {
197
+ throw new MegaConfigError('plugin.duplicate_command', `CLI command '${name}' is already registered.`, {
198
+ details: { name },
199
+ })
200
+ }
201
+ this.#commands.set(name, {
202
+ description: typeof opts.description === 'string' ? opts.description : '',
203
+ handler: opts.handler,
204
+ })
205
+ }
206
+
207
+ /**
208
+ * Fastify 플러그인을 모든 앱 인스턴스에 등록(수집 — 부팅 orchestrator 가 각 MegaApp 에 register).
209
+ * @param {Function} fastifyPlugin
210
+ * @param {object} [opts]
211
+ * @returns {void}
212
+ */
213
+ #useFastifyPlugin(fastifyPlugin, opts) {
214
+ if (typeof fastifyPlugin !== 'function') {
215
+ throw new TypeError('mega.app.use: fastifyPlugin must be a function.')
216
+ }
217
+ this.#fastifyPlugins.push({ plugin: fastifyPlugin, opts })
218
+ }
219
+
220
+ /**
221
+ * 글로벌 미들웨어 등록(수집 — 라우트 파이프라인이 소비).
222
+ * @param {Function} middleware
223
+ * @returns {void}
224
+ */
225
+ #registerGlobalMiddleware(middleware) {
226
+ if (typeof middleware !== 'function') {
227
+ throw new TypeError('mega.middlewares.global: middleware must be a function.')
228
+ }
229
+ this.#globalMiddlewares.push(middleware)
230
+ }
231
+
232
+ /**
233
+ * 스캐폴드 generator 등록. 같은 이름 재등록은 fail-fast.
234
+ * @param {string} name
235
+ * @param {MegaScaffoldDef} def
236
+ * @returns {void}
237
+ */
238
+ #registerGenerator(name, def) {
239
+ if (typeof name !== 'string' || name.length === 0) {
240
+ throw new TypeError('mega.scaffold.register: name must be a non-empty string.')
241
+ }
242
+ if (!def || typeof def.dir !== 'string' || !Array.isArray(def.files)) {
243
+ throw new TypeError(`mega.scaffold.register('${name}'): def must be { dir: string, files: array }.`)
244
+ }
245
+ if (this.#generators.has(name)) {
246
+ throw new MegaConfigError('plugin.duplicate_generator', `Scaffold generator '${name}' is already registered.`, {
247
+ details: { name },
248
+ })
249
+ }
250
+ this.#generators.set(name, { dir: def.dir, files: [...def.files] })
251
+ }
252
+
253
+ /**
254
+ * 코어 lib 노출 등록. 같은 이름 재등록은 fail-fast.
255
+ * @param {string} name
256
+ * @param {Function} Class
257
+ * @returns {void}
258
+ */
259
+ #registerLib(name, Class) {
260
+ if (typeof name !== 'string' || name.length === 0) {
261
+ throw new TypeError('mega.lib.register: name must be a non-empty string.')
262
+ }
263
+ if (typeof Class !== 'function') {
264
+ throw new TypeError(`mega.lib.register('${name}'): Class must be a function/constructor.`)
265
+ }
266
+ if (this.#libs.has(name)) {
267
+ throw new MegaConfigError('plugin.duplicate_lib', `Lib '${name}' is already registered.`, {
268
+ details: { name },
269
+ })
270
+ }
271
+ this.#libs.set(name, Class)
272
+ }
273
+
274
+ /**
275
+ * 잡 등록(수집 — `mega worker` 가 `config.jobs` 정적 등록분과 함께 흡수, ADR-123). 같은 클래스 재등록은
276
+ * fail-fast(silent double-consume 금지 — 형제 register 패턴). subject/bus 검증은 `MegaJobWorker.register`
277
+ * 가 부팅 시 수행하므로 여기선 클래스 형태·중복만 본다.
278
+ * @param {Function} JobClass - MegaJob 서브클래스.
279
+ * @returns {void}
280
+ */
281
+ #registerJob(JobClass) {
282
+ if (typeof JobClass !== 'function') {
283
+ throw new TypeError('mega.jobs.register: JobClass must be a class (MegaJob subclass).')
284
+ }
285
+ if (this.#jobSet.has(JobClass)) {
286
+ throw new MegaConfigError('plugin.duplicate_job', `Job '${JobClass.name}' is already registered.`, {
287
+ details: { name: JobClass.name },
288
+ })
289
+ }
290
+ this.#jobSet.add(JobClass)
291
+ this.#jobs.push(JobClass)
292
+ }
293
+
294
+ /**
295
+ * 스케줄 등록(수집 — `mega scheduler` 가 `config.schedules` 정적 등록분과 함께 흡수, ADR-123). 같은 클래스
296
+ * 재등록은 fail-fast. cron 검증은 `MegaScheduler.register` 가 부팅 시 수행한다.
297
+ * @param {Function} TaskClass - MegaSchedule 서브클래스.
298
+ * @returns {void}
299
+ */
300
+ #registerSchedule(TaskClass) {
301
+ if (typeof TaskClass !== 'function') {
302
+ throw new TypeError('mega.schedules.register: TaskClass must be a class (MegaSchedule subclass).')
303
+ }
304
+ if (this.#scheduleSet.has(TaskClass)) {
305
+ throw new MegaConfigError(
306
+ 'plugin.duplicate_schedule',
307
+ `Schedule '${TaskClass.name}' is already registered.`,
308
+ { details: { name: TaskClass.name } },
309
+ )
310
+ }
311
+ this.#scheduleSet.add(TaskClass)
312
+ this.#schedules.push(TaskClass)
313
+ }
314
+
315
+ /**
316
+ * 워커 등록(수집 — `buildWorkers` 가 `config.workers` 정적 등록분과 함께 흡수, ADR-134). 같은 클래스
317
+ * 재등록은 fail-fast(silent double-consume 금지 — 형제 register 패턴). MegaWorker 서브클래스 검증·`static
318
+ * name` 중복은 `buildWorkers` 가 부팅 시 수행하므로(어댑터 connect 후 인스턴스화 시점) 여기선 클래스
319
+ * 형태·중복만 본다(잡/스케줄 register 와 동일 분업).
320
+ * @param {Function} WorkerClass - MegaWorker 서브클래스.
321
+ * @returns {void}
322
+ */
323
+ #registerWorker(WorkerClass) {
324
+ if (typeof WorkerClass !== 'function') {
325
+ throw new TypeError('mega.workers.register: WorkerClass must be a class (MegaWorker subclass).')
326
+ }
327
+ if (this.#workerSet.has(WorkerClass)) {
328
+ throw new MegaConfigError('plugin.duplicate_worker', `Worker '${WorkerClass.name}' is already registered.`, {
329
+ details: { name: WorkerClass.name },
330
+ })
331
+ }
332
+ this.#workerSet.add(WorkerClass)
333
+ this.#workers.push(WorkerClass)
334
+ }
335
+
336
+ /**
337
+ * 라이프사이클 이벤트 구독. 미지정 이벤트는 `RangeError`(오타 차단 — 형제 MegaScheduler `on` 정책).
338
+ * hook 은 부팅 orchestrator 가 넘기는 boot context 를 선택적으로 받는다(L-2/ADR-123, `(ctx?) => ...`).
339
+ * @param {'beforeBoot' | 'afterBoot' | 'beforeShutdown'} event
340
+ * @param {(ctx?: object) => Promise<void> | void} fn
341
+ * @returns {this}
342
+ */
343
+ on(event, fn) {
344
+ this.#assertLifecycleEvent(event)
345
+ if (typeof fn !== 'function') {
346
+ throw new TypeError(`mega.on('${event}'): fn must be a function.`)
347
+ }
348
+ this.#lifecycle[event].push(fn)
349
+ return this
350
+ }
351
+
352
+ /**
353
+ * 한 lifecycle 단계의 구독 hook 을 **등록 순서대로** 실행한다(부팅 orchestrator 가 호출).
354
+ * beforeBoot/afterBoot 는 fail-fast(에러 전파)로 부팅을 멈춘다. beforeShutdown 은 graceful
355
+ * shutdown 경로에서 보통 `MegaShutdown.register`(per-hook catch)로 브리지된다(ADR-122).
356
+ *
357
+ * **L-2(ADR-123)**: 부팅 orchestrator 가 boot context(`{ config, log, db/cache/bus/lock }`)를
358
+ * 인자로 넘기면 각 hook 이 `fn(ctx)` 로 받는다. ctx 미주입 시 `undefined` 전달(03-api-spec 의
359
+ * `() => void` 시그니처와 하위 호환 — 인자 안 받는 hook 은 그대로 동작).
360
+ * @param {'beforeBoot' | 'afterBoot' | 'beforeShutdown'} event
361
+ * @param {object} [ctx] - boot context(L-2). orchestrator 가 주입, 미주입 시 undefined.
362
+ * @returns {Promise<void>}
363
+ */
364
+ async runLifecycle(event, ctx) {
365
+ this.#assertLifecycleEvent(event)
366
+ for (const fn of this.#lifecycle[event]) {
367
+ this.#logger?.debug?.({ event }, 'plugin.lifecycle run')
368
+ await fn(ctx)
369
+ }
370
+ }
371
+
372
+ /**
373
+ * lifecycle 이벤트명 화이트리스트 검증.
374
+ * @param {string} event
375
+ * @returns {void}
376
+ */
377
+ #assertLifecycleEvent(event) {
378
+ if (!LIFECYCLE_EVENTS.includes(/** @type {any} */ (event))) {
379
+ throw new RangeError(
380
+ `MegaPluginHost: unknown lifecycle event '${event}'. Known: ${LIFECYCLE_EVENTS.join(' | ')}.`,
381
+ )
382
+ }
383
+ }
384
+
385
+ /**
386
+ * loadPlugins 가 install 성공 후 메타를 기록한다(framework-internal).
387
+ * @param {LoadedPluginMeta} meta
388
+ * @returns {void}
389
+ */
390
+ _recordLoaded(meta) {
391
+ this.#loaded.push(meta)
392
+ }
393
+
394
+ // ── queryable 접근자 (후속 Step·테스트가 소비) ──────────────────────────────
395
+
396
+ /** 등록 순서대로 install 된 플러그인 메타 목록. @returns {LoadedPluginMeta[]} */
397
+ get loadedPlugins() {
398
+ return [...this.#loaded]
399
+ }
400
+
401
+ /** 등록된 CLI 명령 이름 목록. @returns {string[]} */
402
+ listCommands() {
403
+ return [...this.#commands.keys()]
404
+ }
405
+
406
+ /** CLI 명령 정의 조회(없으면 undefined). @param {string} name @returns {MegaCliCommandDef | undefined} */
407
+ getCommand(name) {
408
+ return this.#commands.get(name)
409
+ }
410
+
411
+ /** CLI 명령 존재 여부. @param {string} name @returns {boolean} */
412
+ hasCommand(name) {
413
+ return this.#commands.has(name)
414
+ }
415
+
416
+ /** 등록된 스캐폴드 generator 이름 목록. @returns {string[]} */
417
+ listGenerators() {
418
+ return [...this.#generators.keys()]
419
+ }
420
+
421
+ /** 스캐폴드 generator 정의 조회. @param {string} name @returns {MegaScaffoldDef | undefined} */
422
+ getGenerator(name) {
423
+ return this.#generators.get(name)
424
+ }
425
+
426
+ /** 등록된 lib 이름 목록. @returns {string[]} */
427
+ listLibs() {
428
+ return [...this.#libs.keys()]
429
+ }
430
+
431
+ /** lib 클래스 조회. @param {string} name @returns {Function | undefined} */
432
+ getLib(name) {
433
+ return this.#libs.get(name)
434
+ }
435
+
436
+ /** 플러그인이 등록한 잡 클래스 목록(복사본 — `mega worker` 가 소비). @returns {Function[]} */
437
+ listJobs() {
438
+ return [...this.#jobs]
439
+ }
440
+
441
+ /** 플러그인이 등록한 스케줄 클래스 목록(복사본 — `mega scheduler` 가 소비). @returns {Function[]} */
442
+ listSchedules() {
443
+ return [...this.#schedules]
444
+ }
445
+
446
+ /** 플러그인이 등록한 워커 클래스 목록(복사본 — `buildWorkers` 가 소비). @returns {Function[]} */
447
+ listWorkers() {
448
+ return [...this.#workers]
449
+ }
450
+
451
+ /** 등록된 Fastify 플러그인 목록(복사본). @returns {Array<{ plugin: Function, opts: object | undefined }>} */
452
+ get fastifyPlugins() {
453
+ return [...this.#fastifyPlugins]
454
+ }
455
+
456
+ /** 등록된 글로벌 미들웨어 목록(복사본). @returns {Function[]} */
457
+ get globalMiddlewares() {
458
+ return [...this.#globalMiddlewares]
459
+ }
460
+
461
+ /**
462
+ * 한 lifecycle 단계의 구독 hook 목록(복사본).
463
+ * @param {'beforeBoot' | 'afterBoot' | 'beforeShutdown'} event
464
+ * @returns {Function[]}
465
+ */
466
+ lifecycleHooks(event) {
467
+ this.#assertLifecycleEvent(event)
468
+ return [...this.#lifecycle[event]]
469
+ }
470
+ }
471
+
472
+ /**
473
+ * `plugins` 배열을 순서대로 로딩한다 — resolve → shape 검증 → apiVersion 체크 → `install(mega, options)`.
474
+ *
475
+ * 02-architecture §14 부팅 시퀀스 4·5단계. 배열에 **명시된 것만** 로딩(auto-discovery X) → 배열에 없는
476
+ * 패키지는 `node_modules` 에 있어도 `install` 이 호출되지 않는다(ADR-079). 어느 단계든 실패하면
477
+ * `MegaConfigError` 로 fail-fast(부팅 중단, silent skip 금지).
478
+ *
479
+ * @param {MegaPluginsConfig | null | undefined} plugins -
480
+ * `Array<string | { name, options }>`. 테스트·프로그램 편의로 **인라인 플러그인 객체**(`install` 함수
481
+ * 보유)도 원소로 허용한다.
482
+ * @param {MegaPluginHost} host - 코어가 만든 hook 컨텍스트.
483
+ * @param {{ projectRoot?: string, importer?: (spec: string) => Promise<MegaPlugin>, logger?: { debug?: Function } }} [opts] -
484
+ * `projectRoot` 는 로컬 경로(`./...`) 해석 기준. `importer` 주입 시 모듈 해석을 가로채(테스트). `logger`
485
+ * 는 길목 debug 로그.
486
+ * @returns {Promise<LoadedPluginMeta[]>} 로딩된 플러그인 메타 목록(등록 순서).
487
+ * @throws {MegaConfigError} 잘못된 config/entry/shape/apiVersion/로드 실패/중복 플러그인(plugin.duplicate_plugin).
488
+ * @throws {TypeError} host 가 MegaPluginHost 인스턴스가 아님.
489
+ */
490
+ export async function loadPlugins(plugins, host, { projectRoot, importer, logger } = {}) {
491
+ if (plugins === undefined || plugins === null) return []
492
+ if (!Array.isArray(plugins)) {
493
+ throw new MegaConfigError('plugin.invalid_config', 'config.plugins must be an array (MegaPluginsConfig).', {
494
+ details: { type: typeof plugins },
495
+ })
496
+ }
497
+ if (!(host instanceof MegaPluginHost)) {
498
+ throw new TypeError('loadPlugins: host must be a MegaPluginHost instance.')
499
+ }
500
+ const doImport = importer ?? createDefaultImporter(projectRoot)
501
+
502
+ /** @type {LoadedPluginMeta[]} */
503
+ const loaded = []
504
+ // 중복 플러그인 fail-fast(M-1). 같은 name 이 배열에 두 번 기재되면 on/middlewares.global/app.use 가
505
+ // silent double-registration 된다(배열 push 라 dedup 0) → 부팅을 멈춰 의도치 않은 중복 hook 을 차단.
506
+ const seenNames = new Set()
507
+ for (const entry of plugins) {
508
+ const { plugin, options, spec } = await resolvePluginEntry(entry, doImport)
509
+ assertPluginShape(plugin, spec)
510
+ assertApiVersion(plugin)
511
+ if (seenNames.has(plugin.name)) {
512
+ throw new MegaConfigError(
513
+ 'plugin.duplicate_plugin',
514
+ `Plugin '${plugin.name}' is registered more than once in config.plugins.`,
515
+ { details: { name: plugin.name } },
516
+ )
517
+ }
518
+ seenNames.add(plugin.name)
519
+ logger?.debug?.({ plugin: plugin.name, version: plugin.version }, 'plugin.install enter')
520
+ await plugin.install(host, options)
521
+ const meta = { name: plugin.name, version: plugin.version, apiVersion: plugin.apiVersion }
522
+ host._recordLoaded(meta)
523
+ loaded.push(meta)
524
+ logger?.debug?.({ plugin: plugin.name }, 'plugin.install done')
525
+ }
526
+ return loaded
527
+ }
528
+
529
+ /**
530
+ * plugins 배열 원소를 `{ plugin, options, spec }` 로 해석한다.
531
+ * - `string` — npm 패키지명/로컬 경로 → import.
532
+ * - `{ name, options }` — name 을 import, options 를 install 2번째 인자로.
533
+ * - 인라인 플러그인 객체(`install` 함수 보유) — 그대로 사용(options 없음).
534
+ * @param {unknown} entry
535
+ * @param {(spec: string) => Promise<MegaPlugin>} doImport
536
+ * @returns {Promise<{ plugin: MegaPlugin, options: object | undefined, spec: string }>}
537
+ */
538
+ async function resolvePluginEntry(entry, doImport) {
539
+ if (typeof entry === 'string') {
540
+ if (entry.length === 0) {
541
+ throw new MegaConfigError('plugin.invalid_entry', 'plugins entry string must be non-empty.', { details: {} })
542
+ }
543
+ return { plugin: await doImport(entry), options: undefined, spec: entry }
544
+ }
545
+ if (entry && typeof entry === 'object') {
546
+ const obj = /** @type {Record<string, any>} */ (entry)
547
+ // 인라인 플러그인 객체(프로그램·테스트 편의) — install 함수가 있으면 import 없이 그대로.
548
+ if (typeof obj.install === 'function') {
549
+ return { plugin: /** @type {MegaPlugin} */ (entry), options: undefined, spec: obj.name ?? '(inline)' }
550
+ }
551
+ // 정본 { name, options } 형태.
552
+ if (typeof obj.name === 'string' && obj.name.length > 0) {
553
+ return { plugin: await doImport(obj.name), options: obj.options, spec: obj.name }
554
+ }
555
+ }
556
+ throw new MegaConfigError(
557
+ 'plugin.invalid_entry',
558
+ `Invalid plugins entry: expected string | { name, options } | plugin object, got ${describeType(entry)}.`,
559
+ { details: { type: describeType(entry) } },
560
+ )
561
+ }
562
+
563
+ /**
564
+ * 기본 importer — 로컬 경로는 projectRoot 기준 file URL, 그 외는 bare specifier 로 dynamic import.
565
+ * @param {string} [projectRoot]
566
+ * @returns {(spec: string) => Promise<MegaPlugin>}
567
+ */
568
+ function createDefaultImporter(projectRoot) {
569
+ return async (spec) => {
570
+ let target = spec
571
+ const isLocal = spec.startsWith('.') || spec.startsWith('/')
572
+ if (isLocal) {
573
+ if (spec.startsWith('.') && !projectRoot) {
574
+ throw new MegaConfigError(
575
+ 'plugin.load_failed',
576
+ `Cannot resolve local plugin '${spec}' without projectRoot.`,
577
+ { details: { spec } },
578
+ )
579
+ }
580
+ const abs = spec.startsWith('/') ? spec : pathResolve(/** @type {string} */ (projectRoot), spec)
581
+ target = pathToFileURL(abs).href
582
+ }
583
+ let mod
584
+ try {
585
+ mod = await import(target)
586
+ } catch (err) {
587
+ throw new MegaConfigError('plugin.load_failed', `Failed to load plugin '${spec}': ${err.message}`, {
588
+ cause: err,
589
+ })
590
+ }
591
+ if (!mod.default) {
592
+ throw new MegaConfigError('plugin.no_default_export', `Plugin '${spec}' must have a default export.`, {
593
+ details: { spec },
594
+ })
595
+ }
596
+ return mod.default
597
+ }
598
+ }
599
+
600
+ /**
601
+ * 플러그인 default export 모양 검증 — `{ name, version, apiVersion, install }` (ADR-079).
602
+ * @param {unknown} plugin
603
+ * @param {string} spec - 에러 메시지용 식별자(이름 추출 실패 시 fallback).
604
+ * @returns {asserts plugin is MegaPlugin}
605
+ */
606
+ function assertPluginShape(plugin, spec) {
607
+ const p = /** @type {Record<string, any>} */ (plugin)
608
+ const name = p && typeof p.name === 'string' && p.name.length > 0 ? p.name : spec
609
+ const ok =
610
+ p &&
611
+ typeof p === 'object' &&
612
+ typeof p.name === 'string' &&
613
+ p.name.length > 0 &&
614
+ typeof p.version === 'string' &&
615
+ typeof p.apiVersion === 'string' &&
616
+ typeof p.install === 'function'
617
+ if (!ok) {
618
+ throw new MegaConfigError(
619
+ 'plugin.invalid_shape',
620
+ `Plugin '${name}' must export { name, version, apiVersion, install } object`,
621
+ { details: { plugin: name } },
622
+ )
623
+ }
624
+ }
625
+
626
+ /**
627
+ * apiVersion 이 코어 계약 메이저와 일치하는지 검증. mismatch → 부팅 throw(silent skip 금지, ADR-079).
628
+ * @param {MegaPlugin} plugin
629
+ * @returns {void}
630
+ */
631
+ function assertApiVersion(plugin) {
632
+ if (String(plugin.apiVersion) !== String(CORE_API_VERSION)) {
633
+ throw new MegaConfigError(
634
+ 'plugin.api_version_mismatch',
635
+ `Plugin '${plugin.name}' requires apiVersion '${plugin.apiVersion}' but core provides '${CORE_API_VERSION}'`,
636
+ { details: { plugin: plugin.name, required: plugin.apiVersion, provided: CORE_API_VERSION } },
637
+ )
638
+ }
639
+ }
640
+
641
+ /**
642
+ * 에러 메시지용 타입 설명.
643
+ * @param {unknown} v
644
+ * @returns {string}
645
+ */
646
+ function describeType(v) {
647
+ if (v === null) return 'null'
648
+ if (Array.isArray(v)) return 'array'
649
+ return typeof v
650
+ }