arcvision 0.2.0 → 0.2.2
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/arcvision.context.json +7810 -61
- package/dist/index.js +321 -37
- package/package.json +1 -1
- package/scan_output.txt +0 -0
- package/src/core/parser.js +42 -1
- package/src/core/path-resolver.js +9 -9
- package/src/core/scanner.js +323 -26
- package/src/engine/context_builder.js +34 -9
- package/src/plugins/plugin-manager.js +2 -1
package/src/core/scanner.js
CHANGED
|
@@ -35,6 +35,7 @@ async function scan(directory) {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const fileMap = new Map();
|
|
38
|
+
let totalImportsFound = 0; // Track total imports found
|
|
38
39
|
|
|
39
40
|
// Process files with plugins
|
|
40
41
|
for (const file of files) {
|
|
@@ -70,6 +71,11 @@ async function scan(directory) {
|
|
|
70
71
|
|
|
71
72
|
// Process with plugins
|
|
72
73
|
metadata = await pluginManager.processFile(file, metadata);
|
|
74
|
+
|
|
75
|
+
// Count imports found in this file
|
|
76
|
+
if (metadata.imports && Array.isArray(metadata.imports)) {
|
|
77
|
+
totalImportsFound += metadata.imports.length;
|
|
78
|
+
}
|
|
73
79
|
|
|
74
80
|
const node = {
|
|
75
81
|
id: normalizedRelativePath,
|
|
@@ -88,35 +94,293 @@ async function scan(directory) {
|
|
|
88
94
|
// Load tsconfig for path resolution
|
|
89
95
|
const tsconfig = loadTSConfig(directory);
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
// Create a mapping of all possible normalized paths that could match imports
|
|
98
|
+
// This includes both the direct file paths and possible variations
|
|
99
|
+
const allPossiblePaths = new Set();
|
|
100
|
+
const pathToNodeIdMap = new Map(); // This maps normalized paths to node IDs
|
|
101
|
+
|
|
94
102
|
architectureMap.nodes.forEach(node => {
|
|
95
|
-
node.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
const normalizedPath = normalize(node.id);
|
|
104
|
+
allPossiblePaths.add(normalizedPath);
|
|
105
|
+
pathToNodeIdMap.set(normalizedPath, node.id);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Process imports to create edges
|
|
109
|
+
let unresolvedImports = 0;
|
|
110
|
+
let resolvedImports = 0;
|
|
111
|
+
architectureMap.nodes.forEach(node => {
|
|
112
|
+
if (node.metadata.imports && Array.isArray(node.metadata.imports)) {
|
|
113
|
+
node.metadata.imports.forEach(imp => {
|
|
114
|
+
if (imp.source && typeof imp.source === 'string') {
|
|
115
|
+
let targetFound = false;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// First, check if imp.source directly matches any node path (exact match)
|
|
119
|
+
if (allPossiblePaths.has(imp.source)) {
|
|
120
|
+
architectureMap.edges.push({
|
|
121
|
+
source: normalize(node.id),
|
|
122
|
+
target: imp.source,
|
|
123
|
+
type: imp.type || 'import'
|
|
124
|
+
});
|
|
125
|
+
resolvedImports++;
|
|
126
|
+
targetFound = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!targetFound) {
|
|
130
|
+
// Try to resolve the import path using the resolver
|
|
131
|
+
const resolvedPath = resolveImport(
|
|
132
|
+
imp.source,
|
|
133
|
+
path.join(directory, node.id),
|
|
134
|
+
directory,
|
|
135
|
+
tsconfig
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (resolvedPath) {
|
|
139
|
+
// Convert resolved absolute path back to relative path
|
|
140
|
+
const relativeResolvedPath = path.relative(directory, resolvedPath);
|
|
141
|
+
const normalizedResolvedPath = normalize(relativeResolvedPath);
|
|
142
|
+
|
|
143
|
+
// Check if the resolved file exists in our scanned files
|
|
144
|
+
if (allPossiblePaths.has(normalizedResolvedPath)) {
|
|
145
|
+
architectureMap.edges.push({
|
|
146
|
+
source: normalize(node.id),
|
|
147
|
+
target: normalizedResolvedPath,
|
|
148
|
+
type: imp.type || 'import'
|
|
149
|
+
});
|
|
150
|
+
resolvedImports++;
|
|
151
|
+
targetFound = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!targetFound) {
|
|
157
|
+
// As a fallback, check if the import path could match by simple relative path calculation
|
|
158
|
+
// For example, if node is 'src/core/scanner.js' and import is './parser.js',
|
|
159
|
+
// it should resolve to 'src/core/parser.js'
|
|
160
|
+
try {
|
|
161
|
+
const baseDir = path.dirname(path.join(directory, node.id));
|
|
162
|
+
const calculatedAbsolutePath = path.resolve(baseDir, imp.source);
|
|
163
|
+
const calculatedRelativePath = path.relative(directory, calculatedAbsolutePath);
|
|
164
|
+
const calculatedNormalizedPath = normalize(calculatedRelativePath);
|
|
165
|
+
|
|
166
|
+
if (allPossiblePaths.has(calculatedNormalizedPath)) {
|
|
167
|
+
architectureMap.edges.push({
|
|
168
|
+
source: normalize(node.id),
|
|
169
|
+
target: calculatedNormalizedPath,
|
|
170
|
+
type: imp.type || 'import'
|
|
171
|
+
});
|
|
172
|
+
resolvedImports++;
|
|
173
|
+
targetFound = true;
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
console.warn(`⚠️ Path calculation failed for import '${imp.source}' in file '${node.id}': ${e.message}`);
|
|
177
|
+
// If path calculation fails, continue to unresolved
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!targetFound) {
|
|
182
|
+
// Another fallback: if import starts with ./ or ../, try to find a match
|
|
183
|
+
// by appending common extensions to the import path
|
|
184
|
+
if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
|
|
185
|
+
// Try various extensions that might match
|
|
186
|
+
const extensions = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
|
|
187
|
+
|
|
188
|
+
for (const ext of extensions) {
|
|
189
|
+
let testPath = imp.source;
|
|
190
|
+
if (!imp.source.endsWith(ext)) {
|
|
191
|
+
testPath = imp.source + ext;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Calculate the relative path from the project directory
|
|
195
|
+
try {
|
|
196
|
+
const baseDir = path.dirname(path.join(directory, node.id));
|
|
197
|
+
const calculatedAbsolutePath = path.resolve(baseDir, testPath);
|
|
198
|
+
const calculatedRelativePath = path.relative(directory, calculatedAbsolutePath);
|
|
199
|
+
const calculatedNormalizedPath = normalize(calculatedRelativePath);
|
|
200
|
+
|
|
201
|
+
if (allPossiblePaths.has(calculatedNormalizedPath)) {
|
|
202
|
+
architectureMap.edges.push({
|
|
203
|
+
source: normalize(node.id),
|
|
204
|
+
target: calculatedNormalizedPath,
|
|
205
|
+
type: imp.type || 'import'
|
|
206
|
+
});
|
|
207
|
+
resolvedImports++;
|
|
208
|
+
targetFound = true;
|
|
209
|
+
break; // Found a match, exit the loop
|
|
210
|
+
}
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.warn(`⚠️ Extension path calculation failed for import '${imp.source}' with extension '${ext}' in file '${node.id}': ${e.message}`);
|
|
213
|
+
// Continue to next extension if path calculation fails
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!targetFound) {
|
|
220
|
+
// Additional fallback: Check for index files when importing a directory
|
|
221
|
+
if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
|
|
222
|
+
try {
|
|
223
|
+
const baseDir = path.dirname(path.join(directory, node.id));
|
|
224
|
+
const calculatedAbsolutePath = path.resolve(baseDir, imp.source);
|
|
225
|
+
const directoryPath = calculatedAbsolutePath;
|
|
226
|
+
|
|
227
|
+
// Check for index files in the directory
|
|
228
|
+
const indexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
|
|
229
|
+
for (const indexFile of indexFiles) {
|
|
230
|
+
const indexPath = path.join(directoryPath, indexFile);
|
|
231
|
+
const indexRelativePath = path.relative(directory, indexPath);
|
|
232
|
+
const indexNormalizedPath = normalize(indexRelativePath);
|
|
233
|
+
|
|
234
|
+
if (allPossiblePaths.has(indexNormalizedPath)) {
|
|
235
|
+
architectureMap.edges.push({
|
|
236
|
+
source: normalize(node.id),
|
|
237
|
+
target: indexNormalizedPath,
|
|
238
|
+
type: imp.type || 'import'
|
|
239
|
+
});
|
|
240
|
+
resolvedImports++;
|
|
241
|
+
targetFound = true;
|
|
242
|
+
break; // Found a match, exit the loop
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.warn(`⚠️ Index file check failed for import '${imp.source}' in file '${node.id}': ${e.message}`);
|
|
247
|
+
// Continue if index file check fails
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!targetFound) {
|
|
253
|
+
// Additional fallback: Check for absolute imports that might map to common directories
|
|
254
|
+
// For example, if tsconfig has paths like "@/*": ["src/*"], an import like "@/utils/helper"
|
|
255
|
+
// should resolve to "src/utils/helper.js" or similar
|
|
256
|
+
if (imp.source.startsWith('@') || imp.source.startsWith('/') || !imp.source.startsWith('.')) {
|
|
257
|
+
// Try to match against common source directories
|
|
258
|
+
const commonSourceDirs = ['src', 'app', 'lib', 'components', 'utils', 'services', 'assets'];
|
|
259
|
+
|
|
260
|
+
for (const srcDir of commonSourceDirs) {
|
|
261
|
+
// Try appending to common source directories
|
|
262
|
+
const potentialPath = path.join(srcDir, imp.source);
|
|
263
|
+
const potentialRelativePath = path.relative(directory, path.resolve(directory, potentialPath));
|
|
264
|
+
const potentialNormalizedPath = normalize(potentialRelativePath);
|
|
265
|
+
|
|
266
|
+
if (allPossiblePaths.has(potentialNormalizedPath)) {
|
|
267
|
+
architectureMap.edges.push({
|
|
268
|
+
source: normalize(node.id),
|
|
269
|
+
target: potentialNormalizedPath,
|
|
270
|
+
type: imp.type || 'import'
|
|
271
|
+
});
|
|
272
|
+
resolvedImports++;
|
|
273
|
+
targetFound = true;
|
|
274
|
+
break; // Found a match, exit the loop
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Also try with extensions
|
|
278
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
|
|
279
|
+
'index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
|
|
280
|
+
|
|
281
|
+
for (const ext of extensions) {
|
|
282
|
+
let testPath;
|
|
283
|
+
if (ext.startsWith('index')) {
|
|
284
|
+
testPath = path.join(srcDir, imp.source, ext);
|
|
285
|
+
} else {
|
|
286
|
+
testPath = path.join(srcDir, imp.source + ext);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const testRelativePath = path.relative(directory, path.resolve(directory, testPath));
|
|
290
|
+
const testNormalizedPath = normalize(testRelativePath);
|
|
291
|
+
|
|
292
|
+
if (allPossiblePaths.has(testNormalizedPath)) {
|
|
293
|
+
architectureMap.edges.push({
|
|
294
|
+
source: normalize(node.id),
|
|
295
|
+
target: testNormalizedPath,
|
|
296
|
+
type: imp.type || 'import'
|
|
297
|
+
});
|
|
298
|
+
resolvedImports++;
|
|
299
|
+
targetFound = true;
|
|
300
|
+
break; // Found a match, exit the loops
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (targetFound) break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!targetFound) {
|
|
310
|
+
// Additional fallback: Try to handle barrel files (index.js that exports from other files)
|
|
311
|
+
// If import is '@/components/Header' but only '@/components/index.js' exists
|
|
312
|
+
if (imp.source.startsWith('@') || imp.source.startsWith('/')) {
|
|
313
|
+
try {
|
|
314
|
+
// Try to find a directory with an index file that matches
|
|
315
|
+
const parts = imp.source.split('/');
|
|
316
|
+
if (parts.length > 1) {
|
|
317
|
+
// Remove the last part and add index
|
|
318
|
+
const dirParts = parts.slice(0, -1);
|
|
319
|
+
const fileName = parts[parts.length - 1];
|
|
320
|
+
|
|
321
|
+
// Look for patterns like: components/index.js exporting { Header }
|
|
322
|
+
// or components/index.js containing export * from './Header'
|
|
323
|
+
const dirPath = dirParts.join('/') + '/index';
|
|
324
|
+
const indexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
|
|
325
|
+
|
|
326
|
+
for (const indexFile of indexFiles) {
|
|
327
|
+
const indexPath = path.join(dirParts.join('/'), indexFile);
|
|
328
|
+
const indexRelativePath = path.relative(directory, path.resolve(directory, indexPath));
|
|
329
|
+
const indexNormalizedPath = normalize(indexRelativePath);
|
|
330
|
+
|
|
331
|
+
if (allPossiblePaths.has(indexNormalizedPath)) {
|
|
332
|
+
architectureMap.edges.push({
|
|
333
|
+
source: normalize(node.id),
|
|
334
|
+
target: indexNormalizedPath,
|
|
335
|
+
type: imp.type || 'import'
|
|
336
|
+
});
|
|
337
|
+
resolvedImports++;
|
|
338
|
+
targetFound = true;
|
|
339
|
+
break; // Found a match, exit the loop
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.warn(`⚠️ Barrel file check failed for import '${imp.source}' in file '${node.id}': ${e.message}`);
|
|
345
|
+
// Continue if barrel file check fails
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!targetFound) {
|
|
351
|
+
// Additional fallback: Check for exact path matches with different capitalization
|
|
352
|
+
// This handles cases where import uses different casing than actual file
|
|
353
|
+
for (const possiblePath of allPossiblePaths) {
|
|
354
|
+
if (possiblePath.toLowerCase() === imp.source.toLowerCase()) {
|
|
355
|
+
architectureMap.edges.push({
|
|
356
|
+
source: normalize(node.id),
|
|
357
|
+
target: possiblePath,
|
|
358
|
+
type: imp.type || 'import'
|
|
359
|
+
});
|
|
360
|
+
resolvedImports++;
|
|
361
|
+
targetFound = true;
|
|
362
|
+
break; // Found a case-insensitive match
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch (e) {
|
|
367
|
+
console.error(`❌ Critical error processing import '${imp.source}' in file '${node.id}': ${e.message}`);
|
|
368
|
+
console.error(e.stack);
|
|
369
|
+
// Even if there's an error processing this import, increment unresolved counter
|
|
370
|
+
unresolvedImports++;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!targetFound) {
|
|
374
|
+
// Track imports that couldn't be matched to existing nodes
|
|
375
|
+
unresolvedImports++;
|
|
376
|
+
}
|
|
116
377
|
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
119
380
|
});
|
|
381
|
+
console.log('RESOLVED IMPORTS:', resolvedImports);
|
|
382
|
+
console.log('UNRESOLVED IMPORTS:', unresolvedImports);
|
|
383
|
+
console.log('EDGES CREATED:', architectureMap.edges.length);
|
|
120
384
|
|
|
121
385
|
// Calculate blast radius for each file
|
|
122
386
|
const reverseGraph = buildReverseDependencyGraph(architectureMap);
|
|
@@ -127,6 +391,39 @@ async function scan(directory) {
|
|
|
127
391
|
node.metadata.blast_radius = blastRadiusMap[node.id] || 0;
|
|
128
392
|
});
|
|
129
393
|
|
|
394
|
+
console.log('BEFORE DEDUPLICATION EDGES:', architectureMap.edges.length);
|
|
395
|
+
|
|
396
|
+
// Process edges to ensure they match existing nodes and remove duplicates
|
|
397
|
+
const validEdges = [];
|
|
398
|
+
const edgeSet = new Set(); // To track unique edges
|
|
399
|
+
|
|
400
|
+
architectureMap.edges.forEach(edge => {
|
|
401
|
+
// Normalize both source and target paths for consistent matching
|
|
402
|
+
const normalizedSource = normalize(edge.source);
|
|
403
|
+
const normalizedTarget = normalize(edge.target);
|
|
404
|
+
|
|
405
|
+
// Create a unique key for the edge to avoid duplicates
|
|
406
|
+
const edgeKey = `${normalizedSource}→${normalizedTarget}`;
|
|
407
|
+
|
|
408
|
+
// Check if both source and target nodes exist in our architecture map
|
|
409
|
+
const sourceNodeExists = architectureMap.nodes.some(node => node.id === normalizedSource);
|
|
410
|
+
const targetNodeExists = architectureMap.nodes.some(node => node.id === normalizedTarget);
|
|
411
|
+
|
|
412
|
+
if (sourceNodeExists && targetNodeExists && !edgeSet.has(edgeKey)) {
|
|
413
|
+
edgeSet.add(edgeKey);
|
|
414
|
+
validEdges.push({
|
|
415
|
+
source: normalizedSource,
|
|
416
|
+
target: normalizedTarget,
|
|
417
|
+
type: edge.type
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
console.log('AFTER DEDUPLICATION EDGES:', validEdges.length);
|
|
423
|
+
|
|
424
|
+
// Replace the edges with only valid ones
|
|
425
|
+
architectureMap.edges = validEdges;
|
|
426
|
+
|
|
130
427
|
// Add top blast radius files to the architecture map
|
|
131
428
|
const totalFiles = architectureMap.nodes.length;
|
|
132
429
|
const { computeBlastRadiusWithPercentage, analyzeCriticality } = require('./blastRadius');
|
|
@@ -33,12 +33,17 @@ function buildContext(files, edges, options = {}) {
|
|
|
33
33
|
role = 'Interface';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// Extract dependencies more accurately
|
|
36
|
+
// Extract dependencies more accurately and remove duplicates
|
|
37
37
|
const dependencies = [];
|
|
38
|
+
const uniqueDeps = new Set();
|
|
38
39
|
if (file.metadata.imports && Array.isArray(file.metadata.imports)) {
|
|
39
40
|
file.metadata.imports.forEach(imp => {
|
|
40
41
|
if (imp.source && typeof imp.source === 'string') {
|
|
41
|
-
dependencies
|
|
42
|
+
// Only add unique dependencies
|
|
43
|
+
if (!uniqueDeps.has(imp.source)) {
|
|
44
|
+
uniqueDeps.add(imp.source);
|
|
45
|
+
dependencies.push(imp.source);
|
|
46
|
+
}
|
|
42
47
|
}
|
|
43
48
|
});
|
|
44
49
|
}
|
|
@@ -60,13 +65,33 @@ function buildContext(files, edges, options = {}) {
|
|
|
60
65
|
const sourceNode = nodes.find(n => n.path === edge.source);
|
|
61
66
|
const targetNode = nodes.find(n => n.path === edge.target);
|
|
62
67
|
|
|
63
|
-
//
|
|
64
|
-
if (sourceNode
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
})
|
|
68
|
+
// Include edges where source exists (even if target doesn't exist as a node)
|
|
69
|
+
if (sourceNode) {
|
|
70
|
+
// Normalize the edge type to fit the schema
|
|
71
|
+
let relationType = 'imports';
|
|
72
|
+
if (edge.type === 'imports' || edge.type === 'require' || edge.type === 'export-from' || edge.type === 'export-all' || edge.type === 'dynamic-import' || edge.type === 'require-assignment') {
|
|
73
|
+
relationType = 'imports';
|
|
74
|
+
} else if (edge.type === 'calls') {
|
|
75
|
+
relationType = 'calls';
|
|
76
|
+
} else if (edge.type === 'owns') {
|
|
77
|
+
relationType = 'owns';
|
|
78
|
+
} else if (edge.type === 'depends_on') {
|
|
79
|
+
relationType = 'depends_on';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (targetNode) {
|
|
83
|
+
// Both source and target nodes exist
|
|
84
|
+
schemaEdges.push({
|
|
85
|
+
from: sourceNode.id,
|
|
86
|
+
to: targetNode.id,
|
|
87
|
+
relation: relationType
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
// Source exists but target doesn't (external dependency like node_modules)
|
|
91
|
+
// We'll still include the edge to preserve the relationship
|
|
92
|
+
// However, since the schema requires valid node IDs, we'll only add if the target is a known node
|
|
93
|
+
// For now, we'll skip edges to non-existent targets to maintain schema compliance
|
|
94
|
+
}
|
|
70
95
|
}
|
|
71
96
|
}
|
|
72
97
|
|
|
@@ -27,7 +27,8 @@ class PluginManager {
|
|
|
27
27
|
enhancedMetadata = { ...enhancedMetadata, ...result };
|
|
28
28
|
}
|
|
29
29
|
} catch (error) {
|
|
30
|
-
console.warn(`Plugin ${plugin.name} failed: ${error.message}`);
|
|
30
|
+
console.warn(`Plugin ${plugin.name} failed: ${error.message || error}`);
|
|
31
|
+
// Continue processing with original metadata if plugin fails
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|