@contentful/experience-design-system-extraction 2.35.0-alpha.0 → 2.35.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-extraction",
3
- "version": "2.35.0-alpha.0",
3
+ "version": "2.35.0",
4
4
  "description": "Component extraction engine for the Contentful Experience Design System SDK",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -226,6 +226,7 @@ function choosePreferredComponent(existing, candidate) {
226
226
  }
227
227
  export async function extractComponents(filePaths, onProgress, opts) {
228
228
  const filesByExtractor = new Map();
229
+ const exclusions = [];
229
230
  for (const extractor of extractors) {
230
231
  filesByExtractor.set(extractor, []);
231
232
  }
@@ -234,6 +235,18 @@ export async function extractComponents(filePaths, onProgress, opts) {
234
235
  if (extractor.fileFilter(filePath)) {
235
236
  filesByExtractor.get(extractor).push(filePath);
236
237
  }
238
+ else if (extractor.name === 'svelte' && filePath.endsWith('.svelte')) {
239
+ const filename = filePath.replace(/\\/g, '/').split('/').pop() ?? '';
240
+ if (/^\+(page|layout|error)\.svelte$/.test(filename)) {
241
+ exclusions.push({
242
+ itemType: 'file',
243
+ name: filename,
244
+ source: filePath,
245
+ reason: 'SvelteKit route entrypoints are framework-managed, not authorable components',
246
+ stage: 'file-filter',
247
+ });
248
+ }
249
+ }
237
250
  }
238
251
  }
239
252
  const perExtractorFiles = new Map();
@@ -276,6 +289,7 @@ export async function extractComponents(filePaths, onProgress, opts) {
276
289
  }
277
290
  for (const result of results) {
278
291
  allWarnings.push(...result.warnings);
292
+ exclusions.push(...(result.exclusions ?? []));
279
293
  for (const component of result.components) {
280
294
  const scopeKey = getFamilyScopeKey(component.source, component.name, topLevelFamiliesByRoot);
281
295
  const identityKey = `${component.name}::${scopeKey}`;
@@ -283,6 +297,13 @@ export async function extractComponents(filePaths, onProgress, opts) {
283
297
  if (existing) {
284
298
  const selected = choosePreferredComponent(existing, component);
285
299
  allWarnings.push(`Duplicate component "${component.name}" found in ${component.source} (already seen in ${existing.source}); ${selected.reason}`);
300
+ exclusions.push({
301
+ itemType: 'component',
302
+ name: selected.loser.name,
303
+ source: selected.loser.source,
304
+ reason: `duplicate identity; ${selected.reason}`,
305
+ stage: 'duplicate-filter',
306
+ });
286
307
  componentsByKey.set(identityKey, selected.winner);
287
308
  continue;
288
309
  }
@@ -303,6 +324,13 @@ export async function extractComponents(filePaths, onProgress, opts) {
303
324
  for (const component of allComponents) {
304
325
  if (/^use[A-Z]/.test(component.name)) {
305
326
  allWarnings.push(`Skipped hook: ${component.name} (hooks are not renderable components)`);
327
+ exclusions.push({
328
+ itemType: 'component',
329
+ name: component.name,
330
+ source: component.source,
331
+ reason: 'Hook names are not renderable components',
332
+ stage: 'component-filter',
333
+ });
306
334
  continue;
307
335
  }
308
336
  filteredComponents.push(component);
@@ -310,5 +338,6 @@ export async function extractComponents(filePaths, onProgress, opts) {
310
338
  return {
311
339
  components: filteredComponents.sort((a, b) => a.name.localeCompare(b.name)),
312
340
  warnings: allWarnings,
341
+ exclusions,
313
342
  };
314
343
  }
@@ -1,5 +1,5 @@
1
1
  import { Node, SyntaxKind, } from 'ts-morph';
2
- import { extractAllowedValues, getNodeDefinitions, getTypeReferenceName, getTypeTargetDeclarations, getTsxExtractionContext, getValueTargetDeclarations, getJsxTagNameNode, isIntrinsicJsxElement, resolveDefaultExportName, } from './tsx-shared.js';
2
+ import { extractAllowedValues, getNodeDefinitions, getTypeReferenceName, getTypeTargetDeclarations, getValueTargetDeclarations, getJsxTagNameNode, extractTsxComponents, getRenderableExports, isIntrinsicJsxElement, } from './tsx-shared.js';
3
3
  import { shouldBeSlot } from './slot-detection.js';
4
4
  import { extractAllowedComponentsFromTypeText, extractAllowedComponentsFromJsdoc } from './slot-allowed-components.js';
5
5
  import { getSourceLineMetadata } from './source-line-metadata.js';
@@ -1626,29 +1626,13 @@ function hasImplementationChildrenHint(funcNode, param) {
1626
1626
  .some((attr) => isComponentSpreadAttribute(attr, new Set([paramName])));
1627
1627
  }
1628
1628
  export async function extractReactComponents(filePaths) {
1629
- const extractionContext = getTsxExtractionContext(filePaths, /\.[jt]sx$/);
1630
- if (!extractionContext) {
1631
- return { components: [], warnings: [] };
1632
- }
1633
- const { componentFiles, project } = extractionContext;
1634
- const warnings = [];
1635
- const components = [];
1636
- for (const filePath of componentFiles) {
1637
- try {
1638
- const sourceFile = project.getSourceFile(filePath);
1639
- if (!sourceFile)
1640
- continue;
1641
- if (isStencilFile(sourceFile))
1642
- continue;
1643
- const fileExports = [...sourceFile.getExportedDeclarations().keys()];
1644
- const isNext = isNextJsComponent(sourceFile.getFilePath(), fileExports);
1645
- const extracted = extractFromSourceFile(sourceFile, isNext);
1646
- components.push(...extracted);
1647
- }
1648
- catch (e) {
1649
- warnings.push(`Failed to extract from ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
1650
- }
1651
- }
1629
+ const { components, warnings, exclusions, project } = extractTsxComponents(filePaths, /\.[jt]sx$/, (sourceFile, exclusions) => {
1630
+ if (isStencilFile(sourceFile))
1631
+ return [];
1632
+ const fileExports = [...sourceFile.getExportedDeclarations().keys()];
1633
+ const isNext = isNextJsComponent(sourceFile.getFilePath(), fileExports);
1634
+ return extractFromSourceFile(sourceFile, isNext, exclusions);
1635
+ });
1652
1636
  const propsToComponent = new Map();
1653
1637
  const componentNames = new Set();
1654
1638
  for (const c of components) {
@@ -1690,7 +1674,7 @@ export async function extractReactComponents(filePaths) {
1690
1674
  const cached = structuralByFile.get(filePath);
1691
1675
  if (cached)
1692
1676
  return cached;
1693
- const sourceFile = project.getSourceFile(filePath);
1677
+ const sourceFile = project?.getSourceFile(filePath);
1694
1678
  if (!sourceFile)
1695
1679
  return [];
1696
1680
  const ctx = { propsToComponent, componentNames };
@@ -1764,24 +1748,16 @@ export async function extractReactComponents(filePaths) {
1764
1748
  return {
1765
1749
  components: components.sort((a, b) => a.name.localeCompare(b.name)),
1766
1750
  warnings,
1751
+ exclusions,
1767
1752
  };
1768
1753
  }
1769
- function extractFromSourceFile(sourceFile, isNext) {
1754
+ function extractFromSourceFile(sourceFile, isNext, exclusions) {
1770
1755
  const components = [];
1771
- const exported = sourceFile.getExportedDeclarations();
1772
1756
  const usesCreateContext = sourceFileUsesCreateContext(sourceFile);
1773
- for (const [exportKey, declarations] of exported) {
1774
- let name = exportKey;
1775
- if (exportKey === 'default') {
1776
- const defaultExportName = resolveDefaultExportName(declarations, exported, true);
1777
- if (!defaultExportName)
1778
- continue;
1779
- name = defaultExportName;
1780
- }
1781
- if (!/^[A-Z]/.test(name))
1782
- continue;
1783
- if (name.startsWith('use'))
1784
- continue;
1757
+ for (const { name, declarations } of getRenderableExports(sourceFile, exclusions, {
1758
+ allowVariableDeclaration: true,
1759
+ hookReason: 'React hook names are not renderable components',
1760
+ })) {
1785
1761
  const funcNode = resolveBestFunctionNode(declarations);
1786
1762
  if (!funcNode)
1787
1763
  continue;
@@ -1,11 +1,25 @@
1
- import { Node, Project, type Type } from 'ts-morph';
1
+ import { Node, Project, type SourceFile, type Type } from 'ts-morph';
2
+ import type { ExtractionExclusion } from '../types.js';
2
3
  export declare function getTsxExtractionContext(filePaths: string[], componentFilePattern: RegExp): {
3
4
  componentFiles: string[];
4
5
  project: Project;
5
6
  } | undefined;
7
+ export declare function extractTsxComponents<T>(filePaths: string[], componentFilePattern: RegExp, extract: (sourceFile: SourceFile, exclusions: ExtractionExclusion[]) => T[]): {
8
+ components: T[];
9
+ warnings: string[];
10
+ exclusions: ExtractionExclusion[];
11
+ project?: Project;
12
+ };
6
13
  export declare function resolveDefaultExportName(declarations: Node[], exported: {
7
14
  has(name: string): boolean;
8
15
  }, allowVariableDeclaration?: boolean): string | undefined;
16
+ export declare function getRenderableExports(sourceFile: SourceFile, exclusions: ExtractionExclusion[], options: {
17
+ allowVariableDeclaration?: boolean;
18
+ hookReason: string;
19
+ }): Array<{
20
+ name: string;
21
+ declarations: Node[];
22
+ }>;
9
23
  export declare function kebabToPascal(input: string): string;
10
24
  export declare function extractAllowedValues(type: Type): string[] | undefined;
11
25
  /**
@@ -35,6 +35,28 @@ export function getTsxExtractionContext(filePaths, componentFilePattern) {
35
35
  project: createTsxProject(getTsxProjectFiles(filePaths)),
36
36
  };
37
37
  }
38
+ export function extractTsxComponents(filePaths, componentFilePattern, extract) {
39
+ const extractionContext = getTsxExtractionContext(filePaths, componentFilePattern);
40
+ if (!extractionContext) {
41
+ return { components: [], warnings: [], exclusions: [] };
42
+ }
43
+ const { componentFiles, project } = extractionContext;
44
+ const components = [];
45
+ const warnings = [];
46
+ const exclusions = [];
47
+ for (const filePath of componentFiles) {
48
+ try {
49
+ const sourceFile = project.getSourceFile(filePath);
50
+ if (!sourceFile)
51
+ continue;
52
+ components.push(...extract(sourceFile, exclusions));
53
+ }
54
+ catch (e) {
55
+ warnings.push(`Failed to extract from ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
56
+ }
57
+ }
58
+ return { components, warnings, exclusions, project };
59
+ }
38
60
  export function resolveDefaultExportName(declarations, exported, allowVariableDeclaration = false) {
39
61
  const declaration = declarations[0];
40
62
  const name = Node.isFunctionDeclaration(declaration)
@@ -46,6 +68,29 @@ export function resolveDefaultExportName(declarations, exported, allowVariableDe
46
68
  return undefined;
47
69
  return name;
48
70
  }
71
+ export function getRenderableExports(sourceFile, exclusions, options) {
72
+ const renderable = [];
73
+ const exported = sourceFile.getExportedDeclarations();
74
+ for (const [exportKey, declarations] of exported) {
75
+ const name = exportKey === 'default'
76
+ ? resolveDefaultExportName(declarations, exported, options.allowVariableDeclaration)
77
+ : exportKey;
78
+ if (!name || !/^[A-Z]/.test(name))
79
+ continue;
80
+ if (name.startsWith('use')) {
81
+ exclusions.push({
82
+ itemType: 'component',
83
+ name,
84
+ source: sourceFile.getFilePath(),
85
+ reason: options.hookReason,
86
+ stage: 'component-filter',
87
+ });
88
+ continue;
89
+ }
90
+ renderable.push({ name, declarations });
91
+ }
92
+ return renderable;
93
+ }
49
94
  export function kebabToPascal(input) {
50
95
  return input
51
96
  .split('-')
@@ -1,44 +1,18 @@
1
1
  import { Node, SyntaxKind } from 'ts-morph';
2
- import { extractAllowedValues, getTypeReferenceName, getTypeTargetDeclarations, getTsxExtractionContext, getValueTargetDeclarations, resolveDefaultExportName, } from './tsx-shared.js';
2
+ import { extractAllowedValues, extractTsxComponents, getTypeReferenceName, getTypeTargetDeclarations, getValueTargetDeclarations, getRenderableExports, } from './tsx-shared.js';
3
3
  export async function extractVueTsxComponents(filePaths) {
4
- const extractionContext = getTsxExtractionContext(filePaths, /\.tsx$/);
5
- if (!extractionContext) {
6
- return { components: [], warnings: [] };
7
- }
8
- const { componentFiles, project } = extractionContext;
9
- const warnings = [];
10
- const components = [];
11
- for (const filePath of componentFiles) {
12
- try {
13
- const sourceFile = project.getSourceFile(filePath);
14
- if (!sourceFile)
15
- continue;
16
- components.push(...extractFromSourceFile(sourceFile));
17
- }
18
- catch (e) {
19
- warnings.push(`Failed to extract from ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
20
- }
21
- }
4
+ const { components, warnings, exclusions } = extractTsxComponents(filePaths, /\.tsx$/, extractFromSourceFile);
22
5
  return {
23
6
  components: components.sort((a, b) => a.name.localeCompare(b.name)),
24
7
  warnings,
8
+ exclusions,
25
9
  };
26
10
  }
27
- function extractFromSourceFile(sourceFile) {
11
+ function extractFromSourceFile(sourceFile, exclusions) {
28
12
  const components = [];
29
- const exported = sourceFile.getExportedDeclarations();
30
- for (const [exportKey, declarations] of exported) {
31
- let name = exportKey;
32
- if (exportKey === 'default') {
33
- const defaultExportName = resolveDefaultExportName(declarations, exported);
34
- if (!defaultExportName)
35
- continue;
36
- name = defaultExportName;
37
- }
38
- if (!/^[A-Z]/.test(name))
39
- continue;
40
- if (name.startsWith('use'))
41
- continue;
13
+ for (const { name, declarations } of getRenderableExports(sourceFile, exclusions, {
14
+ hookReason: 'Vue composition hook names are not renderable components',
15
+ })) {
42
16
  const component = extractVueTsxComponent(declarations, name, sourceFile);
43
17
  if (component) {
44
18
  components.push(component);
@@ -1,4 +1,4 @@
1
- export type { RawComponentDefinition, RawPropDefinition, RawSlotDefinition, ComponentExtractionResult, ExtractorProgress, ExtractorOptions, ComponentExtractor, ExtractionValidationIssue, ExtractionValidationIssueCode, } from './types.js';
1
+ export type { RawComponentDefinition, RawPropDefinition, RawSlotDefinition, ComponentExtractionResult, ExtractionExclusion, ExtractorProgress, ExtractorOptions, ComponentExtractor, ExtractionValidationIssue, ExtractionValidationIssueCode, } from './types.js';
2
2
  export { stripScoringFields } from './types.js';
3
3
  export { extractComponents } from './extract/pipeline.js';
4
4
  export { extractReactComponents } from './extract/react.js';
@@ -62,6 +62,15 @@ export interface RawComponentDefinition {
62
62
  export interface ComponentExtractionResult {
63
63
  components: RawComponentDefinition[];
64
64
  warnings: string[];
65
+ exclusions?: ExtractionExclusion[];
66
+ }
67
+ /** A deterministic item removed before the interactive scope gate. */
68
+ export interface ExtractionExclusion {
69
+ itemType: 'component' | 'file' | 'prop';
70
+ name: string;
71
+ source?: string;
72
+ reason: string;
73
+ stage: string;
65
74
  }
66
75
  export type ExtractorProgress = {
67
76
  filesProcessed: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-extraction",
3
- "version": "2.35.0-alpha.0",
3
+ "version": "2.35.0",
4
4
  "description": "Component extraction engine for the Contentful Experience Design System SDK",
5
5
  "license": "MIT",
6
6
  "type": "module",