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.
@@ -0,0 +1,201 @@
1
+ export function resolveFacadeConfigs(data) {
2
+ if (data.config.facades.length > 0) {
3
+ return data.config.facades;
4
+ }
5
+ const detected = [];
6
+ for (const [className, role] of data.classRoles) {
7
+ if (role === 'facade') {
8
+ detected.push({
9
+ className,
10
+ startupNotification: data.config.startupNotification
11
+ });
12
+ }
13
+ }
14
+ if (detected.length > 0) {
15
+ return detected;
16
+ }
17
+ return [
18
+ {
19
+ className: data.config.facadeClassName,
20
+ startupNotification: data.config.startupNotification
21
+ }
22
+ ];
23
+ }
24
+ export function expandStartupTree(root, macroSubCommands) {
25
+ const tree = new Set([root]);
26
+ const queue = [root];
27
+ while (queue.length > 0) {
28
+ const current = queue.pop();
29
+ for (const sub of macroSubCommands.get(current) ?? []) {
30
+ if (!tree.has(sub)) {
31
+ tree.add(sub);
32
+ queue.push(sub);
33
+ }
34
+ }
35
+ }
36
+ return tree;
37
+ }
38
+ function findStartupCommand(data, facade) {
39
+ const candidates = data.commandRegistrations.filter((reg) => reg.notification === facade.startupNotification);
40
+ if (candidates.length === 0) {
41
+ return null;
42
+ }
43
+ const fromFacade = candidates.find((reg) => reg.registrar === facade.className);
44
+ if (fromFacade) {
45
+ return fromFacade.commandClass;
46
+ }
47
+ const withMacro = candidates.find((reg) => data.macroSubCommands.has(reg.commandClass));
48
+ if (withMacro) {
49
+ return withMacro.commandClass;
50
+ }
51
+ return candidates[0].commandClass;
52
+ }
53
+ function isInStartupTree(registrar, startupTree) {
54
+ return registrar !== undefined && startupTree.has(registrar);
55
+ }
56
+ function createEmptyUnscoped() {
57
+ return {
58
+ commands: new Set(),
59
+ proxies: new Set(),
60
+ mediators: new Set(),
61
+ listeners: new Set()
62
+ };
63
+ }
64
+ export function buildFacadePartition(data) {
65
+ const facadeConfigs = resolveFacadeConfigs(data);
66
+ const facades = new Map();
67
+ const classToFacade = new Map();
68
+ const unscoped = createEmptyUnscoped();
69
+ const commandOwners = new Map();
70
+ const proxyOwners = new Map();
71
+ const mediatorOwners = new Map();
72
+ for (const facadeConfig of facadeConfigs) {
73
+ const startupCommand = findStartupCommand(data, facadeConfig);
74
+ const startupTree = startupCommand
75
+ ? expandStartupTree(startupCommand, data.macroSubCommands)
76
+ : new Set();
77
+ if (startupCommand) {
78
+ startupTree.add(startupCommand);
79
+ }
80
+ const scope = {
81
+ facadeClassName: facadeConfig.className,
82
+ startupNotification: facadeConfig.startupNotification,
83
+ startupCommand,
84
+ startupTree,
85
+ commands: new Set(),
86
+ proxies: new Set(),
87
+ mediators: new Set(),
88
+ listeners: new Set()
89
+ };
90
+ for (const reg of data.commandRegistrations) {
91
+ if (reg.notification === facadeConfig.startupNotification) {
92
+ if (reg.commandClass) {
93
+ scope.commands.add(reg.commandClass);
94
+ }
95
+ continue;
96
+ }
97
+ if (isInStartupTree(reg.registrar, startupTree)) {
98
+ scope.commands.add(reg.commandClass);
99
+ }
100
+ }
101
+ for (const proxy of data.proxies) {
102
+ const registrar = data.proxyRegistrars.get(proxy);
103
+ if (isInStartupTree(registrar, startupTree)) {
104
+ scope.proxies.add(proxy);
105
+ }
106
+ }
107
+ for (const mediator of data.mediators) {
108
+ const registrar = data.mediatorRegistrars.get(mediator);
109
+ if (isInStartupTree(registrar, startupTree)) {
110
+ scope.mediators.add(mediator);
111
+ }
112
+ }
113
+ facades.set(facadeConfig.className, scope);
114
+ classToFacade.set(facadeConfig.className, facadeConfig.className);
115
+ for (const command of scope.commands) {
116
+ classToFacade.set(command, facadeConfig.className);
117
+ }
118
+ for (const proxy of scope.proxies) {
119
+ classToFacade.set(proxy, facadeConfig.className);
120
+ }
121
+ for (const mediator of scope.mediators) {
122
+ classToFacade.set(mediator, facadeConfig.className);
123
+ }
124
+ for (const node of startupTree) {
125
+ classToFacade.set(node, facadeConfig.className);
126
+ }
127
+ for (const reg of data.commandRegistrations) {
128
+ if (scope.commands.has(reg.commandClass)) {
129
+ commandOwners.set(reg.commandClass, facadeConfig.className);
130
+ }
131
+ }
132
+ for (const proxy of scope.proxies) {
133
+ proxyOwners.set(proxy, facadeConfig.className);
134
+ }
135
+ for (const mediator of scope.mediators) {
136
+ mediatorOwners.set(mediator, facadeConfig.className);
137
+ }
138
+ }
139
+ for (const reg of data.commandRegistrations) {
140
+ if (!commandOwners.has(reg.commandClass)) {
141
+ unscoped.commands.add(reg.commandClass);
142
+ }
143
+ }
144
+ for (const proxy of data.proxies) {
145
+ if (!proxyOwners.has(proxy)) {
146
+ unscoped.proxies.add(proxy);
147
+ }
148
+ }
149
+ for (const mediator of data.mediators) {
150
+ if (!mediatorOwners.has(mediator)) {
151
+ unscoped.mediators.add(mediator);
152
+ }
153
+ }
154
+ for (const [className, listener] of data.listeners) {
155
+ const owner = classToFacade.get(className) ??
156
+ mediatorOwners.get(className) ??
157
+ commandOwners.get(className) ??
158
+ proxyOwners.get(className);
159
+ if (owner) {
160
+ facades.get(owner)?.listeners.add(className);
161
+ classToFacade.set(className, owner);
162
+ }
163
+ else {
164
+ unscoped.listeners.add(className);
165
+ }
166
+ }
167
+ return { facades, unscoped, classToFacade };
168
+ }
169
+ export function resolveFacadeForClass(className, partition) {
170
+ return partition.classToFacade.get(className) ?? null;
171
+ }
172
+ export function getCommandsForNotificationInScope(data, notification, scope) {
173
+ return data.commandRegistrations.filter((reg) => reg.notification === notification &&
174
+ (scope.commands.has(reg.commandClass) ||
175
+ (reg.notification === scope.startupNotification &&
176
+ reg.commandClass === scope.startupCommand)));
177
+ }
178
+ export function getNotificationsInScope(data, scope) {
179
+ const all = new Set();
180
+ for (const reg of data.commandRegistrations) {
181
+ if (scope.commands.has(reg.commandClass)) {
182
+ all.add(reg.notification);
183
+ }
184
+ }
185
+ for (const [sender, notifications] of data.sends) {
186
+ if (resolveFacadeForClass(sender, data.facadePartition) !== scope.facadeClassName) {
187
+ continue;
188
+ }
189
+ for (const notification of notifications) {
190
+ all.add(notification);
191
+ }
192
+ }
193
+ for (const listener of data.listeners.values()) {
194
+ if (!scope.listeners.has(listener.className))
195
+ continue;
196
+ for (const interest of listener.interests) {
197
+ all.add(interest);
198
+ }
199
+ }
200
+ return [...all].sort();
201
+ }
@@ -0,0 +1,4 @@
1
+ import { type Expression } from 'ts-morph';
2
+ export declare function collectClassNames(sourceFiles: import('ts-morph').SourceFile[]): Set<string>;
3
+ export declare function resolveFactoryClass(expr: Expression | undefined, classNames: Set<string>, depth?: number): string | null;
4
+ export declare function resolveProxyReference(arg: Expression | undefined): string | null;
@@ -0,0 +1,61 @@
1
+ import { Node } from 'ts-morph';
2
+ export function collectClassNames(sourceFiles) {
3
+ const names = new Set();
4
+ for (const file of sourceFiles) {
5
+ for (const cls of file.getClasses()) {
6
+ const name = cls.getName();
7
+ if (name)
8
+ names.add(name);
9
+ }
10
+ }
11
+ return names;
12
+ }
13
+ export function resolveFactoryClass(expr, classNames, depth = 0) {
14
+ if (!expr || depth > 5)
15
+ return null;
16
+ const fromFactory = extractClassFromFactory(expr);
17
+ if (fromFactory)
18
+ return fromFactory;
19
+ if (Node.isIdentifier(expr)) {
20
+ const name = expr.getText();
21
+ if (classNames.has(name))
22
+ return name;
23
+ const decl = expr.getSymbol()?.getValueDeclaration();
24
+ if (decl && Node.isVariableDeclaration(decl)) {
25
+ return resolveFactoryClass(decl.getInitializer(), classNames, depth + 1);
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ function extractClassFromFactory(expr) {
31
+ if (Node.isNewExpression(expr)) {
32
+ return expr.getExpression().getText();
33
+ }
34
+ if (Node.isParenthesizedExpression(expr)) {
35
+ return extractClassFromFactory(expr.getExpression());
36
+ }
37
+ if (Node.isArrowFunction(expr) || Node.isFunctionExpression(expr)) {
38
+ const body = expr.getBody();
39
+ if (!body)
40
+ return null;
41
+ if (Node.isNewExpression(body)) {
42
+ return body.getExpression().getText();
43
+ }
44
+ if (Node.isBlock(body)) {
45
+ for (const stmt of body.getStatements()) {
46
+ if (!Node.isReturnStatement(stmt))
47
+ continue;
48
+ const retExpr = stmt.getExpression();
49
+ if (retExpr && Node.isNewExpression(retExpr)) {
50
+ return retExpr.getExpression().getText();
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ export function resolveProxyReference(arg) {
58
+ if (!arg)
59
+ return null;
60
+ return arg.getText().replace(/\.NAME$/, '');
61
+ }
@@ -0,0 +1,23 @@
1
+ import { type FacadeScope } from './facade-resolver.js';
2
+ import { type GraphData } from './graph-parser.js';
3
+ export declare function facadeFileSlug(facadeClassName: string): string;
4
+ export interface FacadeDiagramFiles {
5
+ slug: string;
6
+ label: string;
7
+ startupMermaid: string;
8
+ runtimeMermaid: string;
9
+ }
10
+ export declare function buildStartupMermaidForScope(data: GraphData, scope: FacadeScope): string;
11
+ export declare function buildRuntimeMermaidForScope(data: GraphData, scope: FacadeScope): string;
12
+ export declare function buildUnscopedStartupMermaid(data: GraphData): string | null;
13
+ export declare function buildUnscopedRuntimeMermaid(data: GraphData): string | null;
14
+ export declare function buildFacadeDiagramFiles(data: GraphData): FacadeDiagramFiles[];
15
+ export declare function buildMarkdown(data: GraphData): string;
16
+ export declare function buildStats(data: GraphData): {
17
+ notifications: number;
18
+ commandRegistrations: number;
19
+ proxies: number;
20
+ mediators: number;
21
+ listeners: number;
22
+ facades: number;
23
+ };
@@ -0,0 +1,301 @@
1
+ import { getCommandsForNotificationInScope, getNotificationsInScope, resolveFacadeForClass } from './facade-resolver.js';
2
+ import { getListenerRoleLabel } from './graph-parser.js';
3
+ function safeId(name) {
4
+ return name.replace(/[^a-zA-Z0-9_]/g, '_');
5
+ }
6
+ export function facadeFileSlug(facadeClassName) {
7
+ return facadeClassName.replace(/[^a-zA-Z0-9_-]/g, '_');
8
+ }
9
+ function wrapMermaid(kind, edges, emptyLabel) {
10
+ if (edges.length === 0) {
11
+ return `flowchart ${kind}\n empty["${emptyLabel}"]`;
12
+ }
13
+ const direction = kind === 'TB' ? 'TB' : 'LR';
14
+ return [`flowchart ${kind}`, ` direction ${direction}`, ...edges].join('\n');
15
+ }
16
+ export function buildStartupMermaidForScope(data, scope) {
17
+ return wrapMermaid('TB', buildStartupEdgesForScope(data, scope), 'No startup wiring detected');
18
+ }
19
+ export function buildRuntimeMermaidForScope(data, scope) {
20
+ return wrapMermaid('LR', buildRuntimeEdgesForScope(data, scope), 'No runtime flow detected');
21
+ }
22
+ export function buildUnscopedStartupMermaid(data) {
23
+ const edges = buildUnscopedStartupEdges(data);
24
+ if (edges.length === 0)
25
+ return null;
26
+ return wrapMermaid('TB', edges, 'No unscoped startup wiring');
27
+ }
28
+ export function buildUnscopedRuntimeMermaid(data) {
29
+ const edges = buildUnscopedRuntimeEdges(data);
30
+ if (edges.length === 0)
31
+ return null;
32
+ return wrapMermaid('LR', edges, 'No unscoped runtime flow');
33
+ }
34
+ export function buildFacadeDiagramFiles(data) {
35
+ return [...data.facadePartition.facades.values()].map((scope) => ({
36
+ slug: facadeFileSlug(scope.facadeClassName),
37
+ label: scope.facadeClassName,
38
+ startupMermaid: buildStartupMermaidForScope(data, scope),
39
+ runtimeMermaid: buildRuntimeMermaidForScope(data, scope)
40
+ }));
41
+ }
42
+ function buildStartupEdgesForScope(data, scope) {
43
+ const edges = [];
44
+ for (const [macro, subs] of data.macroSubCommands) {
45
+ if (!scope.startupTree.has(macro))
46
+ continue;
47
+ for (const sub of subs) {
48
+ if (scope.startupTree.has(sub)) {
49
+ edges.push(` ${safeId(macro)} --> ${safeId(sub)}`);
50
+ }
51
+ }
52
+ }
53
+ const startupCommand = scope.startupCommand ?? scope.startupTree.values().next().value ?? 'StartupCommand';
54
+ edges.push(` ${safeId(scope.facadeClassName)} -->|${scope.startupNotification}| ${safeId(startupCommand)}`);
55
+ for (const reg of data.commandRegistrations) {
56
+ if (reg.notification === scope.startupNotification)
57
+ continue;
58
+ if (!scope.commands.has(reg.commandClass))
59
+ continue;
60
+ const from = reg.registrar ? safeId(reg.registrar) : safeId(startupCommand);
61
+ edges.push(` ${from} -->|${reg.notification}| ${safeId(reg.commandClass)}`);
62
+ }
63
+ for (const proxy of scope.proxies) {
64
+ const registrar = data.proxyRegistrars.get(proxy);
65
+ const from = registrar ? safeId(registrar) : safeId(startupCommand);
66
+ edges.push(` ${from} -->|registerProxy| ${safeId(proxy)}`);
67
+ }
68
+ for (const mediator of scope.mediators) {
69
+ const registrar = data.mediatorRegistrars.get(mediator);
70
+ const from = registrar ? safeId(registrar) : safeId(startupCommand);
71
+ edges.push(` ${from} -->|registerMediator| ${safeId(mediator)}`);
72
+ }
73
+ return edges;
74
+ }
75
+ function buildUnscopedStartupEdges(data) {
76
+ const edges = [];
77
+ const { unscoped } = data.facadePartition;
78
+ for (const reg of data.commandRegistrations) {
79
+ if (!unscoped.commands.has(reg.commandClass))
80
+ continue;
81
+ const from = reg.registrar ? safeId(reg.registrar) : 'unscoped_registrar';
82
+ edges.push(` ${from} -->|${reg.notification}| ${safeId(reg.commandClass)}`);
83
+ }
84
+ for (const proxy of unscoped.proxies) {
85
+ const registrar = data.proxyRegistrars.get(proxy);
86
+ const from = registrar ? safeId(registrar) : 'unscoped_registrar';
87
+ edges.push(` ${from} -->|registerProxy| ${safeId(proxy)}`);
88
+ }
89
+ for (const mediator of unscoped.mediators) {
90
+ const registrar = data.mediatorRegistrars.get(mediator);
91
+ const from = registrar ? safeId(registrar) : 'unscoped_registrar';
92
+ edges.push(` ${from} -->|registerMediator| ${safeId(mediator)}`);
93
+ }
94
+ return edges;
95
+ }
96
+ function buildRuntimeEdgesForScope(data, scope) {
97
+ const edges = [];
98
+ for (const [sender, notifications] of data.sends) {
99
+ if (resolveFacadeForClass(sender, data.facadePartition) !== scope.facadeClassName) {
100
+ continue;
101
+ }
102
+ for (const notification of notifications) {
103
+ const commands = getCommandsForNotificationInScope(data, notification, scope);
104
+ for (const reg of commands) {
105
+ edges.push(` ${safeId(sender)} -->|"${notification}"| ${safeId(reg.commandClass)}`);
106
+ const proxies = data.usesProxy.get(reg.commandClass);
107
+ if (proxies) {
108
+ for (const proxy of proxies) {
109
+ if (scope.proxies.has(proxy)) {
110
+ edges.push(` ${safeId(reg.commandClass)} --> ${safeId(proxy)}`);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ for (const listener of data.listeners.values()) {
116
+ if (!scope.listeners.has(listener.className))
117
+ continue;
118
+ if (!listener.interests.has(notification))
119
+ continue;
120
+ edges.push(` ${safeId(sender)} -->|"${notification}"| ${safeId(listener.className)}`);
121
+ }
122
+ }
123
+ }
124
+ return edges;
125
+ }
126
+ function buildUnscopedRuntimeEdges(data) {
127
+ const edges = [];
128
+ const { unscoped } = data.facadePartition;
129
+ for (const [sender, notifications] of data.sends) {
130
+ if (resolveFacadeForClass(sender, data.facadePartition))
131
+ continue;
132
+ for (const notification of notifications) {
133
+ for (const reg of data.commandRegistrations) {
134
+ if (reg.notification !== notification)
135
+ continue;
136
+ if (!unscoped.commands.has(reg.commandClass))
137
+ continue;
138
+ edges.push(` ${safeId(sender)} -->|"${notification}"| ${safeId(reg.commandClass)}`);
139
+ }
140
+ for (const listener of data.listeners.values()) {
141
+ if (!unscoped.listeners.has(listener.className))
142
+ continue;
143
+ if (!listener.interests.has(notification))
144
+ continue;
145
+ edges.push(` ${safeId(sender)} -->|"${notification}"| ${safeId(listener.className)}`);
146
+ }
147
+ }
148
+ }
149
+ return edges;
150
+ }
151
+ function formatCommandHandler(data, commandClass, scope) {
152
+ const proxies = data.usesProxy.get(commandClass);
153
+ if (!proxies || proxies.size === 0)
154
+ return commandClass;
155
+ const scopedProxies = [...proxies].filter((proxy) => scope.proxies.has(proxy));
156
+ if (scopedProxies.length === 0)
157
+ return commandClass;
158
+ return `${commandClass} → ${scopedProxies.join(', ')}`;
159
+ }
160
+ function buildNotificationTableForScope(data, scope) {
161
+ const rows = [
162
+ '| Notification | Senders | Handlers |',
163
+ '| --- | --- | --- |'
164
+ ];
165
+ for (const notification of getNotificationsInScope(data, scope)) {
166
+ const senders = [...data.sends.entries()]
167
+ .filter(([name, set]) => set.has(notification) &&
168
+ resolveFacadeForClass(name, data.facadePartition) === scope.facadeClassName)
169
+ .map(([name]) => name);
170
+ const handlers = [];
171
+ const seenHandlers = new Set();
172
+ for (const reg of getCommandsForNotificationInScope(data, notification, scope)) {
173
+ const label = formatCommandHandler(data, reg.commandClass, scope);
174
+ if (!seenHandlers.has(label)) {
175
+ seenHandlers.add(label);
176
+ handlers.push(`Command: ${label}`);
177
+ }
178
+ }
179
+ for (const listener of data.listeners.values()) {
180
+ if (!scope.listeners.has(listener.className))
181
+ continue;
182
+ if (!listener.interests.has(notification))
183
+ continue;
184
+ const label = `${getListenerRoleLabel(listener.role)}: ${listener.className}`;
185
+ if (!seenHandlers.has(label)) {
186
+ seenHandlers.add(label);
187
+ handlers.push(label);
188
+ }
189
+ }
190
+ rows.push(`| \`${notification}\` | ${senders.map((s) => `\`${s}\``).join(', ') || '-'} | ${handlers.map((h) => `\`${h}\``).join(', ') || '-'} |`);
191
+ }
192
+ return rows.join('\n');
193
+ }
194
+ function buildFacadeOverviewTable(data) {
195
+ const rows = [
196
+ '| Facade | Startup | Commands | Proxies | Mediators | Observers |',
197
+ '| --- | --- | --- | --- | --- | --- |'
198
+ ];
199
+ for (const scope of data.facadePartition.facades.values()) {
200
+ rows.push(`| \`${scope.facadeClassName}\` | \`${scope.startupNotification}\` | ${scope.commands.size} | ${scope.proxies.size} | ${scope.mediators.size} | ${scope.listeners.size} |`);
201
+ }
202
+ const { unscoped } = data.facadePartition;
203
+ const unscopedTotal = unscoped.commands.size +
204
+ unscoped.proxies.size +
205
+ unscoped.mediators.size +
206
+ unscoped.listeners.size;
207
+ if (unscopedTotal > 0) {
208
+ rows.push(`| Unscoped | - | ${unscoped.commands.size} | ${unscoped.proxies.size} | ${unscoped.mediators.size} | ${unscoped.listeners.size} |`);
209
+ }
210
+ return rows.join('\n');
211
+ }
212
+ function buildComponentsTableForScope(scope) {
213
+ return `| Role | Classes |
214
+ | --- | --- |
215
+ | Command | ${[...scope.commands].map((c) => `\`${c}\``).join(', ') || '-'} |
216
+ | Proxy | ${[...scope.proxies].map((p) => `\`${p}\``).join(', ') || '-'} |
217
+ | Mediator | ${[...scope.mediators].map((m) => `\`${m}\``).join(', ') || '-'} |
218
+ | Observer | ${[...scope.listeners].map((l) => `\`${l}\``).join(', ') || '-'} |`;
219
+ }
220
+ function buildUnscopedSection(data) {
221
+ const { unscoped } = data.facadePartition;
222
+ const total = unscoped.commands.size +
223
+ unscoped.proxies.size +
224
+ unscoped.mediators.size +
225
+ unscoped.listeners.size;
226
+ if (total === 0)
227
+ return '';
228
+ return `## Unscoped Components
229
+
230
+ Components that could not be assigned to a Facade startup tree.
231
+
232
+ | Role | Classes |
233
+ | --- | --- |
234
+ | Command | ${[...unscoped.commands].map((c) => `\`${c}\``).join(', ') || '-'} |
235
+ | Proxy | ${[...unscoped.proxies].map((p) => `\`${p}\``).join(', ') || '-'} |
236
+ | Mediator | ${[...unscoped.mediators].map((m) => `\`${m}\``).join(', ') || '-'} |
237
+ | Observer | ${[...unscoped.listeners].map((l) => `\`${l}\``).join(', ') || '-'} |
238
+ `;
239
+ }
240
+ function buildFacadeSections(data) {
241
+ const sections = [];
242
+ for (const scope of data.facadePartition.facades.values()) {
243
+ const slug = facadeFileSlug(scope.facadeClassName);
244
+ sections.push(`## ${scope.facadeClassName}
245
+
246
+ | Diagram | File |
247
+ | --- | --- |
248
+ | Startup | [startup.mmd](${slug}/startup.mmd) |
249
+ | Runtime | [runtime.mmd](${slug}/runtime.mmd) |
250
+
251
+ ### Notification Map
252
+
253
+ ${buildNotificationTableForScope(data, scope)}
254
+
255
+ ### Components
256
+
257
+ ${buildComponentsTableForScope(scope)}
258
+ `);
259
+ }
260
+ return sections.join('\n');
261
+ }
262
+ function buildUnscopedFilesSection(data) {
263
+ const { unscoped } = data.facadePartition;
264
+ const hasStartup = unscoped.commands.size > 0 || unscoped.proxies.size > 0 || unscoped.mediators.size > 0;
265
+ const hasRuntime = unscoped.listeners.size > 0 || unscoped.commands.size > 0;
266
+ if (!hasStartup && !hasRuntime)
267
+ return '';
268
+ const rows = ['| Diagram | File |', '| --- | --- |'];
269
+ if (hasStartup)
270
+ rows.push('| Startup | [startup.mmd](unscoped/startup.mmd) |');
271
+ if (hasRuntime)
272
+ rows.push('| Runtime | [runtime.mmd](unscoped/runtime.mmd) |');
273
+ return `## Unscoped
274
+
275
+ ${rows.join('\n')}
276
+
277
+ ${buildUnscopedSection(data)}`;
278
+ }
279
+ export function buildMarkdown(data) {
280
+ const generatedAt = new Date().toISOString();
281
+ return `# PureMVC Architecture Graph
282
+
283
+ > Auto-generated by \`npx puremvc-ts-graph\` at ${generatedAt}
284
+
285
+ ## Facades Overview
286
+
287
+ ${buildFacadeOverviewTable(data)}
288
+
289
+ ${buildFacadeSections(data)}
290
+ ${buildUnscopedFilesSection(data)}`;
291
+ }
292
+ export function buildStats(data) {
293
+ return {
294
+ notifications: data.constants.propertyCount,
295
+ commandRegistrations: data.commandRegistrations.length,
296
+ proxies: data.proxies.size,
297
+ mediators: data.mediators.size,
298
+ listeners: data.listeners.size,
299
+ facades: data.facadePartition.facades.size
300
+ };
301
+ }
@@ -0,0 +1,31 @@
1
+ export interface FacadeConfig {
2
+ className: string;
3
+ startupNotification: string;
4
+ }
5
+ export type ImageFormat = 'png' | 'svg';
6
+ export interface ImageConfig {
7
+ enabled: boolean;
8
+ format: ImageFormat;
9
+ }
10
+ export interface PureMvcGraphConfig {
11
+ srcDir: string;
12
+ docsDir: string;
13
+ facadeClassName: string;
14
+ startupNotification: string;
15
+ facades: FacadeConfig[];
16
+ images: ImageConfig;
17
+ constants: {
18
+ glob: string[];
19
+ exportNames: string[];
20
+ };
21
+ }
22
+ export interface CliOptions {
23
+ projectRoot: string;
24
+ configPath?: string;
25
+ imageFormat?: ImageFormat;
26
+ }
27
+ export declare function loadConfig(root: string, configPath?: string): PureMvcGraphConfig;
28
+ export declare function isHelpRequested(argv: string[]): boolean;
29
+ export declare function printCliHelp(): void;
30
+ export declare function parseCliArgs(argv: string[]): CliOptions;
31
+ export declare function resolveImageGeneration(argv: string[], config: PureMvcGraphConfig): ImageFormat[] | undefined;