@opkod-france/strapi-plugin-component-usage 2.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/LICENSE +21 -0
- package/README.md +722 -0
- package/assets/logo.png +0 -0
- package/dist/_chunks/en-Ca3Kbviz.js +45 -0
- package/dist/_chunks/en-d3o2mwdE.mjs +45 -0
- package/dist/_chunks/index-5hFGuOYV.mjs +63 -0
- package/dist/_chunks/index-BrC4CJvo.js +62 -0
- package/dist/_chunks/index-CWocrxjE.js +883 -0
- package/dist/_chunks/index-PpaRPQKE.mjs +883 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +5 -0
- package/dist/server/index.js +849 -0
- package/dist/server/index.mjs +850 -0
- package/package.json +98 -0
- package/strapi-server.js +3 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
const bootstrap = async ({ strapi }) => {
|
|
2
|
+
setTimeout(async () => {
|
|
3
|
+
try {
|
|
4
|
+
const trackerService = strapi.plugin("component-usage").service("usage-tracker");
|
|
5
|
+
await trackerService.initializeTracking();
|
|
6
|
+
await trackerService.recalculateAllUsage();
|
|
7
|
+
strapi.log.info("[Component Usage] Bootstrap complete");
|
|
8
|
+
} catch (error) {
|
|
9
|
+
strapi.log.error("[Component Usage] Bootstrap error:", error);
|
|
10
|
+
}
|
|
11
|
+
}, 5e3);
|
|
12
|
+
const contentTypes2 = Object.keys(strapi.contentTypes).filter(
|
|
13
|
+
(uid) => {
|
|
14
|
+
return uid.startsWith("api::") || uid.startsWith("plugin::") && !uid.startsWith("plugin::upload.") && !uid.startsWith("plugin::users-permissions.") && !uid.startsWith("plugin::component-usage.");
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
for (const uid of contentTypes2) {
|
|
18
|
+
strapi.db.lifecycles.subscribe({
|
|
19
|
+
models: [uid],
|
|
20
|
+
async afterCreate(event) {
|
|
21
|
+
const { result } = event;
|
|
22
|
+
try {
|
|
23
|
+
const trackerService = strapi.plugin("component-usage").service("usage-tracker");
|
|
24
|
+
await trackerService.updateUsageForEntry(uid, result);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
strapi.log.warn(
|
|
27
|
+
`[Component Usage] Lifecycle error in afterCreate: ${error.message}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
strapi.db.lifecycles.subscribe({
|
|
33
|
+
models: [uid],
|
|
34
|
+
async afterUpdate(event) {
|
|
35
|
+
const { result } = event;
|
|
36
|
+
try {
|
|
37
|
+
const trackerService = strapi.plugin("component-usage").service("usage-tracker");
|
|
38
|
+
await trackerService.updateUsageForEntry(uid, result);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
strapi.log.warn(
|
|
41
|
+
`[Component Usage] Lifecycle error in afterUpdate: ${error.message}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
strapi.db.lifecycles.subscribe({
|
|
47
|
+
models: [uid],
|
|
48
|
+
async afterDelete() {
|
|
49
|
+
try {
|
|
50
|
+
const trackerService = strapi.plugin("component-usage").service("usage-tracker");
|
|
51
|
+
setTimeout(async () => {
|
|
52
|
+
await trackerService.recalculateAllUsage();
|
|
53
|
+
}, 1e3);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
strapi.log.warn(
|
|
56
|
+
`[Component Usage] Lifecycle error in afterDelete: ${error.message}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
strapi.log.info(
|
|
63
|
+
`[Component Usage] Registered lifecycle hooks for ${contentTypes2.length} content types`
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
const searchInObject = (obj, componentName, path = "") => {
|
|
67
|
+
const results = [];
|
|
68
|
+
if (!obj || typeof obj !== "object") {
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
if (obj.__component === componentName) {
|
|
72
|
+
results.push(path || "root");
|
|
73
|
+
}
|
|
74
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
75
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
value.forEach((item, index2) => {
|
|
78
|
+
const arrayPath = `${currentPath}[${index2}]`;
|
|
79
|
+
results.push(...searchInObject(item, componentName, arrayPath));
|
|
80
|
+
});
|
|
81
|
+
} else if (value && typeof value === "object") {
|
|
82
|
+
results.push(...searchInObject(value, componentName, currentPath));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
};
|
|
87
|
+
const getScannableContentTypes = (strapi) => {
|
|
88
|
+
return Object.keys(strapi.contentTypes).filter((uid) => {
|
|
89
|
+
return uid.startsWith("api::") || uid.startsWith("plugin::") && !uid.startsWith("plugin::upload.") && !uid.startsWith("plugin::users-permissions.") && !uid.startsWith("plugin::component-usage.");
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
const CONTENT_TYPE_UID = "plugin::component-usage.component-usage-tracker";
|
|
93
|
+
const componentUsage$1 = ({ strapi }) => ({
|
|
94
|
+
async index(ctx) {
|
|
95
|
+
try {
|
|
96
|
+
const data = await strapi.plugin("component-usage").service("component-usage").getAllComponentsWithUsage();
|
|
97
|
+
ctx.body = { data };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
ctx.throw(500, err);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
async indexFast(ctx) {
|
|
103
|
+
try {
|
|
104
|
+
const trackers = await strapi.plugin("component-usage").service("usage-tracker").getAllTrackedComponents();
|
|
105
|
+
const allComponents = await strapi.plugin("component-usage").service("component-usage").getAllComponents();
|
|
106
|
+
const data = allComponents.map((comp) => {
|
|
107
|
+
const tracker = trackers.find(
|
|
108
|
+
(t) => t.componentUid === comp.uid
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
...comp,
|
|
112
|
+
usageCount: tracker?.usageCount || 0
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
ctx.body = { data };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
ctx.throw(500, err);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
async show(ctx) {
|
|
121
|
+
try {
|
|
122
|
+
const { uid } = ctx.params;
|
|
123
|
+
const usage = await strapi.plugin("component-usage").service("usage-tracker").getComponentUsage(uid);
|
|
124
|
+
ctx.body = { data: usage };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
ctx.throw(500, err);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
async getComponents(ctx) {
|
|
130
|
+
try {
|
|
131
|
+
const data = await strapi.plugin("component-usage").service("component-usage").getAllComponents();
|
|
132
|
+
ctx.body = { data };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
ctx.throw(500, err);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async delete(ctx) {
|
|
138
|
+
try {
|
|
139
|
+
const { uid } = ctx.params;
|
|
140
|
+
if (!uid) {
|
|
141
|
+
return ctx.badRequest("Component UID is required");
|
|
142
|
+
}
|
|
143
|
+
const result = await strapi.plugin("component-usage").service("component-usage").deleteComponent(uid);
|
|
144
|
+
const tracker = await strapi.documents(CONTENT_TYPE_UID).findMany({
|
|
145
|
+
filters: { componentUid: uid },
|
|
146
|
+
limit: 1
|
|
147
|
+
});
|
|
148
|
+
if (tracker && tracker.length > 0) {
|
|
149
|
+
await strapi.documents(CONTENT_TYPE_UID).delete({
|
|
150
|
+
documentId: tracker[0].documentId
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
ctx.body = { data: result };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
strapi.log.error("Error deleting component:", err);
|
|
156
|
+
if (err.message.includes("not found")) {
|
|
157
|
+
return ctx.notFound(err.message);
|
|
158
|
+
}
|
|
159
|
+
if (err.message.includes("being used")) {
|
|
160
|
+
return ctx.badRequest(err.message);
|
|
161
|
+
}
|
|
162
|
+
ctx.throw(500, err);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
async clearCache(ctx) {
|
|
166
|
+
try {
|
|
167
|
+
const cacheResult = strapi.plugin("component-usage").service("component-usage").clearCache();
|
|
168
|
+
const trackerService = strapi.plugin("component-usage").service("usage-tracker");
|
|
169
|
+
await trackerService.recalculateAllUsage();
|
|
170
|
+
ctx.body = {
|
|
171
|
+
data: {
|
|
172
|
+
...cacheResult,
|
|
173
|
+
message: "Cache cleared and usage recalculated"
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
} catch (err) {
|
|
177
|
+
ctx.throw(500, err);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
async recalculate(ctx) {
|
|
181
|
+
try {
|
|
182
|
+
const trackerService = strapi.plugin("component-usage").service("usage-tracker");
|
|
183
|
+
await trackerService.recalculateAllUsage();
|
|
184
|
+
ctx.body = {
|
|
185
|
+
data: {
|
|
186
|
+
success: true,
|
|
187
|
+
message: "Component usage recalculated successfully"
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
} catch (err) {
|
|
191
|
+
ctx.throw(500, err);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async getRelationships(ctx) {
|
|
195
|
+
try {
|
|
196
|
+
const { uid } = ctx.params;
|
|
197
|
+
const relationshipsService = strapi.plugin("component-usage").service("component-relationships");
|
|
198
|
+
const data = relationshipsService.getComponentWithRelationships(uid);
|
|
199
|
+
if (!data) {
|
|
200
|
+
return ctx.notFound(`Component ${uid} not found`);
|
|
201
|
+
}
|
|
202
|
+
ctx.body = { data };
|
|
203
|
+
} catch (err) {
|
|
204
|
+
ctx.throw(500, err);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
async getDependencyGraph(ctx) {
|
|
208
|
+
try {
|
|
209
|
+
const relationshipsService = strapi.plugin("component-usage").service("component-relationships");
|
|
210
|
+
const data = relationshipsService.getDependencyGraph();
|
|
211
|
+
ctx.body = { data };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
ctx.throw(500, err);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async getTotalUsage(ctx) {
|
|
217
|
+
try {
|
|
218
|
+
const { uid } = ctx.params;
|
|
219
|
+
const relationshipsService = strapi.plugin("component-usage").service("component-relationships");
|
|
220
|
+
const data = await relationshipsService.getTotalUsageCount(uid);
|
|
221
|
+
ctx.body = { data };
|
|
222
|
+
} catch (err) {
|
|
223
|
+
ctx.throw(500, err);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
const controllers = {
|
|
228
|
+
"component-usage": componentUsage$1
|
|
229
|
+
};
|
|
230
|
+
const routes = [
|
|
231
|
+
{
|
|
232
|
+
method: "GET",
|
|
233
|
+
path: "/components",
|
|
234
|
+
handler: "component-usage.indexFast",
|
|
235
|
+
config: {
|
|
236
|
+
policies: [],
|
|
237
|
+
auth: false
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
method: "GET",
|
|
242
|
+
path: "/components/full",
|
|
243
|
+
handler: "component-usage.index",
|
|
244
|
+
config: {
|
|
245
|
+
policies: [],
|
|
246
|
+
auth: false
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
method: "GET",
|
|
251
|
+
path: "/components/:uid/usage",
|
|
252
|
+
handler: "component-usage.show",
|
|
253
|
+
config: {
|
|
254
|
+
policies: [],
|
|
255
|
+
auth: false
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
method: "GET",
|
|
260
|
+
path: "/components-list",
|
|
261
|
+
handler: "component-usage.getComponents",
|
|
262
|
+
config: {
|
|
263
|
+
policies: [],
|
|
264
|
+
auth: false
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
method: "DELETE",
|
|
269
|
+
path: "/components/:uid",
|
|
270
|
+
handler: "component-usage.delete",
|
|
271
|
+
config: {
|
|
272
|
+
policies: [],
|
|
273
|
+
auth: false
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
method: "POST",
|
|
278
|
+
path: "/cache/clear",
|
|
279
|
+
handler: "component-usage.clearCache",
|
|
280
|
+
config: {
|
|
281
|
+
policies: [],
|
|
282
|
+
auth: false
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
method: "POST",
|
|
287
|
+
path: "/recalculate",
|
|
288
|
+
handler: "component-usage.recalculate",
|
|
289
|
+
config: {
|
|
290
|
+
policies: [],
|
|
291
|
+
auth: false
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
method: "GET",
|
|
296
|
+
path: "/components/:uid/relationships",
|
|
297
|
+
handler: "component-usage.getRelationships",
|
|
298
|
+
config: {
|
|
299
|
+
policies: [],
|
|
300
|
+
auth: false
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
method: "GET",
|
|
305
|
+
path: "/components/:uid/total-usage",
|
|
306
|
+
handler: "component-usage.getTotalUsage",
|
|
307
|
+
config: {
|
|
308
|
+
policies: [],
|
|
309
|
+
auth: false
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
method: "GET",
|
|
314
|
+
path: "/dependency-graph",
|
|
315
|
+
handler: "component-usage.getDependencyGraph",
|
|
316
|
+
config: {
|
|
317
|
+
policies: [],
|
|
318
|
+
auth: false
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
const cache = {
|
|
323
|
+
data: /* @__PURE__ */ new Map(),
|
|
324
|
+
timestamps: /* @__PURE__ */ new Map(),
|
|
325
|
+
TTL: 5 * 60 * 1e3,
|
|
326
|
+
// 5 minutes
|
|
327
|
+
set(key, value) {
|
|
328
|
+
this.data.set(key, value);
|
|
329
|
+
this.timestamps.set(key, Date.now());
|
|
330
|
+
},
|
|
331
|
+
get(key) {
|
|
332
|
+
const timestamp = this.timestamps.get(key);
|
|
333
|
+
if (!timestamp || Date.now() - timestamp > this.TTL) {
|
|
334
|
+
this.data.delete(key);
|
|
335
|
+
this.timestamps.delete(key);
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return this.data.get(key);
|
|
339
|
+
},
|
|
340
|
+
clear() {
|
|
341
|
+
this.data.clear();
|
|
342
|
+
this.timestamps.clear();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const componentUsage = ({ strapi }) => ({
|
|
346
|
+
async getAllComponents() {
|
|
347
|
+
const components = Object.keys(strapi.components).map((uid) => {
|
|
348
|
+
const component = strapi.components[uid];
|
|
349
|
+
return {
|
|
350
|
+
uid,
|
|
351
|
+
category: component.category,
|
|
352
|
+
displayName: component.info?.displayName || component.modelName,
|
|
353
|
+
icon: component.info?.icon,
|
|
354
|
+
attributes: component.attributes
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
return components;
|
|
358
|
+
},
|
|
359
|
+
async getComponentUsageCount(componentUid) {
|
|
360
|
+
const cacheKey = `count:${componentUid}`;
|
|
361
|
+
const cached = cache.get(cacheKey);
|
|
362
|
+
if (cached !== null) {
|
|
363
|
+
return cached;
|
|
364
|
+
}
|
|
365
|
+
let totalCount = 0;
|
|
366
|
+
const contentTypes2 = getScannableContentTypes(strapi);
|
|
367
|
+
for (const contentTypeUid of contentTypes2) {
|
|
368
|
+
try {
|
|
369
|
+
const entries = await strapi.documents(contentTypeUid).findMany({
|
|
370
|
+
populate: "*",
|
|
371
|
+
status: "draft"
|
|
372
|
+
});
|
|
373
|
+
if (!entries || entries.length === 0) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
for (const entry of entries) {
|
|
377
|
+
const matches = searchInObject(entry, componentUid);
|
|
378
|
+
totalCount += matches.length;
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
strapi.log.warn(
|
|
382
|
+
`Could not count usage for ${contentTypeUid}: ${error.message}`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
cache.set(cacheKey, totalCount);
|
|
387
|
+
return totalCount;
|
|
388
|
+
},
|
|
389
|
+
async getComponentUsage(componentUid) {
|
|
390
|
+
const cacheKey = `usage:${componentUid}`;
|
|
391
|
+
const cached = cache.get(cacheKey);
|
|
392
|
+
if (cached !== null) {
|
|
393
|
+
return cached;
|
|
394
|
+
}
|
|
395
|
+
const contentTypes2 = getScannableContentTypes(strapi);
|
|
396
|
+
const usageData = [];
|
|
397
|
+
for (const contentTypeUid of contentTypes2) {
|
|
398
|
+
try {
|
|
399
|
+
const entries = await strapi.documents(contentTypeUid).findMany({
|
|
400
|
+
populate: "*",
|
|
401
|
+
status: "draft"
|
|
402
|
+
});
|
|
403
|
+
if (!entries || entries.length === 0) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
for (const entry of entries) {
|
|
407
|
+
const matches = searchInObject(entry, componentUid);
|
|
408
|
+
if (matches.length > 0) {
|
|
409
|
+
const contentTypeName = contentTypeUid.replace("api::", "").replace("plugin::", "");
|
|
410
|
+
matches.forEach((fieldPath) => {
|
|
411
|
+
usageData.push({
|
|
412
|
+
contentType: contentTypeName,
|
|
413
|
+
contentTypeUid,
|
|
414
|
+
entryId: entry.documentId || entry.id?.toString() || "N/A",
|
|
415
|
+
fieldPath
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
strapi.log.warn(
|
|
422
|
+
`Could not fetch entries for ${contentTypeUid}: ${error.message}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
cache.set(cacheKey, usageData);
|
|
427
|
+
return usageData;
|
|
428
|
+
},
|
|
429
|
+
async getAllComponentsWithUsage() {
|
|
430
|
+
const components = await this.getAllComponents();
|
|
431
|
+
const componentsWithUsage = await Promise.all(
|
|
432
|
+
components.map(async (component) => {
|
|
433
|
+
const usage = await this.getComponentUsage(component.uid);
|
|
434
|
+
return {
|
|
435
|
+
...component,
|
|
436
|
+
usageCount: usage.length,
|
|
437
|
+
usage
|
|
438
|
+
};
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
return componentsWithUsage;
|
|
442
|
+
},
|
|
443
|
+
async deleteComponent(componentUid) {
|
|
444
|
+
const fs = require("fs");
|
|
445
|
+
if (!strapi.components[componentUid]) {
|
|
446
|
+
throw new Error(`Component ${componentUid} not found`);
|
|
447
|
+
}
|
|
448
|
+
const usageCount = await this.getComponentUsageCount(componentUid);
|
|
449
|
+
if (usageCount > 0) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`Cannot delete component ${componentUid}. It is being used in ${usageCount} place(s).`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const component = strapi.components[componentUid];
|
|
455
|
+
const componentPath = component.__filename__;
|
|
456
|
+
if (!componentPath) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`Could not find file path for component ${componentUid}`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
if (fs.existsSync(componentPath)) {
|
|
463
|
+
fs.unlinkSync(componentPath);
|
|
464
|
+
strapi.log.info(`Deleted component file: ${componentPath}`);
|
|
465
|
+
}
|
|
466
|
+
delete strapi.components[componentUid];
|
|
467
|
+
cache.clear();
|
|
468
|
+
return {
|
|
469
|
+
success: true,
|
|
470
|
+
message: `Component ${componentUid} deleted successfully`
|
|
471
|
+
};
|
|
472
|
+
} catch (error) {
|
|
473
|
+
strapi.log.error(`Error deleting component file: ${error.message}`);
|
|
474
|
+
throw new Error(`Failed to delete component file: ${error.message}`);
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
clearCache() {
|
|
478
|
+
cache.clear();
|
|
479
|
+
return { success: true, message: "Cache cleared" };
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
const usageTracker = ({ strapi }) => ({
|
|
483
|
+
async initializeTracking() {
|
|
484
|
+
strapi.log.info("[Component Usage] Initializing component tracking...");
|
|
485
|
+
const components = Object.keys(strapi.components).map((uid) => {
|
|
486
|
+
const component = strapi.components[uid];
|
|
487
|
+
return {
|
|
488
|
+
uid,
|
|
489
|
+
category: component.category,
|
|
490
|
+
displayName: component.info?.displayName || component.modelName
|
|
491
|
+
};
|
|
492
|
+
});
|
|
493
|
+
for (const component of components) {
|
|
494
|
+
try {
|
|
495
|
+
const existing = await strapi.documents(CONTENT_TYPE_UID).findMany({
|
|
496
|
+
filters: { componentUid: component.uid },
|
|
497
|
+
limit: 1
|
|
498
|
+
});
|
|
499
|
+
if (!existing || existing.length === 0) {
|
|
500
|
+
await strapi.documents(CONTENT_TYPE_UID).create({
|
|
501
|
+
data: {
|
|
502
|
+
componentUid: component.uid,
|
|
503
|
+
componentName: component.displayName,
|
|
504
|
+
componentCategory: component.category,
|
|
505
|
+
usageCount: 0,
|
|
506
|
+
usages: [],
|
|
507
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
strapi.log.error(
|
|
513
|
+
`[Component Usage] Error creating tracker for ${component.uid}: ${error.message}`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
strapi.log.info("[Component Usage] Component tracking initialized");
|
|
518
|
+
},
|
|
519
|
+
async recalculateAllUsage() {
|
|
520
|
+
strapi.log.info("[Component Usage] Recalculating all component usage...");
|
|
521
|
+
const components = Object.keys(strapi.components);
|
|
522
|
+
const contentTypes2 = getScannableContentTypes(strapi);
|
|
523
|
+
for (const componentUid of components) {
|
|
524
|
+
const usages = [];
|
|
525
|
+
for (const contentTypeUid of contentTypes2) {
|
|
526
|
+
try {
|
|
527
|
+
const entries = await strapi.documents(contentTypeUid).findMany({
|
|
528
|
+
populate: "*",
|
|
529
|
+
status: "draft"
|
|
530
|
+
});
|
|
531
|
+
if (!entries || entries.length === 0) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
for (const entry of entries) {
|
|
535
|
+
const matches = searchInObject(entry, componentUid);
|
|
536
|
+
if (matches.length > 0) {
|
|
537
|
+
const contentTypeName = contentTypeUid.replace("api::", "").replace("plugin::", "");
|
|
538
|
+
matches.forEach((fieldPath) => {
|
|
539
|
+
usages.push({
|
|
540
|
+
contentType: contentTypeName,
|
|
541
|
+
contentTypeUid,
|
|
542
|
+
entryId: entry.documentId || entry.id?.toString() || "N/A",
|
|
543
|
+
fieldPath
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} catch (error) {
|
|
549
|
+
strapi.log.warn(
|
|
550
|
+
`[Component Usage] Could not scan ${contentTypeUid} for ${componentUid}: ${error.message}`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
await this.updateComponentUsage(componentUid, usages);
|
|
555
|
+
}
|
|
556
|
+
strapi.log.info("[Component Usage] Recalculation complete");
|
|
557
|
+
},
|
|
558
|
+
async updateComponentUsage(componentUid, usages = []) {
|
|
559
|
+
try {
|
|
560
|
+
const existing = await strapi.documents(CONTENT_TYPE_UID).findMany({
|
|
561
|
+
filters: { componentUid },
|
|
562
|
+
limit: 1
|
|
563
|
+
});
|
|
564
|
+
if (existing && existing.length > 0) {
|
|
565
|
+
await strapi.documents(CONTENT_TYPE_UID).update({
|
|
566
|
+
documentId: existing[0].documentId,
|
|
567
|
+
data: {
|
|
568
|
+
usageCount: usages.length,
|
|
569
|
+
usages,
|
|
570
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
strapi.log.error(
|
|
576
|
+
`[Component Usage] Error updating tracker for ${componentUid}: ${error.message}`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
async updateUsageForEntry(contentTypeUid, entry) {
|
|
581
|
+
if (!entry) return;
|
|
582
|
+
const components = Object.keys(strapi.components);
|
|
583
|
+
for (const componentUid of components) {
|
|
584
|
+
const matches = searchInObject(entry, componentUid);
|
|
585
|
+
if (matches.length > 0) {
|
|
586
|
+
await this.recalculateComponentUsage(componentUid);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
async recalculateComponentUsage(componentUid) {
|
|
591
|
+
const contentTypes2 = getScannableContentTypes(strapi);
|
|
592
|
+
const usages = [];
|
|
593
|
+
for (const contentTypeUid of contentTypes2) {
|
|
594
|
+
try {
|
|
595
|
+
const entries = await strapi.documents(contentTypeUid).findMany({
|
|
596
|
+
populate: "*",
|
|
597
|
+
status: "draft"
|
|
598
|
+
});
|
|
599
|
+
if (!entries || entries.length === 0) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
for (const entry of entries) {
|
|
603
|
+
const matches = searchInObject(entry, componentUid);
|
|
604
|
+
if (matches.length > 0) {
|
|
605
|
+
const contentTypeName = contentTypeUid.replace("api::", "").replace("plugin::", "");
|
|
606
|
+
matches.forEach((fieldPath) => {
|
|
607
|
+
usages.push({
|
|
608
|
+
contentType: contentTypeName,
|
|
609
|
+
contentTypeUid,
|
|
610
|
+
entryId: entry.documentId || entry.id?.toString() || "N/A",
|
|
611
|
+
fieldPath
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
strapi.log.warn(
|
|
618
|
+
`[Component Usage] Could not scan ${contentTypeUid}: ${error.message}`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
await this.updateComponentUsage(componentUid, usages);
|
|
623
|
+
},
|
|
624
|
+
async getAllTrackedComponents() {
|
|
625
|
+
try {
|
|
626
|
+
const trackers = await strapi.documents(CONTENT_TYPE_UID).findMany({
|
|
627
|
+
limit: 1e3
|
|
628
|
+
});
|
|
629
|
+
return trackers || [];
|
|
630
|
+
} catch (error) {
|
|
631
|
+
strapi.log.error(
|
|
632
|
+
"[Component Usage] Error fetching trackers:",
|
|
633
|
+
error.message
|
|
634
|
+
);
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
async getComponentUsage(componentUid) {
|
|
639
|
+
try {
|
|
640
|
+
const tracker = await strapi.documents(CONTENT_TYPE_UID).findMany({
|
|
641
|
+
filters: { componentUid },
|
|
642
|
+
limit: 1
|
|
643
|
+
});
|
|
644
|
+
if (tracker && tracker.length > 0) {
|
|
645
|
+
return tracker[0].usages || [];
|
|
646
|
+
}
|
|
647
|
+
return [];
|
|
648
|
+
} catch (error) {
|
|
649
|
+
strapi.log.error(
|
|
650
|
+
`[Component Usage] Error fetching usage for ${componentUid}: ${error.message}`
|
|
651
|
+
);
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
const findNestedComponents = (attributes2) => {
|
|
657
|
+
const nestedComponents = [];
|
|
658
|
+
if (!attributes2 || typeof attributes2 !== "object") {
|
|
659
|
+
return nestedComponents;
|
|
660
|
+
}
|
|
661
|
+
for (const [attrName, attrConfig] of Object.entries(attributes2)) {
|
|
662
|
+
if (attrConfig.type === "component" && attrConfig.component) {
|
|
663
|
+
nestedComponents.push({
|
|
664
|
+
componentUid: attrConfig.component,
|
|
665
|
+
attributeName: attrName,
|
|
666
|
+
repeatable: attrConfig.repeatable || false
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if (attrConfig.type === "dynamiczone" && attrConfig.components) {
|
|
670
|
+
attrConfig.components.forEach((componentUid) => {
|
|
671
|
+
nestedComponents.push({
|
|
672
|
+
componentUid,
|
|
673
|
+
attributeName: attrName,
|
|
674
|
+
isDynamicZone: true
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return nestedComponents;
|
|
680
|
+
};
|
|
681
|
+
const buildDependencyGraph = (components) => {
|
|
682
|
+
const graph = {};
|
|
683
|
+
Object.keys(components).forEach((uid) => {
|
|
684
|
+
graph[uid] = {
|
|
685
|
+
uid,
|
|
686
|
+
displayName: components[uid].info?.displayName || components[uid].modelName,
|
|
687
|
+
category: components[uid].category,
|
|
688
|
+
uses: [],
|
|
689
|
+
usedIn: []
|
|
690
|
+
};
|
|
691
|
+
});
|
|
692
|
+
Object.keys(components).forEach((parentUid) => {
|
|
693
|
+
const component = components[parentUid];
|
|
694
|
+
const nestedComponents = findNestedComponents(component.attributes);
|
|
695
|
+
nestedComponents.forEach(
|
|
696
|
+
({ componentUid, attributeName, repeatable, isDynamicZone }) => {
|
|
697
|
+
graph[parentUid].uses.push({
|
|
698
|
+
componentUid,
|
|
699
|
+
attributeName,
|
|
700
|
+
repeatable,
|
|
701
|
+
isDynamicZone
|
|
702
|
+
});
|
|
703
|
+
if (graph[componentUid]) {
|
|
704
|
+
graph[componentUid].usedIn.push({
|
|
705
|
+
componentUid: parentUid,
|
|
706
|
+
attributeName,
|
|
707
|
+
repeatable,
|
|
708
|
+
isDynamicZone
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
);
|
|
713
|
+
});
|
|
714
|
+
return graph;
|
|
715
|
+
};
|
|
716
|
+
const componentRelationships = ({ strapi }) => ({
|
|
717
|
+
getComponentDependencies(componentUid) {
|
|
718
|
+
const component = strapi.components[componentUid];
|
|
719
|
+
if (!component) {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
return findNestedComponents(component.attributes);
|
|
723
|
+
},
|
|
724
|
+
getComponentUsedIn(componentUid) {
|
|
725
|
+
const usedIn = [];
|
|
726
|
+
Object.keys(strapi.components).forEach((parentUid) => {
|
|
727
|
+
const parentComponent = strapi.components[parentUid];
|
|
728
|
+
const nestedComponents = findNestedComponents(
|
|
729
|
+
parentComponent.attributes
|
|
730
|
+
);
|
|
731
|
+
const isUsed = nestedComponents.some(
|
|
732
|
+
(nested) => nested.componentUid === componentUid
|
|
733
|
+
);
|
|
734
|
+
if (isUsed) {
|
|
735
|
+
usedIn.push({
|
|
736
|
+
componentUid: parentUid,
|
|
737
|
+
displayName: parentComponent.info?.displayName || parentComponent.modelName,
|
|
738
|
+
category: parentComponent.category,
|
|
739
|
+
attributes: nestedComponents.filter((nested) => nested.componentUid === componentUid).map((nested) => ({
|
|
740
|
+
name: nested.attributeName,
|
|
741
|
+
repeatable: nested.repeatable,
|
|
742
|
+
isDynamicZone: nested.isDynamicZone
|
|
743
|
+
}))
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
return usedIn;
|
|
748
|
+
},
|
|
749
|
+
getDependencyGraph() {
|
|
750
|
+
return buildDependencyGraph(strapi.components);
|
|
751
|
+
},
|
|
752
|
+
getComponentWithRelationships(componentUid) {
|
|
753
|
+
const component = strapi.components[componentUid];
|
|
754
|
+
if (!component) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
uid: componentUid,
|
|
759
|
+
displayName: component.info?.displayName || component.modelName,
|
|
760
|
+
category: component.category,
|
|
761
|
+
attributes: component.attributes,
|
|
762
|
+
uses: this.getComponentDependencies(componentUid),
|
|
763
|
+
usedIn: this.getComponentUsedIn(componentUid)
|
|
764
|
+
};
|
|
765
|
+
},
|
|
766
|
+
async getTotalUsageCount(componentUid) {
|
|
767
|
+
const componentUsageService = strapi.plugin("component-usage").service("component-usage");
|
|
768
|
+
const directUsageCount = await componentUsageService.getComponentUsageCount(componentUid);
|
|
769
|
+
const usedInComponents = this.getComponentUsedIn(componentUid);
|
|
770
|
+
return {
|
|
771
|
+
directUsage: directUsageCount,
|
|
772
|
+
indirectUsage: usedInComponents.length,
|
|
773
|
+
totalUsage: directUsageCount + usedInComponents.length,
|
|
774
|
+
usedInComponents
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
const services = {
|
|
779
|
+
"component-usage": componentUsage,
|
|
780
|
+
"usage-tracker": usageTracker,
|
|
781
|
+
"component-relationships": componentRelationships
|
|
782
|
+
};
|
|
783
|
+
const kind = "collectionType";
|
|
784
|
+
const collectionName = "component_usage_tracker";
|
|
785
|
+
const info = {
|
|
786
|
+
singularName: "component-usage-tracker",
|
|
787
|
+
pluralName: "component-usage-trackers",
|
|
788
|
+
displayName: "Component Usage Tracker",
|
|
789
|
+
description: "Tracks component usage across content types"
|
|
790
|
+
};
|
|
791
|
+
const options = {
|
|
792
|
+
draftAndPublish: false
|
|
793
|
+
};
|
|
794
|
+
const pluginOptions = {
|
|
795
|
+
"content-manager": {
|
|
796
|
+
visible: false
|
|
797
|
+
},
|
|
798
|
+
"content-type-builder": {
|
|
799
|
+
visible: false
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
const attributes = {
|
|
803
|
+
componentUid: {
|
|
804
|
+
type: "string",
|
|
805
|
+
required: true,
|
|
806
|
+
unique: true
|
|
807
|
+
},
|
|
808
|
+
componentName: {
|
|
809
|
+
type: "string",
|
|
810
|
+
required: true
|
|
811
|
+
},
|
|
812
|
+
componentCategory: {
|
|
813
|
+
type: "string",
|
|
814
|
+
required: true
|
|
815
|
+
},
|
|
816
|
+
usageCount: {
|
|
817
|
+
type: "integer",
|
|
818
|
+
"default": 0,
|
|
819
|
+
required: true
|
|
820
|
+
},
|
|
821
|
+
usages: {
|
|
822
|
+
type: "json",
|
|
823
|
+
required: false,
|
|
824
|
+
"default": []
|
|
825
|
+
},
|
|
826
|
+
lastUpdated: {
|
|
827
|
+
type: "datetime"
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
const schema = {
|
|
831
|
+
kind,
|
|
832
|
+
collectionName,
|
|
833
|
+
info,
|
|
834
|
+
options,
|
|
835
|
+
pluginOptions,
|
|
836
|
+
attributes
|
|
837
|
+
};
|
|
838
|
+
const contentTypes = {
|
|
839
|
+
"component-usage-tracker": { schema }
|
|
840
|
+
};
|
|
841
|
+
const index = {
|
|
842
|
+
bootstrap,
|
|
843
|
+
controllers,
|
|
844
|
+
routes,
|
|
845
|
+
services,
|
|
846
|
+
contentTypes
|
|
847
|
+
};
|
|
848
|
+
export {
|
|
849
|
+
index as default
|
|
850
|
+
};
|