@contentful/experience-design-system-cli 2.11.4-dev-build-a9b4d34.0 → 2.11.4-dev-build-8cd3b18.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.11.4-dev-build-a9b4d34.0",
3
+ "version": "2.11.4-dev-build-8cd3b18.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- const score = computeExtractionScore(component);
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
- component.needsReview = deriveNeedsReview(score.confidence);
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
@@ -220,19 +229,83 @@ async function extractPropsFromCall(ctx) {
220
229
  const typeMembers = annotation
221
230
  ? await resolveTypeMembers(annotation, ctx.instance, ctx.moduleScript, ctx.filePath, ctx.source)
222
231
  : null;
232
+ // Detect "we tried to resolve a real type and got nothing back" — distinct from
233
+ // the user genuinely typing an empty literal. Surface as both a warning (CLI
234
+ // stderr / analyze report) and a review reason (TUI / downstream gating).
235
+ const unresolved = classifyUnresolved(annotation, typeMembers, ctx.instance, ctx.moduleScript);
236
+ const additionalReasons = [];
237
+ if (unresolved) {
238
+ const refLabel = describeAnnotationForUser(annotation);
239
+ const heritageNote = unresolved === 'partial-heritage' ? ' (heritage clauses extending unreachable types)' : '';
240
+ 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. ` +
241
+ `See https://github.com/contentful/experience-design-system-sdk-public/pull/44 for context and partner workarounds.`);
242
+ additionalReasons.push('props-type-unresolved');
243
+ }
223
244
  if (idType === 'ObjectPattern') {
224
- return extractFromDestructure(ctx.propsCall, ctx, typeMembers, warnings);
245
+ return extractFromDestructure(ctx.propsCall, ctx, typeMembers, warnings, additionalReasons);
225
246
  }
226
247
  if (idType === 'Identifier') {
227
248
  // const props: Props = $props(); — no destructure, no defaults, no per-name binding.
228
- if (typeMembers) {
229
- return extractFromTypeMembersOnly(typeMembers);
249
+ if (typeMembers && typeMembers.length > 0) {
250
+ return { ...extractFromTypeMembersOnly(typeMembers), warnings, additionalReasons };
230
251
  }
231
- warnings.push(`${ctx.filePath}: $props() called without destructuring; cannot extract individual props`);
232
- return { props: [], snippetNames: new Set(), snippetSlots: [], warnings };
252
+ if (!unresolved) {
253
+ warnings.push(`${ctx.filePath}: $props() called without destructuring; cannot extract individual props`);
254
+ }
255
+ return { props: [], snippetNames: new Set(), snippetSlots: [], warnings, additionalReasons };
233
256
  }
234
257
  warnings.push(`${ctx.filePath}: unrecognized $props() binding pattern (${idType})`);
235
- return { props: [], snippetNames: new Set(), snippetSlots: [], warnings };
258
+ return { props: [], snippetNames: new Set(), snippetSlots: [], warnings, additionalReasons };
259
+ }
260
+ /**
261
+ * Classify whether the user's declared Props type failed to resolve usefully.
262
+ *
263
+ * - `'empty'`: the resolver returned no members for a non-trivial annotation.
264
+ * - `'partial-heritage'`: the source declaration has heritage clauses (extends /
265
+ * intersection in module-script) but the resolver only surfaced Snippet-typed
266
+ * members — strong signal that one or more parents pointed at unreachable
267
+ * types (e.g. cross-package node_modules) and were silently dropped.
268
+ * - `null`: nothing to surface — either the user genuinely typed an empty
269
+ * literal, omitted the annotation, or the resolver returned regular props.
270
+ */
271
+ function classifyUnresolved(annotation, members, instance, moduleScript) {
272
+ if (!annotation)
273
+ return null;
274
+ if (annotation.type === 'TSTypeLiteral') {
275
+ const litMembers = annotation['members'] ?? [];
276
+ if (litMembers.length === 0)
277
+ return null; // user genuinely typed `{}`
278
+ }
279
+ if (members === null || members.length === 0)
280
+ return 'empty';
281
+ // Partial-heritage signal: declaration has extends and every surviving member
282
+ // is Snippet-typed. Real prop interfaces almost never declare every prop as a
283
+ // Snippet, so this is a strong "something fell off the resolution edge" hint.
284
+ if (annotation.type === 'TSTypeReference') {
285
+ const refName = annotation['typeName']?.['name'] ?? null;
286
+ if (refName) {
287
+ const decl = findLocalTypeDeclaration(instance, refName, moduleScript);
288
+ if (decl && declarationHasHeritage(decl) && members.every((m) => m.isSnippet)) {
289
+ return 'partial-heritage';
290
+ }
291
+ }
292
+ }
293
+ return null;
294
+ }
295
+ function describeAnnotationForUser(annotation) {
296
+ if (!annotation)
297
+ return '<unknown>';
298
+ if (annotation.type === 'TSTypeReference') {
299
+ const name = annotation['typeName']?.['name'] ?? null;
300
+ return name ? `'${name}'` : '<unnamed reference>';
301
+ }
302
+ if (annotation.type === 'TSIntersectionType')
303
+ return '<intersection>';
304
+ if (annotation.type === 'TSUnionType')
305
+ return '<union>';
306
+ if (annotation.type === 'TSTypeLiteral')
307
+ return '<inline literal>';
308
+ return `<${annotation.type}>`;
236
309
  }
237
310
  async function resolveTypeMembers(annotation, instance, moduleScript, filePath, source) {
238
311
  // Snippet imports may live in either script block; collect from both.
@@ -569,7 +642,7 @@ function extractJsdocText(comments) {
569
642
  // ---------------------------------------------------------------------------
570
643
  // Extraction: ObjectPattern destructure + type-members
571
644
  // ---------------------------------------------------------------------------
572
- function extractFromDestructure(propsCall, ctx, typeMembers, warnings) {
645
+ function extractFromDestructure(propsCall, ctx, typeMembers, warnings, additionalReasons) {
573
646
  const id = propsCall['id'];
574
647
  const properties = id['properties'] ?? [];
575
648
  const typeByName = new Map();
@@ -633,6 +706,7 @@ function extractFromDestructure(propsCall, ctx, typeMembers, warnings) {
633
706
  snippetNames,
634
707
  snippetSlots,
635
708
  warnings,
709
+ ...(additionalReasons && additionalReasons.length > 0 ? { additionalReasons } : {}),
636
710
  };
637
711
  }
638
712
  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-a9b4d34.0",
3
+ "version": "2.11.4-dev-build-8cd3b18.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-a9b4d34.0"
40
+ "@contentful/experience-design-system-types": "2.11.4-dev-build-8cd3b18.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@tsconfig/node24": "^24.0.3",