puremvc-typescript-graph 0.1.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vizyman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # puremvc-ts-graph
2
+
3
+ PureMVC TypeScript 코드를 정적 분석하여 **알림(notification) 흐름**과 **시작 시점(startup) 배선**을 Mermaid 다이어그램으로 자동 생성하는 CLI 도구입니다.
4
+
5
+ > English documentation: [README-en.md](./README-en.md)
6
+
7
+ ## 빠른 시작
8
+
9
+ ```bash
10
+ npx puremvc-ts-graph
11
+ ```
12
+
13
+ PureMVC 프로젝트 루트에서 실행하면 TypeScript 소스를 스캔하고 아래 구조로 결과를 출력합니다.
14
+
15
+ ```
16
+ docs/
17
+ puremvc-graph.md # 개요 + 알림 매핑 테이블
18
+ AppFacade/
19
+ startup.mmd # 시작 시점 배선 다이어그램
20
+ runtime.mmd # 런타임 알림 흐름 다이어그램
21
+ startup.png # --format png 또는 images.enabled
22
+ runtime.png
23
+ TodoModuleFacade/
24
+ startup.mmd
25
+ runtime.mmd
26
+ unscoped/ # Facade에 할당되지 않은 컴포넌트가 있을 때만
27
+ startup.mmd
28
+ runtime.mmd
29
+ ```
30
+
31
+ ## 연결을 어떻게 찾고 시각화하는가
32
+
33
+ 이 도구는 **런타임 실행 없이** TypeScript AST(추상 구문 트리)를 분석합니다. [ts-morph](https://github.com/dsherret/ts-morph)로 `srcDir` 아래 `.ts` 파일을 읽고, PureMVC API 호출 패턴과 상수 참조를 추적해 그래프를 만듭니다.
34
+
35
+ ### 1단계: 소스 스캔 및 상수 해석
36
+
37
+ | 분석 대상 | 찾는 방법 | 용도 |
38
+ | --- | --- | --- |
39
+ | 알림 상수 | glob에 맞는 파일의 `export const` 객체 리터럴 (`constants.glob`, `constants.exportNames`) | `AppConstants.STARTUP` 같은 참조를 실제 문자열 값으로 변환 |
40
+ | 클래스 이름 | 프로젝트 내 모든 `class` 선언 | 팩토리 표현식에서 Command/Proxy/Mediator 클래스 식별 |
41
+ | 클래스 역할 (보조) | `extends` 절의 **직접 부모** 이름 패턴 매칭 | Facade 자동 감지, Observer 역할 라벨 표시 |
42
+
43
+ 상수 해석 예:
44
+
45
+ ```typescript
46
+ // AppConstants.ts
47
+ export const AppConstants = { STARTUP: 'startup' };
48
+
49
+ // AppFacade.ts
50
+ this.sendNotification(AppConstants.STARTUP);
51
+ // → notification: "startup"
52
+ ```
53
+
54
+ ### 2단계: PureMVC API 호출 추출
55
+
56
+ 소스 코드에서 아래 호출을 찾아 **누가(registrar), 무엇을, 어떻게** 연결했는지 기록합니다.
57
+
58
+ | API 호출 | 추출 정보 | 그래프에서의 의미 |
59
+ | --- | --- | --- |
60
+ | `registerCommand(notif, factory)` | 알림 이름, Command 클래스, 등록한 클래스(Facade/Command) | Command 등록 관계 |
61
+ | `registerProxy(factory)` | Proxy 클래스, 등록한 Command | Proxy 등록 관계 |
62
+ | `registerMediator(mediator)` | Mediator 클래스, 등록한 Command | Mediator 등록 관계 |
63
+ | `addSubCommand(factory)` | MacroCommand → 하위 Command | MacroCommand 실행 순서 |
64
+ | `sendNotification(notif)` | 발신 클래스, 알림 이름 | 런타임 알림 발송 |
65
+ | `listNotificationInterests()` | 관찰 클래스, 구독 알림 목록 | Observer(Mediator/Command) 연결 |
66
+ | `retrieveProxy(name)` | Command 클래스, 사용 Proxy | Command → Proxy 의존 |
67
+
68
+ 팩토리 표현식(`() => new StartupCommand()`, `() => StartupCommand` 등)에서 실제 클래스 이름을 추출합니다.
69
+
70
+ ### 3단계: Facade별 컴포넌트 분류
71
+
72
+ 여러 Facade를 지원하기 위해, 각 컴포넌트를 **어느 Facade에 속하는지** 결정합니다.
73
+
74
+ 1. **Facade 목록 결정**
75
+ - `facades` 설정이 있으면 그대로 사용
76
+ - 없으면 `extends Facade`인 클래스를 자동 감지
77
+ - 둘 다 없으면 `facadeClassName` + `startupNotification` 폴백
78
+
79
+ 2. **Startup 트리 구성**
80
+ - Facade의 `startupNotification`에 등록된 Command를 시작점으로 잡음
81
+ - `addSubCommand` 체인을 따라 MacroCommand 하위 Command를 재귀적으로 확장
82
+
83
+ 3. **컴포넌트 할당**
84
+ - Command: startup 트리 안의 클래스가 `registerCommand`한 Command
85
+ - Proxy/Mediator: startup 트리 안의 클래스가 `registerProxy` / `registerMediator`한 인스턴스
86
+ - Facade에 할당되지 않은 컴포넌트 → `unscoped/` 출력
87
+
88
+ ```
89
+ AppFacade
90
+ └─ startup notification
91
+ └─ StartupCommand (MacroCommand)
92
+ ├─ PrepModelCommand → registerProxy(TodoProxy)
93
+ ├─ PrepViewCommand → registerMediator(TodoFormMediator)
94
+ └─ PrepControllerCommand → registerCommand(...)
95
+ ```
96
+
97
+ ### 4단계: 다이어그램 생성
98
+
99
+ 분석 결과를 Mermaid `flowchart`로 변환합니다.
100
+
101
+ #### Startup 다이어그램 (`startup.mmd`) — 세로(TB)
102
+
103
+ 앱 **기동 시 한 번** 일어나는 배선을 표현합니다.
104
+
105
+ - `Facade → startupNotification → StartupCommand`
106
+ - MacroCommand `addSubCommand` 순서
107
+ - Command가 등록한 다른 Command (`registrar → notification → Command`)
108
+ - Command가 등록한 Proxy / Mediator
109
+
110
+ #### Runtime 다이어그램 (`runtime.mmd`) — 가로(LR)
111
+
112
+ **실행 중** 알림이 어떻게 흐르는지 표현합니다.
113
+
114
+ - `sendNotification` 발신자 → 해당 알림에 등록된 Command
115
+ - `sendNotification` 발신자 → `listNotificationInterests`로 구독 중인 Observer
116
+ - Command가 `retrieveProxy`로 사용하는 Proxy
117
+
118
+ #### Markdown 개요 (`puremvc-graph.md`)
119
+
120
+ Facade별로 알림 매핑 테이블을 생성합니다.
121
+
122
+ | Notification | Senders | Handlers |
123
+ | --- | --- | --- |
124
+ | 각 알림 | `sendNotification`을 호출하는 클래스 | 등록된 Command + Observer |
125
+
126
+ ### extends vs 등록 API (알아두면 좋은 점)
127
+
128
+ 다이어그램에 **연결을 잡는 기준은 `extends`가 아니라 PureMVC API 호출**입니다.
129
+ Command, Proxy, Mediator가 중간 베이스 클래스를 거쳐 구현되더라도, 아래 API로 등록·사용되면 그래프에 포함됩니다.
130
+
131
+ | API | 그래프 반영 |
132
+ | --- | --- |
133
+ | `registerCommand` | Command 등록 및 startup/runtime 연결 |
134
+ | `registerProxy` | Proxy 등록 |
135
+ | `registerMediator` | Mediator 등록 |
136
+ | `addSubCommand` | MacroCommand 실행 순서 |
137
+ | `sendNotification` | 런타임 알림 발송 |
138
+ | `listNotificationInterests` | Observer 구독 |
139
+ | `retrieveProxy` | Command → Proxy 의존 |
140
+
141
+ `extends` 분석은 **보조 정보**로만 사용됩니다.
142
+
143
+ | `extends` 용도 | 영향 |
144
+ | --- | --- |
145
+ | Facade 자동 감지 | `extends Facade`인 클래스만 config 없이 Facade로 인식 |
146
+ | Observer 역할 라벨 | `listNotificationInterests` 구독자를 Mediator / Command (observer) / Observer 로 표시 |
147
+
148
+ 예를 들어 `class MyCommand extends BaseCommand` (BaseCommand → SimpleCommand)처럼 2단계 상속이어도 `registerCommand`로 등록되어 있으면 **startup·runtime 다이어그램 모두에 정상 표시**됩니다. 역할 라벨만 `Observer`로 나올 수 있습니다.
149
+
150
+ **실제로 주의할 점**
151
+
152
+ - **2단계 Facade 상속**: `class ModuleFacade extends AppFacade`처럼 PureMVC `Facade`를 직접 extends하지 않으면 자동 감지되지 않습니다. config `facades`에 명시하세요.
153
+ - **동적 알림 이름**: 변수나 함수 결과로 만든 알림은 상수로 해석되지 않으면 추적하지 못할 수 있습니다.
154
+
155
+ ## 설정
156
+
157
+ 예시 설정을 프로젝트에 복사합니다.
158
+
159
+ ```bash
160
+ cp node_modules/puremvc-ts-graph/puremvc-graph.config.example.json puremvc-graph.config.json
161
+ ```
162
+
163
+ 또는 프로젝트 루트에 `puremvc-graph.config.json`을 직접 작성합니다.
164
+
165
+ ```json
166
+ {
167
+ "srcDir": "src",
168
+ "docsDir": "docs",
169
+ "facades": [
170
+ { "className": "AppFacade", "startupNotification": "startup" },
171
+ { "className": "TodoModuleFacade", "startupNotification": "moduleStartup" }
172
+ ],
173
+ "constants": {
174
+ "glob": [
175
+ "**/*Constants.ts",
176
+ "**/notifications.ts"
177
+ ],
178
+ "exportNames": ["AppConstants", "TodoConstants"]
179
+ },
180
+ "images": {
181
+ "enabled": false,
182
+ "format": "png"
183
+ }
184
+ }
185
+ ```
186
+
187
+ | 옵션 | 기본값 | 설명 |
188
+ | --- | --- | --- |
189
+ | `srcDir` | `src` | 스캔할 TypeScript 소스 디렉터리 |
190
+ | `docsDir` | `docs` | 생성 결과 출력 디렉터리 |
191
+ | `facades` | `[]` | Facade 목록과 각 Facade의 startup 알림 |
192
+ | `facadeClassName` | `AppFacade` | `facades`가 비어 있을 때 단일 Facade 폴백 |
193
+ | `startupNotification` | `startup` | `facades`가 비어 있을 때 startup 알림 폴백 |
194
+ | `constants.glob` | `["**/*Constants.ts"]` | 알림 상수 파일 glob (문자열 하나 또는 배열, OR 조건) |
195
+ | `constants.exportNames` | `[]` | glob에 맞는 파일 안에서 파싱할 const 객체 이름 (빈 배열 = 전부) |
196
+ | `images.enabled` | `false` | 매 실행마다 이미지 생성 (`images.format` 사용) |
197
+ | `images.format` | `png` | 기본 이미지 형식: `png` 또는 `svg` |
198
+
199
+ ## CLI 옵션
200
+
201
+ ```bash
202
+ npx puremvc-ts-graph
203
+ npx puremvc-ts-graph --config ./my-config.json
204
+ npx puremvc-ts-graph --cwd /path/to/project
205
+ npx puremvc-ts-graph --format png
206
+ npx puremvc-ts-graph --format svg
207
+ npx puremvc-ts-graph --format png,svg # 두 형식 모두
208
+ npx puremvc-ts-graph --png # --format png 단축
209
+ npx puremvc-ts-graph --svg # --format svg 단축
210
+ npx puremvc-ts-graph --images # config의 images.format 사용
211
+ npx puremvc-ts-graph --help
212
+ ```
213
+
214
+ 이미지 생성은 `@mermaid-js/mermaid-cli`를 사용합니다(optional dependency). 최초 실행 시 Chromium 다운로드가 필요할 수 있습니다.
215
+
216
+ ## dev dependency로 설치
217
+
218
+ ```bash
219
+ npm install -D puremvc-ts-graph
220
+ ```
221
+
222
+ ```json
223
+ {
224
+ "scripts": {
225
+ "graph": "puremvc-graph"
226
+ }
227
+ }
228
+ ```
229
+
230
+ ## 배포 안내
231
+
232
+ npm 패키지명은 `puremvc-ts-graph`입니다. PureMVC npm org가 준비되면 `@puremvc/puremvc-ts-graph` 스코프 배포를 검토할 예정입니다.
233
+
234
+ ## License
235
+
236
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { Project } from 'ts-morph';
5
+ import { generateImages } from './lib/image-generator.js';
6
+ import { isHelpRequested, loadConfig, parseCliArgs, printCliHelp, resolveImageGeneration } from './lib/graph-config.js';
7
+ import { buildFacadeDiagramFiles, buildMarkdown, buildStats, buildUnscopedRuntimeMermaid, buildUnscopedStartupMermaid } from './lib/graph-builder.js';
8
+ import { collectGraphData } from './lib/graph-parser.js';
9
+ function createProject(root) {
10
+ const tsConfigPath = join(root, 'tsconfig.json');
11
+ return new Project({
12
+ ...(existsSync(tsConfigPath) ? { tsConfigFilePath: tsConfigPath } : {}),
13
+ skipAddingFilesFromTsConfig: true
14
+ });
15
+ }
16
+ function writeDiagramSet(dir, startupMermaid, runtimeMermaid, mmdPaths) {
17
+ mkdirSync(dir, { recursive: true });
18
+ const startupPath = join(dir, 'startup.mmd');
19
+ const runtimePath = join(dir, 'runtime.mmd');
20
+ writeFileSync(startupPath, startupMermaid, 'utf8');
21
+ writeFileSync(runtimePath, runtimeMermaid, 'utf8');
22
+ mmdPaths.push(startupPath, runtimePath);
23
+ }
24
+ function main() {
25
+ const argv = process.argv.slice(2);
26
+ if (isHelpRequested(argv)) {
27
+ printCliHelp();
28
+ return;
29
+ }
30
+ const { projectRoot, configPath } = parseCliArgs(argv);
31
+ const config = loadConfig(projectRoot, configPath);
32
+ let imageFormats;
33
+ try {
34
+ imageFormats = resolveImageGeneration(argv, config);
35
+ }
36
+ catch (error) {
37
+ console.error(error instanceof Error ? error.message : String(error));
38
+ process.exit(1);
39
+ }
40
+ const srcDir = join(projectRoot, config.srcDir);
41
+ const docsDir = join(projectRoot, config.docsDir);
42
+ if (!existsSync(srcDir)) {
43
+ console.error(`Source directory not found: ${srcDir}`);
44
+ process.exit(1);
45
+ }
46
+ const project = createProject(projectRoot);
47
+ const sourceFiles = project.addSourceFilesAtPaths(join(srcDir, '**/*.ts'));
48
+ const data = collectGraphData(sourceFiles, config);
49
+ if (!existsSync(docsDir)) {
50
+ mkdirSync(docsDir, { recursive: true });
51
+ }
52
+ const stats = buildStats(data);
53
+ const mmdPaths = [];
54
+ const outputDirs = [];
55
+ const mdPath = join(docsDir, 'puremvc-graph.md');
56
+ writeFileSync(mdPath, buildMarkdown(data), 'utf8');
57
+ for (const facade of buildFacadeDiagramFiles(data)) {
58
+ const facadeDir = join(docsDir, facade.slug);
59
+ writeDiagramSet(facadeDir, facade.startupMermaid, facade.runtimeMermaid, mmdPaths);
60
+ outputDirs.push(facadeDir);
61
+ }
62
+ const unscopedStartup = buildUnscopedStartupMermaid(data);
63
+ const unscopedRuntime = buildUnscopedRuntimeMermaid(data);
64
+ if (unscopedStartup || unscopedRuntime) {
65
+ const unscopedDir = join(docsDir, 'unscoped');
66
+ mkdirSync(unscopedDir, { recursive: true });
67
+ outputDirs.push(unscopedDir);
68
+ if (unscopedStartup) {
69
+ const startupPath = join(unscopedDir, 'startup.mmd');
70
+ writeFileSync(startupPath, unscopedStartup, 'utf8');
71
+ mmdPaths.push(startupPath);
72
+ }
73
+ if (unscopedRuntime) {
74
+ const runtimePath = join(unscopedDir, 'runtime.mmd');
75
+ writeFileSync(runtimePath, unscopedRuntime, 'utf8');
76
+ mmdPaths.push(runtimePath);
77
+ }
78
+ }
79
+ const resolvedConfig = configPath ?? join(projectRoot, 'puremvc-graph.config.json');
80
+ console.log('PureMVC graph generated:');
81
+ console.log(` Project: ${projectRoot}`);
82
+ console.log(` Config: ${resolvedConfig}`);
83
+ console.log(` ${mdPath}`);
84
+ for (const dir of outputDirs) {
85
+ console.log(` ${dir}/`);
86
+ if (existsSync(join(dir, 'startup.mmd')))
87
+ console.log(` startup.mmd`);
88
+ if (existsSync(join(dir, 'runtime.mmd')))
89
+ console.log(` runtime.mmd`);
90
+ }
91
+ console.log('');
92
+ console.log(`Constant properties: ${stats.notifications}`);
93
+ console.log(`Command registrations: ${stats.commandRegistrations}`);
94
+ console.log(`Proxies: ${stats.proxies}`);
95
+ console.log(`Mediators: ${stats.mediators}`);
96
+ console.log(`Observers (listNotificationInterests): ${stats.listeners}`);
97
+ console.log(`Facades: ${stats.facades}`);
98
+ if (imageFormats) {
99
+ console.log('');
100
+ let totalFailed = 0;
101
+ for (const imageFormat of imageFormats) {
102
+ console.log(`Generating ${imageFormat.toUpperCase()} images...`);
103
+ const { generated, failed } = generateImages(mmdPaths, imageFormat);
104
+ for (const path of generated) {
105
+ console.log(` ${path}`);
106
+ }
107
+ totalFailed += failed.length;
108
+ }
109
+ if (totalFailed > 0) {
110
+ console.error(`Failed to generate ${totalFailed} image(s).`);
111
+ process.exit(1);
112
+ }
113
+ }
114
+ }
115
+ main();
@@ -0,0 +1,18 @@
1
+ import { ClassDeclaration, Node, SourceFile, type Expression } from 'ts-morph';
2
+ import type { PureMvcGraphConfig } from './graph-config.js';
3
+ export declare class ConstantsStore {
4
+ /** objectName -> propertyName -> notification string value */
5
+ private readonly objects;
6
+ /** identifierName -> resolved notification names (for array constants) */
7
+ private readonly arrayConstants;
8
+ get objectCount(): number;
9
+ get propertyCount(): number;
10
+ parseFromSourceFiles(sourceFiles: SourceFile[], config: PureMvcGraphConfig): void;
11
+ resolve(expr: Expression | Node | undefined): string | null;
12
+ resolveArray(expr: Expression | undefined, classDecl: ClassDeclaration | undefined): string[];
13
+ private matchesAnyConstantsGlob;
14
+ private matchesConstantsGlob;
15
+ private parseObjectLiterals;
16
+ private parseArrayConstants;
17
+ private unwrapAsConst;
18
+ }
@@ -0,0 +1,163 @@
1
+ import { Node } from 'ts-morph';
2
+ export class ConstantsStore {
3
+ /** objectName -> propertyName -> notification string value */
4
+ objects = new Map();
5
+ /** identifierName -> resolved notification names (for array constants) */
6
+ arrayConstants = new Map();
7
+ get objectCount() {
8
+ return this.objects.size;
9
+ }
10
+ get propertyCount() {
11
+ let count = 0;
12
+ for (const props of this.objects.values()) {
13
+ count += props.size;
14
+ }
15
+ return count;
16
+ }
17
+ parseFromSourceFiles(sourceFiles, config) {
18
+ for (const file of sourceFiles) {
19
+ if (!this.matchesAnyConstantsGlob(file.getFilePath(), config.constants.glob)) {
20
+ continue;
21
+ }
22
+ this.parseObjectLiterals(file, config.constants.exportNames);
23
+ this.parseArrayConstants(file);
24
+ }
25
+ }
26
+ resolve(expr) {
27
+ if (!expr)
28
+ return null;
29
+ if (Node.isStringLiteral(expr) || Node.isNoSubstitutionTemplateLiteral(expr)) {
30
+ return expr.getLiteralText();
31
+ }
32
+ if (Node.isPropertyAccessExpression(expr)) {
33
+ const objectName = expr.getExpression().getText();
34
+ const propName = expr.getName();
35
+ return this.objects.get(objectName)?.get(propName) ?? null;
36
+ }
37
+ const text = expr.getText();
38
+ const match = text.match(/^(\w+)\.(\w+)$/);
39
+ if (match) {
40
+ return this.objects.get(match[1])?.get(match[2]) ?? null;
41
+ }
42
+ return null;
43
+ }
44
+ resolveArray(expr, classDecl) {
45
+ if (!expr)
46
+ return [];
47
+ if (Node.isArrayLiteralExpression(expr)) {
48
+ const result = [];
49
+ for (const element of expr.getElements()) {
50
+ if (Node.isSpreadElement(element)) {
51
+ result.push(...this.resolveArray(element.getExpression(), classDecl));
52
+ continue;
53
+ }
54
+ const resolved = this.resolve(element);
55
+ if (resolved)
56
+ result.push(resolved);
57
+ }
58
+ return result;
59
+ }
60
+ if (Node.isIdentifier(expr)) {
61
+ const name = expr.getText();
62
+ const fromCache = this.arrayConstants.get(name);
63
+ if (fromCache)
64
+ return [...fromCache];
65
+ if (classDecl) {
66
+ const staticProp = classDecl.getStaticProperty(name);
67
+ if (staticProp && Node.isPropertyDeclaration(staticProp)) {
68
+ const staticInit = staticProp.getInitializer();
69
+ if (staticInit) {
70
+ return this.resolveArray(staticInit, classDecl);
71
+ }
72
+ }
73
+ }
74
+ const sourceFile = expr.getSourceFile();
75
+ const varDecl = sourceFile.getVariableDeclaration(name);
76
+ const varInit = varDecl?.getInitializer();
77
+ if (varInit) {
78
+ return this.resolveArray(varInit, classDecl);
79
+ }
80
+ }
81
+ return [];
82
+ }
83
+ matchesAnyConstantsGlob(filePath, globs) {
84
+ return globs.some((glob) => this.matchesConstantsGlob(filePath, glob));
85
+ }
86
+ matchesConstantsGlob(filePath, glob) {
87
+ const normalized = filePath.replace(/\\/g, '/');
88
+ const pattern = glob
89
+ .replace(/\\/g, '/')
90
+ .replace(/\*\*/g, '§§')
91
+ .replace(/\*/g, '[^/]*')
92
+ .replace(/§§/g, '.*');
93
+ return new RegExp(`${pattern}$`).test(normalized);
94
+ }
95
+ parseObjectLiterals(file, exportNames) {
96
+ for (const decl of file.getVariableDeclarations()) {
97
+ const name = decl.getName();
98
+ if (exportNames.length > 0 && !exportNames.includes(name))
99
+ continue;
100
+ const init = decl.getInitializer();
101
+ if (!init)
102
+ continue;
103
+ const objectLiteral = this.unwrapAsConst(init);
104
+ if (!objectLiteral || !Node.isObjectLiteralExpression(objectLiteral))
105
+ continue;
106
+ const props = new Map();
107
+ for (const prop of objectLiteral.getProperties()) {
108
+ if (!Node.isPropertyAssignment(prop))
109
+ continue;
110
+ const propInit = prop.getInitializer();
111
+ if (!propInit)
112
+ continue;
113
+ if (!Node.isStringLiteral(propInit) && !Node.isNoSubstitutionTemplateLiteral(propInit)) {
114
+ continue;
115
+ }
116
+ props.set(prop.getName(), propInit.getLiteralText());
117
+ }
118
+ if (props.size > 0) {
119
+ this.objects.set(name, props);
120
+ }
121
+ }
122
+ }
123
+ parseArrayConstants(file) {
124
+ for (const decl of file.getVariableDeclarations()) {
125
+ const name = decl.getName();
126
+ const init = decl.getInitializer();
127
+ if (!init)
128
+ continue;
129
+ const array = Node.isArrayLiteralExpression(init)
130
+ ? init
131
+ : Node.isAsExpression(init) && Node.isArrayLiteralExpression(init.getExpression())
132
+ ? init.getExpression()
133
+ : null;
134
+ if (!array)
135
+ continue;
136
+ const values = this.resolveArray(array, undefined);
137
+ if (values.length > 0) {
138
+ this.arrayConstants.set(name, values);
139
+ }
140
+ }
141
+ for (const cls of file.getClasses()) {
142
+ for (const prop of cls.getStaticProperties()) {
143
+ if (!Node.isPropertyDeclaration(prop))
144
+ continue;
145
+ const propName = prop.getName();
146
+ const init = prop.getInitializer();
147
+ if (!init || !Node.isArrayLiteralExpression(init))
148
+ continue;
149
+ const values = this.resolveArray(init, cls);
150
+ if (values.length > 0) {
151
+ this.arrayConstants.set(`${cls.getName()}.${propName}`, values);
152
+ this.arrayConstants.set(propName, values);
153
+ }
154
+ }
155
+ }
156
+ }
157
+ unwrapAsConst(expr) {
158
+ if (Node.isAsExpression(expr)) {
159
+ return expr.getExpression();
160
+ }
161
+ return expr;
162
+ }
163
+ }
@@ -0,0 +1,29 @@
1
+ import type { FacadeConfig } from './graph-config.js';
2
+ import type { CommandRegistration, GraphData } from './graph-parser.js';
3
+ export interface FacadeScope {
4
+ facadeClassName: string;
5
+ startupNotification: string;
6
+ startupCommand: string | null;
7
+ startupTree: Set<string>;
8
+ commands: Set<string>;
9
+ proxies: Set<string>;
10
+ mediators: Set<string>;
11
+ listeners: Set<string>;
12
+ }
13
+ export interface UnscopedComponents {
14
+ commands: Set<string>;
15
+ proxies: Set<string>;
16
+ mediators: Set<string>;
17
+ listeners: Set<string>;
18
+ }
19
+ export interface FacadePartition {
20
+ facades: Map<string, FacadeScope>;
21
+ unscoped: UnscopedComponents;
22
+ classToFacade: Map<string, string>;
23
+ }
24
+ export declare function resolveFacadeConfigs(data: GraphData): FacadeConfig[];
25
+ export declare function expandStartupTree(root: string, macroSubCommands: Map<string, string[]>): Set<string>;
26
+ export declare function buildFacadePartition(data: GraphData): FacadePartition;
27
+ export declare function resolveFacadeForClass(className: string, partition: FacadePartition): string | null;
28
+ export declare function getCommandsForNotificationInScope(data: GraphData, notification: string, scope: FacadeScope): CommandRegistration[];
29
+ export declare function getNotificationsInScope(data: GraphData, scope: FacadeScope): string[];