@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.
@@ -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
+ };