@contentful/experience-design-system-extraction 2.26.1

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/dist/package.json +59 -0
  3. package/dist/src/extract/astro.d.ts +5 -0
  4. package/dist/src/extract/astro.js +289 -0
  5. package/dist/src/extract/non-authorable-filter.d.ts +16 -0
  6. package/dist/src/extract/non-authorable-filter.js +60 -0
  7. package/dist/src/extract/pipeline.d.ts +6 -0
  8. package/dist/src/extract/pipeline.js +314 -0
  9. package/dist/src/extract/react.d.ts +2 -0
  10. package/dist/src/extract/react.js +2310 -0
  11. package/dist/src/extract/scoring.d.ts +12 -0
  12. package/dist/src/extract/scoring.js +108 -0
  13. package/dist/src/extract/slot-allowed-components.d.ts +6 -0
  14. package/dist/src/extract/slot-allowed-components.js +40 -0
  15. package/dist/src/extract/slot-detection.d.ts +35 -0
  16. package/dist/src/extract/slot-detection.js +101 -0
  17. package/dist/src/extract/source-inspection.d.ts +13 -0
  18. package/dist/src/extract/source-inspection.js +189 -0
  19. package/dist/src/extract/stencil.d.ts +2 -0
  20. package/dist/src/extract/stencil.js +328 -0
  21. package/dist/src/extract/structural-slot-evidence.d.ts +36 -0
  22. package/dist/src/extract/structural-slot-evidence.js +121 -0
  23. package/dist/src/extract/svelte.d.ts +5 -0
  24. package/dist/src/extract/svelte.js +1357 -0
  25. package/dist/src/extract/tsx-shared.d.ts +15 -0
  26. package/dist/src/extract/tsx-shared.js +276 -0
  27. package/dist/src/extract/validate.d.ts +16 -0
  28. package/dist/src/extract/validate.js +94 -0
  29. package/dist/src/extract/vue-tsx.d.ts +2 -0
  30. package/dist/src/extract/vue-tsx.js +498 -0
  31. package/dist/src/extract/vue.d.ts +5 -0
  32. package/dist/src/extract/vue.js +653 -0
  33. package/dist/src/extract/web-components.d.ts +2 -0
  34. package/dist/src/extract/web-components.js +876 -0
  35. package/dist/src/index.d.ts +20 -0
  36. package/dist/src/index.js +22 -0
  37. package/dist/src/pre-classify.d.ts +17 -0
  38. package/dist/src/pre-classify.js +225 -0
  39. package/dist/src/types.d.ts +91 -0
  40. package/dist/src/types.js +4 -0
  41. package/package.json +58 -0
@@ -0,0 +1,20 @@
1
+ export type { RawComponentDefinition, RawPropDefinition, RawSlotDefinition, ComponentExtractionResult, ExtractorProgress, ExtractorOptions, ComponentExtractor, ExtractionValidationIssue, ExtractionValidationIssueCode, } from './types.js';
2
+ export { stripScoringFields } from './types.js';
3
+ export { extractComponents } from './extract/pipeline.js';
4
+ export { extractReactComponents } from './extract/react.js';
5
+ export { extractVueComponents } from './extract/vue.js';
6
+ export { extractVueTsxComponents } from './extract/vue-tsx.js';
7
+ export { extractStencilComponents } from './extract/stencil.js';
8
+ export { extractAstroComponents } from './extract/astro.js';
9
+ export { extractWebComponentDefinitions } from './extract/web-components.js';
10
+ export { extractSvelteComponents } from './extract/svelte.js';
11
+ export { extractAllowedComponentsFromTypeText, extractAllowedComponentsFromJsdoc, } from './extract/slot-allowed-components.js';
12
+ export { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
13
+ export { computeExtractionScore, deriveNeedsReview } from './extract/scoring.js';
14
+ export type { ExtractionScore, ExtractionScoreOptions, ExtractionConfidence } from './extract/scoring.js';
15
+ export { inspectComponentSource, describeReviewReasons, describeReviewReason, isDataWrapperReviewReason, HIGH_CONFIDENCE_DATA_FETCH_WRAPPER_REASON, POSSIBLE_DATA_FETCH_WRAPPER_REASON, ZERO_SURFACE_RENDERED_UI_REASON, } from './extract/source-inspection.js';
16
+ export type { ComponentSourceInspection } from './extract/source-inspection.js';
17
+ export { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from './extract/validate.js';
18
+ export { CONTENT_NAME_EXCEPTIONS, isReactNodeType, isArrayReactNodeType, shouldBeSlot, } from './extract/slot-detection.js';
19
+ export { preClassifyProp, preClassifyComponent } from './pre-classify.js';
20
+ export type { PreClassification } from './pre-classify.js';
@@ -0,0 +1,22 @@
1
+ export { stripScoringFields } from './types.js';
2
+ // Core extraction pipeline
3
+ export { extractComponents } from './extract/pipeline.js';
4
+ // Framework-specific extractors
5
+ export { extractReactComponents } from './extract/react.js';
6
+ export { extractVueComponents } from './extract/vue.js';
7
+ export { extractVueTsxComponents } from './extract/vue-tsx.js';
8
+ export { extractStencilComponents } from './extract/stencil.js';
9
+ export { extractAstroComponents } from './extract/astro.js';
10
+ export { extractWebComponentDefinitions } from './extract/web-components.js';
11
+ export { extractSvelteComponents } from './extract/svelte.js';
12
+ export { extractAllowedComponentsFromTypeText, extractAllowedComponentsFromJsdoc, } from './extract/slot-allowed-components.js';
13
+ // Post-extraction filtering and scoring
14
+ export { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
15
+ export { computeExtractionScore, deriveNeedsReview } from './extract/scoring.js';
16
+ export { inspectComponentSource, describeReviewReasons, describeReviewReason, isDataWrapperReviewReason, HIGH_CONFIDENCE_DATA_FETCH_WRAPPER_REASON, POSSIBLE_DATA_FETCH_WRAPPER_REASON, ZERO_SURFACE_RENDERED_UI_REASON, } from './extract/source-inspection.js';
17
+ // Validation
18
+ export { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from './extract/validate.js';
19
+ // Slot detection helpers
20
+ export { CONTENT_NAME_EXCEPTIONS, isReactNodeType, isArrayReactNodeType, shouldBeSlot, } from './extract/slot-detection.js';
21
+ // Pre-classification
22
+ export { preClassifyProp, preClassifyComponent } from './pre-classify.js';
@@ -0,0 +1,17 @@
1
+ import type { RawPropDefinition, RawComponentDefinition } from './types.js';
2
+ export interface PreClassification {
3
+ category: 'content' | 'design' | 'state' | 'exclude';
4
+ cdfTypeHint?: 'string' | 'enum' | 'richtext' | 'media' | 'boolean';
5
+ }
6
+ /**
7
+ * Deterministic pre-classification rule engine.
8
+ * Applies rules in priority order and returns on the first match.
9
+ */
10
+ export declare function preClassifyProp(prop: RawPropDefinition): PreClassification | undefined;
11
+ /**
12
+ * Applies pre-classification to all props in a component definition.
13
+ * - Leaves existing category values unchanged
14
+ * - Sets category for content/design/state matches
15
+ * - Removes props that match an exclusion rule so they cannot reach agent classification
16
+ */
17
+ export declare function preClassifyComponent(component: RawComponentDefinition): RawComponentDefinition;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Determines whether a type string represents a simple/primitive type
3
+ * (string, boolean, number, or string literal union) vs a complex type
4
+ * (object, function, array, generic, etc.).
5
+ */
6
+ function isSimpleType(type) {
7
+ const t = type.trim();
8
+ if (t === 'string' || t === 'boolean' || t === 'number')
9
+ return true;
10
+ // String literal union (e.g., "'a' | 'b'")
11
+ if (isStringLiteralUnion(t))
12
+ return true;
13
+ return false;
14
+ }
15
+ function isStringLiteralUnion(type) {
16
+ return type.includes('|') && type.includes("'");
17
+ }
18
+ function isBooleanType(type) {
19
+ return type.trim() === 'boolean';
20
+ }
21
+ function isStringType(type) {
22
+ return type.trim() === 'string';
23
+ }
24
+ function isNumberType(type) {
25
+ return type.trim() === 'number';
26
+ }
27
+ function isComplexType(type) {
28
+ return !isSimpleType(type);
29
+ }
30
+ const DOM_PASS_THROUGH_PROPS = new Set([
31
+ // Bare HTML / framework styling pass-through
32
+ 'className',
33
+ 'class',
34
+ 'classes',
35
+ 'classNames',
36
+ 'rootClassName',
37
+ 'prefixCls',
38
+ 'style',
39
+ 'styles',
40
+ // Bare HTML attributes
41
+ 'id',
42
+ 'role',
43
+ 'tabIndex',
44
+ 'tabindex',
45
+ 'htmlFor',
46
+ 'for',
47
+ 'slot',
48
+ 'is',
49
+ 'lang',
50
+ 'dir',
51
+ 'hidden',
52
+ 'draggable',
53
+ 'spellCheck',
54
+ 'spellcheck',
55
+ 'contentEditable',
56
+ 'contenteditable',
57
+ 'inputMode',
58
+ 'inputmode',
59
+ 'autoComplete',
60
+ 'autocomplete',
61
+ 'autoFocus',
62
+ 'autofocus',
63
+ 'translate',
64
+ 'part',
65
+ 'exportparts',
66
+ 'aria',
67
+ // Framework theming / pass-through escape hatches — dev-facing, never marketer-configurable
68
+ 'dt',
69
+ 'pt',
70
+ 'ptOptions',
71
+ 'unstyled',
72
+ 'sx',
73
+ // Polymorphic component props — change rendered HTML/component, not marketer-visible behavior
74
+ 'as',
75
+ 'element',
76
+ 'component',
77
+ // QA / vendor test attributes
78
+ 'dataQa',
79
+ 'data-qa',
80
+ // Vue v-model internals — framework wiring, never marketer-configurable
81
+ 'modelValue',
82
+ 'modelModifiers',
83
+ ]);
84
+ function isDomPassThroughProp(name) {
85
+ if (DOM_PASS_THROUGH_PROPS.has(name))
86
+ return true;
87
+ // aria-label, aria-hidden, ariaLabel, ariaHidden — both kebab and camel forms
88
+ if (/^aria[-A-Z]/.test(name))
89
+ return true;
90
+ if (name.startsWith('data-'))
91
+ return true;
92
+ return false;
93
+ }
94
+ /**
95
+ * Deterministic pre-classification rule engine.
96
+ * Applies rules in priority order and returns on the first match.
97
+ */
98
+ export function preClassifyProp(prop) {
99
+ const { name, type } = prop;
100
+ // Rule 1: Event handlers
101
+ // name starts with `on` + uppercase, OR type contains `=> void` or `EventHandler`
102
+ if (/^on[A-Z]/.test(name) || type.includes('=> void') || type.includes('EventHandler')) {
103
+ return { category: 'exclude' };
104
+ }
105
+ // Rule 2: Refs
106
+ if (name === 'ref' || name === 'innerRef' || type.includes('Ref<') || type.includes('RefObject<')) {
107
+ return { category: 'exclude' };
108
+ }
109
+ // Rule 3: Test IDs
110
+ if (name === 'testId' || name === 'data-testid' || name === 'dataTestId') {
111
+ return { category: 'exclude' };
112
+ }
113
+ // Rule 4: Key prop
114
+ if (name === 'key') {
115
+ return { category: 'exclude' };
116
+ }
117
+ // Rule 5: Dispatch/setter
118
+ if (type.includes('Dispatch<') || type.includes('SetStateAction')) {
119
+ return { category: 'exclude' };
120
+ }
121
+ // Rule 6: DOM / a11y / framework pass-through props
122
+ // These are escape hatches developers use to wire components into the DOM.
123
+ // Marketers should never configure them in the ExO editor; exposing them
124
+ // generates noise that obscures the props that actually carry intent.
125
+ if (isDomPassThroughProp(name)) {
126
+ return { category: 'exclude' };
127
+ }
128
+ // `name` is overloaded: form controls forward it to the DOM, while icons,
129
+ // flags, and animations often use it as an authorable semantic selector.
130
+ // Exclude only when the extractor retained concrete DOM provenance.
131
+ if (name === 'name' && prop.domAttribute) {
132
+ return { category: 'exclude' };
133
+ }
134
+ // Rule 7: String literal union
135
+ if (isStringLiteralUnion(type)) {
136
+ return { category: 'design', cdfTypeHint: 'enum' };
137
+ }
138
+ // Rule 8: Design name patterns (only for simple types)
139
+ if (!isComplexType(type)) {
140
+ const designNameStart = /^(variant|size|spacing|gap|color|bg|theme|align|layout|orientation|position)/i;
141
+ const designNameEnd = /(Color|Size|Variant|Style)$/;
142
+ if (designNameStart.test(name) || designNameEnd.test(name)) {
143
+ return { category: 'design', cdfTypeHint: 'string' };
144
+ }
145
+ }
146
+ // Rule 10: Boolean + state names (checked before rule 9 since state names
147
+ // like "disabled" would otherwise match the visual toggle prefix "disable")
148
+ if (isBooleanType(type)) {
149
+ const stateNames = ['disabled', 'loading', 'expanded', 'isOpen', 'selected', 'checked', 'active', 'preview'];
150
+ if (stateNames.includes(name)) {
151
+ return { category: 'state', cdfTypeHint: 'boolean' };
152
+ }
153
+ }
154
+ // Rule 9: Boolean + visual toggle name
155
+ if (isBooleanType(type)) {
156
+ const visualToggle = /^(hide|show|enable|disable|vertical|horizontal|reverse|bold|italic|imageOn|with)/i;
157
+ if (visualToggle.test(name)) {
158
+ return { category: 'design', cdfTypeHint: 'boolean' };
159
+ }
160
+ }
161
+ // Rule 11: State identifiers
162
+ if (name === 'componentId' || name === 'sectionKey' || name === 'locale' || name === 'variantIndex') {
163
+ return { category: 'state', cdfTypeHint: 'string' };
164
+ }
165
+ // Rule 12: URL patterns (string type only)
166
+ if (isStringType(type)) {
167
+ const urlNameStart = /^(href|url|link|src)/i;
168
+ const urlNameEnd = /(Url|Href|Link|Src)$/;
169
+ if (urlNameStart.test(name) || urlNameEnd.test(name)) {
170
+ return { category: 'content', cdfTypeHint: 'string' };
171
+ }
172
+ }
173
+ // Rule 13: Text patterns (string type only)
174
+ if (isStringType(type)) {
175
+ const textNameStart = /^(label|title|text|description|caption|heading|subheading|body|alt|name|placeholder|summary)/i;
176
+ const textNameEnd = /(Text|Label|Title|Name)$/;
177
+ if (textNameStart.test(name) || textNameEnd.test(name)) {
178
+ return { category: 'content', cdfTypeHint: 'string' };
179
+ }
180
+ }
181
+ // Rule 14: Remaining strings
182
+ if (isStringType(type)) {
183
+ return { category: 'content', cdfTypeHint: 'string' };
184
+ }
185
+ // Rule 15: Remaining booleans
186
+ if (isBooleanType(type)) {
187
+ return { category: 'design', cdfTypeHint: 'boolean' };
188
+ }
189
+ // Rule 16: Remaining numbers
190
+ if (isNumberType(type)) {
191
+ return { category: 'design', cdfTypeHint: 'string' };
192
+ }
193
+ // Rule 17: Complex/object/function/array types — no hint
194
+ return undefined;
195
+ }
196
+ /**
197
+ * Applies pre-classification to all props in a component definition.
198
+ * - Leaves existing category values unchanged
199
+ * - Sets category for content/design/state matches
200
+ * - Removes props that match an exclusion rule so they cannot reach agent classification
201
+ */
202
+ export function preClassifyComponent(component) {
203
+ const props = component.props.flatMap((prop) => {
204
+ const result = preClassifyProp(prop);
205
+ // Excluded props are developer-facing wiring and must not be included in
206
+ // the component payload passed to classification or generation, even if a
207
+ // prior step assigned them a category.
208
+ if (result?.category === 'exclude') {
209
+ return [];
210
+ }
211
+ // Provenance is extraction-only evidence. Do not add it to the payload
212
+ // presented to downstream agents.
213
+ const { domAttribute: _domAttribute, ...authorableProp } = prop;
214
+ // If category is already set, leave unchanged
215
+ if (prop.category) {
216
+ return [authorableProp];
217
+ }
218
+ if (!result) {
219
+ return [authorableProp];
220
+ }
221
+ // Set category hint
222
+ return [{ ...authorableProp, category: result.category }];
223
+ });
224
+ return { ...component, props };
225
+ }
@@ -0,0 +1,91 @@
1
+ export type ExtractionValidationIssueCode = 'EMPTY_COMPONENT_NAME' | 'EMPTY_PROP_NAME' | 'EMPTY_SLOT_NAME' | 'PROP_SLOT_NAME_COLLISION' | 'DUPLICATE_COMPONENT_NAME' | 'EMPTY_COMPONENT' | 'SERVER_VALIDATION_FAILED';
2
+ export type ExtractionValidationIssue = {
3
+ severity: 'error' | 'warning';
4
+ code: ExtractionValidationIssueCode;
5
+ message: string;
6
+ field?: string;
7
+ };
8
+ export interface RawPropDefinition {
9
+ name: string;
10
+ type: string;
11
+ required: boolean;
12
+ /**
13
+ * True when extraction can prove that the prop is forwarded to the
14
+ * same-named attribute on an intrinsic DOM element. This distinguishes
15
+ * overloaded names such as the form attribute `name` from semantic props
16
+ * that select visible content (for example an icon or flag name).
17
+ */
18
+ domAttribute?: boolean;
19
+ category?: 'content' | 'design' | 'state';
20
+ defaultValue?: string;
21
+ allowedValues?: string[];
22
+ description?: string;
23
+ tokenReference?: string;
24
+ /** 1-indexed source line where this prop's declaration begins; relative to RawComponentDefinition.source. */
25
+ sourceStartLine?: number;
26
+ /** 1-indexed inclusive source line where this prop's declaration ends; relative to RawComponentDefinition.source. */
27
+ sourceEndLine?: number;
28
+ }
29
+ export interface RawSlotDefinition {
30
+ name: string;
31
+ isDefault: boolean;
32
+ description?: string;
33
+ allowedComponents?: string[];
34
+ /**
35
+ * Component names implied by usage evidence (a runtime type-predicate
36
+ * function, a `.type === Component` identity check, or direct JSX
37
+ * instantiation) rather than a declared slot contract. Kept separate from
38
+ * `allowedComponents` so downstream provenance stays honest — this is a
39
+ * lower-trust signal that a declared contract always overrides.
40
+ */
41
+ structuralAllowedComponents?: string[];
42
+ }
43
+ export interface RawComponentDefinition {
44
+ name: string;
45
+ source: string;
46
+ framework: 'react' | 'next' | 'vue' | 'astro' | 'web-component' | 'stencil' | 'svelte';
47
+ props: RawPropDefinition[];
48
+ slots: RawSlotDefinition[];
49
+ /**
50
+ * True when the source file declaring this component calls
51
+ * React.createContext / createContext. Used downstream to filter
52
+ * non-authorable context-provider components.
53
+ */
54
+ usesCreateContext?: boolean;
55
+ extractionConfidence?: number | null;
56
+ reviewReasons?: string[];
57
+ needsReview?: boolean;
58
+ validationIssues?: ExtractionValidationIssue[];
59
+ /** Absolute path to the source file this component was extracted from. Null for synthesized / multi-file. */
60
+ sourcePath?: string;
61
+ }
62
+ export interface ComponentExtractionResult {
63
+ components: RawComponentDefinition[];
64
+ warnings: string[];
65
+ }
66
+ export type ExtractorProgress = {
67
+ filesProcessed: number;
68
+ componentsFound: number;
69
+ };
70
+ /**
71
+ * Optional extraction-time settings forwarded from the CLI through the
72
+ * pipeline to each extractor. Only the Svelte extractor currently consumes
73
+ * any of these; other extractors ignore the value.
74
+ */
75
+ export interface ExtractorOptions {
76
+ /**
77
+ * Whether the Svelte extractor should run a retry pass for components whose
78
+ * declared Props type couldn't be resolved on the first pass (cross-package
79
+ * extends, path-alias-only types, etc.). See svelte.ts for the policy.
80
+ */
81
+ resolveUnreachable?: 'auto' | 'always' | 'never';
82
+ /** Absolute project root — used by the retry pass to locate tsconfig.json and node_modules. */
83
+ projectRoot?: string;
84
+ }
85
+ export interface ComponentExtractor {
86
+ name: string;
87
+ fileFilter: (filePath: string) => boolean;
88
+ extract(filePaths: string[], onProgress?: (p: ExtractorProgress) => void, opts?: ExtractorOptions): Promise<ComponentExtractionResult>;
89
+ }
90
+ /** Strip internal scoring fields before serialising a RawComponentDefinition for display or editing. */
91
+ export declare function stripScoringFields({ extractionConfidence: _c, reviewReasons: _r, needsReview: _n, ...rest }: RawComponentDefinition): Omit<RawComponentDefinition, 'extractionConfidence' | 'reviewReasons' | 'needsReview'>;
@@ -0,0 +1,4 @@
1
+ /** Strip internal scoring fields before serialising a RawComponentDefinition for display or editing. */
2
+ export function stripScoringFields({ extractionConfidence: _c, reviewReasons: _r, needsReview: _n, ...rest }) {
3
+ return rest;
4
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@contentful/experience-design-system-extraction",
3
+ "version": "2.26.1",
4
+ "description": "Component extraction engine for the Contentful Experience Design System SDK",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/src/index.js",
8
+ "types": "./dist/src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/src/index.d.ts",
12
+ "import": "./dist/src/index.js",
13
+ "node": "./dist/src/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://npm.pkg.github.com/"
19
+ },
20
+ "files": [
21
+ "dist/"
22
+ ],
23
+ "dependencies": {
24
+ "@vue/compiler-sfc": "^3.5.31",
25
+ "svelte": "^5.56.4",
26
+ "ts-morph": "^27.0.2",
27
+ "typescript": "^5.9.3"
28
+ },
29
+ "devDependencies": {
30
+ "@tsconfig/node24": "^24.0.4",
31
+ "@types/node": "^24.0.3",
32
+ "eslint": "^9.39.5",
33
+ "eslint-config-prettier": "^10.1.8",
34
+ "eslint-plugin-prettier": "^5.5.6",
35
+ "typescript-eslint": "^8.67.0",
36
+ "vitest": "^4.0.16"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/contentful/experience-design-system-sdk-public.git",
41
+ "directory": "packages/experience-design-system-extraction"
42
+ },
43
+ "homepage": "https://github.com/contentful/experience-design-system-sdk-public#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/contentful/experience-design-system-sdk-public/issues"
46
+ },
47
+ "engines": {
48
+ "node": ">=24"
49
+ },
50
+ "scripts": {
51
+ "build": "nx build experience-design-system-extraction",
52
+ "typecheck": "nx typecheck experience-design-system-extraction",
53
+ "clean": "nx clean experience-design-system-extraction",
54
+ "test": "nx test experience-design-system-extraction",
55
+ "lint": "nx lint experience-design-system-extraction",
56
+ "lint:fix": "nx lint:fix experience-design-system-extraction"
57
+ }
58
+ }