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 +21 -0
- package/README.md +236 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +115 -0
- package/dist/lib/constants-resolver.d.ts +18 -0
- package/dist/lib/constants-resolver.js +163 -0
- package/dist/lib/facade-resolver.d.ts +29 -0
- package/dist/lib/facade-resolver.js +201 -0
- package/dist/lib/factory-resolver.d.ts +4 -0
- package/dist/lib/factory-resolver.js +61 -0
- package/dist/lib/graph-builder.d.ts +23 -0
- package/dist/lib/graph-builder.js +301 -0
- package/dist/lib/graph-config.d.ts +31 -0
- package/dist/lib/graph-config.js +124 -0
- package/dist/lib/graph-parser.d.ts +35 -0
- package/dist/lib/graph-parser.js +226 -0
- package/dist/lib/image-generator.d.ts +8 -0
- package/dist/lib/image-generator.js +56 -0
- package/package.json +49 -0
- package/puremvc-graph.config.example.json +25 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
function normalizeConstantsGlob(glob) {
|
|
4
|
+
if (!glob)
|
|
5
|
+
return [...DEFAULT_CONFIG.constants.glob];
|
|
6
|
+
return Array.isArray(glob) ? glob : [glob];
|
|
7
|
+
}
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
srcDir: 'src',
|
|
10
|
+
docsDir: 'docs',
|
|
11
|
+
facadeClassName: 'AppFacade',
|
|
12
|
+
startupNotification: 'startup',
|
|
13
|
+
facades: [],
|
|
14
|
+
images: {
|
|
15
|
+
enabled: false,
|
|
16
|
+
format: 'png'
|
|
17
|
+
},
|
|
18
|
+
constants: {
|
|
19
|
+
glob: ['**/*Constants.ts'],
|
|
20
|
+
exportNames: []
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function normalizeFacades(raw) {
|
|
24
|
+
if (raw.facades && raw.facades.length > 0) {
|
|
25
|
+
return raw.facades.map((facade) => ({
|
|
26
|
+
className: facade.className,
|
|
27
|
+
startupNotification: facade.startupNotification
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
export function loadConfig(root, configPath) {
|
|
33
|
+
const resolvedPath = configPath
|
|
34
|
+
? isAbsolute(configPath)
|
|
35
|
+
? configPath
|
|
36
|
+
: join(root, configPath)
|
|
37
|
+
: join(root, 'puremvc-graph.config.json');
|
|
38
|
+
if (!existsSync(resolvedPath)) {
|
|
39
|
+
return { ...DEFAULT_CONFIG };
|
|
40
|
+
}
|
|
41
|
+
const raw = JSON.parse(readFileSync(resolvedPath, 'utf8'));
|
|
42
|
+
return {
|
|
43
|
+
srcDir: raw.srcDir ?? DEFAULT_CONFIG.srcDir,
|
|
44
|
+
docsDir: raw.docsDir ?? DEFAULT_CONFIG.docsDir,
|
|
45
|
+
facadeClassName: raw.facadeClassName ?? DEFAULT_CONFIG.facadeClassName,
|
|
46
|
+
startupNotification: raw.startupNotification ?? DEFAULT_CONFIG.startupNotification,
|
|
47
|
+
facades: normalizeFacades(raw),
|
|
48
|
+
images: {
|
|
49
|
+
enabled: raw.images?.enabled ?? DEFAULT_CONFIG.images.enabled,
|
|
50
|
+
format: raw.images?.format ?? DEFAULT_CONFIG.images.format
|
|
51
|
+
},
|
|
52
|
+
constants: {
|
|
53
|
+
glob: normalizeConstantsGlob(raw.constants?.glob),
|
|
54
|
+
exportNames: raw.constants?.exportNames ?? DEFAULT_CONFIG.constants.exportNames
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const IMAGE_FORMATS = ['png', 'svg'];
|
|
59
|
+
function isImageFormat(value) {
|
|
60
|
+
return IMAGE_FORMATS.includes(value);
|
|
61
|
+
}
|
|
62
|
+
export function isHelpRequested(argv) {
|
|
63
|
+
return argv.includes('--help') || argv.includes('-h');
|
|
64
|
+
}
|
|
65
|
+
export function printCliHelp() {
|
|
66
|
+
console.log(`Usage: puremvc-graph [options]
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
--config <path> Config file path (default: puremvc-graph.config.json)
|
|
70
|
+
--cwd <path> Project root directory (default: current directory)
|
|
71
|
+
--format <fmt> Image format: png, svg, or png,svg (generates selected formats)
|
|
72
|
+
--png Generate PNG images (shortcut for --format png)
|
|
73
|
+
--svg Generate SVG images (shortcut for --format svg)
|
|
74
|
+
--images Generate images using config images.format
|
|
75
|
+
-h, --help Show this help
|
|
76
|
+
|
|
77
|
+
Config (puremvc-graph.config.json):
|
|
78
|
+
images.enabled Generate images on every run (default: false)
|
|
79
|
+
images.format Default format when using --images or images.enabled (default: png)
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
function parseImageFormats(argv, configFormat) {
|
|
83
|
+
const formats = new Set();
|
|
84
|
+
const formatIndex = argv.indexOf('--format');
|
|
85
|
+
if (formatIndex !== -1) {
|
|
86
|
+
const value = argv[formatIndex + 1];
|
|
87
|
+
if (!value || value.startsWith('-')) {
|
|
88
|
+
throw new Error('--format requires a value: png, svg, or png,svg');
|
|
89
|
+
}
|
|
90
|
+
for (const part of value.split(',')) {
|
|
91
|
+
const normalized = part.trim().toLowerCase();
|
|
92
|
+
if (!normalized)
|
|
93
|
+
continue;
|
|
94
|
+
if (!isImageFormat(normalized)) {
|
|
95
|
+
throw new Error(`Invalid image format "${part}". Use png, svg, or png,svg.`);
|
|
96
|
+
}
|
|
97
|
+
formats.add(normalized);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (argv.includes('--png'))
|
|
101
|
+
formats.add('png');
|
|
102
|
+
if (argv.includes('--svg'))
|
|
103
|
+
formats.add('svg');
|
|
104
|
+
if (argv.includes('--images') && formats.size === 0)
|
|
105
|
+
formats.add(configFormat);
|
|
106
|
+
return [...formats];
|
|
107
|
+
}
|
|
108
|
+
export function parseCliArgs(argv) {
|
|
109
|
+
const cwdIndex = argv.indexOf('--cwd');
|
|
110
|
+
const projectRoot = cwdIndex !== -1 && argv[cwdIndex + 1]
|
|
111
|
+
? resolve(argv[cwdIndex + 1])
|
|
112
|
+
: process.cwd();
|
|
113
|
+
const configIndex = argv.indexOf('--config');
|
|
114
|
+
const configPath = configIndex !== -1 && argv[configIndex + 1] ? argv[configIndex + 1] : undefined;
|
|
115
|
+
return { projectRoot, configPath };
|
|
116
|
+
}
|
|
117
|
+
export function resolveImageGeneration(argv, config) {
|
|
118
|
+
const fromCli = parseImageFormats(argv, config.images.format);
|
|
119
|
+
if (fromCli.length > 0)
|
|
120
|
+
return fromCli;
|
|
121
|
+
if (config.images.enabled)
|
|
122
|
+
return [config.images.format];
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SourceFile } from 'ts-morph';
|
|
2
|
+
import { ConstantsStore } from './constants-resolver.js';
|
|
3
|
+
import { type FacadePartition } from './facade-resolver.js';
|
|
4
|
+
import type { PureMvcGraphConfig } from './graph-config.js';
|
|
5
|
+
export type ClassRole = 'command' | 'mediator' | 'proxy' | 'facade' | 'other';
|
|
6
|
+
export interface CommandRegistration {
|
|
7
|
+
notification: string;
|
|
8
|
+
commandClass: string;
|
|
9
|
+
registrar?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ListenerInfo {
|
|
12
|
+
className: string;
|
|
13
|
+
role: ClassRole;
|
|
14
|
+
interests: Set<string>;
|
|
15
|
+
}
|
|
16
|
+
export interface GraphData {
|
|
17
|
+
constants: ConstantsStore;
|
|
18
|
+
commandRegistrations: CommandRegistration[];
|
|
19
|
+
proxies: Set<string>;
|
|
20
|
+
mediators: Set<string>;
|
|
21
|
+
proxyRegistrars: Map<string, string>;
|
|
22
|
+
mediatorRegistrars: Map<string, string>;
|
|
23
|
+
sends: Map<string, Set<string>>;
|
|
24
|
+
listeners: Map<string, ListenerInfo>;
|
|
25
|
+
usesProxy: Map<string, Set<string>>;
|
|
26
|
+
macroSubCommands: Map<string, string[]>;
|
|
27
|
+
classRoles: Map<string, ClassRole>;
|
|
28
|
+
facadePartition: FacadePartition;
|
|
29
|
+
config: PureMvcGraphConfig;
|
|
30
|
+
}
|
|
31
|
+
export declare function collectGraphData(sourceFiles: SourceFile[], config: PureMvcGraphConfig): GraphData;
|
|
32
|
+
export declare function getCommandsForNotification(data: GraphData, notification: string): CommandRegistration[];
|
|
33
|
+
export declare function getRegisteredCommandClasses(data: GraphData): string[];
|
|
34
|
+
export declare function getListenerRoleLabel(role: ClassRole): string;
|
|
35
|
+
export declare function getAllNotifications(data: GraphData): string[];
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { SyntaxKind } from 'ts-morph';
|
|
2
|
+
import { ConstantsStore } from './constants-resolver.js';
|
|
3
|
+
import { buildFacadePartition } from './facade-resolver.js';
|
|
4
|
+
import { collectClassNames, resolveFactoryClass, resolveProxyReference } from './factory-resolver.js';
|
|
5
|
+
function getEnclosingClassName(node) {
|
|
6
|
+
const cls = node.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
|
|
7
|
+
return cls?.getName() ?? null;
|
|
8
|
+
}
|
|
9
|
+
function inferClassRole(classDecl) {
|
|
10
|
+
const extendsExpr = classDecl.getExtends()?.getText() ?? '';
|
|
11
|
+
if (/Mediator/.test(extendsExpr))
|
|
12
|
+
return 'mediator';
|
|
13
|
+
if (/Proxy/.test(extendsExpr))
|
|
14
|
+
return 'proxy';
|
|
15
|
+
if (/SimpleCommand|MacroCommand/.test(extendsExpr))
|
|
16
|
+
return 'command';
|
|
17
|
+
if (/Facade/.test(extendsExpr))
|
|
18
|
+
return 'facade';
|
|
19
|
+
return 'other';
|
|
20
|
+
}
|
|
21
|
+
function buildClassRoles(sourceFiles) {
|
|
22
|
+
const roles = new Map();
|
|
23
|
+
for (const file of sourceFiles) {
|
|
24
|
+
for (const cls of file.getClasses()) {
|
|
25
|
+
const name = cls.getName();
|
|
26
|
+
if (name)
|
|
27
|
+
roles.set(name, inferClassRole(cls));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return roles;
|
|
31
|
+
}
|
|
32
|
+
function parseRegisterCommands(sourceFiles, constants, classNames, registrations) {
|
|
33
|
+
for (const file of sourceFiles) {
|
|
34
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
35
|
+
if (!call.getExpression().getText().endsWith('registerCommand'))
|
|
36
|
+
continue;
|
|
37
|
+
const args = call.getArguments();
|
|
38
|
+
if (args.length < 2)
|
|
39
|
+
continue;
|
|
40
|
+
const notification = constants.resolve(args[0]);
|
|
41
|
+
const commandClass = resolveFactoryClass(args[1], classNames);
|
|
42
|
+
const registrar = getEnclosingClassName(call) ?? undefined;
|
|
43
|
+
if (notification && commandClass) {
|
|
44
|
+
registrations.push({ notification, commandClass, registrar });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseMacroSubCommands(sourceFiles, classNames, macroSubCommands) {
|
|
50
|
+
for (const file of sourceFiles) {
|
|
51
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
52
|
+
if (call.getExpression().getText() !== 'this.addSubCommand')
|
|
53
|
+
continue;
|
|
54
|
+
const className = getEnclosingClassName(call);
|
|
55
|
+
const subCommand = resolveFactoryClass(call.getArguments()[0], classNames);
|
|
56
|
+
if (!className || !subCommand)
|
|
57
|
+
continue;
|
|
58
|
+
const existing = macroSubCommands.get(className) ?? [];
|
|
59
|
+
existing.push(subCommand);
|
|
60
|
+
macroSubCommands.set(className, existing);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function parseRegistrations(sourceFiles, classNames, proxies, mediators, proxyRegistrars, mediatorRegistrars) {
|
|
65
|
+
for (const file of sourceFiles) {
|
|
66
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
67
|
+
const expr = call.getExpression().getText();
|
|
68
|
+
const arg = call.getArguments()[0];
|
|
69
|
+
const registrar = getEnclosingClassName(call);
|
|
70
|
+
if (expr.endsWith('registerProxy')) {
|
|
71
|
+
const proxyClass = resolveFactoryClass(arg, classNames);
|
|
72
|
+
if (proxyClass) {
|
|
73
|
+
proxies.add(proxyClass);
|
|
74
|
+
if (registrar)
|
|
75
|
+
proxyRegistrars.set(proxyClass, registrar);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (expr.endsWith('registerMediator')) {
|
|
79
|
+
const mediatorClass = resolveFactoryClass(arg, classNames);
|
|
80
|
+
if (mediatorClass) {
|
|
81
|
+
mediators.add(mediatorClass);
|
|
82
|
+
if (registrar)
|
|
83
|
+
mediatorRegistrars.set(mediatorClass, registrar);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function parseSendNotifications(sourceFiles, constants, sends) {
|
|
90
|
+
for (const file of sourceFiles) {
|
|
91
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
92
|
+
if (!call.getExpression().getText().endsWith('sendNotification'))
|
|
93
|
+
continue;
|
|
94
|
+
const className = getEnclosingClassName(call);
|
|
95
|
+
const notification = constants.resolve(call.getArguments()[0]);
|
|
96
|
+
if (!className || !notification)
|
|
97
|
+
continue;
|
|
98
|
+
const existing = sends.get(className) ?? new Set();
|
|
99
|
+
existing.add(notification);
|
|
100
|
+
sends.set(className, existing);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function parseListeners(sourceFiles, constants, classRoles, listeners) {
|
|
105
|
+
for (const file of sourceFiles) {
|
|
106
|
+
for (const method of file.getDescendantsOfKind(SyntaxKind.MethodDeclaration)) {
|
|
107
|
+
if (method.getName() !== 'listNotificationInterests')
|
|
108
|
+
continue;
|
|
109
|
+
const classDecl = method.getParentIfKind(SyntaxKind.ClassDeclaration);
|
|
110
|
+
const className = classDecl?.getName();
|
|
111
|
+
if (!className)
|
|
112
|
+
continue;
|
|
113
|
+
const interests = new Set();
|
|
114
|
+
for (const ret of method.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
115
|
+
const expr = ret.getExpression();
|
|
116
|
+
if (!expr)
|
|
117
|
+
continue;
|
|
118
|
+
for (const name of constants.resolveArray(expr, classDecl)) {
|
|
119
|
+
interests.add(name);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (interests.size > 0) {
|
|
123
|
+
listeners.set(className, {
|
|
124
|
+
className,
|
|
125
|
+
role: classRoles.get(className) ?? 'other',
|
|
126
|
+
interests
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function parseUsesProxy(sourceFiles, usesProxy) {
|
|
133
|
+
for (const file of sourceFiles) {
|
|
134
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
135
|
+
if (!call.getExpression().getText().endsWith('retrieveProxy'))
|
|
136
|
+
continue;
|
|
137
|
+
const className = getEnclosingClassName(call);
|
|
138
|
+
const proxyName = resolveProxyReference(call.getArguments()[0]);
|
|
139
|
+
if (!className || !proxyName)
|
|
140
|
+
continue;
|
|
141
|
+
const existing = usesProxy.get(className) ?? new Set();
|
|
142
|
+
existing.add(proxyName);
|
|
143
|
+
usesProxy.set(className, existing);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function collectGraphData(sourceFiles, config) {
|
|
148
|
+
const constants = new ConstantsStore();
|
|
149
|
+
constants.parseFromSourceFiles(sourceFiles, config);
|
|
150
|
+
const classNames = collectClassNames(sourceFiles);
|
|
151
|
+
const classRoles = buildClassRoles(sourceFiles);
|
|
152
|
+
const commandRegistrations = [];
|
|
153
|
+
const proxies = new Set();
|
|
154
|
+
const mediators = new Set();
|
|
155
|
+
const proxyRegistrars = new Map();
|
|
156
|
+
const mediatorRegistrars = new Map();
|
|
157
|
+
const sends = new Map();
|
|
158
|
+
const listeners = new Map();
|
|
159
|
+
const usesProxy = new Map();
|
|
160
|
+
const macroSubCommands = new Map();
|
|
161
|
+
parseRegisterCommands(sourceFiles, constants, classNames, commandRegistrations);
|
|
162
|
+
parseMacroSubCommands(sourceFiles, classNames, macroSubCommands);
|
|
163
|
+
parseRegistrations(sourceFiles, classNames, proxies, mediators, proxyRegistrars, mediatorRegistrars);
|
|
164
|
+
parseSendNotifications(sourceFiles, constants, sends);
|
|
165
|
+
parseListeners(sourceFiles, constants, classRoles, listeners);
|
|
166
|
+
parseUsesProxy(sourceFiles, usesProxy);
|
|
167
|
+
const partialData = {
|
|
168
|
+
constants,
|
|
169
|
+
commandRegistrations,
|
|
170
|
+
proxies,
|
|
171
|
+
mediators,
|
|
172
|
+
proxyRegistrars,
|
|
173
|
+
mediatorRegistrars,
|
|
174
|
+
sends,
|
|
175
|
+
listeners,
|
|
176
|
+
usesProxy,
|
|
177
|
+
macroSubCommands,
|
|
178
|
+
classRoles,
|
|
179
|
+
facadePartition: {
|
|
180
|
+
facades: new Map(),
|
|
181
|
+
unscoped: {
|
|
182
|
+
commands: new Set(),
|
|
183
|
+
proxies: new Set(),
|
|
184
|
+
mediators: new Set(),
|
|
185
|
+
listeners: new Set()
|
|
186
|
+
},
|
|
187
|
+
classToFacade: new Map()
|
|
188
|
+
},
|
|
189
|
+
config
|
|
190
|
+
};
|
|
191
|
+
partialData.facadePartition = buildFacadePartition(partialData);
|
|
192
|
+
return partialData;
|
|
193
|
+
}
|
|
194
|
+
export function getCommandsForNotification(data, notification) {
|
|
195
|
+
return data.commandRegistrations.filter((r) => r.notification === notification);
|
|
196
|
+
}
|
|
197
|
+
export function getRegisteredCommandClasses(data) {
|
|
198
|
+
return [...new Set(data.commandRegistrations.map((r) => r.commandClass))];
|
|
199
|
+
}
|
|
200
|
+
export function getListenerRoleLabel(role) {
|
|
201
|
+
switch (role) {
|
|
202
|
+
case 'mediator':
|
|
203
|
+
return 'Mediator';
|
|
204
|
+
case 'command':
|
|
205
|
+
return 'Command (observer)';
|
|
206
|
+
case 'proxy':
|
|
207
|
+
return 'Proxy (observer)';
|
|
208
|
+
default:
|
|
209
|
+
return 'Observer';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export function getAllNotifications(data) {
|
|
213
|
+
const all = new Set();
|
|
214
|
+
for (const reg of data.commandRegistrations) {
|
|
215
|
+
all.add(reg.notification);
|
|
216
|
+
}
|
|
217
|
+
for (const interests of [...data.sends.values()].flatMap((s) => [...s])) {
|
|
218
|
+
all.add(interests);
|
|
219
|
+
}
|
|
220
|
+
for (const listener of data.listeners.values()) {
|
|
221
|
+
for (const interest of listener.interests) {
|
|
222
|
+
all.add(interest);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return [...all].sort();
|
|
226
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ImageFormat } from './graph-config.js';
|
|
2
|
+
export interface ImageGenerationResult {
|
|
3
|
+
generated: string[];
|
|
4
|
+
failed: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function mmdcAvailable(): boolean;
|
|
7
|
+
export declare function getMmdcInstallHint(): string;
|
|
8
|
+
export declare function generateImages(mmdPaths: string[], format: ImageFormat): ImageGenerationResult;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const require = createRequire(fileURLToPath(import.meta.url));
|
|
6
|
+
function resolveMmdcPath() {
|
|
7
|
+
try {
|
|
8
|
+
const entryPath = require.resolve('@mermaid-js/mermaid-cli');
|
|
9
|
+
return join(dirname(entryPath), 'cli.js');
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function runMmdc(inputPath, outputPath, format) {
|
|
16
|
+
const mmdcPath = resolveMmdcPath();
|
|
17
|
+
if (!mmdcPath) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const result = spawnSync(process.execPath, [mmdcPath, '-i', inputPath, '-o', outputPath, '-e', format], { stdio: 'pipe', encoding: 'utf8' });
|
|
21
|
+
if (result.status !== 0) {
|
|
22
|
+
const stderr = result.stderr?.trim();
|
|
23
|
+
if (stderr) {
|
|
24
|
+
console.error(stderr);
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
export function mmdcAvailable() {
|
|
31
|
+
return resolveMmdcPath() !== null;
|
|
32
|
+
}
|
|
33
|
+
export function getMmdcInstallHint() {
|
|
34
|
+
return 'Install @mermaid-js/mermaid-cli: npm install -D @mermaid-js/mermaid-cli';
|
|
35
|
+
}
|
|
36
|
+
export function generateImages(mmdPaths, format) {
|
|
37
|
+
const generated = [];
|
|
38
|
+
const failed = [];
|
|
39
|
+
if (!mmdcAvailable()) {
|
|
40
|
+
console.error(getMmdcInstallHint());
|
|
41
|
+
return { generated, failed: mmdPaths.map((p) => replaceExtension(p, format)) };
|
|
42
|
+
}
|
|
43
|
+
for (const mmdPath of mmdPaths) {
|
|
44
|
+
const outputPath = resolve(replaceExtension(mmdPath, format));
|
|
45
|
+
if (runMmdc(resolve(mmdPath), outputPath, format)) {
|
|
46
|
+
generated.push(outputPath);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
failed.push(outputPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { generated, failed };
|
|
53
|
+
}
|
|
54
|
+
function replaceExtension(filePath, format) {
|
|
55
|
+
return filePath.replace(/\.mmd$/i, `.${format}`);
|
|
56
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "puremvc-typescript-graph",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "CLI tool that scans PureMVC TypeScript code and auto-generates Mermaid diagrams of notification flows and startup wiring (Command, Proxy, Mediator).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"puremvc-graph": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"puremvc-graph.config.example.json"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"puremvc",
|
|
19
|
+
"typescript",
|
|
20
|
+
"mermaid",
|
|
21
|
+
"diagram",
|
|
22
|
+
"architecture",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "vizyman <viz.anything@gmail.com>",
|
|
27
|
+
"homepage": "https://github.com/puremvc/puremvc-typescript-graph#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/puremvc/puremvc-typescript-graph/issues"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/puremvc/puremvc-typescript-graph.git"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"ts-morph": "^25.0.0"
|
|
40
|
+
},
|
|
41
|
+
"optionalDependencies": {
|
|
42
|
+
"@mermaid-js/mermaid-cli": "^11.4.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@mermaid-js/mermaid-cli": "^11.4.0",
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"typescript": "^5.7.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"srcDir": "src",
|
|
3
|
+
"docsDir": "docs",
|
|
4
|
+
"facades": [
|
|
5
|
+
{
|
|
6
|
+
"className": "AppFacade",
|
|
7
|
+
"startupNotification": "startup"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"className": "TodoModuleFacade",
|
|
11
|
+
"startupNotification": "moduleStartup"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"images": {
|
|
15
|
+
"enabled": false,
|
|
16
|
+
"format": "png"
|
|
17
|
+
},
|
|
18
|
+
"constants": {
|
|
19
|
+
"glob": [
|
|
20
|
+
"**/*Constants.ts",
|
|
21
|
+
"**/notifications.ts"
|
|
22
|
+
],
|
|
23
|
+
"exportNames": ["AppConstants", "TodoConstants"]
|
|
24
|
+
}
|
|
25
|
+
}
|