@gaonjs/config 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @gaonjs/config
2
+
3
+ Gaon 루트 설정 로더 · 자동 배선 (§3.3 · M9-A).
4
+
5
+ - `defineConfig()` — 사용자가 `gaon.config.ts` 에서 쓰는 타입 안전 헬퍼
6
+ - `loadGaonConfig(cwd)` — `<cwd>/gaon.config.ts` 를 동적 import
7
+ - `resolveApps(cwd)` — `apps/*` 스캔 (기본 앱 로더)
8
+ - `wireGaon(config, apps)` — 명시된 배터리만 `configureX()` 호출 (fail-closed)
9
+
10
+ 사용자는 파사드 서브패스 `gaonjs/config` 로 쓴다. `gaon serve/dev` 가
11
+ 내부적으로 이 로더를 호출한다 — 앱 코드가 직접 부를 일은 드물다.
package/dist/apps.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { AppSpec } from '@gaonjs/web';
2
+ /**
3
+ * 한 앱의 디스크상 표현. wireGaon 이 이걸 읽어 AppSpec 을 만든다.
4
+ * app.config.ts (있으면) 의 값을 담아 두어 wire 단계에서 세션·인증
5
+ * 옵션을 조립한다.
6
+ */
7
+ export interface DiscoveredApp {
8
+ readonly name: string;
9
+ readonly dir: string;
10
+ /** apps/<name>/routes.ts default export (RouteDef). */
11
+ readonly routes: unknown;
12
+ /** apps/<name>/controllers/*.ts 의 default export 를 이름으로 모은 맵. */
13
+ readonly controllers: Record<string, object>;
14
+ /** apps/<name>/app.config.ts 의 default export (있으면 · 임의 shape). */
15
+ readonly appConfig?: Record<string, unknown>;
16
+ /** apps/<name>/channels/*.ts default export 를 이름으로 모은 맵(있으면). */
17
+ readonly channels?: Record<string, object>;
18
+ }
19
+ /**
20
+ * cwd/apps/* 를 스캔해 앱 스펙을 만든다. routes.ts 가 없는 폴더는 건너뛴다.
21
+ * 이름순 정렬(deterministic — 등록 순서를 사용자가 예측 가능하게).
22
+ */
23
+ export declare function resolveApps(cwd?: string): Promise<DiscoveredApp[]>;
24
+ /**
25
+ * DiscoveredApp 을 AppSpec 으로 변환. 세션·인증 옵션은 wire.ts 가 config
26
+ * 를 반영해 덧붙인다. 여기서는 순수 이름·라우트·컨트롤러만.
27
+ */
28
+ export declare function toAppSpec(app: DiscoveredApp): AppSpec;
package/dist/apps.js ADDED
@@ -0,0 +1,80 @@
1
+ // @gaonjs/config · apps/* 자동 발견 (§3.3)
2
+ //
3
+ // 앱은 등록이 아니라 관례다 — apps/<이름>/ 폴더가 존재하고 routes.ts 가
4
+ // 있으면 그 자체로 앱이다(§3.3 "앱의 등록 자체는 설정이 아니라 관례").
5
+ // controllers/ 아래의 default export 를 이름으로 모아 컨트롤러 맵을 만든다.
6
+ // app.config.ts 는 선택적(관례에서 벗어날 때만 채운다 — §3.3).
7
+ import { existsSync, readdirSync, statSync } from 'node:fs';
8
+ import { basename, extname, join, resolve } from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ const isSourceTs = (f) => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('.test.ts');
11
+ async function loadDefault(path) {
12
+ if (!existsSync(path))
13
+ return undefined;
14
+ const mod = (await import(pathToFileURL(path).href));
15
+ return mod.default;
16
+ }
17
+ /** 폴더의 소스 .ts (default export 만) 를 파일명(확장자 제외)으로 모은다. */
18
+ async function loadNamedDefaults(dir) {
19
+ if (!existsSync(dir))
20
+ return {};
21
+ const out = {};
22
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
+ if (!entry.isFile() || !isSourceTs(entry.name))
24
+ continue;
25
+ const path = join(dir, entry.name);
26
+ const def = await loadDefault(path);
27
+ if (def && typeof def === 'object')
28
+ out[basename(entry.name, extname(entry.name))] = def;
29
+ }
30
+ return out;
31
+ }
32
+ /**
33
+ * cwd/apps/* 를 스캔해 앱 스펙을 만든다. routes.ts 가 없는 폴더는 건너뛴다.
34
+ * 이름순 정렬(deterministic — 등록 순서를 사용자가 예측 가능하게).
35
+ */
36
+ export async function resolveApps(cwd = process.cwd()) {
37
+ const appsDir = join(resolve(cwd), 'apps');
38
+ if (!existsSync(appsDir))
39
+ return [];
40
+ const found = [];
41
+ const entries = readdirSync(appsDir, { withFileTypes: true })
42
+ .filter((e) => e.isDirectory())
43
+ .sort((a, b) => a.name.localeCompare(b.name));
44
+ for (const entry of entries) {
45
+ const dir = join(appsDir, entry.name);
46
+ const routesPath = join(dir, 'routes.ts');
47
+ if (!existsSync(routesPath))
48
+ continue;
49
+ const routes = await loadDefault(routesPath);
50
+ if (routes == null)
51
+ continue;
52
+ const controllers = await loadNamedDefaults(join(dir, 'controllers'));
53
+ const appConfigPath = join(dir, 'app.config.ts');
54
+ const appConfig = existsSync(appConfigPath)
55
+ ? await loadDefault(appConfigPath)
56
+ : undefined;
57
+ // channels/ 는 있을 때만 (M6 실시간 앱). 폴더가 없으면 undefined.
58
+ const channelsDir = join(dir, 'channels');
59
+ let channels;
60
+ if (existsSync(channelsDir) && statSync(channelsDir).isDirectory()) {
61
+ const map = await loadNamedDefaults(channelsDir);
62
+ if (Object.keys(map).length > 0)
63
+ channels = map;
64
+ }
65
+ found.push({ name: entry.name, dir, routes, controllers, appConfig, channels });
66
+ }
67
+ return found;
68
+ }
69
+ /**
70
+ * DiscoveredApp 을 AppSpec 으로 변환. 세션·인증 옵션은 wire.ts 가 config
71
+ * 를 반영해 덧붙인다. 여기서는 순수 이름·라우트·컨트롤러만.
72
+ */
73
+ export function toAppSpec(app) {
74
+ return {
75
+ name: app.name,
76
+ routes: app.routes,
77
+ controllers: app.controllers,
78
+ channels: app.channels,
79
+ };
80
+ }
@@ -0,0 +1,4 @@
1
+ export { defineConfig, type GaonConfig, type DbConfig, type RedisConfig, type NatsConfig, type HubConfig, type MailConfig, type StorageConfig, type StorageDiskConfig, type I18nConfig, type WebConfig, } from './types.js';
2
+ export { loadGaonConfig, findConfigPath } from './load.js';
3
+ export { resolveApps, toAppSpec, type DiscoveredApp, } from './apps.js';
4
+ export { wireGaon, type WiredGaon } from './wire.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // @gaonjs/config — 루트 설정 로더·자동 배선 (§3.3 · M9-A)
2
+ //
3
+ // 사용자는 파사드 서브패스 `gaonjs/config` 로 쓴다:
4
+ // import { defineConfig } from 'gaonjs/config'
5
+ // export default defineConfig({ db: { main: { adapter: 'postgres', url: ... } } })
6
+ //
7
+ // `gaon serve`/`gaon dev` 가 loadGaonConfig + wireGaon 을 조립해서
8
+ // Fastify 인스턴스를 만들고 listen 한다.
9
+ export { defineConfig, } from './types.js';
10
+ export { loadGaonConfig, findConfigPath } from './load.js';
11
+ export { resolveApps, toAppSpec, } from './apps.js';
12
+ export { wireGaon } from './wire.js';
package/dist/load.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { GaonConfig } from './types.js';
2
+ /** 프로젝트 루트에서 gaon.config.{ts,js,mjs} 를 찾는다. */
3
+ export declare function findConfigPath(cwd: string): string | undefined;
4
+ /**
5
+ * cwd 의 gaon.config.ts 를 로드한다. 파일이 없으면 `{}` — 최소 앱도 뜬다.
6
+ * 파일이 있으면 default export 가 GaonConfig 여야 한다(defineConfig 반환값).
7
+ */
8
+ export declare function loadGaonConfig(cwd?: string): Promise<GaonConfig>;
package/dist/load.js ADDED
@@ -0,0 +1,38 @@
1
+ // @gaonjs/config · gaon.config.ts 로더
2
+ //
3
+ // cwd 관례: 프로젝트 루트의 `gaon.config.ts` (없으면 .js) 를 동적 import.
4
+ // 파일이 없으면 빈 config 를 반환한다 — apps/ 만 있는 최소 프로젝트도
5
+ // 기동되게 한다(60초 실측·M9-F 대비).
6
+ //
7
+ // 동적 import 는 절대 URL 로 한다(Windows·pnpm store 경로 호환). tsx/tsResolve
8
+ // 훅이 이미 등록된 프로세스라면 .ts 도 그대로 로드된다(cli 가 등록).
9
+ import { existsSync } from 'node:fs';
10
+ import { join, resolve } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+ /** 프로젝트 루트에서 gaon.config.{ts,js,mjs} 를 찾는다. */
13
+ export function findConfigPath(cwd) {
14
+ const root = resolve(cwd);
15
+ for (const name of ['gaon.config.ts', 'gaon.config.mjs', 'gaon.config.js']) {
16
+ const p = join(root, name);
17
+ if (existsSync(p))
18
+ return p;
19
+ }
20
+ return undefined;
21
+ }
22
+ /**
23
+ * cwd 의 gaon.config.ts 를 로드한다. 파일이 없으면 `{}` — 최소 앱도 뜬다.
24
+ * 파일이 있으면 default export 가 GaonConfig 여야 한다(defineConfig 반환값).
25
+ */
26
+ export async function loadGaonConfig(cwd = process.cwd()) {
27
+ const path = findConfigPath(cwd);
28
+ if (!path)
29
+ return {};
30
+ const url = pathToFileURL(path).href;
31
+ const mod = (await import(url));
32
+ const cfg = mod.default;
33
+ if (cfg == null || typeof cfg !== 'object') {
34
+ throw new Error(`gaon.config 의 default export 가 객체가 아닙니다: ${path}\n` +
35
+ `→ export default defineConfig({ ... }) 형태로 두세요.`);
36
+ }
37
+ return cfg;
38
+ }
@@ -0,0 +1,97 @@
1
+ import type { ConnectionConfig } from '@gaonjs/data';
2
+ /**
3
+ * DB 커넥션 맵 — 키는 커넥션 이름(§4.5), 값은 어댑터 설정. `main` 은 관례
4
+ * (스키마가 `{ db: 'legacy' }` 처럼 명시하지 않으면 main 으로 붙는다).
5
+ */
6
+ export interface DbConfig {
7
+ readonly [key: string]: ConnectionConfig;
8
+ }
9
+ /** Redis (세션 스토어·캐시). 세션 배터리가 자동으로 이 URL 을 소비한다. */
10
+ export interface RedisConfig {
11
+ readonly url: string;
12
+ }
13
+ /** NATS (실시간·비동기 백본 · §7). */
14
+ export interface NatsConfig {
15
+ /** 접속지. 생략 시 env GAON_NATS_URL, 그다음 nats://127.0.0.1:4222. */
16
+ readonly url?: string;
17
+ /** 연결 이름(모니터링 식별). 생략 시 프로세스 종류(serve·work·hub). */
18
+ readonly name?: string;
19
+ }
20
+ /** 실시간 허브 (§7 · M6). NATS 가 켜져 있을 때만 유효. */
21
+ export interface HubConfig {
22
+ /** 허브 TCP 주소 'host:port'(errata E-2). 생략 시 NATS KV 공지 발견. */
23
+ readonly addr?: string;
24
+ /** 프레즌스 하트비트 주기(ms). 생략 시 런타임 기본. */
25
+ readonly heartbeatMs?: number;
26
+ }
27
+ /** 메일 전송 — SMTP 기본(개발은 MailPit, 운영은 실 SMTP). */
28
+ export interface MailConfig {
29
+ readonly host: string;
30
+ readonly port: number;
31
+ readonly user?: string;
32
+ readonly pass?: string;
33
+ readonly secure?: boolean;
34
+ /** from 헤더 기본값. */
35
+ readonly defaultFrom?: string;
36
+ }
37
+ /** 스토리지 — 로컬(기본) 또는 S3 호환. */
38
+ export type StorageDiskConfig = {
39
+ readonly driver: 'local';
40
+ readonly root: string;
41
+ readonly publicUrl?: string;
42
+ } | {
43
+ readonly driver: 's3';
44
+ readonly bucket: string;
45
+ readonly region?: string;
46
+ readonly endpoint?: string;
47
+ readonly accessKeyId?: string;
48
+ readonly secretAccessKey?: string;
49
+ readonly forcePathStyle?: boolean;
50
+ readonly publicUrl?: string;
51
+ };
52
+ export interface StorageConfig {
53
+ /** 기본 디스크 이름(예: 'local'). */
54
+ readonly default: string;
55
+ readonly disks: Record<string, StorageDiskConfig>;
56
+ }
57
+ /** i18n — locales/ 로드 옵션. */
58
+ export interface I18nConfig {
59
+ /** locales/ 디렉토리 경로 (cwd 기준 상대). 생략 시 'locales'. */
60
+ readonly dir?: string;
61
+ /** 기본 언어. */
62
+ readonly fallbackLng: string;
63
+ /** 허용 언어(생략 시 locales 폴더 하위 언어들). */
64
+ readonly supportedLngs?: readonly string[];
65
+ }
66
+ /** 웹 서버 부팅 옵션 — `gaon serve` 가 소비. */
67
+ export interface WebConfig {
68
+ /** 리슨 포트. 생략 시 env PORT, 그다음 3000. */
69
+ readonly port?: number;
70
+ /** 리슨 호스트. 생략 시 '0.0.0.0'. */
71
+ readonly host?: string;
72
+ /** 서명 쿠키·CSRF 용 비밀. 생략 시 env GAON_COOKIE_SECRET. */
73
+ readonly cookieSecret?: string;
74
+ /** Inertia 에셋 버전. 생략 시 이 프로세스의 시작 시각(개발 편의). */
75
+ readonly assetsVersion?: string;
76
+ }
77
+ /** 루트 설정 — gaon.config.ts 의 defineConfig 인자. */
78
+ export interface GaonConfig {
79
+ readonly db?: DbConfig;
80
+ readonly redis?: RedisConfig;
81
+ readonly nats?: NatsConfig;
82
+ readonly hub?: HubConfig;
83
+ readonly mail?: MailConfig;
84
+ readonly storage?: StorageConfig;
85
+ readonly i18n?: I18nConfig;
86
+ readonly web?: WebConfig;
87
+ /**
88
+ * 타임존(§3.3 정본에 명시). 현재는 문서상 값이며 M9-A 에서는 저장만 —
89
+ * 실 사용처(스케줄러·로깅 포맷)는 각 배터리가 후속 웨이브에서 소비한다.
90
+ */
91
+ readonly timezone?: string;
92
+ }
93
+ /**
94
+ * 타입 안전 헬퍼 — 사용자는 `export default defineConfig({...})` 로 쓴다.
95
+ * 반환값은 그대로 통과시켜 IDE 자동완성·형 검사만 제공한다.
96
+ */
97
+ export declare function defineConfig(config: GaonConfig): GaonConfig;
package/dist/types.js ADDED
@@ -0,0 +1,17 @@
1
+ // @gaonjs/config · gaon.config.ts 사용자 마주 타입 (§3.3)
2
+ //
3
+ // 루트 설정 파일의 shape 만 여기서 정의한다. 실제 배선(configureX 호출)은
4
+ // wire.ts 가 담당한다. 각 필드는 **선언적 옵션**만 담고, 어댑터 인스턴스는
5
+ // 담지 않는다 — 사용자가 값을 짜 넣고, 프레임웍이 어댑터를 조립한다.
6
+ //
7
+ // One Way(§1.1): 각 배터리는 미지정 시 **비활성**이다. 옵션이 있으면
8
+ // 켜지고, 없으면 조용히 꺼진다(fail-closed) — 세션·CSRF 처럼 보안 기본이
9
+ // 켜지는 것과는 다르다: 보안 기본은 배터리가 켜졌을 때의 내부 기본값이고,
10
+ // 배터리 자체의 on/off 는 이 config 파일이 결정한다.
11
+ /**
12
+ * 타입 안전 헬퍼 — 사용자는 `export default defineConfig({...})` 로 쓴다.
13
+ * 반환값은 그대로 통과시켜 IDE 자동완성·형 검사만 제공한다.
14
+ */
15
+ export function defineConfig(config) {
16
+ return config;
17
+ }
package/dist/wire.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { Redis } from 'ioredis';
3
+ import { type GaonNats } from '@gaonjs/async';
4
+ import type { GaonConfig } from './types.js';
5
+ import { type DiscoveredApp } from './apps.js';
6
+ /** 부팅 결과 — 이후 serve 가 listen 한다. close() 로 배선을 뒤집는다. */
7
+ export interface WiredGaon {
8
+ readonly app: FastifyInstance;
9
+ readonly config: GaonConfig;
10
+ readonly apps: readonly DiscoveredApp[];
11
+ readonly nats?: GaonNats;
12
+ readonly redis?: Redis;
13
+ /** graceful 종료 — Fastify · NATS · Redis · DB 를 순서대로 닫는다. */
14
+ close(): Promise<void>;
15
+ }
16
+ /**
17
+ * gaon.config.ts + apps/ 를 바탕으로 배터리를 배선하고 Fastify 앱을 만든다.
18
+ *
19
+ * 배선 순서(의존):
20
+ * 1) DB — 다른 배터리가 이미 configureX 시점에 커넥션을 만지지는 않지만,
21
+ * 사용 시점(모델 조회)에 필요. 실패는 즉시 던진다.
22
+ * 2) Redis — 세션 배터리가 쓴다. redis 인스턴스는 앱 스펙 조립 시 주입.
23
+ * 3) NATS — 실시간(웹) · 비동기 잡(work)이 공유. 여기서는 웹만 배선.
24
+ * 4) mail · storage · i18n — 프로세스 전역 configureX() 호출.
25
+ * 5) createApp — 위에서 만든 세션·realtime 설정을 담아 Fastify 인스턴스 생성.
26
+ */
27
+ export declare function wireGaon(config: GaonConfig, cwd?: string): Promise<WiredGaon>;
package/dist/wire.js ADDED
@@ -0,0 +1,147 @@
1
+ // @gaonjs/config · 자동 배선 (§3.3 · M9-A)
2
+ //
3
+ // gaon.config.ts + apps/* 를 읽어 배터리를 실제로 초기화하고, 하나의
4
+ // Fastify 인스턴스를 반환한다. 명시된 배터리만 켠다(fail-closed) — DB 가
5
+ // 없으면 등록만 건너뛰고, 인프라 부팅은 사용자 몫이다(compose 는 M9-F).
6
+ //
7
+ // 세션은 세션 옵션이 세팅된 앱에만 등록된다. AppSessionOptions.secret 은
8
+ // 필수이므로 auth 앱은 명시적으로 secret 을 제공해야 한다(env 로 주입 권장).
9
+ import { Redis } from 'ioredis';
10
+ import { createDb, registerConnection, destroyAllConnections, } from '@gaonjs/data';
11
+ import { connectNats } from '@gaonjs/async';
12
+ import { configureMailer, smtpTransport } from '@gaonjs/mail';
13
+ import { configureStorage, localDisk, s3Disk, resetStorage } from '@gaonjs/storage';
14
+ import { configureI18n, loadLocales, resetI18n } from '@gaonjs/i18n';
15
+ import { createApp, redisSessionStore } from '@gaonjs/web';
16
+ import { resolveApps, toAppSpec } from './apps.js';
17
+ import { resolve as resolvePath, join } from 'node:path';
18
+ /**
19
+ * gaon.config.ts + apps/ 를 바탕으로 배터리를 배선하고 Fastify 앱을 만든다.
20
+ *
21
+ * 배선 순서(의존):
22
+ * 1) DB — 다른 배터리가 이미 configureX 시점에 커넥션을 만지지는 않지만,
23
+ * 사용 시점(모델 조회)에 필요. 실패는 즉시 던진다.
24
+ * 2) Redis — 세션 배터리가 쓴다. redis 인스턴스는 앱 스펙 조립 시 주입.
25
+ * 3) NATS — 실시간(웹) · 비동기 잡(work)이 공유. 여기서는 웹만 배선.
26
+ * 4) mail · storage · i18n — 프로세스 전역 configureX() 호출.
27
+ * 5) createApp — 위에서 만든 세션·realtime 설정을 담아 Fastify 인스턴스 생성.
28
+ */
29
+ export async function wireGaon(config, cwd = process.cwd()) {
30
+ const root = resolvePath(cwd);
31
+ // 1) DB 커넥션 — 키별로 등록. 실 연결 검증은 첫 쿼리에서(pool 은 lazy).
32
+ if (config.db) {
33
+ for (const [key, cfg] of Object.entries(config.db)) {
34
+ const db = createDb(cfg);
35
+ registerConnection(key, db, cfg.adapter);
36
+ }
37
+ }
38
+ // 2) Redis — 세션 스토어. 앱 스펙 조립 시 세션 옵션에 주입한다.
39
+ let redis;
40
+ if (config.redis) {
41
+ redis = new Redis(config.redis.url, { lazyConnect: false, maxRetriesPerRequest: 3 });
42
+ }
43
+ // 3) NATS — 웹 프로세스도 채널·잡 publish 를 위해 연결한다.
44
+ let nats;
45
+ if (config.nats) {
46
+ nats = await connectNats({ servers: config.nats.url, name: config.nats.name ?? 'gaon-serve' });
47
+ }
48
+ // 4) mail — SMTP 전송(개발은 MailPit, 운영은 실 SMTP).
49
+ if (config.mail) {
50
+ const m = config.mail;
51
+ configureMailer({
52
+ transport: smtpTransport({
53
+ host: m.host,
54
+ port: m.port,
55
+ secure: m.secure,
56
+ auth: m.user && m.pass ? { user: m.user, pass: m.pass } : undefined,
57
+ }),
58
+ defaultFrom: m.defaultFrom,
59
+ });
60
+ }
61
+ // 4') storage — 로컬(기본) 또는 S3.
62
+ if (config.storage) {
63
+ const disks = {};
64
+ for (const [name, d] of Object.entries(config.storage.disks)) {
65
+ if (d.driver === 'local') {
66
+ // root 는 프로젝트 상대 경로면 cwd 로 절대화.
67
+ const root2 = d.root.startsWith('/') ? d.root : join(root, d.root);
68
+ disks[name] = localDisk({ root: root2, baseUrl: d.publicUrl });
69
+ }
70
+ else {
71
+ disks[name] = s3Disk({
72
+ bucket: d.bucket,
73
+ region: d.region,
74
+ endpoint: d.endpoint,
75
+ credentials: d.accessKeyId && d.secretAccessKey
76
+ ? { accessKeyId: d.accessKeyId, secretAccessKey: d.secretAccessKey }
77
+ : undefined,
78
+ forcePathStyle: d.forcePathStyle,
79
+ publicBaseUrl: d.publicUrl,
80
+ });
81
+ }
82
+ }
83
+ configureStorage({ default: config.storage.default, disks });
84
+ }
85
+ // 4'') i18n — locales/ 로드 후 초기화.
86
+ if (config.i18n) {
87
+ const dir = config.i18n.dir ?? 'locales';
88
+ const abs = dir.startsWith('/') ? dir : join(root, dir);
89
+ const resources = loadLocales(abs);
90
+ await configureI18n({
91
+ resources,
92
+ fallbackLng: config.i18n.fallbackLng,
93
+ supportedLngs: config.i18n.supportedLngs,
94
+ });
95
+ }
96
+ // 5) 앱 스캔 · createApp.
97
+ const apps = await resolveApps(root);
98
+ const specs = apps.map((a) => {
99
+ const spec = toAppSpec(a);
100
+ // app.config.ts 가 session 을 두면 세션을 켠다(app.config 는 임의 shape 라
101
+ // 얕게 검사). Redis 인스턴스는 wireGaon 이 소유하는 걸 재사용.
102
+ const ac = a.appConfig;
103
+ if (ac?.session?.secret && redis) {
104
+ return {
105
+ ...spec,
106
+ session: {
107
+ redis,
108
+ secret: ac.session.secret,
109
+ cookieName: ac.session.cookieName,
110
+ ttlSeconds: ac.session.ttlSeconds,
111
+ },
112
+ };
113
+ }
114
+ return spec;
115
+ });
116
+ const opts = {
117
+ apps: specs,
118
+ version: config.web?.assetsVersion,
119
+ cookieSecret: config.web?.cookieSecret,
120
+ realtime: nats ? { nats, hubAddr: config.hub?.addr, heartbeatMs: config.hub?.heartbeatMs } : undefined,
121
+ };
122
+ const app = await createApp(opts);
123
+ return {
124
+ app,
125
+ config,
126
+ apps,
127
+ nats,
128
+ redis,
129
+ async close() {
130
+ try {
131
+ await app.close();
132
+ }
133
+ catch {
134
+ /* 이미 닫혔을 수 있음 — graceful */
135
+ }
136
+ if (nats)
137
+ await nats.close();
138
+ if (redis)
139
+ redis.disconnect();
140
+ await destroyAllConnections();
141
+ resetStorage();
142
+ resetI18n();
143
+ },
144
+ };
145
+ }
146
+ // redisSessionStore 는 참조만 유지(트리 셰이킹 방지 · 향후 캐시 재사용).
147
+ void redisSessionStore;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@gaonjs/config",
3
+ "version": "0.1.0",
4
+ "description": "Gaon 루트 설정 로더·자동 배선 (gaon.config.ts · §3.3 · M9-A)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://gaonjs.dev",
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "dependencies": {
27
+ "ioredis": "^5.4.1",
28
+ "@gaonjs/async": "0.2.2",
29
+ "@gaonjs/mail": "0.1.0",
30
+ "@gaonjs/storage": "0.1.0",
31
+ "@gaonjs/web": "0.3.0",
32
+ "@gaonjs/data": "0.3.0",
33
+ "@gaonjs/core": "0.1.4",
34
+ "@gaonjs/i18n": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "fastify": "^5.0.0"
38
+ },
39
+ "scripts": {
40
+ "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json"
41
+ }
42
+ }