@uniformdev/transformer 1.0.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/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +2399 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +320 -0
- package/dist/index.js +1116 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,2399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/commands/propagate-root-component-property.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/core/services/file-system.service.ts
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { glob } from "glob";
|
|
13
|
+
import * as YAML from "yaml";
|
|
14
|
+
|
|
15
|
+
// src/core/errors.ts
|
|
16
|
+
var TransformError = class extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "TransformError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var ComponentNotFoundError = class extends TransformError {
|
|
23
|
+
constructor(componentType, path2) {
|
|
24
|
+
const pathInfo = path2 ? ` (searched: ${path2})` : "";
|
|
25
|
+
super(`Component not found: ${componentType}${pathInfo}`);
|
|
26
|
+
this.name = "ComponentNotFoundError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var PropertyNotFoundError = class extends TransformError {
|
|
30
|
+
constructor(propertyName, componentType) {
|
|
31
|
+
super(`Property "${propertyName}" not found on component "${componentType}"`);
|
|
32
|
+
this.name = "PropertyNotFoundError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var InvalidYamlError = class extends TransformError {
|
|
36
|
+
constructor(filePath, details) {
|
|
37
|
+
const detailsInfo = details ? `: ${details}` : "";
|
|
38
|
+
super(`Invalid YAML file: ${filePath}${detailsInfo}`);
|
|
39
|
+
this.name = "InvalidYamlError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var FileNotFoundError = class extends TransformError {
|
|
43
|
+
constructor(filePath) {
|
|
44
|
+
super(`File not found: ${filePath}`);
|
|
45
|
+
this.name = "FileNotFoundError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var DuplicateIdError = class extends TransformError {
|
|
49
|
+
constructor(id, arrayProperty, filePath) {
|
|
50
|
+
super(`Duplicate id "${id}" in array "${arrayProperty}" of file: ${filePath}`);
|
|
51
|
+
this.name = "DuplicateIdError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var ComponentAlreadyExistsError = class extends TransformError {
|
|
55
|
+
constructor(componentType, path2) {
|
|
56
|
+
const pathInfo = path2 ? ` (searched: ${path2})` : "";
|
|
57
|
+
super(`Component type "${componentType}" already exists${pathInfo}`);
|
|
58
|
+
this.name = "ComponentAlreadyExistsError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var SlotNotFoundError = class extends TransformError {
|
|
62
|
+
constructor(slotId, componentType) {
|
|
63
|
+
super(`Slot "${slotId}" does not exist on component "${componentType}"`);
|
|
64
|
+
this.name = "SlotNotFoundError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var SlotAlreadyExistsError = class extends TransformError {
|
|
68
|
+
constructor(slotId, componentType) {
|
|
69
|
+
super(`Slot "${slotId}" already exists on component "${componentType}"`);
|
|
70
|
+
this.name = "SlotAlreadyExistsError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/core/services/file-system.service.ts
|
|
75
|
+
var FileSystemService = class {
|
|
76
|
+
isJsonFile(filePath) {
|
|
77
|
+
return filePath.toLowerCase().endsWith(".json");
|
|
78
|
+
}
|
|
79
|
+
async readFile(filePath) {
|
|
80
|
+
if (!fs.existsSync(filePath)) {
|
|
81
|
+
throw new FileNotFoundError(filePath);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
85
|
+
if (this.isJsonFile(filePath)) {
|
|
86
|
+
return JSON.parse(content);
|
|
87
|
+
}
|
|
88
|
+
return YAML.parse(content);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof FileNotFoundError) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
throw new InvalidYamlError(filePath, message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async writeFile(filePath, data) {
|
|
98
|
+
const dir = path.dirname(filePath);
|
|
99
|
+
if (!fs.existsSync(dir)) {
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
let content;
|
|
103
|
+
if (this.isJsonFile(filePath)) {
|
|
104
|
+
content = JSON.stringify(data, null, 2);
|
|
105
|
+
} else {
|
|
106
|
+
content = YAML.stringify(data, {
|
|
107
|
+
lineWidth: 0,
|
|
108
|
+
defaultKeyType: "PLAIN",
|
|
109
|
+
defaultStringType: "QUOTE_DOUBLE"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
113
|
+
}
|
|
114
|
+
async readYamlFile(filePath) {
|
|
115
|
+
return this.readFile(filePath);
|
|
116
|
+
}
|
|
117
|
+
async writeYamlFile(filePath, data) {
|
|
118
|
+
return this.writeFile(filePath, data);
|
|
119
|
+
}
|
|
120
|
+
async findFiles(directory, pattern) {
|
|
121
|
+
const fullPattern = path.join(directory, pattern).replace(/\\/g, "/");
|
|
122
|
+
return glob(fullPattern);
|
|
123
|
+
}
|
|
124
|
+
async fileExists(filePath) {
|
|
125
|
+
return fs.existsSync(filePath);
|
|
126
|
+
}
|
|
127
|
+
resolvePath(...segments) {
|
|
128
|
+
return path.resolve(...segments);
|
|
129
|
+
}
|
|
130
|
+
joinPath(...segments) {
|
|
131
|
+
return path.join(...segments);
|
|
132
|
+
}
|
|
133
|
+
getBasename(filePath, ext) {
|
|
134
|
+
return path.basename(filePath, ext);
|
|
135
|
+
}
|
|
136
|
+
async renameFile(oldPath, newPath) {
|
|
137
|
+
fs.renameSync(oldPath, newPath);
|
|
138
|
+
}
|
|
139
|
+
deleteFile(filePath) {
|
|
140
|
+
fs.unlinkSync(filePath);
|
|
141
|
+
}
|
|
142
|
+
getExtension(filePath) {
|
|
143
|
+
return path.extname(filePath);
|
|
144
|
+
}
|
|
145
|
+
getDirname(filePath) {
|
|
146
|
+
return path.dirname(filePath);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/core/services/component.service.ts
|
|
151
|
+
var ComponentService = class {
|
|
152
|
+
constructor(fileSystem) {
|
|
153
|
+
this.fileSystem = fileSystem;
|
|
154
|
+
}
|
|
155
|
+
compareIds(id1, id2, strict) {
|
|
156
|
+
if (strict) {
|
|
157
|
+
return id1 === id2;
|
|
158
|
+
}
|
|
159
|
+
return id1.toLowerCase() === id2.toLowerCase();
|
|
160
|
+
}
|
|
161
|
+
async loadComponent(componentsDir, componentType, options = {}) {
|
|
162
|
+
const { strict = false } = options;
|
|
163
|
+
const jsonPath = this.fileSystem.joinPath(componentsDir, `${componentType}.json`);
|
|
164
|
+
const yamlPath = this.fileSystem.joinPath(componentsDir, `${componentType}.yaml`);
|
|
165
|
+
const ymlPath = this.fileSystem.joinPath(componentsDir, `${componentType}.yml`);
|
|
166
|
+
if (await this.fileSystem.fileExists(jsonPath)) {
|
|
167
|
+
const component = await this.fileSystem.readFile(jsonPath);
|
|
168
|
+
return { component, filePath: jsonPath };
|
|
169
|
+
}
|
|
170
|
+
if (await this.fileSystem.fileExists(yamlPath)) {
|
|
171
|
+
const component = await this.fileSystem.readFile(yamlPath);
|
|
172
|
+
return { component, filePath: yamlPath };
|
|
173
|
+
}
|
|
174
|
+
if (await this.fileSystem.fileExists(ymlPath)) {
|
|
175
|
+
const component = await this.fileSystem.readFile(ymlPath);
|
|
176
|
+
return { component, filePath: ymlPath };
|
|
177
|
+
}
|
|
178
|
+
if (!strict) {
|
|
179
|
+
const files = await this.fileSystem.findFiles(componentsDir, "*.{json,yaml,yml}");
|
|
180
|
+
for (const filePath of files) {
|
|
181
|
+
const basename2 = this.fileSystem.getBasename(filePath);
|
|
182
|
+
const nameWithoutExt = basename2.replace(/\.(json|yaml|yml)$/i, "");
|
|
183
|
+
if (nameWithoutExt.toLowerCase() === componentType.toLowerCase()) {
|
|
184
|
+
const component = await this.fileSystem.readFile(filePath);
|
|
185
|
+
return { component, filePath };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
throw new ComponentNotFoundError(componentType, componentsDir);
|
|
190
|
+
}
|
|
191
|
+
async saveComponent(filePath, component) {
|
|
192
|
+
await this.fileSystem.writeFile(filePath, component);
|
|
193
|
+
}
|
|
194
|
+
findParameter(component, parameterName, options = {}) {
|
|
195
|
+
const { strict = false } = options;
|
|
196
|
+
return component.parameters?.find((p) => this.compareIds(p.id, parameterName, strict));
|
|
197
|
+
}
|
|
198
|
+
isGroupParameter(parameter) {
|
|
199
|
+
return parameter.type === "group";
|
|
200
|
+
}
|
|
201
|
+
resolveGroupParameters(component, groupParameter, options = {}) {
|
|
202
|
+
if (!this.isGroupParameter(groupParameter)) {
|
|
203
|
+
return [groupParameter];
|
|
204
|
+
}
|
|
205
|
+
const childrenIds = groupParameter.typeConfig?.childrenParams ?? [];
|
|
206
|
+
const resolved = [];
|
|
207
|
+
for (const childId of childrenIds) {
|
|
208
|
+
const childParam = this.findParameter(component, childId, options);
|
|
209
|
+
if (childParam) {
|
|
210
|
+
resolved.push(childParam);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return resolved;
|
|
214
|
+
}
|
|
215
|
+
resolveProperties(component, propertyNames, options = {}) {
|
|
216
|
+
const parameters = [];
|
|
217
|
+
const notFound = [];
|
|
218
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const name of propertyNames) {
|
|
220
|
+
const param = this.findParameter(component, name, options);
|
|
221
|
+
if (!param) {
|
|
222
|
+
notFound.push(name);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (this.isGroupParameter(param)) {
|
|
226
|
+
const groupParams = this.resolveGroupParameters(component, param, options);
|
|
227
|
+
for (const gp of groupParams) {
|
|
228
|
+
if (!seenIds.has(gp.id)) {
|
|
229
|
+
seenIds.add(gp.id);
|
|
230
|
+
parameters.push(gp);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
if (!seenIds.has(param.id)) {
|
|
235
|
+
seenIds.add(param.id);
|
|
236
|
+
parameters.push(param);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { parameters, notFound };
|
|
241
|
+
}
|
|
242
|
+
addParameterToComponent(component, parameter, options = {}) {
|
|
243
|
+
const { strict = false } = options;
|
|
244
|
+
if (!component.parameters) {
|
|
245
|
+
component.parameters = [];
|
|
246
|
+
}
|
|
247
|
+
const existingIndex = component.parameters.findIndex(
|
|
248
|
+
(p) => this.compareIds(p.id, parameter.id, strict)
|
|
249
|
+
);
|
|
250
|
+
if (existingIndex >= 0) {
|
|
251
|
+
component.parameters[existingIndex] = { ...parameter };
|
|
252
|
+
} else {
|
|
253
|
+
component.parameters.push({ ...parameter });
|
|
254
|
+
}
|
|
255
|
+
return component;
|
|
256
|
+
}
|
|
257
|
+
ensureGroupExists(component, groupId, groupName, options = {}) {
|
|
258
|
+
const { strict = false } = options;
|
|
259
|
+
if (!component.parameters) {
|
|
260
|
+
component.parameters = [];
|
|
261
|
+
}
|
|
262
|
+
const existing = component.parameters.find((p) => this.compareIds(p.id, groupId, strict));
|
|
263
|
+
if (existing) {
|
|
264
|
+
return component;
|
|
265
|
+
}
|
|
266
|
+
const groupParam = {
|
|
267
|
+
id: groupId,
|
|
268
|
+
name: groupName ?? groupId,
|
|
269
|
+
type: "group",
|
|
270
|
+
typeConfig: {
|
|
271
|
+
childrenParams: []
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
component.parameters.push(groupParam);
|
|
275
|
+
return component;
|
|
276
|
+
}
|
|
277
|
+
addParameterToGroup(component, groupId, parameterId, options = {}) {
|
|
278
|
+
const { strict = false } = options;
|
|
279
|
+
const group = component.parameters?.find((p) => this.compareIds(p.id, groupId, strict));
|
|
280
|
+
if (!group || !this.isGroupParameter(group)) {
|
|
281
|
+
throw new PropertyNotFoundError(groupId, component.id);
|
|
282
|
+
}
|
|
283
|
+
if (!group.typeConfig) {
|
|
284
|
+
group.typeConfig = { childrenParams: [] };
|
|
285
|
+
}
|
|
286
|
+
if (!group.typeConfig.childrenParams) {
|
|
287
|
+
group.typeConfig.childrenParams = [];
|
|
288
|
+
}
|
|
289
|
+
const alreadyExists = group.typeConfig.childrenParams.some(
|
|
290
|
+
(id) => this.compareIds(id, parameterId, strict)
|
|
291
|
+
);
|
|
292
|
+
if (!alreadyExists) {
|
|
293
|
+
group.typeConfig.childrenParams.push(parameterId);
|
|
294
|
+
}
|
|
295
|
+
return component;
|
|
296
|
+
}
|
|
297
|
+
removeParameter(component, parameterId, options = {}) {
|
|
298
|
+
const { strict = false } = options;
|
|
299
|
+
if (!component.parameters) {
|
|
300
|
+
return component;
|
|
301
|
+
}
|
|
302
|
+
component.parameters = component.parameters.filter(
|
|
303
|
+
(p) => !this.compareIds(p.id, parameterId, strict)
|
|
304
|
+
);
|
|
305
|
+
for (const param of component.parameters) {
|
|
306
|
+
if (this.isGroupParameter(param) && param.typeConfig?.childrenParams) {
|
|
307
|
+
param.typeConfig.childrenParams = param.typeConfig.childrenParams.filter(
|
|
308
|
+
(id) => !this.compareIds(id, parameterId, strict)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return component;
|
|
313
|
+
}
|
|
314
|
+
removeEmptyGroups(component) {
|
|
315
|
+
if (!component.parameters) {
|
|
316
|
+
return component;
|
|
317
|
+
}
|
|
318
|
+
component.parameters = component.parameters.filter((p) => {
|
|
319
|
+
if (!this.isGroupParameter(p)) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
const children = p.typeConfig?.childrenParams ?? [];
|
|
323
|
+
return children.length > 0;
|
|
324
|
+
});
|
|
325
|
+
return component;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/core/services/composition.service.ts
|
|
330
|
+
var CompositionService = class {
|
|
331
|
+
constructor(fileSystem) {
|
|
332
|
+
this.fileSystem = fileSystem;
|
|
333
|
+
}
|
|
334
|
+
compareTypes(type1, type2, strict) {
|
|
335
|
+
if (strict) {
|
|
336
|
+
return type1 === type2;
|
|
337
|
+
}
|
|
338
|
+
return type1.toLowerCase() === type2.toLowerCase();
|
|
339
|
+
}
|
|
340
|
+
async loadComposition(filePath) {
|
|
341
|
+
return this.fileSystem.readFile(filePath);
|
|
342
|
+
}
|
|
343
|
+
async saveComposition(filePath, composition) {
|
|
344
|
+
await this.fileSystem.writeFile(filePath, composition);
|
|
345
|
+
}
|
|
346
|
+
async findCompositionsByType(compositionsDir, compositionType, options = {}) {
|
|
347
|
+
const { strict = false } = options;
|
|
348
|
+
const files = await this.fileSystem.findFiles(compositionsDir, "**/*.{json,yaml,yml}");
|
|
349
|
+
const results = [];
|
|
350
|
+
for (const filePath of files) {
|
|
351
|
+
try {
|
|
352
|
+
const composition = await this.loadComposition(filePath);
|
|
353
|
+
if (composition.composition?.type && this.compareTypes(composition.composition.type, compositionType, strict)) {
|
|
354
|
+
results.push({ composition, filePath });
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return results;
|
|
360
|
+
}
|
|
361
|
+
findComponentInstances(composition, componentType, options = {}) {
|
|
362
|
+
const { strict = false } = options;
|
|
363
|
+
const results = [];
|
|
364
|
+
this.searchSlots(composition.composition.slots ?? {}, componentType, [], results, strict);
|
|
365
|
+
return results;
|
|
366
|
+
}
|
|
367
|
+
searchSlots(slots, componentType, currentPath, results, strict) {
|
|
368
|
+
for (const [slotName, instances] of Object.entries(slots)) {
|
|
369
|
+
if (!Array.isArray(instances)) continue;
|
|
370
|
+
for (let i = 0; i < instances.length; i++) {
|
|
371
|
+
const instance = instances[i];
|
|
372
|
+
const instancePath = [...currentPath, slotName, String(i)];
|
|
373
|
+
if (this.compareTypes(instance.type, componentType, strict)) {
|
|
374
|
+
const instanceId = instance._id ?? `${componentType}-${results.length}`;
|
|
375
|
+
results.push({
|
|
376
|
+
instance,
|
|
377
|
+
instanceId,
|
|
378
|
+
path: instancePath
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (instance.slots) {
|
|
382
|
+
this.searchSlots(instance.slots, componentType, instancePath, results, strict);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
getRootOverrides(composition) {
|
|
388
|
+
const rootId = composition.composition._id;
|
|
389
|
+
const overrides = composition.composition._overrides?.[rootId]?.parameters;
|
|
390
|
+
if (overrides && Object.keys(overrides).length > 0) {
|
|
391
|
+
return overrides;
|
|
392
|
+
}
|
|
393
|
+
return composition.composition.parameters ?? {};
|
|
394
|
+
}
|
|
395
|
+
getInstanceOverrides(composition, instanceId) {
|
|
396
|
+
return composition.composition._overrides?.[instanceId]?.parameters ?? {};
|
|
397
|
+
}
|
|
398
|
+
setInstanceOverride(composition, instanceId, parameterId, value) {
|
|
399
|
+
if (!composition.composition._overrides) {
|
|
400
|
+
composition.composition._overrides = {};
|
|
401
|
+
}
|
|
402
|
+
if (!composition.composition._overrides[instanceId]) {
|
|
403
|
+
composition.composition._overrides[instanceId] = {};
|
|
404
|
+
}
|
|
405
|
+
if (!composition.composition._overrides[instanceId].parameters) {
|
|
406
|
+
composition.composition._overrides[instanceId].parameters = {};
|
|
407
|
+
}
|
|
408
|
+
composition.composition._overrides[instanceId].parameters[parameterId] = value;
|
|
409
|
+
return composition;
|
|
410
|
+
}
|
|
411
|
+
setInstanceOverrides(composition, instanceId, parameters) {
|
|
412
|
+
for (const [paramId, value] of Object.entries(parameters)) {
|
|
413
|
+
this.setInstanceOverride(composition, instanceId, paramId, value);
|
|
414
|
+
}
|
|
415
|
+
return composition;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Sets parameters directly on a component instance (preferred approach).
|
|
419
|
+
* This modifies the instance object in-place.
|
|
420
|
+
*/
|
|
421
|
+
setInstanceParameters(instance, parameters) {
|
|
422
|
+
if (!instance.parameters) {
|
|
423
|
+
instance.parameters = {};
|
|
424
|
+
}
|
|
425
|
+
for (const [paramId, value] of Object.entries(parameters)) {
|
|
426
|
+
instance.parameters[paramId] = value;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Deletes parameters from root overrides.
|
|
431
|
+
* Returns true if any parameters were deleted.
|
|
432
|
+
*/
|
|
433
|
+
deleteRootOverrides(composition, parameterIds, options = {}) {
|
|
434
|
+
const { strict = false } = options;
|
|
435
|
+
const rootId = composition.composition._id;
|
|
436
|
+
let deleted = false;
|
|
437
|
+
const overrides = composition.composition._overrides?.[rootId]?.parameters;
|
|
438
|
+
if (overrides) {
|
|
439
|
+
for (const paramId of parameterIds) {
|
|
440
|
+
for (const key of Object.keys(overrides)) {
|
|
441
|
+
if (strict ? key === paramId : key.toLowerCase() === paramId.toLowerCase()) {
|
|
442
|
+
delete overrides[key];
|
|
443
|
+
deleted = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const rootParams = composition.composition.parameters;
|
|
449
|
+
if (rootParams) {
|
|
450
|
+
for (const paramId of parameterIds) {
|
|
451
|
+
for (const key of Object.keys(rootParams)) {
|
|
452
|
+
if (strict ? key === paramId : key.toLowerCase() === paramId.toLowerCase()) {
|
|
453
|
+
delete rootParams[key];
|
|
454
|
+
deleted = true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return deleted;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/core/services/property-propagator.service.ts
|
|
464
|
+
var PropertyPropagatorService = class {
|
|
465
|
+
constructor(fileSystem, componentService, compositionService, logger) {
|
|
466
|
+
this.fileSystem = fileSystem;
|
|
467
|
+
this.componentService = componentService;
|
|
468
|
+
this.compositionService = compositionService;
|
|
469
|
+
this.logger = logger;
|
|
470
|
+
}
|
|
471
|
+
async propagate(options) {
|
|
472
|
+
const {
|
|
473
|
+
rootDir,
|
|
474
|
+
componentsDir,
|
|
475
|
+
compositionsDir,
|
|
476
|
+
compositionType,
|
|
477
|
+
property,
|
|
478
|
+
targetComponentType,
|
|
479
|
+
targetGroup,
|
|
480
|
+
whatIf,
|
|
481
|
+
strict,
|
|
482
|
+
deleteSourceParameter
|
|
483
|
+
} = options;
|
|
484
|
+
const findOptions = { strict };
|
|
485
|
+
const fullComponentsDir = this.fileSystem.resolvePath(rootDir, componentsDir);
|
|
486
|
+
const fullCompositionsDir = this.fileSystem.resolvePath(rootDir, compositionsDir);
|
|
487
|
+
this.logger.info(`Loading component: ${compositionType}`);
|
|
488
|
+
const { component: sourceComponent, filePath: sourceFilePath } = await this.componentService.loadComponent(fullComponentsDir, compositionType, findOptions);
|
|
489
|
+
this.logger.info(`Loading component: ${targetComponentType}`);
|
|
490
|
+
const { component: targetComponent, filePath: targetFilePath } = await this.componentService.loadComponent(fullComponentsDir, targetComponentType, findOptions);
|
|
491
|
+
const propertyNames = property.split("|").map((p) => p.trim());
|
|
492
|
+
const { parameters: resolvedParams, notFound } = this.componentService.resolveProperties(
|
|
493
|
+
sourceComponent,
|
|
494
|
+
propertyNames,
|
|
495
|
+
findOptions
|
|
496
|
+
);
|
|
497
|
+
if (notFound.length > 0) {
|
|
498
|
+
throw new PropertyNotFoundError(notFound.join(", "), compositionType);
|
|
499
|
+
}
|
|
500
|
+
const resolvedNames = resolvedParams.map((p) => p.id);
|
|
501
|
+
const groupSources = propertyNames.filter((name) => {
|
|
502
|
+
const param = this.componentService.findParameter(sourceComponent, name, findOptions);
|
|
503
|
+
return param && this.componentService.isGroupParameter(param);
|
|
504
|
+
});
|
|
505
|
+
if (groupSources.length > 0) {
|
|
506
|
+
this.logger.info(
|
|
507
|
+
`Resolved properties: ${resolvedNames.join(", ")} (from ${groupSources.join(", ")})`
|
|
508
|
+
);
|
|
509
|
+
} else {
|
|
510
|
+
this.logger.info(`Resolved properties: ${resolvedNames.join(", ")}`);
|
|
511
|
+
}
|
|
512
|
+
let modifiedComponent = { ...targetComponent };
|
|
513
|
+
let componentModified = false;
|
|
514
|
+
const existingGroup = this.componentService.findParameter(
|
|
515
|
+
modifiedComponent,
|
|
516
|
+
targetGroup,
|
|
517
|
+
findOptions
|
|
518
|
+
);
|
|
519
|
+
if (!existingGroup) {
|
|
520
|
+
this.logger.action(whatIf, "CREATE", `Group "${targetGroup}" on ${targetComponentType}`);
|
|
521
|
+
modifiedComponent = this.componentService.ensureGroupExists(
|
|
522
|
+
modifiedComponent,
|
|
523
|
+
targetGroup,
|
|
524
|
+
void 0,
|
|
525
|
+
findOptions
|
|
526
|
+
);
|
|
527
|
+
componentModified = true;
|
|
528
|
+
}
|
|
529
|
+
for (const param of resolvedParams) {
|
|
530
|
+
const existingParam = this.componentService.findParameter(
|
|
531
|
+
modifiedComponent,
|
|
532
|
+
param.id,
|
|
533
|
+
findOptions
|
|
534
|
+
);
|
|
535
|
+
if (!existingParam) {
|
|
536
|
+
this.logger.action(
|
|
537
|
+
whatIf,
|
|
538
|
+
"COPY",
|
|
539
|
+
`Parameter "${param.id}" \u2192 ${targetComponentType}.${targetGroup}`
|
|
540
|
+
);
|
|
541
|
+
modifiedComponent = this.componentService.addParameterToComponent(
|
|
542
|
+
modifiedComponent,
|
|
543
|
+
param,
|
|
544
|
+
findOptions
|
|
545
|
+
);
|
|
546
|
+
modifiedComponent = this.componentService.addParameterToGroup(
|
|
547
|
+
modifiedComponent,
|
|
548
|
+
targetGroup,
|
|
549
|
+
param.id,
|
|
550
|
+
findOptions
|
|
551
|
+
);
|
|
552
|
+
componentModified = true;
|
|
553
|
+
} else {
|
|
554
|
+
this.logger.info(`Parameter "${param.id}" already exists on ${targetComponentType}`);
|
|
555
|
+
const group = this.componentService.findParameter(
|
|
556
|
+
modifiedComponent,
|
|
557
|
+
targetGroup,
|
|
558
|
+
findOptions
|
|
559
|
+
);
|
|
560
|
+
if (group && this.componentService.isGroupParameter(group)) {
|
|
561
|
+
const isInGroup = group.typeConfig?.childrenParams?.some(
|
|
562
|
+
(id) => strict ? id === param.id : id.toLowerCase() === param.id.toLowerCase()
|
|
563
|
+
);
|
|
564
|
+
if (!isInGroup) {
|
|
565
|
+
modifiedComponent = this.componentService.addParameterToGroup(
|
|
566
|
+
modifiedComponent,
|
|
567
|
+
targetGroup,
|
|
568
|
+
param.id,
|
|
569
|
+
findOptions
|
|
570
|
+
);
|
|
571
|
+
componentModified = true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (componentModified && !whatIf) {
|
|
577
|
+
await this.componentService.saveComponent(targetFilePath, modifiedComponent);
|
|
578
|
+
}
|
|
579
|
+
const compositions = await this.compositionService.findCompositionsByType(
|
|
580
|
+
fullCompositionsDir,
|
|
581
|
+
compositionType,
|
|
582
|
+
findOptions
|
|
583
|
+
);
|
|
584
|
+
let modifiedCompositions = 0;
|
|
585
|
+
let propagatedInstances = 0;
|
|
586
|
+
for (const { composition, filePath } of compositions) {
|
|
587
|
+
const rootOverrides = this.compositionService.getRootOverrides(composition);
|
|
588
|
+
const instances = this.compositionService.findComponentInstances(
|
|
589
|
+
composition,
|
|
590
|
+
targetComponentType,
|
|
591
|
+
findOptions
|
|
592
|
+
);
|
|
593
|
+
if (instances.length === 0) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const valuesToPropagate = {};
|
|
597
|
+
for (const param of resolvedParams) {
|
|
598
|
+
if (rootOverrides[param.id]) {
|
|
599
|
+
valuesToPropagate[param.id] = rootOverrides[param.id];
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (Object.keys(valuesToPropagate).length === 0) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
let compositionModified = false;
|
|
606
|
+
const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
|
|
607
|
+
const instanceUpdates = [];
|
|
608
|
+
for (const { instance, instanceId } of instances) {
|
|
609
|
+
const instanceName = instance._id ?? instanceId;
|
|
610
|
+
this.compositionService.setInstanceParameters(instance, valuesToPropagate);
|
|
611
|
+
compositionModified = true;
|
|
612
|
+
propagatedInstances++;
|
|
613
|
+
instanceUpdates.push(
|
|
614
|
+
`${targetComponentType} "${instanceName}": ${Object.keys(valuesToPropagate).join(", ")}`
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (compositionModified) {
|
|
618
|
+
this.logger.action(whatIf, "UPDATE", `composition/${relativePath}`);
|
|
619
|
+
for (const update of instanceUpdates) {
|
|
620
|
+
this.logger.detail(`\u2192 ${update}`);
|
|
621
|
+
}
|
|
622
|
+
if (!whatIf) {
|
|
623
|
+
await this.compositionService.saveComposition(filePath, composition);
|
|
624
|
+
}
|
|
625
|
+
modifiedCompositions++;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
let sourceComponentModified = false;
|
|
629
|
+
if (deleteSourceParameter) {
|
|
630
|
+
let modifiedSource = { ...sourceComponent };
|
|
631
|
+
for (const param of resolvedParams) {
|
|
632
|
+
this.logger.action(whatIf, "DELETE", `Parameter "${param.id}" from ${compositionType}`);
|
|
633
|
+
modifiedSource = this.componentService.removeParameter(modifiedSource, param.id, findOptions);
|
|
634
|
+
sourceComponentModified = true;
|
|
635
|
+
}
|
|
636
|
+
const beforeGroupCount = modifiedSource.parameters?.filter(
|
|
637
|
+
(p) => this.componentService.isGroupParameter(p)
|
|
638
|
+
).length ?? 0;
|
|
639
|
+
modifiedSource = this.componentService.removeEmptyGroups(modifiedSource);
|
|
640
|
+
const afterGroupCount = modifiedSource.parameters?.filter(
|
|
641
|
+
(p) => this.componentService.isGroupParameter(p)
|
|
642
|
+
).length ?? 0;
|
|
643
|
+
if (afterGroupCount < beforeGroupCount) {
|
|
644
|
+
const removedCount = beforeGroupCount - afterGroupCount;
|
|
645
|
+
this.logger.action(
|
|
646
|
+
whatIf,
|
|
647
|
+
"DELETE",
|
|
648
|
+
`${removedCount} empty group(s) from ${compositionType}`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (sourceComponentModified && !whatIf) {
|
|
652
|
+
await this.componentService.saveComponent(sourceFilePath, modifiedSource);
|
|
653
|
+
}
|
|
654
|
+
for (const { composition, filePath } of compositions) {
|
|
655
|
+
const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
|
|
656
|
+
const deleted = this.compositionService.deleteRootOverrides(
|
|
657
|
+
composition,
|
|
658
|
+
resolvedNames,
|
|
659
|
+
findOptions
|
|
660
|
+
);
|
|
661
|
+
if (deleted) {
|
|
662
|
+
this.logger.action(whatIf, "DELETE", `Root overrides from composition/${relativePath}`);
|
|
663
|
+
this.logger.detail(`\u2192 Removed: ${resolvedNames.join(", ")}`);
|
|
664
|
+
if (!whatIf) {
|
|
665
|
+
await this.compositionService.saveComposition(filePath, composition);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
modifiedComponents: (componentModified ? 1 : 0) + (sourceComponentModified ? 1 : 0),
|
|
672
|
+
modifiedCompositions,
|
|
673
|
+
propagatedInstances
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// src/cli/logger.ts
|
|
679
|
+
import chalk from "chalk";
|
|
680
|
+
var Logger = class {
|
|
681
|
+
info(message) {
|
|
682
|
+
console.log(`${chalk.blue("[INFO]")} ${message}`);
|
|
683
|
+
}
|
|
684
|
+
success(message) {
|
|
685
|
+
console.log(`${chalk.green("[DONE]")} ${message}`);
|
|
686
|
+
}
|
|
687
|
+
error(message) {
|
|
688
|
+
console.log(`${chalk.red("[ERROR]")} ${message}`);
|
|
689
|
+
}
|
|
690
|
+
warn(message) {
|
|
691
|
+
console.log(`${chalk.yellow("[WARN]")} ${message}`);
|
|
692
|
+
}
|
|
693
|
+
action(whatIf, actionType, message) {
|
|
694
|
+
const prefix = whatIf ? chalk.yellow("[WOULD]") : chalk.green(`[${actionType}]`);
|
|
695
|
+
console.log(`${prefix} ${message}`);
|
|
696
|
+
}
|
|
697
|
+
detail(message) {
|
|
698
|
+
console.log(` ${chalk.gray(message)}`);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// src/cli/commands/propagate-root-component-property.ts
|
|
703
|
+
function createPropagateRootComponentPropertyCommand() {
|
|
704
|
+
const command = new Command("propagate-root-component-property");
|
|
705
|
+
command.description(
|
|
706
|
+
"Copies property definitions from a composition type's root component to a target component type, then propagates the actual values across all matching compositions."
|
|
707
|
+
).option("--compositionType <type>", "The composition type to process (e.g., HomePage)").option("--property <properties>", "Pipe-separated list of properties and/or groups to copy").option(
|
|
708
|
+
"--targetComponentType <type>",
|
|
709
|
+
"The component type that will receive the copied properties"
|
|
710
|
+
).option(
|
|
711
|
+
"--targetGroup <group>",
|
|
712
|
+
"The group name on the target component where properties will be placed"
|
|
713
|
+
).option(
|
|
714
|
+
"--deleteSourceParameter",
|
|
715
|
+
"Delete the original parameters from the source component after propagation"
|
|
716
|
+
).hook("preAction", (thisCommand) => {
|
|
717
|
+
const opts = thisCommand.opts();
|
|
718
|
+
const requiredOptions = [
|
|
719
|
+
{ name: "compositionType", flag: "--compositionType" },
|
|
720
|
+
{ name: "property", flag: "--property" },
|
|
721
|
+
{ name: "targetComponentType", flag: "--targetComponentType" },
|
|
722
|
+
{ name: "targetGroup", flag: "--targetGroup" }
|
|
723
|
+
];
|
|
724
|
+
const missing = requiredOptions.filter((opt) => !opts[opt.name]).map((opt) => opt.flag);
|
|
725
|
+
if (missing.length > 0) {
|
|
726
|
+
console.error(`error: missing required options: ${missing.join(", ")}`);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
}).action(async (opts, cmd) => {
|
|
730
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
731
|
+
const options = {
|
|
732
|
+
...globalOpts,
|
|
733
|
+
compositionType: opts.compositionType,
|
|
734
|
+
property: opts.property,
|
|
735
|
+
targetComponentType: opts.targetComponentType,
|
|
736
|
+
targetGroup: opts.targetGroup,
|
|
737
|
+
deleteSourceParameter: opts.deleteSourceParameter
|
|
738
|
+
};
|
|
739
|
+
const logger = new Logger();
|
|
740
|
+
const fileSystem = new FileSystemService();
|
|
741
|
+
const componentService = new ComponentService(fileSystem);
|
|
742
|
+
const compositionService = new CompositionService(fileSystem);
|
|
743
|
+
const propagator = new PropertyPropagatorService(
|
|
744
|
+
fileSystem,
|
|
745
|
+
componentService,
|
|
746
|
+
compositionService,
|
|
747
|
+
logger
|
|
748
|
+
);
|
|
749
|
+
try {
|
|
750
|
+
const result = await propagator.propagate({
|
|
751
|
+
rootDir: options.rootDir,
|
|
752
|
+
componentsDir: options.componentsDir,
|
|
753
|
+
compositionsDir: options.compositionsDir,
|
|
754
|
+
compositionType: options.compositionType,
|
|
755
|
+
property: options.property,
|
|
756
|
+
targetComponentType: options.targetComponentType,
|
|
757
|
+
targetGroup: options.targetGroup,
|
|
758
|
+
whatIf: options.whatIf ?? false,
|
|
759
|
+
strict: options.strict ?? false,
|
|
760
|
+
deleteSourceParameter: options.deleteSourceParameter ?? false
|
|
761
|
+
});
|
|
762
|
+
logger.success(
|
|
763
|
+
`Modified ${result.modifiedComponents} component, ${result.modifiedCompositions} compositions`
|
|
764
|
+
);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
if (error instanceof TransformError) {
|
|
767
|
+
logger.error(error.message);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
return command;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/cli/commands/find-composition-pattern-candidates.ts
|
|
777
|
+
import { Command as Command2 } from "commander";
|
|
778
|
+
|
|
779
|
+
// src/core/services/pattern-analyzer.service.ts
|
|
780
|
+
var PatternAnalyzerService = class {
|
|
781
|
+
constructor(fileSystem, compositionService, logger) {
|
|
782
|
+
this.fileSystem = fileSystem;
|
|
783
|
+
this.compositionService = compositionService;
|
|
784
|
+
this.logger = logger;
|
|
785
|
+
}
|
|
786
|
+
async analyze(options) {
|
|
787
|
+
const { rootDir, compositionsDir, projectMapNodesDir, minGroupSize, depth, threshold } = options;
|
|
788
|
+
const compositionsPath = this.fileSystem.resolvePath(rootDir, compositionsDir);
|
|
789
|
+
const projectMapNodesPath = this.fileSystem.resolvePath(rootDir, projectMapNodesDir);
|
|
790
|
+
const projectMapNodes = await this.loadProjectMapNodes(projectMapNodesPath);
|
|
791
|
+
const compositionFiles = await this.fileSystem.findFiles(
|
|
792
|
+
compositionsPath,
|
|
793
|
+
"**/*.{json,yaml,yml}"
|
|
794
|
+
);
|
|
795
|
+
this.logger.info(`Found ${compositionFiles.length} composition files`);
|
|
796
|
+
const loadedCompositions = [];
|
|
797
|
+
for (const filePath of compositionFiles) {
|
|
798
|
+
try {
|
|
799
|
+
const composition = await this.compositionService.loadComposition(filePath);
|
|
800
|
+
if (!composition.composition?.type) {
|
|
801
|
+
this.logger.warn(`Skipping ${filePath}: missing composition type`);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const fingerprint = this.generateFingerprint(composition, depth);
|
|
805
|
+
loadedCompositions.push({ composition, filePath, fingerprint });
|
|
806
|
+
} catch (error) {
|
|
807
|
+
this.logger.warn(
|
|
808
|
+
`Skipping ${filePath}: ${error instanceof Error ? error.message : "invalid file"}`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
let groups;
|
|
813
|
+
if (threshold === 0) {
|
|
814
|
+
const fingerprintGroups = /* @__PURE__ */ new Map();
|
|
815
|
+
for (const item of loadedCompositions) {
|
|
816
|
+
if (!fingerprintGroups.has(item.fingerprint)) {
|
|
817
|
+
fingerprintGroups.set(item.fingerprint, []);
|
|
818
|
+
}
|
|
819
|
+
fingerprintGroups.get(item.fingerprint).push(item);
|
|
820
|
+
}
|
|
821
|
+
groups = Array.from(fingerprintGroups.values());
|
|
822
|
+
} else {
|
|
823
|
+
groups = this.clusterByThreshold(loadedCompositions, threshold, depth);
|
|
824
|
+
}
|
|
825
|
+
const allCompositions = loadedCompositions.map((item) => item.composition);
|
|
826
|
+
const allComponentTypes = this.extractComponentTypesFromCompositions(allCompositions);
|
|
827
|
+
const totalUniqueComponents = allComponentTypes.size;
|
|
828
|
+
const patterns = [];
|
|
829
|
+
const rootTypeCounts = /* @__PURE__ */ new Map();
|
|
830
|
+
const uniqueCompositionsList = [];
|
|
831
|
+
const uniqueCompositionsData = [];
|
|
832
|
+
const patternCompositionsData = /* @__PURE__ */ new Map();
|
|
833
|
+
for (const group of groups) {
|
|
834
|
+
if (group.length < minGroupSize) {
|
|
835
|
+
for (const { composition, filePath } of group) {
|
|
836
|
+
const info = {
|
|
837
|
+
id: composition.composition._id ?? "unknown",
|
|
838
|
+
name: this.getCompositionName(composition),
|
|
839
|
+
projectMapNodeSlug: this.resolveProjectMapSlug(composition, projectMapNodes),
|
|
840
|
+
filePath
|
|
841
|
+
};
|
|
842
|
+
uniqueCompositionsList.push(info);
|
|
843
|
+
uniqueCompositionsData.push({ info, composition });
|
|
844
|
+
}
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
group.sort((a, b) => {
|
|
848
|
+
const idA = a.composition.composition._id ?? "";
|
|
849
|
+
const idB = b.composition.composition._id ?? "";
|
|
850
|
+
return idA.localeCompare(idB);
|
|
851
|
+
});
|
|
852
|
+
const firstComposition = group[0].composition;
|
|
853
|
+
const rootType = firstComposition.composition.type;
|
|
854
|
+
const firstName = this.getCompositionName(firstComposition);
|
|
855
|
+
rootTypeCounts.set(rootType, (rootTypeCounts.get(rootType) ?? 0) + 1);
|
|
856
|
+
const structureDescription = this.generateStructureDescription(firstComposition);
|
|
857
|
+
const compositions = group.map(({ composition, filePath }) => ({
|
|
858
|
+
id: composition.composition._id ?? "unknown",
|
|
859
|
+
name: this.getCompositionName(composition),
|
|
860
|
+
projectMapNodeSlug: this.resolveProjectMapSlug(composition, projectMapNodes),
|
|
861
|
+
filePath
|
|
862
|
+
}));
|
|
863
|
+
const fingerprint = group[0].fingerprint;
|
|
864
|
+
const patternGroup = {
|
|
865
|
+
name: rootType,
|
|
866
|
+
// Temporary name, will be updated below if disambiguation needed
|
|
867
|
+
structureDescription,
|
|
868
|
+
fingerprint,
|
|
869
|
+
compositions,
|
|
870
|
+
_rootType: rootType,
|
|
871
|
+
_firstName: firstName
|
|
872
|
+
};
|
|
873
|
+
patterns.push(patternGroup);
|
|
874
|
+
patternCompositionsData.set(patternGroup, group.map((g) => g.composition));
|
|
875
|
+
}
|
|
876
|
+
for (const pattern of patterns) {
|
|
877
|
+
const p = pattern;
|
|
878
|
+
if (rootTypeCounts.get(p._rootType) > 1) {
|
|
879
|
+
pattern.name = `${p._rootType}-${p._firstName}`;
|
|
880
|
+
}
|
|
881
|
+
delete pattern._rootType;
|
|
882
|
+
delete pattern._firstName;
|
|
883
|
+
}
|
|
884
|
+
patterns.sort((a, b) => b.compositions.length - a.compositions.length);
|
|
885
|
+
const seenComponents = this.computeComponentAnalysis(
|
|
886
|
+
patterns,
|
|
887
|
+
patternCompositionsData,
|
|
888
|
+
totalUniqueComponents
|
|
889
|
+
);
|
|
890
|
+
uniqueCompositionsList.sort((a, b) => a.id.localeCompare(b.id));
|
|
891
|
+
uniqueCompositionsData.sort((a, b) => a.info.id.localeCompare(b.info.id));
|
|
892
|
+
this.computeUniqueCompositionAnalysis(
|
|
893
|
+
uniqueCompositionsData,
|
|
894
|
+
seenComponents,
|
|
895
|
+
totalUniqueComponents
|
|
896
|
+
);
|
|
897
|
+
const totalCompositions = compositionFiles.length;
|
|
898
|
+
const uniquePatterns = patterns.length;
|
|
899
|
+
const compositionsInGroups = patterns.reduce((sum, p) => sum + p.compositions.length, 0);
|
|
900
|
+
const uniqueCompositionsCount = totalCompositions - compositionsInGroups;
|
|
901
|
+
return {
|
|
902
|
+
summary: {
|
|
903
|
+
totalCompositions,
|
|
904
|
+
uniquePatterns,
|
|
905
|
+
compositionsInGroups,
|
|
906
|
+
uniqueCompositions: uniqueCompositionsCount,
|
|
907
|
+
totalUniqueComponents
|
|
908
|
+
},
|
|
909
|
+
patterns,
|
|
910
|
+
uniqueCompositions: uniqueCompositionsList
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Clusters compositions using Union-Find algorithm based on structural distance.
|
|
915
|
+
* Compositions with distance <= threshold are grouped together transitively.
|
|
916
|
+
*/
|
|
917
|
+
clusterByThreshold(compositions, threshold, depth) {
|
|
918
|
+
const n = compositions.length;
|
|
919
|
+
if (n === 0) return [];
|
|
920
|
+
const parsedNodes = compositions.map(
|
|
921
|
+
(item) => this.parseCompositionToNode(item.composition, depth)
|
|
922
|
+
);
|
|
923
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
924
|
+
const rank = Array(n).fill(0);
|
|
925
|
+
const find = (x) => {
|
|
926
|
+
if (parent[x] !== x) {
|
|
927
|
+
parent[x] = find(parent[x]);
|
|
928
|
+
}
|
|
929
|
+
return parent[x];
|
|
930
|
+
};
|
|
931
|
+
const union = (x, y) => {
|
|
932
|
+
const rootX = find(x);
|
|
933
|
+
const rootY = find(y);
|
|
934
|
+
if (rootX !== rootY) {
|
|
935
|
+
if (rank[rootX] < rank[rootY]) {
|
|
936
|
+
parent[rootX] = rootY;
|
|
937
|
+
} else if (rank[rootX] > rank[rootY]) {
|
|
938
|
+
parent[rootY] = rootX;
|
|
939
|
+
} else {
|
|
940
|
+
parent[rootY] = rootX;
|
|
941
|
+
rank[rootX]++;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
for (let i = 0; i < n; i++) {
|
|
946
|
+
for (let j = i + 1; j < n; j++) {
|
|
947
|
+
if (parsedNodes[i].type !== parsedNodes[j].type) {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const distance = this.calculateDistance(parsedNodes[i], parsedNodes[j]);
|
|
951
|
+
if (distance <= threshold) {
|
|
952
|
+
union(i, j);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
const groups = /* @__PURE__ */ new Map();
|
|
957
|
+
for (let i = 0; i < n; i++) {
|
|
958
|
+
const root = find(i);
|
|
959
|
+
if (!groups.has(root)) {
|
|
960
|
+
groups.set(root, []);
|
|
961
|
+
}
|
|
962
|
+
groups.get(root).push(compositions[i]);
|
|
963
|
+
}
|
|
964
|
+
return Array.from(groups.values());
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Parses a composition into a ComponentNode tree for structural comparison.
|
|
968
|
+
*/
|
|
969
|
+
parseCompositionToNode(composition, depth) {
|
|
970
|
+
const root = composition.composition;
|
|
971
|
+
const remainingDepth = depth === 0 ? -1 : depth - 1;
|
|
972
|
+
return this.parseInstanceToNode(root.type, root.slots, remainingDepth);
|
|
973
|
+
}
|
|
974
|
+
parseInstanceToNode(type, slots, remainingDepth) {
|
|
975
|
+
const node = {
|
|
976
|
+
type,
|
|
977
|
+
slots: /* @__PURE__ */ new Map()
|
|
978
|
+
};
|
|
979
|
+
if (remainingDepth === 0 || !slots || Object.keys(slots).length === 0) {
|
|
980
|
+
return node;
|
|
981
|
+
}
|
|
982
|
+
const sortedSlotNames = Object.keys(slots).sort();
|
|
983
|
+
for (const slotName of sortedSlotNames) {
|
|
984
|
+
const instances = slots[slotName];
|
|
985
|
+
if (!Array.isArray(instances)) {
|
|
986
|
+
node.slots.set(slotName, []);
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const nextDepth = remainingDepth === -1 ? -1 : remainingDepth - 1;
|
|
990
|
+
const childNodes = instances.map(
|
|
991
|
+
(instance) => this.parseInstanceToNode(instance.type, instance.slots, nextDepth)
|
|
992
|
+
);
|
|
993
|
+
node.slots.set(slotName, childNodes);
|
|
994
|
+
}
|
|
995
|
+
return node;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Calculates the structural distance between two ComponentNode trees.
|
|
999
|
+
* Distance is the number of component differences.
|
|
1000
|
+
*/
|
|
1001
|
+
calculateDistance(node1, node2) {
|
|
1002
|
+
let distance = 0;
|
|
1003
|
+
if (node1.type !== node2.type) {
|
|
1004
|
+
return Infinity;
|
|
1005
|
+
}
|
|
1006
|
+
const allSlotNames = /* @__PURE__ */ new Set([...node1.slots.keys(), ...node2.slots.keys()]);
|
|
1007
|
+
for (const slotName of allSlotNames) {
|
|
1008
|
+
const children1 = node1.slots.get(slotName) ?? [];
|
|
1009
|
+
const children2 = node2.slots.get(slotName) ?? [];
|
|
1010
|
+
const maxLen = Math.max(children1.length, children2.length);
|
|
1011
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1012
|
+
const child1 = children1[i];
|
|
1013
|
+
const child2 = children2[i];
|
|
1014
|
+
if (!child1 || !child2) {
|
|
1015
|
+
distance++;
|
|
1016
|
+
} else if (child1.type !== child2.type) {
|
|
1017
|
+
distance++;
|
|
1018
|
+
} else {
|
|
1019
|
+
distance += this.calculateDistance(child1, child2);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return distance;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Extracts all unique component types from a composition recursively.
|
|
1027
|
+
*/
|
|
1028
|
+
extractComponentTypes(composition) {
|
|
1029
|
+
const types = /* @__PURE__ */ new Set();
|
|
1030
|
+
const root = composition.composition;
|
|
1031
|
+
types.add(root.type);
|
|
1032
|
+
this.collectComponentTypesFromSlots(root.slots, types);
|
|
1033
|
+
return types;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Helper to recursively collect component types from slots.
|
|
1037
|
+
*/
|
|
1038
|
+
collectComponentTypesFromSlots(slots, types) {
|
|
1039
|
+
if (!slots) return;
|
|
1040
|
+
for (const slotName of Object.keys(slots)) {
|
|
1041
|
+
const instances = slots[slotName];
|
|
1042
|
+
if (!Array.isArray(instances)) continue;
|
|
1043
|
+
for (const instance of instances) {
|
|
1044
|
+
types.add(instance.type);
|
|
1045
|
+
this.collectComponentTypesFromSlots(instance.slots, types);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Extracts the union of all component types from multiple compositions.
|
|
1051
|
+
*/
|
|
1052
|
+
extractComponentTypesFromCompositions(compositions) {
|
|
1053
|
+
const types = /* @__PURE__ */ new Set();
|
|
1054
|
+
for (const composition of compositions) {
|
|
1055
|
+
const compositionTypes = this.extractComponentTypes(composition);
|
|
1056
|
+
for (const type of compositionTypes) {
|
|
1057
|
+
types.add(type);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return types;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Computes component analysis for all patterns.
|
|
1064
|
+
* Returns the set of seen components for continuation with unique compositions.
|
|
1065
|
+
*/
|
|
1066
|
+
computeComponentAnalysis(patterns, patternCompositionsData, totalUniqueComponents) {
|
|
1067
|
+
const seenComponents = /* @__PURE__ */ new Set();
|
|
1068
|
+
for (const pattern of patterns) {
|
|
1069
|
+
const compositions = patternCompositionsData.get(pattern) ?? [];
|
|
1070
|
+
const patternComponents = this.extractComponentTypesFromCompositions(compositions);
|
|
1071
|
+
const componentsUsed = Array.from(patternComponents).sort();
|
|
1072
|
+
const reusedComponents = [];
|
|
1073
|
+
const newComponents = [];
|
|
1074
|
+
for (const component of componentsUsed) {
|
|
1075
|
+
if (seenComponents.has(component)) {
|
|
1076
|
+
reusedComponents.push(component);
|
|
1077
|
+
} else {
|
|
1078
|
+
newComponents.push(component);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
for (const component of componentsUsed) {
|
|
1082
|
+
seenComponents.add(component);
|
|
1083
|
+
}
|
|
1084
|
+
const cumulativeComponentCount = seenComponents.size;
|
|
1085
|
+
const progressionPercentage = totalUniqueComponents > 0 ? Math.round(cumulativeComponentCount / totalUniqueComponents * 100) : 0;
|
|
1086
|
+
pattern.componentAnalysis = {
|
|
1087
|
+
componentsUsed,
|
|
1088
|
+
reusedComponents: reusedComponents.sort(),
|
|
1089
|
+
newComponents: newComponents.sort(),
|
|
1090
|
+
cumulativeComponentCount,
|
|
1091
|
+
progressionPercentage
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return seenComponents;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Computes component analysis for unique compositions.
|
|
1098
|
+
* Continues from the seenComponents set populated by pattern analysis.
|
|
1099
|
+
*/
|
|
1100
|
+
computeUniqueCompositionAnalysis(uniqueCompositionsData, seenComponents, totalUniqueComponents) {
|
|
1101
|
+
for (const { info, composition } of uniqueCompositionsData) {
|
|
1102
|
+
const compositionComponents = this.extractComponentTypes(composition);
|
|
1103
|
+
const componentsUsed = Array.from(compositionComponents).sort();
|
|
1104
|
+
const reusedComponents = [];
|
|
1105
|
+
const newComponents = [];
|
|
1106
|
+
for (const component of componentsUsed) {
|
|
1107
|
+
if (seenComponents.has(component)) {
|
|
1108
|
+
reusedComponents.push(component);
|
|
1109
|
+
} else {
|
|
1110
|
+
newComponents.push(component);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
for (const component of componentsUsed) {
|
|
1114
|
+
seenComponents.add(component);
|
|
1115
|
+
}
|
|
1116
|
+
const cumulativeComponentCount = seenComponents.size;
|
|
1117
|
+
const progressionPercentage = totalUniqueComponents > 0 ? Math.round(cumulativeComponentCount / totalUniqueComponents * 100) : 0;
|
|
1118
|
+
info.componentAnalysis = {
|
|
1119
|
+
componentsUsed,
|
|
1120
|
+
reusedComponents: reusedComponents.sort(),
|
|
1121
|
+
newComponents: newComponents.sort(),
|
|
1122
|
+
cumulativeComponentCount,
|
|
1123
|
+
progressionPercentage
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Generates a structural fingerprint for a composition.
|
|
1129
|
+
* The fingerprint captures the component type hierarchy ignoring parameter values.
|
|
1130
|
+
* @param depth - How many levels deep to compare. 0 = unlimited, 1 = root only, 2 = root + children, etc.
|
|
1131
|
+
*/
|
|
1132
|
+
generateFingerprint(composition, depth = 0) {
|
|
1133
|
+
const root = composition.composition;
|
|
1134
|
+
const remainingDepth = depth === 0 ? -1 : depth - 1;
|
|
1135
|
+
return this.generateNodeFingerprint(root.type, root.slots, remainingDepth);
|
|
1136
|
+
}
|
|
1137
|
+
generateNodeFingerprint(type, slots, remainingDepth) {
|
|
1138
|
+
if (remainingDepth === 0 || !slots || Object.keys(slots).length === 0) {
|
|
1139
|
+
return type;
|
|
1140
|
+
}
|
|
1141
|
+
const sortedSlotNames = Object.keys(slots).sort();
|
|
1142
|
+
const slotFingerprints = sortedSlotNames.map((slotName) => {
|
|
1143
|
+
const instances = slots[slotName];
|
|
1144
|
+
if (!Array.isArray(instances) || instances.length === 0) {
|
|
1145
|
+
return `${slotName}:[]`;
|
|
1146
|
+
}
|
|
1147
|
+
const nextDepth = remainingDepth === -1 ? -1 : remainingDepth - 1;
|
|
1148
|
+
const instanceFingerprints = instances.map(
|
|
1149
|
+
(instance) => this.generateNodeFingerprint(instance.type, instance.slots, nextDepth)
|
|
1150
|
+
);
|
|
1151
|
+
return `${slotName}:[${instanceFingerprints.join(",")}]`;
|
|
1152
|
+
});
|
|
1153
|
+
return `${type}{${slotFingerprints.join(";")}}`;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Generates a human-readable structure description.
|
|
1157
|
+
*/
|
|
1158
|
+
generateStructureDescription(composition) {
|
|
1159
|
+
const root = composition.composition;
|
|
1160
|
+
return this.generateNodeDescription(root.type, root.slots, 0);
|
|
1161
|
+
}
|
|
1162
|
+
generateNodeDescription(type, slots, depth) {
|
|
1163
|
+
if (!slots || Object.keys(slots).length === 0) {
|
|
1164
|
+
return type;
|
|
1165
|
+
}
|
|
1166
|
+
const sortedSlotNames = Object.keys(slots).sort();
|
|
1167
|
+
const slotDescriptions = sortedSlotNames.map((slotName) => {
|
|
1168
|
+
const instances = slots[slotName];
|
|
1169
|
+
if (!Array.isArray(instances) || instances.length === 0) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
const instanceTypes = instances.map((instance) => {
|
|
1173
|
+
if (instance.slots && Object.keys(instance.slots).length > 0) {
|
|
1174
|
+
return this.generateNodeDescription(instance.type, instance.slots, depth + 1);
|
|
1175
|
+
}
|
|
1176
|
+
return instance.type;
|
|
1177
|
+
});
|
|
1178
|
+
return `${slotName}[${instanceTypes.join(", ")}]`;
|
|
1179
|
+
}).filter(Boolean);
|
|
1180
|
+
if (slotDescriptions.length === 0) {
|
|
1181
|
+
return type;
|
|
1182
|
+
}
|
|
1183
|
+
return `${type} > ${slotDescriptions.join(" > ")}`;
|
|
1184
|
+
}
|
|
1185
|
+
getCompositionName(composition) {
|
|
1186
|
+
const possibleNames = [
|
|
1187
|
+
composition.name,
|
|
1188
|
+
composition.composition._name,
|
|
1189
|
+
composition.composition.name,
|
|
1190
|
+
composition.composition._id
|
|
1191
|
+
];
|
|
1192
|
+
for (const name of possibleNames) {
|
|
1193
|
+
if (name && typeof name === "string") {
|
|
1194
|
+
return name;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return "Unnamed";
|
|
1198
|
+
}
|
|
1199
|
+
async loadProjectMapNodes(projectMapNodesPath) {
|
|
1200
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1201
|
+
try {
|
|
1202
|
+
const exists = await this.fileSystem.fileExists(projectMapNodesPath);
|
|
1203
|
+
if (!exists) {
|
|
1204
|
+
return nodes;
|
|
1205
|
+
}
|
|
1206
|
+
const files = await this.fileSystem.findFiles(projectMapNodesPath, "**/*.{json,yaml,yml}");
|
|
1207
|
+
for (const filePath of files) {
|
|
1208
|
+
try {
|
|
1209
|
+
const node = await this.fileSystem.readFile(filePath);
|
|
1210
|
+
if (node.compositionId) {
|
|
1211
|
+
nodes.set(node.compositionId, node);
|
|
1212
|
+
}
|
|
1213
|
+
if (node.id) {
|
|
1214
|
+
nodes.set(node.id, node);
|
|
1215
|
+
}
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
return nodes;
|
|
1222
|
+
}
|
|
1223
|
+
resolveProjectMapSlug(composition, nodes) {
|
|
1224
|
+
const compositionId = composition.composition._id;
|
|
1225
|
+
if (!compositionId) {
|
|
1226
|
+
return "(not mapped)";
|
|
1227
|
+
}
|
|
1228
|
+
const node = nodes.get(compositionId);
|
|
1229
|
+
if (!node) {
|
|
1230
|
+
return "(not mapped)";
|
|
1231
|
+
}
|
|
1232
|
+
return node.path ?? node.slug ?? "(not mapped)";
|
|
1233
|
+
}
|
|
1234
|
+
formatTextOutput(result, options) {
|
|
1235
|
+
const brief = options?.brief ?? false;
|
|
1236
|
+
const lines = [];
|
|
1237
|
+
const padWidth = String(result.patterns.length).length.toString().length >= 3 ? String(result.patterns.length).length : 3;
|
|
1238
|
+
lines.push("=== COMPOSITION PATTERN CANDIDATES ===");
|
|
1239
|
+
lines.push("");
|
|
1240
|
+
lines.push(`Total compositions analyzed: ${result.summary.totalCompositions}`);
|
|
1241
|
+
lines.push(`Unique structural patterns found: ${result.summary.uniquePatterns}`);
|
|
1242
|
+
const percentage = result.summary.totalCompositions > 0 ? Math.round(
|
|
1243
|
+
result.summary.compositionsInGroups / result.summary.totalCompositions * 100
|
|
1244
|
+
) : 0;
|
|
1245
|
+
lines.push(`Compositions in pattern groups: ${result.summary.compositionsInGroups} (${percentage}%)`);
|
|
1246
|
+
lines.push(`Unique compositions (no matches): ${result.summary.uniqueCompositions}`);
|
|
1247
|
+
lines.push(`Total unique components: ${result.summary.totalUniqueComponents}`);
|
|
1248
|
+
lines.push("");
|
|
1249
|
+
if (result.patterns.length === 0) {
|
|
1250
|
+
lines.push("No pattern candidates found (all compositions have unique structures).");
|
|
1251
|
+
return lines.join("\n");
|
|
1252
|
+
}
|
|
1253
|
+
lines.push("Pattern candidates index:");
|
|
1254
|
+
result.patterns.forEach((pattern, index) => {
|
|
1255
|
+
const patternNum = String(index + 1).padStart(padWidth, "0");
|
|
1256
|
+
lines.push(` ${patternNum}. ${pattern.name}`);
|
|
1257
|
+
lines.push(` (${pattern.compositions.length} compositions)`);
|
|
1258
|
+
});
|
|
1259
|
+
lines.push("");
|
|
1260
|
+
lines.push("Pattern candidates:");
|
|
1261
|
+
lines.push("");
|
|
1262
|
+
result.patterns.forEach((pattern, index) => {
|
|
1263
|
+
const patternNum = String(index + 1).padStart(padWidth, "0");
|
|
1264
|
+
lines.push(` Pattern ${patternNum}: ${pattern.name} (${pattern.compositions.length} compositions)`);
|
|
1265
|
+
if (pattern.componentAnalysis) {
|
|
1266
|
+
const ca = pattern.componentAnalysis;
|
|
1267
|
+
const totalComponents = result.summary.totalUniqueComponents;
|
|
1268
|
+
lines.push(` Components used: ${ca.componentsUsed.length}/${totalComponents} (${ca.componentsUsed.join(", ")})`);
|
|
1269
|
+
lines.push(` Reused components: ${ca.reusedComponents.length}/${ca.componentsUsed.length}${ca.reusedComponents.length > 0 ? ` (${ca.reusedComponents.join(", ")})` : ""}`);
|
|
1270
|
+
lines.push(` New components: ${ca.newComponents.length}/${totalComponents} (${ca.newComponents.join(", ")})`);
|
|
1271
|
+
lines.push(` Components progression: ${ca.progressionPercentage}% (${ca.cumulativeComponentCount}/${totalComponents})`);
|
|
1272
|
+
}
|
|
1273
|
+
if (!brief) {
|
|
1274
|
+
lines.push(" Structure:");
|
|
1275
|
+
const treeLines = this.generateStructureTree(pattern.structureDescription);
|
|
1276
|
+
treeLines.forEach((treeLine) => {
|
|
1277
|
+
lines.push(` ${treeLine}`);
|
|
1278
|
+
});
|
|
1279
|
+
lines.push("");
|
|
1280
|
+
lines.push(" Compositions:");
|
|
1281
|
+
pattern.compositions.forEach((comp, compIndex) => {
|
|
1282
|
+
lines.push(` ${compIndex + 1}. ID: ${comp.id}`);
|
|
1283
|
+
lines.push(` Name: ${comp.name}`);
|
|
1284
|
+
lines.push(` Node: ${comp.projectMapNodeSlug}`);
|
|
1285
|
+
lines.push("");
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
lines.push("");
|
|
1289
|
+
});
|
|
1290
|
+
if (result.uniqueCompositions.length > 0) {
|
|
1291
|
+
lines.push("");
|
|
1292
|
+
lines.push("=== UNIQUE COMPOSITIONS (NO MATCHES) ===");
|
|
1293
|
+
lines.push("");
|
|
1294
|
+
result.uniqueCompositions.forEach((comp, index) => {
|
|
1295
|
+
lines.push(` ${index + 1}. ID: ${comp.id}`);
|
|
1296
|
+
lines.push(` Name: ${comp.name}`);
|
|
1297
|
+
lines.push(` Node: ${comp.projectMapNodeSlug}`);
|
|
1298
|
+
if (comp.componentAnalysis) {
|
|
1299
|
+
const ca = comp.componentAnalysis;
|
|
1300
|
+
const totalComponents = result.summary.totalUniqueComponents;
|
|
1301
|
+
lines.push(` Components used: ${ca.componentsUsed.length}/${totalComponents} (${ca.componentsUsed.join(", ")})`);
|
|
1302
|
+
lines.push(` Reused components: ${ca.reusedComponents.length}/${ca.componentsUsed.length}${ca.reusedComponents.length > 0 ? ` (${ca.reusedComponents.join(", ")})` : ""}`);
|
|
1303
|
+
lines.push(` New components: ${ca.newComponents.length}/${totalComponents} (${ca.newComponents.join(", ")})`);
|
|
1304
|
+
lines.push(` Components progression: ${ca.progressionPercentage}% (${ca.cumulativeComponentCount}/${totalComponents})`);
|
|
1305
|
+
}
|
|
1306
|
+
lines.push("");
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
return lines.join("\n");
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Generates a tree-style representation of the structure description.
|
|
1313
|
+
*/
|
|
1314
|
+
generateStructureTree(structureDescription) {
|
|
1315
|
+
const lines = [];
|
|
1316
|
+
this.parseStructureToTree(structureDescription, 0, lines);
|
|
1317
|
+
return lines;
|
|
1318
|
+
}
|
|
1319
|
+
parseStructureToTree(str, indent, lines) {
|
|
1320
|
+
const indentStr = " ".repeat(indent);
|
|
1321
|
+
const arrowIndex = str.indexOf(" > ");
|
|
1322
|
+
if (arrowIndex === -1) {
|
|
1323
|
+
lines.push(`${indentStr}${str}`);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const rootType = str.substring(0, arrowIndex);
|
|
1327
|
+
lines.push(`${indentStr}${rootType}`);
|
|
1328
|
+
const slotsStr = str.substring(arrowIndex + 3);
|
|
1329
|
+
this.parseSlotsToTree(slotsStr, indent, lines);
|
|
1330
|
+
}
|
|
1331
|
+
parseSlotsToTree(str, indent, lines) {
|
|
1332
|
+
const indentStr = " ".repeat(indent);
|
|
1333
|
+
const slots = this.splitTopLevelSlots(str);
|
|
1334
|
+
for (const slot of slots) {
|
|
1335
|
+
const bracketStart = slot.indexOf("[");
|
|
1336
|
+
if (bracketStart === -1) {
|
|
1337
|
+
lines.push(`${indentStr}${slot}`);
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const slotName = slot.substring(0, bracketStart);
|
|
1341
|
+
const bracketEnd = this.findMatchingBracket(slot, bracketStart);
|
|
1342
|
+
const contents = slot.substring(bracketStart + 1, bracketEnd);
|
|
1343
|
+
lines.push(`${indentStr}:${slotName}`);
|
|
1344
|
+
const components = this.splitTopLevelComponents(contents);
|
|
1345
|
+
for (const component of components) {
|
|
1346
|
+
const compArrow = component.indexOf(" > ");
|
|
1347
|
+
if (compArrow !== -1) {
|
|
1348
|
+
this.parseStructureToTree(component, indent + 2, lines);
|
|
1349
|
+
} else {
|
|
1350
|
+
lines.push(`${indentStr} ${component}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
splitTopLevelSlots(str) {
|
|
1356
|
+
const result = [];
|
|
1357
|
+
let depth = 0;
|
|
1358
|
+
let current = "";
|
|
1359
|
+
for (let i = 0; i < str.length; i++) {
|
|
1360
|
+
const char = str[i];
|
|
1361
|
+
if (char === "[") {
|
|
1362
|
+
depth++;
|
|
1363
|
+
current += char;
|
|
1364
|
+
} else if (char === "]") {
|
|
1365
|
+
depth--;
|
|
1366
|
+
current += char;
|
|
1367
|
+
} else if (char === " " && str.substring(i, i + 3) === " > " && depth === 0) {
|
|
1368
|
+
if (current.trim()) {
|
|
1369
|
+
result.push(current.trim());
|
|
1370
|
+
}
|
|
1371
|
+
current = "";
|
|
1372
|
+
i += 2;
|
|
1373
|
+
} else {
|
|
1374
|
+
current += char;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (current.trim()) {
|
|
1378
|
+
result.push(current.trim());
|
|
1379
|
+
}
|
|
1380
|
+
return result;
|
|
1381
|
+
}
|
|
1382
|
+
splitTopLevelComponents(str) {
|
|
1383
|
+
const result = [];
|
|
1384
|
+
let depth = 0;
|
|
1385
|
+
let current = "";
|
|
1386
|
+
for (let i = 0; i < str.length; i++) {
|
|
1387
|
+
const char = str[i];
|
|
1388
|
+
if (char === "[" || char === "{") {
|
|
1389
|
+
depth++;
|
|
1390
|
+
current += char;
|
|
1391
|
+
} else if (char === "]" || char === "}") {
|
|
1392
|
+
depth--;
|
|
1393
|
+
current += char;
|
|
1394
|
+
} else if (char === "," && depth === 0) {
|
|
1395
|
+
if (current.trim()) {
|
|
1396
|
+
result.push(current.trim());
|
|
1397
|
+
}
|
|
1398
|
+
current = "";
|
|
1399
|
+
} else {
|
|
1400
|
+
current += char;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (current.trim()) {
|
|
1404
|
+
result.push(current.trim());
|
|
1405
|
+
}
|
|
1406
|
+
return result;
|
|
1407
|
+
}
|
|
1408
|
+
findMatchingBracket(str, start) {
|
|
1409
|
+
let depth = 0;
|
|
1410
|
+
for (let i = start; i < str.length; i++) {
|
|
1411
|
+
if (str[i] === "[") depth++;
|
|
1412
|
+
else if (str[i] === "]") {
|
|
1413
|
+
depth--;
|
|
1414
|
+
if (depth === 0) return i;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return str.length;
|
|
1418
|
+
}
|
|
1419
|
+
formatJsonOutput(result) {
|
|
1420
|
+
const cleanedPatterns = result.patterns.map(({ fingerprint, ...pattern }) => ({
|
|
1421
|
+
...pattern,
|
|
1422
|
+
compositions: pattern.compositions.map(({ filePath, ...comp }) => comp)
|
|
1423
|
+
}));
|
|
1424
|
+
const cleanedUniqueCompositions = result.uniqueCompositions.map(({ filePath, ...comp }) => comp);
|
|
1425
|
+
return JSON.stringify(
|
|
1426
|
+
{
|
|
1427
|
+
summary: result.summary,
|
|
1428
|
+
patterns: cleanedPatterns,
|
|
1429
|
+
uniqueCompositions: cleanedUniqueCompositions
|
|
1430
|
+
},
|
|
1431
|
+
null,
|
|
1432
|
+
2
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// src/cli/commands/find-composition-pattern-candidates.ts
|
|
1438
|
+
function createSilentLogger() {
|
|
1439
|
+
return {
|
|
1440
|
+
info: () => {
|
|
1441
|
+
},
|
|
1442
|
+
success: () => {
|
|
1443
|
+
},
|
|
1444
|
+
error: () => {
|
|
1445
|
+
},
|
|
1446
|
+
warn: () => {
|
|
1447
|
+
},
|
|
1448
|
+
action: () => {
|
|
1449
|
+
},
|
|
1450
|
+
detail: () => {
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
function createFindCompositionPatternCandidatesCommand() {
|
|
1455
|
+
const command = new Command2("find-composition-pattern-candidates");
|
|
1456
|
+
command.description(
|
|
1457
|
+
"Analyzes all compositions to identify groups with identical structural patterns\u2014same component types arranged in the same slot hierarchy\u2014regardless of parameter values."
|
|
1458
|
+
).option(
|
|
1459
|
+
"--minGroupSize <size>",
|
|
1460
|
+
"Minimum number of compositions required to form a pattern candidate group",
|
|
1461
|
+
"2"
|
|
1462
|
+
).option(
|
|
1463
|
+
"--depth <levels>",
|
|
1464
|
+
"How many levels deep to compare (0 = unlimited, 1 = root only, 2 = root + children)",
|
|
1465
|
+
"0"
|
|
1466
|
+
).option(
|
|
1467
|
+
"--threshold <count>",
|
|
1468
|
+
"Number of component differences allowed while still matching (0 = exact match)",
|
|
1469
|
+
"0"
|
|
1470
|
+
).option("--format <format>", "Output format: text or json", "text").option("--brief", "Skip structure and composition details in text output").hook("preAction", (thisCommand) => {
|
|
1471
|
+
const opts = thisCommand.opts();
|
|
1472
|
+
const minGroupSize = parseInt(opts.minGroupSize, 10);
|
|
1473
|
+
if (isNaN(minGroupSize) || minGroupSize < 2) {
|
|
1474
|
+
console.error("error: --minGroupSize must be a number >= 2");
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
const depth = parseInt(opts.depth, 10);
|
|
1478
|
+
if (isNaN(depth) || depth < 0) {
|
|
1479
|
+
console.error("error: --depth must be a number >= 0");
|
|
1480
|
+
process.exit(1);
|
|
1481
|
+
}
|
|
1482
|
+
const threshold = parseInt(opts.threshold, 10);
|
|
1483
|
+
if (isNaN(threshold) || threshold < 0) {
|
|
1484
|
+
console.error("error: --threshold must be a number >= 0");
|
|
1485
|
+
process.exit(1);
|
|
1486
|
+
}
|
|
1487
|
+
if (opts.format !== "text" && opts.format !== "json") {
|
|
1488
|
+
console.error('error: --format must be "text" or "json"');
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
}).action(async (opts, cmd) => {
|
|
1492
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1493
|
+
const options = {
|
|
1494
|
+
...globalOpts,
|
|
1495
|
+
minGroupSize: parseInt(opts.minGroupSize, 10),
|
|
1496
|
+
depth: parseInt(opts.depth, 10),
|
|
1497
|
+
threshold: parseInt(opts.threshold, 10),
|
|
1498
|
+
format: opts.format,
|
|
1499
|
+
brief: opts.brief ?? false
|
|
1500
|
+
};
|
|
1501
|
+
const logger = new Logger();
|
|
1502
|
+
const fileSystem = new FileSystemService();
|
|
1503
|
+
const compositionService = new CompositionService(fileSystem);
|
|
1504
|
+
try {
|
|
1505
|
+
const analysisLogger = options.format === "json" ? createSilentLogger() : logger;
|
|
1506
|
+
const analyzer = new PatternAnalyzerService(fileSystem, compositionService, analysisLogger);
|
|
1507
|
+
const result = await analyzer.analyze({
|
|
1508
|
+
rootDir: options.rootDir,
|
|
1509
|
+
compositionsDir: options.compositionsDir,
|
|
1510
|
+
projectMapNodesDir: options.projectMapNodesDir,
|
|
1511
|
+
minGroupSize: options.minGroupSize,
|
|
1512
|
+
depth: options.depth,
|
|
1513
|
+
threshold: options.threshold,
|
|
1514
|
+
strict: options.strict ?? false
|
|
1515
|
+
});
|
|
1516
|
+
if (options.format === "json") {
|
|
1517
|
+
console.log(analyzer.formatJsonOutput(result));
|
|
1518
|
+
} else {
|
|
1519
|
+
console.log(analyzer.formatTextOutput(result, { brief: options.brief }));
|
|
1520
|
+
}
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
if (error instanceof TransformError) {
|
|
1523
|
+
logger.error(error.message);
|
|
1524
|
+
process.exit(1);
|
|
1525
|
+
}
|
|
1526
|
+
throw error;
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
return command;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// src/cli/commands/unpack-serialization.ts
|
|
1533
|
+
import { Command as Command3 } from "commander";
|
|
1534
|
+
|
|
1535
|
+
// src/core/services/serialization-packer.service.ts
|
|
1536
|
+
function isPlainObject(value) {
|
|
1537
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1538
|
+
}
|
|
1539
|
+
function isSplittableArray(value) {
|
|
1540
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => isPlainObject(item) && typeof item.id === "string");
|
|
1541
|
+
}
|
|
1542
|
+
function isStubArray(value) {
|
|
1543
|
+
return Array.isArray(value) && value.length > 0 && value.every(
|
|
1544
|
+
(item) => isPlainObject(item) && typeof item.id === "string" && Object.keys(item).length === 1
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
var SerializationPackerService = class {
|
|
1548
|
+
constructor(fileSystem, logger) {
|
|
1549
|
+
this.fileSystem = fileSystem;
|
|
1550
|
+
this.logger = logger;
|
|
1551
|
+
}
|
|
1552
|
+
async unpack(options) {
|
|
1553
|
+
const inputFiles = await this.resolveInputFiles(options);
|
|
1554
|
+
let skeletonFilesWritten = 0;
|
|
1555
|
+
let fragmentFilesWritten = 0;
|
|
1556
|
+
for (const filePath of inputFiles) {
|
|
1557
|
+
const fileName = this.fileSystem.getBasename(filePath);
|
|
1558
|
+
if (fileName.includes("!!")) {
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
this.logger.info(`Processing: ${fileName}`);
|
|
1562
|
+
const data = await this.fileSystem.readFile(filePath);
|
|
1563
|
+
const skeleton = structuredClone(data);
|
|
1564
|
+
const fragments = [];
|
|
1565
|
+
const dir = options.outputDir ? this.fileSystem.resolvePath(options.rootDir, options.outputDir) : this.fileSystem.getDirname(filePath);
|
|
1566
|
+
this.walkForUnpack(skeleton, fileName, dir, filePath, fragments, options.format);
|
|
1567
|
+
if (fragments.length === 0) {
|
|
1568
|
+
this.logger.detail("No splittable arrays found, skipping");
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
const skeletonPath = options.outputDir ? this.fileSystem.joinPath(dir, fileName) : filePath;
|
|
1572
|
+
this.logger.action(options.whatIf, "WRITE", `Skeleton: ${this.fileSystem.getBasename(skeletonPath)}`);
|
|
1573
|
+
if (!options.whatIf) {
|
|
1574
|
+
await this.fileSystem.writeFile(skeletonPath, skeleton);
|
|
1575
|
+
}
|
|
1576
|
+
skeletonFilesWritten++;
|
|
1577
|
+
for (const fragment of fragments) {
|
|
1578
|
+
this.logger.action(options.whatIf, "WRITE", `Fragment: ${this.fileSystem.getBasename(fragment.path)}`);
|
|
1579
|
+
if (!options.whatIf) {
|
|
1580
|
+
await this.fileSystem.writeFile(fragment.path, fragment.data);
|
|
1581
|
+
}
|
|
1582
|
+
fragmentFilesWritten++;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
processedFiles: inputFiles.filter((f) => !this.fileSystem.getBasename(f).includes("!!")).length,
|
|
1587
|
+
skeletonFilesWritten,
|
|
1588
|
+
fragmentFilesWritten
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
async pack(options) {
|
|
1592
|
+
const inputFiles = await this.resolveInputFiles(options);
|
|
1593
|
+
let packedFilesWritten = 0;
|
|
1594
|
+
let fragmentsConsumed = 0;
|
|
1595
|
+
let fragmentsDeleted = 0;
|
|
1596
|
+
const consumedFragments = /* @__PURE__ */ new Set();
|
|
1597
|
+
for (const filePath of inputFiles) {
|
|
1598
|
+
const fileName = this.fileSystem.getBasename(filePath);
|
|
1599
|
+
if (fileName.includes("!!")) {
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
this.logger.info(`Processing: ${fileName}`);
|
|
1603
|
+
const data = await this.fileSystem.readFile(filePath);
|
|
1604
|
+
if (!this.hasStubArrays(data)) {
|
|
1605
|
+
this.logger.detail("No stub arrays found, skipping");
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const dir = this.fileSystem.getDirname(filePath);
|
|
1609
|
+
const consumed = await this.walkForPack(data, fileName, dir);
|
|
1610
|
+
fragmentsConsumed += consumed.length;
|
|
1611
|
+
for (const f of consumed) {
|
|
1612
|
+
consumedFragments.add(f);
|
|
1613
|
+
}
|
|
1614
|
+
const outputPath = options.outputDir ? this.fileSystem.joinPath(
|
|
1615
|
+
this.fileSystem.resolvePath(options.rootDir, options.outputDir),
|
|
1616
|
+
fileName
|
|
1617
|
+
) : filePath;
|
|
1618
|
+
this.logger.action(options.whatIf, "WRITE", `Packed: ${this.fileSystem.getBasename(outputPath)}`);
|
|
1619
|
+
if (!options.whatIf) {
|
|
1620
|
+
await this.fileSystem.writeFile(outputPath, data);
|
|
1621
|
+
}
|
|
1622
|
+
packedFilesWritten++;
|
|
1623
|
+
if (!options.keepFragments) {
|
|
1624
|
+
for (const fragmentPath of consumed) {
|
|
1625
|
+
this.logger.action(options.whatIf, "DELETE", `Fragment: ${this.fileSystem.getBasename(fragmentPath)}`);
|
|
1626
|
+
if (!options.whatIf) {
|
|
1627
|
+
this.fileSystem.deleteFile(fragmentPath);
|
|
1628
|
+
}
|
|
1629
|
+
fragmentsDeleted++;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
await this.warnOrphanedFragments(inputFiles, consumedFragments);
|
|
1634
|
+
return {
|
|
1635
|
+
processedFiles: inputFiles.filter((f) => !this.fileSystem.getBasename(f).includes("!!")).length,
|
|
1636
|
+
packedFilesWritten,
|
|
1637
|
+
fragmentsConsumed,
|
|
1638
|
+
fragmentsDeleted
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
walkForUnpack(obj, chain, dir, sourceFilePath, fragments, format) {
|
|
1642
|
+
if (!isPlainObject(obj)) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1646
|
+
if (isSplittableArray(value)) {
|
|
1647
|
+
const ids = value.map((item) => item.id);
|
|
1648
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1649
|
+
for (const id of ids) {
|
|
1650
|
+
if (seen.has(id)) {
|
|
1651
|
+
throw new DuplicateIdError(id, key, sourceFilePath);
|
|
1652
|
+
}
|
|
1653
|
+
seen.add(id);
|
|
1654
|
+
}
|
|
1655
|
+
const stubs = [];
|
|
1656
|
+
for (const item of value) {
|
|
1657
|
+
const id = item.id;
|
|
1658
|
+
stubs.push({ id });
|
|
1659
|
+
const fragmentClone = structuredClone(item);
|
|
1660
|
+
const ext = format === "json" ? ".json" : ".yaml";
|
|
1661
|
+
const fragmentFileName = `${chain}!!${key}!!-id!!${id}${ext}`;
|
|
1662
|
+
const fragmentPath = this.fileSystem.joinPath(dir, fragmentFileName);
|
|
1663
|
+
this.walkForUnpack(fragmentClone, fragmentFileName, dir, sourceFilePath, fragments, format);
|
|
1664
|
+
fragments.push({ path: fragmentPath, data: fragmentClone });
|
|
1665
|
+
}
|
|
1666
|
+
obj[key] = stubs;
|
|
1667
|
+
} else if (isPlainObject(value)) {
|
|
1668
|
+
this.walkForUnpack(value, chain, dir, sourceFilePath, fragments, format);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
hasStubArrays(obj) {
|
|
1673
|
+
if (!isPlainObject(obj)) {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
for (const value of Object.values(obj)) {
|
|
1677
|
+
if (isStubArray(value)) {
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
if (isPlainObject(value) && this.hasStubArrays(value)) {
|
|
1681
|
+
return true;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
async walkForPack(obj, chain, dir) {
|
|
1687
|
+
if (!isPlainObject(obj)) {
|
|
1688
|
+
return [];
|
|
1689
|
+
}
|
|
1690
|
+
const consumed = [];
|
|
1691
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1692
|
+
if (isStubArray(value)) {
|
|
1693
|
+
const reassembled = [];
|
|
1694
|
+
for (const stub of value) {
|
|
1695
|
+
const id = stub.id;
|
|
1696
|
+
let fragmentPath = this.fileSystem.joinPath(dir, `${chain}!!${key}!!-id!!${id}.yaml`);
|
|
1697
|
+
let exists = await this.fileSystem.fileExists(fragmentPath);
|
|
1698
|
+
if (!exists) {
|
|
1699
|
+
fragmentPath = this.fileSystem.joinPath(dir, `${chain}!!${key}!!-id!!${id}.json`);
|
|
1700
|
+
exists = await this.fileSystem.fileExists(fragmentPath);
|
|
1701
|
+
}
|
|
1702
|
+
if (!exists) {
|
|
1703
|
+
throw new FileNotFoundError(fragmentPath);
|
|
1704
|
+
}
|
|
1705
|
+
const fragmentData = await this.fileSystem.readFile(fragmentPath);
|
|
1706
|
+
consumed.push(fragmentPath);
|
|
1707
|
+
const fragmentFileName = this.fileSystem.getBasename(fragmentPath);
|
|
1708
|
+
const nestedConsumed = await this.walkForPack(fragmentData, fragmentFileName, dir);
|
|
1709
|
+
consumed.push(...nestedConsumed);
|
|
1710
|
+
reassembled.push(fragmentData);
|
|
1711
|
+
}
|
|
1712
|
+
obj[key] = reassembled;
|
|
1713
|
+
} else if (isPlainObject(value)) {
|
|
1714
|
+
const nestedConsumed = await this.walkForPack(value, chain, dir);
|
|
1715
|
+
consumed.push(...nestedConsumed);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return consumed;
|
|
1719
|
+
}
|
|
1720
|
+
async warnOrphanedFragments(inputFiles, consumedFragments) {
|
|
1721
|
+
const allFragments = inputFiles.filter((f) => this.fileSystem.getBasename(f).includes("!!"));
|
|
1722
|
+
for (const fragmentPath of allFragments) {
|
|
1723
|
+
if (!consumedFragments.has(fragmentPath)) {
|
|
1724
|
+
this.logger.warn(`Orphaned fragment: ${this.fileSystem.getBasename(fragmentPath)}`);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
async resolveInputFiles(options) {
|
|
1729
|
+
if (options.file) {
|
|
1730
|
+
const filePath = this.fileSystem.resolvePath(options.rootDir, options.file);
|
|
1731
|
+
return [filePath];
|
|
1732
|
+
}
|
|
1733
|
+
if (options.sourceDir) {
|
|
1734
|
+
const dir = this.fileSystem.resolvePath(options.rootDir, options.sourceDir);
|
|
1735
|
+
const yamlFiles = await this.fileSystem.findFiles(dir, "*.yaml");
|
|
1736
|
+
const jsonFiles = await this.fileSystem.findFiles(dir, "*.json");
|
|
1737
|
+
return [...yamlFiles, ...jsonFiles].sort();
|
|
1738
|
+
}
|
|
1739
|
+
return [];
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
// src/cli/commands/unpack-serialization.ts
|
|
1744
|
+
function createUnpackSerializationCommand() {
|
|
1745
|
+
const command = new Command3("unpack-serialization");
|
|
1746
|
+
command.description(
|
|
1747
|
+
"Splits serialization files into skeleton + fragment files keyed by id, enabling clean per-item git diffs."
|
|
1748
|
+
).option("--sourceDir <dir>", "Directory containing serialization files to unpack").option("--file <path>", "Single serialization file to unpack").option("--outputDir <dir>", "Output directory (defaults to same directory as source)").option("--format <format>", "Output format: yaml or json", "yaml").hook("preAction", (thisCommand) => {
|
|
1749
|
+
const opts = thisCommand.opts();
|
|
1750
|
+
if (!opts.sourceDir && !opts.file) {
|
|
1751
|
+
console.error("error: --sourceDir or --file is required");
|
|
1752
|
+
process.exit(1);
|
|
1753
|
+
}
|
|
1754
|
+
if (opts.sourceDir && opts.file) {
|
|
1755
|
+
console.error("error: --sourceDir and --file are mutually exclusive");
|
|
1756
|
+
process.exit(1);
|
|
1757
|
+
}
|
|
1758
|
+
if (opts.format !== "yaml" && opts.format !== "json") {
|
|
1759
|
+
console.error('error: --format must be "yaml" or "json"');
|
|
1760
|
+
process.exit(1);
|
|
1761
|
+
}
|
|
1762
|
+
}).action(async (opts, cmd) => {
|
|
1763
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1764
|
+
const options = {
|
|
1765
|
+
...globalOpts,
|
|
1766
|
+
sourceDir: opts.sourceDir,
|
|
1767
|
+
file: opts.file,
|
|
1768
|
+
outputDir: opts.outputDir,
|
|
1769
|
+
format: opts.format
|
|
1770
|
+
};
|
|
1771
|
+
const logger = new Logger();
|
|
1772
|
+
const fileSystem = new FileSystemService();
|
|
1773
|
+
const packer = new SerializationPackerService(fileSystem, logger);
|
|
1774
|
+
try {
|
|
1775
|
+
const result = await packer.unpack({
|
|
1776
|
+
sourceDir: options.sourceDir,
|
|
1777
|
+
file: options.file,
|
|
1778
|
+
outputDir: options.outputDir,
|
|
1779
|
+
format: options.format,
|
|
1780
|
+
whatIf: options.whatIf ?? false,
|
|
1781
|
+
rootDir: options.rootDir
|
|
1782
|
+
});
|
|
1783
|
+
logger.success(
|
|
1784
|
+
`Unpack complete: ${result.processedFiles} file(s) processed, ${result.skeletonFilesWritten} skeleton(s), ${result.fragmentFilesWritten} fragment(s)`
|
|
1785
|
+
);
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
if (error instanceof TransformError) {
|
|
1788
|
+
logger.error(error.message);
|
|
1789
|
+
process.exit(1);
|
|
1790
|
+
}
|
|
1791
|
+
throw error;
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
return command;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// src/cli/commands/pack-serialization.ts
|
|
1798
|
+
import { Command as Command4 } from "commander";
|
|
1799
|
+
function createPackSerializationCommand() {
|
|
1800
|
+
const command = new Command4("pack-serialization");
|
|
1801
|
+
command.description(
|
|
1802
|
+
"Reassembles skeleton + fragment files back into complete serialization files."
|
|
1803
|
+
).option("--sourceDir <dir>", "Directory containing skeleton and fragment files to pack").option("--file <path>", "Single skeleton file to pack").option("--outputDir <dir>", "Output directory (defaults to same directory as source)").option("--keepFragments", "Keep fragment files after packing (default: delete them)").option("--format <format>", "Output format: yaml or json", "yaml").hook("preAction", (thisCommand) => {
|
|
1804
|
+
const opts = thisCommand.opts();
|
|
1805
|
+
if (!opts.sourceDir && !opts.file) {
|
|
1806
|
+
console.error("error: --sourceDir or --file is required");
|
|
1807
|
+
process.exit(1);
|
|
1808
|
+
}
|
|
1809
|
+
if (opts.sourceDir && opts.file) {
|
|
1810
|
+
console.error("error: --sourceDir and --file are mutually exclusive");
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
}
|
|
1813
|
+
if (opts.format !== "yaml" && opts.format !== "json") {
|
|
1814
|
+
console.error('error: --format must be "yaml" or "json"');
|
|
1815
|
+
process.exit(1);
|
|
1816
|
+
}
|
|
1817
|
+
}).action(async (opts, cmd) => {
|
|
1818
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1819
|
+
const options = {
|
|
1820
|
+
...globalOpts,
|
|
1821
|
+
sourceDir: opts.sourceDir,
|
|
1822
|
+
file: opts.file,
|
|
1823
|
+
outputDir: opts.outputDir,
|
|
1824
|
+
keepFragments: opts.keepFragments ?? false,
|
|
1825
|
+
format: opts.format
|
|
1826
|
+
};
|
|
1827
|
+
const logger = new Logger();
|
|
1828
|
+
const fileSystem = new FileSystemService();
|
|
1829
|
+
const packer = new SerializationPackerService(fileSystem, logger);
|
|
1830
|
+
try {
|
|
1831
|
+
const result = await packer.pack({
|
|
1832
|
+
sourceDir: options.sourceDir,
|
|
1833
|
+
file: options.file,
|
|
1834
|
+
outputDir: options.outputDir,
|
|
1835
|
+
keepFragments: options.keepFragments,
|
|
1836
|
+
format: options.format,
|
|
1837
|
+
whatIf: options.whatIf ?? false,
|
|
1838
|
+
rootDir: options.rootDir
|
|
1839
|
+
});
|
|
1840
|
+
logger.success(
|
|
1841
|
+
`Pack complete: ${result.processedFiles} file(s) processed, ${result.packedFilesWritten} packed, ${result.fragmentsConsumed} fragment(s) consumed, ${result.fragmentsDeleted} fragment(s) deleted`
|
|
1842
|
+
);
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
if (error instanceof TransformError) {
|
|
1845
|
+
logger.error(error.message);
|
|
1846
|
+
process.exit(1);
|
|
1847
|
+
}
|
|
1848
|
+
throw error;
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
return command;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/cli/commands/rename-slot.ts
|
|
1855
|
+
import { Command as Command5 } from "commander";
|
|
1856
|
+
|
|
1857
|
+
// src/core/services/slot-renamer.service.ts
|
|
1858
|
+
var SlotRenamerService = class {
|
|
1859
|
+
constructor(fileSystem, componentService, logger) {
|
|
1860
|
+
this.fileSystem = fileSystem;
|
|
1861
|
+
this.componentService = componentService;
|
|
1862
|
+
this.logger = logger;
|
|
1863
|
+
}
|
|
1864
|
+
compareIds(id1, id2, strict) {
|
|
1865
|
+
if (strict) {
|
|
1866
|
+
return id1 === id2;
|
|
1867
|
+
}
|
|
1868
|
+
return id1.toLowerCase() === id2.toLowerCase();
|
|
1869
|
+
}
|
|
1870
|
+
async rename(options) {
|
|
1871
|
+
const {
|
|
1872
|
+
rootDir,
|
|
1873
|
+
componentsDir,
|
|
1874
|
+
compositionsDir,
|
|
1875
|
+
compositionPatternsDir,
|
|
1876
|
+
componentPatternsDir,
|
|
1877
|
+
componentType,
|
|
1878
|
+
slotId,
|
|
1879
|
+
newSlotId,
|
|
1880
|
+
newSlotName,
|
|
1881
|
+
whatIf,
|
|
1882
|
+
strict
|
|
1883
|
+
} = options;
|
|
1884
|
+
const findOptions = { strict };
|
|
1885
|
+
const fullComponentsDir = this.fileSystem.resolvePath(rootDir, componentsDir);
|
|
1886
|
+
const fullCompositionsDir = this.fileSystem.resolvePath(rootDir, compositionsDir);
|
|
1887
|
+
const fullCompositionPatternsDir = this.fileSystem.resolvePath(rootDir, compositionPatternsDir);
|
|
1888
|
+
const fullComponentPatternsDir = this.fileSystem.resolvePath(rootDir, componentPatternsDir);
|
|
1889
|
+
if (this.compareIds(slotId, newSlotId, strict)) {
|
|
1890
|
+
throw new TransformError("New slot ID is the same as the current slot ID");
|
|
1891
|
+
}
|
|
1892
|
+
this.logger.info(`Loading component: ${componentType}`);
|
|
1893
|
+
const { component, filePath: componentFilePath } = await this.componentService.loadComponent(fullComponentsDir, componentType, findOptions);
|
|
1894
|
+
if (!component.slots || component.slots.length === 0) {
|
|
1895
|
+
throw new SlotNotFoundError(slotId, componentType);
|
|
1896
|
+
}
|
|
1897
|
+
const slotIndex = component.slots.findIndex(
|
|
1898
|
+
(s) => this.compareIds(s.id, slotId, strict)
|
|
1899
|
+
);
|
|
1900
|
+
if (slotIndex === -1) {
|
|
1901
|
+
throw new SlotNotFoundError(slotId, componentType);
|
|
1902
|
+
}
|
|
1903
|
+
const existingNewSlot = component.slots.find(
|
|
1904
|
+
(s) => this.compareIds(s.id, newSlotId, strict)
|
|
1905
|
+
);
|
|
1906
|
+
if (existingNewSlot) {
|
|
1907
|
+
throw new SlotAlreadyExistsError(newSlotId, componentType);
|
|
1908
|
+
}
|
|
1909
|
+
this.logger.action(
|
|
1910
|
+
whatIf,
|
|
1911
|
+
"UPDATE",
|
|
1912
|
+
`Slot "${slotId}" \u2192 "${newSlotId}" in component/${this.fileSystem.getBasename(componentFilePath)}`
|
|
1913
|
+
);
|
|
1914
|
+
component.slots[slotIndex].id = newSlotId;
|
|
1915
|
+
if (newSlotName !== void 0) {
|
|
1916
|
+
component.slots[slotIndex].name = newSlotName;
|
|
1917
|
+
}
|
|
1918
|
+
if (!whatIf) {
|
|
1919
|
+
await this.componentService.saveComponent(componentFilePath, component);
|
|
1920
|
+
}
|
|
1921
|
+
const compositionsResult = await this.renameSlotInDirectory(
|
|
1922
|
+
fullCompositionsDir,
|
|
1923
|
+
componentType,
|
|
1924
|
+
slotId,
|
|
1925
|
+
newSlotId,
|
|
1926
|
+
whatIf,
|
|
1927
|
+
strict,
|
|
1928
|
+
"composition"
|
|
1929
|
+
);
|
|
1930
|
+
const compositionPatternsResult = await this.renameSlotInDirectory(
|
|
1931
|
+
fullCompositionPatternsDir,
|
|
1932
|
+
componentType,
|
|
1933
|
+
slotId,
|
|
1934
|
+
newSlotId,
|
|
1935
|
+
whatIf,
|
|
1936
|
+
strict,
|
|
1937
|
+
"compositionPattern"
|
|
1938
|
+
);
|
|
1939
|
+
const componentPatternsResult = await this.renameSlotInDirectory(
|
|
1940
|
+
fullComponentPatternsDir,
|
|
1941
|
+
componentType,
|
|
1942
|
+
slotId,
|
|
1943
|
+
newSlotId,
|
|
1944
|
+
whatIf,
|
|
1945
|
+
strict,
|
|
1946
|
+
"componentPattern"
|
|
1947
|
+
);
|
|
1948
|
+
const totalFiles = compositionsResult.filesModified + compositionPatternsResult.filesModified + componentPatternsResult.filesModified;
|
|
1949
|
+
const totalInstances = compositionsResult.instancesRenamed + compositionPatternsResult.instancesRenamed + componentPatternsResult.instancesRenamed;
|
|
1950
|
+
this.logger.info("");
|
|
1951
|
+
this.logger.info(
|
|
1952
|
+
`Summary: 1 component definition, ${totalFiles} file(s) (${totalInstances} instance(s)) updated.`
|
|
1953
|
+
);
|
|
1954
|
+
return {
|
|
1955
|
+
componentDefinitionUpdated: true,
|
|
1956
|
+
compositionsModified: compositionsResult.filesModified,
|
|
1957
|
+
compositionPatternsModified: compositionPatternsResult.filesModified,
|
|
1958
|
+
componentPatternsModified: componentPatternsResult.filesModified,
|
|
1959
|
+
instancesRenamed: totalInstances
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
async renameSlotInDirectory(directory, componentType, oldSlotId, newSlotId, whatIf, strict, label) {
|
|
1963
|
+
let files;
|
|
1964
|
+
try {
|
|
1965
|
+
files = await this.fileSystem.findFiles(directory, "**/*.{json,yaml,yml}");
|
|
1966
|
+
} catch {
|
|
1967
|
+
return { filesModified: 0, instancesRenamed: 0 };
|
|
1968
|
+
}
|
|
1969
|
+
if (files.length === 0) {
|
|
1970
|
+
return { filesModified: 0, instancesRenamed: 0 };
|
|
1971
|
+
}
|
|
1972
|
+
let filesModified = 0;
|
|
1973
|
+
let instancesRenamed = 0;
|
|
1974
|
+
for (const filePath of files) {
|
|
1975
|
+
let composition;
|
|
1976
|
+
try {
|
|
1977
|
+
composition = await this.fileSystem.readFile(filePath);
|
|
1978
|
+
} catch {
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
if (!composition?.composition) {
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
const count = this.renameSlotInTree(
|
|
1985
|
+
composition.composition,
|
|
1986
|
+
componentType,
|
|
1987
|
+
oldSlotId,
|
|
1988
|
+
newSlotId,
|
|
1989
|
+
strict
|
|
1990
|
+
);
|
|
1991
|
+
if (count > 0) {
|
|
1992
|
+
const relativePath = filePath.replace(directory, "").replace(/^[/\\]/, "");
|
|
1993
|
+
this.logger.action(
|
|
1994
|
+
whatIf,
|
|
1995
|
+
"UPDATE",
|
|
1996
|
+
`${label}/${relativePath} (${count} instance(s) of ${componentType})`
|
|
1997
|
+
);
|
|
1998
|
+
if (!whatIf) {
|
|
1999
|
+
await this.fileSystem.writeFile(filePath, composition);
|
|
2000
|
+
}
|
|
2001
|
+
filesModified++;
|
|
2002
|
+
instancesRenamed += count;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
return { filesModified, instancesRenamed };
|
|
2006
|
+
}
|
|
2007
|
+
renameSlotInTree(node, componentType, oldSlotId, newSlotId, strict) {
|
|
2008
|
+
let count = 0;
|
|
2009
|
+
if (this.compareIds(node.type, componentType, strict) && node.slots) {
|
|
2010
|
+
const result = this.renameSlotKey(node.slots, oldSlotId, newSlotId, strict);
|
|
2011
|
+
if (result.renamed) {
|
|
2012
|
+
node.slots = result.slots;
|
|
2013
|
+
count++;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
if (node.slots) {
|
|
2017
|
+
for (const slotInstances of Object.values(node.slots)) {
|
|
2018
|
+
if (!Array.isArray(slotInstances)) continue;
|
|
2019
|
+
for (const instance of slotInstances) {
|
|
2020
|
+
count += this.renameSlotInTree(instance, componentType, oldSlotId, newSlotId, strict);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return count;
|
|
2025
|
+
}
|
|
2026
|
+
renameSlotKey(slots, oldSlotId, newSlotId, strict) {
|
|
2027
|
+
const matchingKey = Object.keys(slots).find(
|
|
2028
|
+
(key) => this.compareIds(key, oldSlotId, strict)
|
|
2029
|
+
);
|
|
2030
|
+
if (!matchingKey) {
|
|
2031
|
+
return { renamed: false, slots };
|
|
2032
|
+
}
|
|
2033
|
+
const newSlots = {};
|
|
2034
|
+
for (const key of Object.keys(slots)) {
|
|
2035
|
+
if (key === matchingKey) {
|
|
2036
|
+
newSlots[newSlotId] = slots[key];
|
|
2037
|
+
} else {
|
|
2038
|
+
newSlots[key] = slots[key];
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return { renamed: true, slots: newSlots };
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
|
|
2045
|
+
// src/cli/commands/rename-slot.ts
|
|
2046
|
+
function createRenameSlotCommand() {
|
|
2047
|
+
const command = new Command5("rename-slot");
|
|
2048
|
+
command.description(
|
|
2049
|
+
"Renames a slot in a component definition and updates all compositions, composition patterns, and component patterns that reference it."
|
|
2050
|
+
).option("--componentType <type>", "The component type that owns the slot being renamed").option("--slotId <id>", "The current slot ID to rename").option("--newSlotId <id>", "The new slot ID").option("--newSlotName <name>", "New display name for the slot (optional)").hook("preAction", (thisCommand) => {
|
|
2051
|
+
const opts = thisCommand.opts();
|
|
2052
|
+
const requiredOptions = [
|
|
2053
|
+
{ name: "componentType", flag: "--componentType" },
|
|
2054
|
+
{ name: "slotId", flag: "--slotId" },
|
|
2055
|
+
{ name: "newSlotId", flag: "--newSlotId" }
|
|
2056
|
+
];
|
|
2057
|
+
const missing = requiredOptions.filter((opt) => !opts[opt.name]).map((opt) => opt.flag);
|
|
2058
|
+
if (missing.length > 0) {
|
|
2059
|
+
console.error(`error: missing required options: ${missing.join(", ")}`);
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
}
|
|
2062
|
+
}).action(async (opts, cmd) => {
|
|
2063
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2064
|
+
const options = {
|
|
2065
|
+
...globalOpts,
|
|
2066
|
+
componentType: opts.componentType,
|
|
2067
|
+
slotId: opts.slotId,
|
|
2068
|
+
newSlotId: opts.newSlotId,
|
|
2069
|
+
newSlotName: opts.newSlotName
|
|
2070
|
+
};
|
|
2071
|
+
const logger = new Logger();
|
|
2072
|
+
const fileSystem = new FileSystemService();
|
|
2073
|
+
const componentService = new ComponentService(fileSystem);
|
|
2074
|
+
const renamer = new SlotRenamerService(fileSystem, componentService, logger);
|
|
2075
|
+
try {
|
|
2076
|
+
const result = await renamer.rename({
|
|
2077
|
+
rootDir: options.rootDir,
|
|
2078
|
+
componentsDir: options.componentsDir,
|
|
2079
|
+
compositionsDir: options.compositionsDir,
|
|
2080
|
+
compositionPatternsDir: options.compositionPatternsDir,
|
|
2081
|
+
componentPatternsDir: options.componentPatternsDir,
|
|
2082
|
+
componentType: options.componentType,
|
|
2083
|
+
slotId: options.slotId,
|
|
2084
|
+
newSlotId: options.newSlotId,
|
|
2085
|
+
newSlotName: options.newSlotName,
|
|
2086
|
+
whatIf: options.whatIf ?? false,
|
|
2087
|
+
strict: options.strict ?? false
|
|
2088
|
+
});
|
|
2089
|
+
logger.success(
|
|
2090
|
+
`Renamed slot: ${result.compositionsModified} composition(s), ${result.compositionPatternsModified} composition pattern(s), ${result.componentPatternsModified} component pattern(s) updated`
|
|
2091
|
+
);
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
if (error instanceof TransformError) {
|
|
2094
|
+
logger.error(error.message);
|
|
2095
|
+
process.exit(1);
|
|
2096
|
+
}
|
|
2097
|
+
throw error;
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
return command;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/cli/commands/rename-component.ts
|
|
2104
|
+
import { Command as Command6 } from "commander";
|
|
2105
|
+
|
|
2106
|
+
// src/core/services/component-renamer.service.ts
|
|
2107
|
+
var ComponentRenamerService = class {
|
|
2108
|
+
constructor(fileSystem, componentService, logger) {
|
|
2109
|
+
this.fileSystem = fileSystem;
|
|
2110
|
+
this.componentService = componentService;
|
|
2111
|
+
this.logger = logger;
|
|
2112
|
+
}
|
|
2113
|
+
compareIds(id1, id2, strict) {
|
|
2114
|
+
if (strict) {
|
|
2115
|
+
return id1 === id2;
|
|
2116
|
+
}
|
|
2117
|
+
return id1.toLowerCase() === id2.toLowerCase();
|
|
2118
|
+
}
|
|
2119
|
+
async rename(options) {
|
|
2120
|
+
const {
|
|
2121
|
+
rootDir,
|
|
2122
|
+
componentsDir,
|
|
2123
|
+
compositionsDir,
|
|
2124
|
+
compositionPatternsDir,
|
|
2125
|
+
componentPatternsDir,
|
|
2126
|
+
componentType,
|
|
2127
|
+
newComponentType,
|
|
2128
|
+
newComponentName,
|
|
2129
|
+
whatIf,
|
|
2130
|
+
strict
|
|
2131
|
+
} = options;
|
|
2132
|
+
const fullComponentsDir = this.fileSystem.resolvePath(rootDir, componentsDir);
|
|
2133
|
+
const fullCompositionsDir = this.fileSystem.resolvePath(rootDir, compositionsDir);
|
|
2134
|
+
const fullCompositionPatternsDir = this.fileSystem.resolvePath(rootDir, compositionPatternsDir);
|
|
2135
|
+
const fullComponentPatternsDir = this.fileSystem.resolvePath(rootDir, componentPatternsDir);
|
|
2136
|
+
const findOptions = { strict };
|
|
2137
|
+
if (this.compareIds(componentType, newComponentType, strict)) {
|
|
2138
|
+
throw new TransformError("New component type is the same as the current component type");
|
|
2139
|
+
}
|
|
2140
|
+
this.logger.info(`Loading component: ${componentType}`);
|
|
2141
|
+
const { component, filePath: componentFilePath } = await this.componentService.loadComponent(fullComponentsDir, componentType, findOptions);
|
|
2142
|
+
let targetExists = false;
|
|
2143
|
+
try {
|
|
2144
|
+
await this.componentService.loadComponent(fullComponentsDir, newComponentType, findOptions);
|
|
2145
|
+
targetExists = true;
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
if (!(error instanceof ComponentNotFoundError)) {
|
|
2148
|
+
throw error;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if (targetExists) {
|
|
2152
|
+
throw new ComponentAlreadyExistsError(newComponentType, fullComponentsDir);
|
|
2153
|
+
}
|
|
2154
|
+
this.logger.action(
|
|
2155
|
+
whatIf,
|
|
2156
|
+
"UPDATE",
|
|
2157
|
+
`Component ID "${componentType}" \u2192 "${newComponentType}" in component/${this.fileSystem.getBasename(componentFilePath)}`
|
|
2158
|
+
);
|
|
2159
|
+
component.id = newComponentType;
|
|
2160
|
+
if (newComponentName !== void 0) {
|
|
2161
|
+
component.name = newComponentName;
|
|
2162
|
+
}
|
|
2163
|
+
if (!whatIf) {
|
|
2164
|
+
await this.componentService.saveComponent(componentFilePath, component);
|
|
2165
|
+
}
|
|
2166
|
+
const ext = this.fileSystem.getExtension(componentFilePath);
|
|
2167
|
+
const dir = this.fileSystem.getDirname(componentFilePath);
|
|
2168
|
+
const newFilePath = this.fileSystem.joinPath(dir, `${newComponentType}${ext}`);
|
|
2169
|
+
let fileRenamed = false;
|
|
2170
|
+
if (componentFilePath !== newFilePath) {
|
|
2171
|
+
this.logger.action(
|
|
2172
|
+
whatIf,
|
|
2173
|
+
"RENAME",
|
|
2174
|
+
`component/${this.fileSystem.getBasename(componentFilePath)} \u2192 component/${this.fileSystem.getBasename(newFilePath)}`
|
|
2175
|
+
);
|
|
2176
|
+
if (!whatIf) {
|
|
2177
|
+
await this.fileSystem.renameFile(componentFilePath, newFilePath);
|
|
2178
|
+
}
|
|
2179
|
+
fileRenamed = true;
|
|
2180
|
+
}
|
|
2181
|
+
const allowedComponentsUpdated = await this.updateAllowedComponents(
|
|
2182
|
+
fullComponentsDir,
|
|
2183
|
+
componentType,
|
|
2184
|
+
newComponentType,
|
|
2185
|
+
componentFilePath,
|
|
2186
|
+
whatIf,
|
|
2187
|
+
strict
|
|
2188
|
+
);
|
|
2189
|
+
const compositionsResult = await this.renameTypeInDirectory(
|
|
2190
|
+
fullCompositionsDir,
|
|
2191
|
+
componentType,
|
|
2192
|
+
newComponentType,
|
|
2193
|
+
whatIf,
|
|
2194
|
+
strict,
|
|
2195
|
+
"composition"
|
|
2196
|
+
);
|
|
2197
|
+
const compositionPatternsResult = await this.renameTypeInDirectory(
|
|
2198
|
+
fullCompositionPatternsDir,
|
|
2199
|
+
componentType,
|
|
2200
|
+
newComponentType,
|
|
2201
|
+
whatIf,
|
|
2202
|
+
strict,
|
|
2203
|
+
"compositionPattern"
|
|
2204
|
+
);
|
|
2205
|
+
const componentPatternsResult = await this.renameTypeInDirectory(
|
|
2206
|
+
fullComponentPatternsDir,
|
|
2207
|
+
componentType,
|
|
2208
|
+
newComponentType,
|
|
2209
|
+
whatIf,
|
|
2210
|
+
strict,
|
|
2211
|
+
"componentPattern"
|
|
2212
|
+
);
|
|
2213
|
+
const totalCompositionFiles = compositionsResult.filesModified + compositionPatternsResult.filesModified + componentPatternsResult.filesModified;
|
|
2214
|
+
const totalInstances = compositionsResult.instancesRenamed + compositionPatternsResult.instancesRenamed + componentPatternsResult.instancesRenamed;
|
|
2215
|
+
this.logger.info("");
|
|
2216
|
+
this.logger.info(
|
|
2217
|
+
`Summary: 1 component renamed, ${allowedComponentsUpdated} component(s) with allowedComponents updated, ${totalCompositionFiles} composition file(s) (${totalInstances} instance(s)) updated.`
|
|
2218
|
+
);
|
|
2219
|
+
return {
|
|
2220
|
+
componentRenamed: true,
|
|
2221
|
+
fileRenamed,
|
|
2222
|
+
allowedComponentsUpdated,
|
|
2223
|
+
compositionsModified: compositionsResult.filesModified,
|
|
2224
|
+
compositionPatternsModified: compositionPatternsResult.filesModified,
|
|
2225
|
+
componentPatternsModified: componentPatternsResult.filesModified,
|
|
2226
|
+
instancesRenamed: totalInstances
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
async updateAllowedComponents(componentsDir, oldType, newType, sourceFilePath, whatIf, strict) {
|
|
2230
|
+
let files;
|
|
2231
|
+
try {
|
|
2232
|
+
files = await this.fileSystem.findFiles(componentsDir, "*.{json,yaml,yml}");
|
|
2233
|
+
} catch {
|
|
2234
|
+
return 0;
|
|
2235
|
+
}
|
|
2236
|
+
let updatedCount = 0;
|
|
2237
|
+
for (const filePath of files) {
|
|
2238
|
+
if (filePath === sourceFilePath) continue;
|
|
2239
|
+
let comp;
|
|
2240
|
+
try {
|
|
2241
|
+
comp = await this.fileSystem.readFile(filePath);
|
|
2242
|
+
} catch {
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
if (!comp.slots || comp.slots.length === 0) continue;
|
|
2246
|
+
let fileModified = false;
|
|
2247
|
+
for (const slot of comp.slots) {
|
|
2248
|
+
if (!slot.allowedComponents) continue;
|
|
2249
|
+
for (let i = 0; i < slot.allowedComponents.length; i++) {
|
|
2250
|
+
if (this.compareIds(slot.allowedComponents[i], oldType, strict)) {
|
|
2251
|
+
slot.allowedComponents[i] = newType;
|
|
2252
|
+
fileModified = true;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
if (fileModified) {
|
|
2257
|
+
this.logger.action(
|
|
2258
|
+
whatIf,
|
|
2259
|
+
"UPDATE",
|
|
2260
|
+
`allowedComponents in component/${this.fileSystem.getBasename(filePath)}: ${oldType} \u2192 ${newType}`
|
|
2261
|
+
);
|
|
2262
|
+
if (!whatIf) {
|
|
2263
|
+
await this.fileSystem.writeFile(filePath, comp);
|
|
2264
|
+
}
|
|
2265
|
+
updatedCount++;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return updatedCount;
|
|
2269
|
+
}
|
|
2270
|
+
async renameTypeInDirectory(directory, oldType, newType, whatIf, strict, label) {
|
|
2271
|
+
let files;
|
|
2272
|
+
try {
|
|
2273
|
+
files = await this.fileSystem.findFiles(directory, "**/*.{json,yaml,yml}");
|
|
2274
|
+
} catch {
|
|
2275
|
+
return { filesModified: 0, instancesRenamed: 0 };
|
|
2276
|
+
}
|
|
2277
|
+
if (files.length === 0) {
|
|
2278
|
+
return { filesModified: 0, instancesRenamed: 0 };
|
|
2279
|
+
}
|
|
2280
|
+
let filesModified = 0;
|
|
2281
|
+
let instancesRenamed = 0;
|
|
2282
|
+
for (const filePath of files) {
|
|
2283
|
+
let composition;
|
|
2284
|
+
try {
|
|
2285
|
+
composition = await this.fileSystem.readFile(filePath);
|
|
2286
|
+
} catch {
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
if (!composition?.composition) continue;
|
|
2290
|
+
const count = this.renameTypeInTree(composition.composition, oldType, newType, strict);
|
|
2291
|
+
if (count > 0) {
|
|
2292
|
+
const relativePath = filePath.replace(directory, "").replace(/^[/\\]/, "");
|
|
2293
|
+
this.logger.action(
|
|
2294
|
+
whatIf,
|
|
2295
|
+
"UPDATE",
|
|
2296
|
+
`${label}/${relativePath} (${count} instance(s))`
|
|
2297
|
+
);
|
|
2298
|
+
if (!whatIf) {
|
|
2299
|
+
await this.fileSystem.writeFile(filePath, composition);
|
|
2300
|
+
}
|
|
2301
|
+
filesModified++;
|
|
2302
|
+
instancesRenamed += count;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return { filesModified, instancesRenamed };
|
|
2306
|
+
}
|
|
2307
|
+
renameTypeInTree(node, oldType, newType, strict) {
|
|
2308
|
+
let count = 0;
|
|
2309
|
+
if (this.compareIds(node.type, oldType, strict)) {
|
|
2310
|
+
node.type = newType;
|
|
2311
|
+
count++;
|
|
2312
|
+
}
|
|
2313
|
+
if (node.slots) {
|
|
2314
|
+
for (const slotInstances of Object.values(node.slots)) {
|
|
2315
|
+
if (!Array.isArray(slotInstances)) continue;
|
|
2316
|
+
for (const instance of slotInstances) {
|
|
2317
|
+
count += this.renameTypeInTree(instance, oldType, newType, strict);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return count;
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
// src/cli/commands/rename-component.ts
|
|
2326
|
+
function createRenameComponentCommand() {
|
|
2327
|
+
const command = new Command6("rename-component");
|
|
2328
|
+
command.description(
|
|
2329
|
+
"Renames a component type across definitions, filenames, allowedComponents, and all compositions."
|
|
2330
|
+
).option("--componentType <type>", "The current component type ID to rename").option("--newComponentType <type>", "The new component type ID").option("--newComponentName <name>", "New display name for the component (optional)").hook("preAction", (thisCommand) => {
|
|
2331
|
+
const opts = thisCommand.opts();
|
|
2332
|
+
const requiredOptions = [
|
|
2333
|
+
{ name: "componentType", flag: "--componentType" },
|
|
2334
|
+
{ name: "newComponentType", flag: "--newComponentType" }
|
|
2335
|
+
];
|
|
2336
|
+
const missing = requiredOptions.filter((opt) => !opts[opt.name]).map((opt) => opt.flag);
|
|
2337
|
+
if (missing.length > 0) {
|
|
2338
|
+
console.error(`error: missing required options: ${missing.join(", ")}`);
|
|
2339
|
+
process.exit(1);
|
|
2340
|
+
}
|
|
2341
|
+
}).action(async (opts, cmd) => {
|
|
2342
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2343
|
+
const options = {
|
|
2344
|
+
...globalOpts,
|
|
2345
|
+
componentType: opts.componentType,
|
|
2346
|
+
newComponentType: opts.newComponentType,
|
|
2347
|
+
newComponentName: opts.newComponentName
|
|
2348
|
+
};
|
|
2349
|
+
const logger = new Logger();
|
|
2350
|
+
const fileSystem = new FileSystemService();
|
|
2351
|
+
const componentService = new ComponentService(fileSystem);
|
|
2352
|
+
const renamer = new ComponentRenamerService(fileSystem, componentService, logger);
|
|
2353
|
+
try {
|
|
2354
|
+
const result = await renamer.rename({
|
|
2355
|
+
rootDir: options.rootDir,
|
|
2356
|
+
componentsDir: options.componentsDir,
|
|
2357
|
+
compositionsDir: options.compositionsDir,
|
|
2358
|
+
compositionPatternsDir: options.compositionPatternsDir,
|
|
2359
|
+
componentPatternsDir: options.componentPatternsDir,
|
|
2360
|
+
componentType: options.componentType,
|
|
2361
|
+
newComponentType: options.newComponentType,
|
|
2362
|
+
newComponentName: options.newComponentName,
|
|
2363
|
+
whatIf: options.whatIf ?? false,
|
|
2364
|
+
strict: options.strict ?? false
|
|
2365
|
+
});
|
|
2366
|
+
logger.success(
|
|
2367
|
+
`Renamed component: ${result.allowedComponentsUpdated} allowedComponents ref(s), ${result.compositionsModified} composition(s), ${result.compositionPatternsModified} composition pattern(s), ${result.componentPatternsModified} component pattern(s) updated`
|
|
2368
|
+
);
|
|
2369
|
+
} catch (error) {
|
|
2370
|
+
if (error instanceof TransformError) {
|
|
2371
|
+
logger.error(error.message);
|
|
2372
|
+
process.exit(1);
|
|
2373
|
+
}
|
|
2374
|
+
throw error;
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
return command;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// src/cli/index.ts
|
|
2381
|
+
var program = new Command7();
|
|
2382
|
+
program.name("uniform-transform").description("CLI tool for transforming Uniform.dev serialization files offline").version("1.0.0");
|
|
2383
|
+
program.requiredOption("--rootDir <path>", "Path to the serialization project root").option("--what-if", "Dry run mode - preview changes without modifying files", false).option("--strict", "Enable strict mode for case-sensitive matching", false).option("--compositionsDir <dir>", "Compositions directory name", "composition").option("--componentsDir <dir>", "Components directory name", "component").option("--contentTypesDir <dir>", "Content types directory name", "contentType").option("--assetsDir <dir>", "Assets directory name", "asset").option("--categoriesDir <dir>", "Categories directory name", "category").option("--componentPatternsDir <dir>", "Component patterns directory name", "componentPattern").option(
|
|
2384
|
+
"--compositionPatternsDir <dir>",
|
|
2385
|
+
"Composition patterns directory name",
|
|
2386
|
+
"compositionPattern"
|
|
2387
|
+
).option("--dataTypesDir <dir>", "Data types directory name", "dataType").option("--entriesDir <dir>", "Entries directory name", "entry").option("--filesDir <dir>", "Files directory name", "files").option("--previewUrlsDir <dir>", "Preview URLs directory name", "previewUrl").option("--previewViewportsDir <dir>", "Preview viewports directory name", "previewViewport").option(
|
|
2388
|
+
"--projectMapDefinitionsDir <dir>",
|
|
2389
|
+
"Project map definitions directory name",
|
|
2390
|
+
"projectMapDefinition"
|
|
2391
|
+
).option("--projectMapNodesDir <dir>", "Project map nodes directory name", "projectMapNode").option("--quirksDir <dir>", "Quirks directory name", "quirk");
|
|
2392
|
+
program.addCommand(createPropagateRootComponentPropertyCommand());
|
|
2393
|
+
program.addCommand(createFindCompositionPatternCandidatesCommand());
|
|
2394
|
+
program.addCommand(createUnpackSerializationCommand());
|
|
2395
|
+
program.addCommand(createPackSerializationCommand());
|
|
2396
|
+
program.addCommand(createRenameSlotCommand());
|
|
2397
|
+
program.addCommand(createRenameComponentCommand());
|
|
2398
|
+
program.parse();
|
|
2399
|
+
//# sourceMappingURL=index.js.map
|