@contentful/experience-design-system-cli 2.11.4-dev-build-8f1adf3.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 +1 -1
- package/dist/src/analyze/extract/svelte.js +130 -20
- package/package.json +2 -2
package/dist/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { existsSync, statSync } from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { parse as parseSvelte } from 'svelte/compiler';
|
|
6
|
-
import { Project, Node } from 'ts-morph';
|
|
6
|
+
import { Project, Node, ScriptTarget, ModuleKind, ts } from 'ts-morph';
|
|
7
7
|
import { computeExtractionScore, deriveNeedsReview } from './scoring.js';
|
|
8
8
|
const SVELTE_EXTRACT_CONCURRENCY = Number(process.env['EDS_EXTRACT_CONCURRENCY'] ?? 0) || os.cpus().length;
|
|
9
9
|
export async function extractSvelteComponents(filePaths, onProgress) {
|
|
@@ -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
|
|
@@ -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 };
|
|
251
|
+
}
|
|
252
|
+
if (!unresolved) {
|
|
253
|
+
warnings.push(`${ctx.filePath}: $props() called without destructuring; cannot extract individual props`);
|
|
230
254
|
}
|
|
231
|
-
|
|
232
|
-
return { props: [], snippetNames: new Set(), snippetSlots: [], warnings };
|
|
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.
|
|
@@ -290,7 +363,13 @@ async function resolveViaTypeChecker(annotation, instance, moduleScript, filePat
|
|
|
290
363
|
const instanceText = sliceScriptContent(source, instance);
|
|
291
364
|
const synthetic = [moduleText, instanceText, `type __SveltePropsT__ = ${annotationText};`].filter(Boolean).join('\n');
|
|
292
365
|
const project = new Project({
|
|
293
|
-
compilerOptions: {
|
|
366
|
+
compilerOptions: {
|
|
367
|
+
strict: false,
|
|
368
|
+
target: ScriptTarget.ESNext,
|
|
369
|
+
module: ModuleKind.ESNext,
|
|
370
|
+
allowJs: true,
|
|
371
|
+
jsx: ts.JsxEmit.Preserve,
|
|
372
|
+
},
|
|
294
373
|
useInMemoryFileSystem: false,
|
|
295
374
|
skipAddingFilesFromTsConfig: true,
|
|
296
375
|
});
|
|
@@ -563,7 +642,7 @@ function extractJsdocText(comments) {
|
|
|
563
642
|
// ---------------------------------------------------------------------------
|
|
564
643
|
// Extraction: ObjectPattern destructure + type-members
|
|
565
644
|
// ---------------------------------------------------------------------------
|
|
566
|
-
function extractFromDestructure(propsCall, ctx, typeMembers, warnings) {
|
|
645
|
+
function extractFromDestructure(propsCall, ctx, typeMembers, warnings, additionalReasons) {
|
|
567
646
|
const id = propsCall['id'];
|
|
568
647
|
const properties = id['properties'] ?? [];
|
|
569
648
|
const typeByName = new Map();
|
|
@@ -627,9 +706,10 @@ function extractFromDestructure(propsCall, ctx, typeMembers, warnings) {
|
|
|
627
706
|
snippetNames,
|
|
628
707
|
snippetSlots,
|
|
629
708
|
warnings,
|
|
709
|
+
...(additionalReasons && additionalReasons.length > 0 ? { additionalReasons } : {}),
|
|
630
710
|
};
|
|
631
711
|
}
|
|
632
|
-
function extractFromTypeMembersOnly(typeMembers
|
|
712
|
+
function extractFromTypeMembersOnly(typeMembers) {
|
|
633
713
|
const props = [];
|
|
634
714
|
const snippetNames = new Set();
|
|
635
715
|
const snippetSlots = [];
|
|
@@ -759,13 +839,22 @@ function resolveLocalScriptModule(importingFilePath, specifier) {
|
|
|
759
839
|
}
|
|
760
840
|
function readMembersFromExternalFile(filePath, exportName) {
|
|
761
841
|
const project = new Project({
|
|
762
|
-
compilerOptions: {
|
|
842
|
+
compilerOptions: {
|
|
843
|
+
strict: false,
|
|
844
|
+
target: ScriptTarget.ESNext,
|
|
845
|
+
module: ModuleKind.ESNext,
|
|
846
|
+
allowJs: true,
|
|
847
|
+
},
|
|
763
848
|
useInMemoryFileSystem: false,
|
|
764
849
|
skipAddingFilesFromTsConfig: true,
|
|
765
850
|
});
|
|
766
851
|
const sf = project.addSourceFileAtPathIfExists(filePath);
|
|
767
852
|
if (!sf)
|
|
768
853
|
return null;
|
|
854
|
+
// Collect Snippet locals from the resolved file's own imports so that
|
|
855
|
+
// aliased imports (`import { Snippet as LocalSnippet } from 'svelte'`) are
|
|
856
|
+
// honored. Mirrors collectSnippetImportLocals() but adapted to ts-morph.
|
|
857
|
+
const snippetLocals = collectSnippetLocalsFromSourceFile(sf);
|
|
769
858
|
// Use getExportedDeclarations() so re-export chains (`export { ... } from './x'`,
|
|
770
859
|
// `export * from './x'`, named or namespace) resolve transparently. ts-morph
|
|
771
860
|
// follows them recursively and returns the underlying declaration.
|
|
@@ -775,18 +864,39 @@ function readMembersFromExternalFile(filePath, exportName) {
|
|
|
775
864
|
return null;
|
|
776
865
|
for (const decl of decls) {
|
|
777
866
|
if (Node.isInterfaceDeclaration(decl)) {
|
|
778
|
-
|
|
867
|
+
// The declaration may live in a different source file than `sf` if the
|
|
868
|
+
// export chain walked across files; pull snippet locals from that file
|
|
869
|
+
// so aliased imports are recognized.
|
|
870
|
+
const declSf = decl.getSourceFile();
|
|
871
|
+
const declLocals = declSf === sf ? snippetLocals : collectSnippetLocalsFromSourceFile(declSf);
|
|
872
|
+
return readInterfaceMembers(decl, declLocals);
|
|
779
873
|
}
|
|
780
874
|
if (Node.isTypeAliasDeclaration(decl)) {
|
|
781
875
|
const typeNode = decl.getTypeNode();
|
|
782
876
|
if (typeNode && Node.isTypeLiteral(typeNode)) {
|
|
783
|
-
|
|
877
|
+
const declSf = decl.getSourceFile();
|
|
878
|
+
const declLocals = declSf === sf ? snippetLocals : collectSnippetLocalsFromSourceFile(declSf);
|
|
879
|
+
return readTypeLiteralMembers(typeNode, declLocals);
|
|
784
880
|
}
|
|
785
881
|
}
|
|
786
882
|
}
|
|
787
883
|
return null;
|
|
788
884
|
}
|
|
789
|
-
function
|
|
885
|
+
function collectSnippetLocalsFromSourceFile(sf) {
|
|
886
|
+
const locals = new Set();
|
|
887
|
+
for (const importDecl of sf.getImportDeclarations()) {
|
|
888
|
+
if (importDecl.getModuleSpecifierValue() !== 'svelte')
|
|
889
|
+
continue;
|
|
890
|
+
for (const named of importDecl.getNamedImports()) {
|
|
891
|
+
if (named.getName() === 'Snippet') {
|
|
892
|
+
const aliasNode = named.getAliasNode();
|
|
893
|
+
locals.add(aliasNode ? aliasNode.getText() : named.getName());
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return locals;
|
|
898
|
+
}
|
|
899
|
+
function readInterfaceMembers(iface, snippetLocals) {
|
|
790
900
|
return iface.getProperties().map((prop) => {
|
|
791
901
|
const typeNode = prop.getTypeNode();
|
|
792
902
|
const typeText = typeNode ? typeNode.getText() : prop.getType().getText(prop);
|
|
@@ -797,7 +907,7 @@ function readInterfaceMembers(iface) {
|
|
|
797
907
|
name: prop.getName(),
|
|
798
908
|
optional: prop.hasQuestionToken(),
|
|
799
909
|
typeText,
|
|
800
|
-
isSnippet: typeText
|
|
910
|
+
isSnippet: isSnippetTypeText(typeText, snippetLocals),
|
|
801
911
|
...(allowed ? { allowedValues: allowed } : {}),
|
|
802
912
|
...(description ? { description } : {}),
|
|
803
913
|
line: prop.getStartLineNumber(),
|
|
@@ -805,7 +915,7 @@ function readInterfaceMembers(iface) {
|
|
|
805
915
|
};
|
|
806
916
|
});
|
|
807
917
|
}
|
|
808
|
-
function readTypeLiteralMembers(typeNode) {
|
|
918
|
+
function readTypeLiteralMembers(typeNode, snippetLocals) {
|
|
809
919
|
return typeNode.getMembers().flatMap((m) => {
|
|
810
920
|
if (!Node.isPropertySignature(m))
|
|
811
921
|
return [];
|
|
@@ -819,7 +929,7 @@ function readTypeLiteralMembers(typeNode) {
|
|
|
819
929
|
name: m.getName(),
|
|
820
930
|
optional: m.hasQuestionToken(),
|
|
821
931
|
typeText,
|
|
822
|
-
isSnippet: typeText
|
|
932
|
+
isSnippet: isSnippetTypeText(typeText, snippetLocals),
|
|
823
933
|
...(allowed ? { allowedValues: allowed } : {}),
|
|
824
934
|
...(description ? { description } : {}),
|
|
825
935
|
line: m.getStartLineNumber(),
|
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-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-
|
|
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",
|