@contentful/experience-design-system-cli 2.11.4-dev-build-a9b4d34.0 → 2.11.4-dev-build-355c69b.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/package.json +1 -1
- package/dist/src/analyze/extract/svelte.js +98 -13
- package/package.json +2 -2
package/dist/package.json
CHANGED
|
@@ -71,6 +71,7 @@ async function extractFromSvelteFile(filePath, source) {
|
|
|
71
71
|
let props = [];
|
|
72
72
|
let propNamesTreatedAsSnippet = new Set();
|
|
73
73
|
let snippetSlotsFromProps = [];
|
|
74
|
+
const extractionReasons = [];
|
|
74
75
|
if (propsCall) {
|
|
75
76
|
const result = await extractPropsFromCall({
|
|
76
77
|
propsCall,
|
|
@@ -84,6 +85,8 @@ async function extractFromSvelteFile(filePath, source) {
|
|
|
84
85
|
propNamesTreatedAsSnippet = result.snippetNames;
|
|
85
86
|
snippetSlotsFromProps = result.snippetSlots;
|
|
86
87
|
warnings.push(...result.warnings);
|
|
88
|
+
if (result.additionalReasons)
|
|
89
|
+
extractionReasons.push(...result.additionalReasons);
|
|
87
90
|
}
|
|
88
91
|
else if (instance) {
|
|
89
92
|
// No $props() call. We've already ruled out v4 above; this means $props was used
|
|
@@ -107,11 +110,17 @@ async function extractFromSvelteFile(filePath, source) {
|
|
|
107
110
|
props,
|
|
108
111
|
slots,
|
|
109
112
|
};
|
|
110
|
-
// Score & flag review.
|
|
111
|
-
|
|
113
|
+
// Score & flag review. Forward extraction-time reasons (e.g. props-type-unresolved)
|
|
114
|
+
// so they count toward the confidence score AND surface in reviewReasons.
|
|
115
|
+
const score = computeExtractionScore(component, {
|
|
116
|
+
additionalIssueCount: extractionReasons.length,
|
|
117
|
+
additionalReasons: extractionReasons,
|
|
118
|
+
});
|
|
112
119
|
component.extractionConfidence = score.confidence;
|
|
113
120
|
component.reviewReasons = score.reasons;
|
|
114
|
-
|
|
121
|
+
// A type-resolution failure is a strong signal something is wrong; force
|
|
122
|
+
// human review even when other heuristics keep confidence above the threshold.
|
|
123
|
+
component.needsReview = deriveNeedsReview(score.confidence) || extractionReasons.includes('props-type-unresolved');
|
|
115
124
|
// Validation issues (EMPTY_COMPONENT_NAME / EMPTY_PROP_NAME / PROP_SLOT_NAME_COLLISION /
|
|
116
125
|
// DUPLICATE_COMPONENT_NAME / EMPTY_COMPONENT / EMPTY_SLOT_NAME) are populated
|
|
117
126
|
// centrally by validateExtractedComponents() in analyze/command.ts after all
|
|
@@ -123,12 +132,23 @@ async function extractFromSvelteFile(filePath, source) {
|
|
|
123
132
|
// ---------------------------------------------------------------------------
|
|
124
133
|
// Component name
|
|
125
134
|
// ---------------------------------------------------------------------------
|
|
135
|
+
// Folder names that are pure scaffolding (anatomy / parts conventions popularized
|
|
136
|
+
// by Ark UI, Zag, Skeleton). When a component file sits directly inside one of
|
|
137
|
+
// these, the meaningful namespace is the grandparent directory.
|
|
138
|
+
const ANATOMY_FOLDERS = new Set(['anatomy', 'parts']);
|
|
126
139
|
function getSvelteComponentName(filePath) {
|
|
127
140
|
const file = basename(filePath, '.svelte');
|
|
141
|
+
const parentDir = basename(dirname(filePath));
|
|
128
142
|
// index.svelte → use parent directory name (mirrors index.vue behavior).
|
|
129
|
-
if (file === 'index')
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
if (file === 'index')
|
|
144
|
+
return toPascalCase(parentDir);
|
|
145
|
+
// accordion/anatomy/root.svelte → AccordionRoot (avoids massive collisions
|
|
146
|
+
// when a single library has dozens of components named Root/Item/Trigger).
|
|
147
|
+
if (ANATOMY_FOLDERS.has(parentDir)) {
|
|
148
|
+
const grandparent = basename(dirname(dirname(filePath)));
|
|
149
|
+
if (grandparent && grandparent !== '.' && grandparent !== '/') {
|
|
150
|
+
return `${toPascalCase(grandparent)}${toPascalCase(file)}`;
|
|
151
|
+
}
|
|
132
152
|
}
|
|
133
153
|
return toPascalCase(file);
|
|
134
154
|
}
|
|
@@ -220,19 +240,83 @@ async function extractPropsFromCall(ctx) {
|
|
|
220
240
|
const typeMembers = annotation
|
|
221
241
|
? await resolveTypeMembers(annotation, ctx.instance, ctx.moduleScript, ctx.filePath, ctx.source)
|
|
222
242
|
: null;
|
|
243
|
+
// Detect "we tried to resolve a real type and got nothing back" — distinct from
|
|
244
|
+
// the user genuinely typing an empty literal. Surface as both a warning (CLI
|
|
245
|
+
// stderr / analyze report) and a review reason (TUI / downstream gating).
|
|
246
|
+
const unresolved = classifyUnresolved(annotation, typeMembers, ctx.instance, ctx.moduleScript);
|
|
247
|
+
const additionalReasons = [];
|
|
248
|
+
if (unresolved) {
|
|
249
|
+
const refLabel = describeAnnotationForUser(annotation);
|
|
250
|
+
const heritageNote = unresolved === 'partial-heritage' ? ' (heritage clauses extending unreachable types)' : '';
|
|
251
|
+
warnings.push(`${ctx.filePath}: declared Props type ${refLabel} resolved to ${unresolved === 'empty' ? '0' : 'only Snippet-typed'} properties${heritageNote} — possible cross-package extends or unreachable type. ` +
|
|
252
|
+
`See https://github.com/contentful/experience-design-system-sdk-public/pull/44 for context and partner workarounds.`);
|
|
253
|
+
additionalReasons.push('props-type-unresolved');
|
|
254
|
+
}
|
|
223
255
|
if (idType === 'ObjectPattern') {
|
|
224
|
-
return extractFromDestructure(ctx.propsCall, ctx, typeMembers, warnings);
|
|
256
|
+
return extractFromDestructure(ctx.propsCall, ctx, typeMembers, warnings, additionalReasons);
|
|
225
257
|
}
|
|
226
258
|
if (idType === 'Identifier') {
|
|
227
259
|
// const props: Props = $props(); — no destructure, no defaults, no per-name binding.
|
|
228
|
-
if (typeMembers) {
|
|
229
|
-
return extractFromTypeMembersOnly(typeMembers);
|
|
260
|
+
if (typeMembers && typeMembers.length > 0) {
|
|
261
|
+
return { ...extractFromTypeMembersOnly(typeMembers), warnings, additionalReasons };
|
|
230
262
|
}
|
|
231
|
-
|
|
232
|
-
|
|
263
|
+
if (!unresolved) {
|
|
264
|
+
warnings.push(`${ctx.filePath}: $props() called without destructuring; cannot extract individual props`);
|
|
265
|
+
}
|
|
266
|
+
return { props: [], snippetNames: new Set(), snippetSlots: [], warnings, additionalReasons };
|
|
233
267
|
}
|
|
234
268
|
warnings.push(`${ctx.filePath}: unrecognized $props() binding pattern (${idType})`);
|
|
235
|
-
return { props: [], snippetNames: new Set(), snippetSlots: [], warnings };
|
|
269
|
+
return { props: [], snippetNames: new Set(), snippetSlots: [], warnings, additionalReasons };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Classify whether the user's declared Props type failed to resolve usefully.
|
|
273
|
+
*
|
|
274
|
+
* - `'empty'`: the resolver returned no members for a non-trivial annotation.
|
|
275
|
+
* - `'partial-heritage'`: the source declaration has heritage clauses (extends /
|
|
276
|
+
* intersection in module-script) but the resolver only surfaced Snippet-typed
|
|
277
|
+
* members — strong signal that one or more parents pointed at unreachable
|
|
278
|
+
* types (e.g. cross-package node_modules) and were silently dropped.
|
|
279
|
+
* - `null`: nothing to surface — either the user genuinely typed an empty
|
|
280
|
+
* literal, omitted the annotation, or the resolver returned regular props.
|
|
281
|
+
*/
|
|
282
|
+
function classifyUnresolved(annotation, members, instance, moduleScript) {
|
|
283
|
+
if (!annotation)
|
|
284
|
+
return null;
|
|
285
|
+
if (annotation.type === 'TSTypeLiteral') {
|
|
286
|
+
const litMembers = annotation['members'] ?? [];
|
|
287
|
+
if (litMembers.length === 0)
|
|
288
|
+
return null; // user genuinely typed `{}`
|
|
289
|
+
}
|
|
290
|
+
if (members === null || members.length === 0)
|
|
291
|
+
return 'empty';
|
|
292
|
+
// Partial-heritage signal: declaration has extends and every surviving member
|
|
293
|
+
// is Snippet-typed. Real prop interfaces almost never declare every prop as a
|
|
294
|
+
// Snippet, so this is a strong "something fell off the resolution edge" hint.
|
|
295
|
+
if (annotation.type === 'TSTypeReference') {
|
|
296
|
+
const refName = annotation['typeName']?.['name'] ?? null;
|
|
297
|
+
if (refName) {
|
|
298
|
+
const decl = findLocalTypeDeclaration(instance, refName, moduleScript);
|
|
299
|
+
if (decl && declarationHasHeritage(decl) && members.every((m) => m.isSnippet)) {
|
|
300
|
+
return 'partial-heritage';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
function describeAnnotationForUser(annotation) {
|
|
307
|
+
if (!annotation)
|
|
308
|
+
return '<unknown>';
|
|
309
|
+
if (annotation.type === 'TSTypeReference') {
|
|
310
|
+
const name = annotation['typeName']?.['name'] ?? null;
|
|
311
|
+
return name ? `'${name}'` : '<unnamed reference>';
|
|
312
|
+
}
|
|
313
|
+
if (annotation.type === 'TSIntersectionType')
|
|
314
|
+
return '<intersection>';
|
|
315
|
+
if (annotation.type === 'TSUnionType')
|
|
316
|
+
return '<union>';
|
|
317
|
+
if (annotation.type === 'TSTypeLiteral')
|
|
318
|
+
return '<inline literal>';
|
|
319
|
+
return `<${annotation.type}>`;
|
|
236
320
|
}
|
|
237
321
|
async function resolveTypeMembers(annotation, instance, moduleScript, filePath, source) {
|
|
238
322
|
// Snippet imports may live in either script block; collect from both.
|
|
@@ -569,7 +653,7 @@ function extractJsdocText(comments) {
|
|
|
569
653
|
// ---------------------------------------------------------------------------
|
|
570
654
|
// Extraction: ObjectPattern destructure + type-members
|
|
571
655
|
// ---------------------------------------------------------------------------
|
|
572
|
-
function extractFromDestructure(propsCall, ctx, typeMembers, warnings) {
|
|
656
|
+
function extractFromDestructure(propsCall, ctx, typeMembers, warnings, additionalReasons) {
|
|
573
657
|
const id = propsCall['id'];
|
|
574
658
|
const properties = id['properties'] ?? [];
|
|
575
659
|
const typeByName = new Map();
|
|
@@ -633,6 +717,7 @@ function extractFromDestructure(propsCall, ctx, typeMembers, warnings) {
|
|
|
633
717
|
snippetNames,
|
|
634
718
|
snippetSlots,
|
|
635
719
|
warnings,
|
|
720
|
+
...(additionalReasons && additionalReasons.length > 0 ? { additionalReasons } : {}),
|
|
636
721
|
};
|
|
637
722
|
}
|
|
638
723
|
function extractFromTypeMembersOnly(typeMembers) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.11.4-dev-build-
|
|
3
|
+
"version": "2.11.4-dev-build-355c69b.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"svelte": "^5.56.4",
|
|
38
38
|
"ts-morph": "^27.0.2",
|
|
39
39
|
"typescript": "^5.9.3",
|
|
40
|
-
"@contentful/experience-design-system-types": "2.11.4-dev-build-
|
|
40
|
+
"@contentful/experience-design-system-types": "2.11.4-dev-build-355c69b.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@tsconfig/node24": "^24.0.3",
|