@contentful/experience-design-system-cli 2.12.3 → 2.12.4-dev-build-262febd.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.12.3",
3
+ "version": "2.12.4-dev-build-262febd.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,20 @@
1
1
  import { Project, Node, SyntaxKind, } from 'ts-morph';
2
2
  import { extractAllowedValues, getNodeDefinitions, getTypeReferenceName, getTypeTargetDeclarations, } from './tsx-shared.js';
3
3
  import { shouldBeSlot } from './slot-detection.js';
4
+ import { extractAllowedComponentsFromTypeText, extractAllowedComponentsFromJsdoc } from './slot-allowed-components.js';
5
+ /**
6
+ * Matches ReactElement<XProps ...> (with optional `React.` prefix) anywhere in
7
+ * a type text. TS often resolves this to ReactElement<XProps, string | JSXElementConstructor<any>>,
8
+ * so we allow anything after the first type argument.
9
+ */
10
+ const REACT_ELEMENT_GENERIC_TEST = /(?:React\.)?ReactElement\s*<\s*[A-Za-z_$][\w$.]*/;
11
+ /**
12
+ * True when a type text references ReactElement<XProps> — either directly, as a
13
+ * union with null/undefined, or as a union of ReactElement<...> types.
14
+ */
15
+ function isReactElementGenericSlotType(typeText) {
16
+ return REACT_ELEMENT_GENERIC_TEST.test(typeText);
17
+ }
4
18
  const PROP_WRAPPER_TYPE_NAMES = new Set(['ExpandProps']);
5
19
  const CHILD_WRAPPER_TYPE_NAMES = new Set(['PropsWithChildren']);
6
20
  const TRANSPARENT_POLYMORPHIC_TYPE_NAMES = new Set(['PolymorphicProps', 'PropsWithAs', 'PropsWithHTMLElement']);
@@ -948,7 +962,7 @@ function extractDestructuredBindingFallbackProps(func, param, existingPropNames,
948
962
  function extractSlots(type, hasChildren) {
949
963
  const slots = [];
950
964
  if (hasChildren) {
951
- slots.push({ name: 'default', isDefault: true });
965
+ slots.push({ name: 'children', isDefault: true });
952
966
  }
953
967
  for (const property of type.getProperties()) {
954
968
  const name = property.getName();
@@ -1653,6 +1667,40 @@ export async function extractReactComponents(filePaths) {
1653
1667
  warnings.push(`Failed to extract from ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
1654
1668
  }
1655
1669
  }
1670
+ // Post-pass: resolve $allowedComponents for slots whose type text referenced
1671
+ // ReactElement<XProps>. Build a props-type-name → component-name map from the
1672
+ // full run first so cross-file references resolve.
1673
+ const propsToComponent = new Map();
1674
+ const componentNames = new Set();
1675
+ for (const c of components) {
1676
+ componentNames.add(c.name);
1677
+ if (c._propsTypeName)
1678
+ propsToComponent.set(c._propsTypeName, c.name);
1679
+ }
1680
+ for (const c of components) {
1681
+ for (const slot of c.slots) {
1682
+ const found = new Set();
1683
+ if (slot._rawTypeText) {
1684
+ for (const n of extractAllowedComponentsFromTypeText(slot._rawTypeText, {
1685
+ propsToComponent,
1686
+ componentNames,
1687
+ })) {
1688
+ found.add(n);
1689
+ }
1690
+ }
1691
+ if (slot._rawJsdoc) {
1692
+ for (const n of extractAllowedComponentsFromJsdoc(slot._rawJsdoc, componentNames)) {
1693
+ found.add(n);
1694
+ }
1695
+ }
1696
+ delete slot._rawTypeText;
1697
+ delete slot._rawJsdoc;
1698
+ if (found.size > 0) {
1699
+ slot.allowedComponents = [...found].sort();
1700
+ }
1701
+ }
1702
+ delete c._propsTypeName;
1703
+ }
1656
1704
  return {
1657
1705
  components: components.sort((a, b) => a.name.localeCompare(b.name)),
1658
1706
  warnings,
@@ -1763,19 +1811,28 @@ function extractFromSourceFile(sourceFile, isNext) {
1763
1811
  };
1764
1812
  });
1765
1813
  const filteredProps = filterImplementationOnlyAliasProps(propsWithDefaults, funcNode);
1766
- // Second pass: expand ReactNode-typed props into slots
1814
+ // Second pass: expand ReactNode-typed and ReactElement<XProps>-typed props into slots
1767
1815
  const existingSlotNames = new Set(slots.map((s) => s.name));
1768
1816
  const expandedSlots = [];
1769
1817
  const propsAfterSlotExpansion = filteredProps.filter((prop) => {
1770
1818
  if (existingSlotNames.has(prop.name))
1771
1819
  return true; // already handled
1772
- if (shouldBeSlot(prop.name, prop.type)) {
1773
- expandedSlots.push({ name: prop.name, isDefault: false });
1820
+ const isElementSlot = isReactElementGenericSlotType(prop.type);
1821
+ if (shouldBeSlot(prop.name, prop.type) || isElementSlot) {
1822
+ expandedSlots.push({
1823
+ name: prop.name,
1824
+ isDefault: false,
1825
+ ...(isElementSlot ? { _rawTypeText: prop.type } : {}),
1826
+ });
1774
1827
  return false;
1775
1828
  }
1776
1829
  return true;
1777
1830
  });
1778
1831
  const finalSlots = [...slots, ...expandedSlots];
1832
+ // Capture the props-type-name so the run-level post-pass can build a
1833
+ // props-type → component-name map for $allowedComponents resolution.
1834
+ const propsTypeName = firstParamTypeNode?.getText?.().trim();
1835
+ const propsTypeNameCapture = propsTypeName && /^[A-Za-z_$][\w$]*$/.test(propsTypeName) ? propsTypeName : undefined;
1779
1836
  components.push({
1780
1837
  name,
1781
1838
  source: sourceFile.getFilePath(),
@@ -1784,6 +1841,7 @@ function extractFromSourceFile(sourceFile, isNext) {
1784
1841
  props: propsAfterSlotExpansion,
1785
1842
  slots: finalSlots,
1786
1843
  ...(usesCreateContext && { usesCreateContext: true }),
1844
+ ...(propsTypeNameCapture ? { _propsTypeName: propsTypeNameCapture } : {}),
1787
1845
  });
1788
1846
  }
1789
1847
  return components;
@@ -0,0 +1,8 @@
1
+ export interface AllowedComponentsContext {
2
+ /** e.g. { HeadingProps -> Heading, ButtonProps -> Button } */
3
+ propsToComponent: ReadonlyMap<string, string>;
4
+ /** ComponentType names known to the project (post-filter). */
5
+ componentNames: ReadonlySet<string>;
6
+ }
7
+ export declare function extractAllowedComponentsFromTypeText(typeText: string, ctx: AllowedComponentsContext): string[];
8
+ export declare function extractAllowedComponentsFromJsdoc(jsdocText: string, componentNames: ReadonlySet<string>): string[];
@@ -0,0 +1,39 @@
1
+ // Matches ReactElement<XProps> and ReactElement<XProps, ...> (TS often
2
+ // expands the second generic argument to string | JSXElementConstructor<any>).
3
+ // Only the first generic argument (the props type name) is captured.
4
+ const REACT_ELEMENT_GENERIC = /(?:React\.)?ReactElement\s*<\s*([A-Za-z_$][\w$.]*)(?![\w$.])/g;
5
+ // Svelte 5 typed snippets: `Snippet<[XProps]>`. The type argument is a tuple
6
+ // listing render args; when a snippet is authored to render a nested
7
+ // ComponentType, the single tuple element is that component's Props type.
8
+ // We only match the single-element tuple form — non-props render args
9
+ // (e.g. `Snippet<[year: number]>`) don't reference a ComponentType and are
10
+ // filtered out below via propsToComponent lookup.
11
+ const SVELTE_SNIPPET_GENERIC = /Snippet\s*<\s*\[\s*([A-Za-z_$][\w$.]*)\s*\]\s*>/g;
12
+ export function extractAllowedComponentsFromTypeText(typeText, ctx) {
13
+ const found = new Set();
14
+ for (const re of [REACT_ELEMENT_GENERIC, SVELTE_SNIPPET_GENERIC]) {
15
+ re.lastIndex = 0;
16
+ let m;
17
+ while ((m = re.exec(typeText)) !== null) {
18
+ const propsTypeName = m[1];
19
+ const componentName = ctx.propsToComponent.get(propsTypeName);
20
+ if (componentName && ctx.componentNames.has(componentName)) {
21
+ found.add(componentName);
22
+ }
23
+ }
24
+ }
25
+ return [...found].sort();
26
+ }
27
+ const JSDOC_TAG = /@allowedComponents\s+([^\n*]+)/;
28
+ export function extractAllowedComponentsFromJsdoc(jsdocText, componentNames) {
29
+ const m = JSDOC_TAG.exec(jsdocText);
30
+ if (!m)
31
+ return [];
32
+ const found = new Set();
33
+ for (const raw of m[1].split(',')) {
34
+ const name = raw.trim();
35
+ if (name && componentNames.has(name))
36
+ found.add(name);
37
+ }
38
+ return [...found].sort();
39
+ }
@@ -6,6 +6,7 @@ import os from 'node:os';
6
6
  import { parse as parseSvelte } from 'svelte/compiler';
7
7
  import { Project, Node, ScriptTarget, ModuleKind, ts } from 'ts-morph';
8
8
  import { computeExtractionScore, deriveNeedsReview } from './scoring.js';
9
+ import { extractAllowedComponentsFromTypeText } from './slot-allowed-components.js';
9
10
  const SVELTE_EXTRACT_CONCURRENCY = Number(process.env['EDS_EXTRACT_CONCURRENCY'] ?? 0) || os.cpus().length;
10
11
  export async function extractSvelteComponents(filePaths, onProgress, opts) {
11
12
  const svelteFiles = filePaths.filter((f) => f.endsWith('.svelte'));
@@ -42,11 +43,36 @@ export async function extractSvelteComponents(filePaths, onProgress, opts) {
42
43
  }
43
44
  await Promise.all(Array.from({ length: Math.min(SVELTE_EXTRACT_CONCURRENCY, svelteFiles.length) }, worker));
44
45
  const finalWarnings = await maybeRunResolveUnreachableRetry(components, warnings, retryContexts, opts);
46
+ // Post-pass: resolve $allowedComponents for snippet slots whose type text
47
+ // referenced Snippet<[XProps]>. Build a props-type-name → component-name map
48
+ // from the full run first so cross-file references resolve.
49
+ resolveAllowedComponents(components);
45
50
  return {
46
51
  components: components.sort((a, b) => a.name.localeCompare(b.name)),
47
52
  warnings: collapseUnresolvedTypeWarnings(finalWarnings),
48
53
  };
49
54
  }
55
+ function resolveAllowedComponents(components) {
56
+ const propsToComponent = new Map();
57
+ const componentNames = new Set();
58
+ for (const c of components) {
59
+ componentNames.add(c.name);
60
+ if (c._propsTypeName)
61
+ propsToComponent.set(c._propsTypeName, c.name);
62
+ }
63
+ for (const c of components) {
64
+ for (const slot of c.slots) {
65
+ const raw = slot._rawTypeText;
66
+ if (raw) {
67
+ const found = extractAllowedComponentsFromTypeText(raw, { propsToComponent, componentNames });
68
+ if (found.length > 0)
69
+ slot.allowedComponents = found;
70
+ }
71
+ delete slot._rawTypeText;
72
+ }
73
+ delete c._propsTypeName;
74
+ }
75
+ }
50
76
  // ---------------------------------------------------------------------------
51
77
  // Resolve-unreachable retry pass (Approach B)
52
78
  //
@@ -548,12 +574,26 @@ async function extractFromSvelteFile(filePath, source) {
548
574
  if (mixedWarning) {
549
575
  warnings.push(`${name}: mixed Snippet and <slot> usage detected (${filePath}); preferring Snippet entries`);
550
576
  }
577
+ // Capture the props-type-name (if any) so the run-level post-pass can build
578
+ // a props-type → component-name map for $allowedComponents resolution
579
+ // (mirrors the React extractor's `_propsTypeName` mechanism).
580
+ let propsTypeNameCapture;
581
+ if (propsCall) {
582
+ const id = propsCall['id'];
583
+ const annotation = id?.['typeAnnotation']?.['typeAnnotation'];
584
+ if (annotation?.type === 'TSTypeReference') {
585
+ const tn = annotation['typeName']?.['name'];
586
+ if (tn && /^[A-Za-z_$][\w$]*$/.test(tn))
587
+ propsTypeNameCapture = tn;
588
+ }
589
+ }
551
590
  const component = {
552
591
  name,
553
592
  source: filePath,
554
593
  framework: 'svelte',
555
594
  props,
556
595
  slots,
596
+ ...(propsTypeNameCapture ? { _propsTypeName: propsTypeNameCapture } : {}),
557
597
  };
558
598
  // Score & flag review. Forward extraction-time reasons (e.g. props-type-unresolved)
559
599
  // so they count toward the confidence score AND surface in reviewReasons.
@@ -899,6 +939,7 @@ async function resolveViaTypeChecker(annotation, instance, moduleScript, filePat
899
939
  name,
900
940
  optional,
901
941
  typeText,
942
+ ...(declaredTypeText ? { declaredTypeText } : {}),
902
943
  isSnippet,
903
944
  ...(allowed ? { allowedValues: allowed } : {}),
904
945
  ...(description ? { description } : {}),
@@ -1120,6 +1161,19 @@ function renderType(typeNode) {
1120
1161
  case 'TSArrayType': {
1121
1162
  return `${renderType(typeNode['elementType'])}[]`;
1122
1163
  }
1164
+ case 'TSTupleType': {
1165
+ // Tuple element types can be plain (`T`) or labeled (`name: T` →
1166
+ // TSNamedTupleMember). Render each element by drilling to the type.
1167
+ const elements = typeNode['elementTypes'] ?? [];
1168
+ const rendered = elements.map((el) => {
1169
+ if (el.type === 'TSNamedTupleMember') {
1170
+ const inner = el['elementType'];
1171
+ return renderType(inner);
1172
+ }
1173
+ return renderType(el);
1174
+ });
1175
+ return `[${rendered.join(', ')}]`;
1176
+ }
1123
1177
  case 'TSUnionType': {
1124
1178
  const members = typeNode['types'] ?? [];
1125
1179
  return members.map(renderType).join(' | ');
@@ -1204,11 +1258,19 @@ function extractFromDestructure(propsCall, _ctx, typeMembers, warnings, addition
1204
1258
  const typeMember = typeByName.get(name);
1205
1259
  if (typeMember?.isSnippet) {
1206
1260
  snippetNames.add(name);
1207
- snippetSlots.push({
1261
+ const slot = {
1208
1262
  name,
1209
1263
  isDefault: name === 'children',
1210
1264
  ...(typeMember.description ? { description: typeMember.description } : {}),
1211
- });
1265
+ };
1266
+ // Stash the author-facing type text so the run-level post-pass can
1267
+ // resolve $allowedComponents from `Snippet<[XProps]>`.
1268
+ // Prefer the author-facing `Snippet<[XProps]>` shape; the resolved
1269
+ // typeText may be an expanded call signature that loses the tuple form.
1270
+ const authorText = typeMember.declaredTypeText ?? typeMember.typeText;
1271
+ if (authorText)
1272
+ slot._rawTypeText = authorText;
1273
+ snippetSlots.push(slot);
1212
1274
  continue;
1213
1275
  }
1214
1276
  const required = typeMember ? !typeMember.optional && !hasDefault : !hasDefault;
@@ -1253,11 +1315,15 @@ function extractFromTypeMembersOnly(typeMembers) {
1253
1315
  for (const m of typeMembers) {
1254
1316
  if (m.isSnippet) {
1255
1317
  snippetNames.add(m.name);
1256
- snippetSlots.push({
1318
+ const slot = {
1257
1319
  name: m.name,
1258
1320
  isDefault: m.name === 'children',
1259
1321
  ...(m.description ? { description: m.description } : {}),
1260
- });
1322
+ };
1323
+ const authorText = m.declaredTypeText ?? m.typeText;
1324
+ if (authorText)
1325
+ slot._rawTypeText = authorText;
1326
+ snippetSlots.push(slot);
1261
1327
  continue;
1262
1328
  }
1263
1329
  const propDef = {
@@ -1,5 +1,15 @@
1
1
  import React from 'react';
2
2
  import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
3
+ export interface PreviewDiffLine {
4
+ key: string;
5
+ color: 'green' | 'red' | 'yellow' | 'gray';
6
+ text: string;
7
+ }
8
+ /**
9
+ * Pure builder for the wizard preview diff lines. Extracted from the render
10
+ * layer so it can be unit-tested independently of Ink.
11
+ */
12
+ export declare function buildPreviewDiffLines(preview: ServerPreviewResponse): PreviewDiffLine[];
3
13
  type WizardPreviewStepProps = {
4
14
  preview: ServerPreviewResponse;
5
15
  spaceId: string;
@@ -1,9 +1,99 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useMemo } from 'react';
3
3
  import { Box, Text, useStdout } from 'ink';
4
4
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
5
5
  import { hasBreakingChangesWithImpact } from '../../../apply/manifest.js';
6
6
  import { computeComponentDiffLines } from './preview-diff.js';
7
+ /**
8
+ * Pure builder for the wizard preview diff lines. Extracted from the render
9
+ * layer so it can be unit-tested independently of Ink.
10
+ */
11
+ export function buildPreviewDiffLines(preview) {
12
+ const lines = [];
13
+ const { components, tokens } = preview;
14
+ for (const item of components.new) {
15
+ const raw = item;
16
+ const name = raw.key ?? raw.$name ?? 'unknown';
17
+ lines.push({ key: `comp-new-${name}`, color: 'green', text: ` + ${name}` });
18
+ const slots = (raw.$slots ?? {});
19
+ for (const slotName of Object.keys(slots).sort()) {
20
+ lines.push({
21
+ key: `comp-new-${name}-slot-${slotName}`,
22
+ color: 'green',
23
+ text: ` slot: ${slotName}`,
24
+ });
25
+ const allowed = slots[slotName]?.['$allowedComponents'];
26
+ if (Array.isArray(allowed) && allowed.length > 0) {
27
+ const names = allowed.filter((n) => typeof n === 'string');
28
+ lines.push({
29
+ key: `comp-new-${name}-slot-${slotName}-allow`,
30
+ color: 'green',
31
+ text: ` allowedComponents: [${names.join(', ')}]`,
32
+ });
33
+ }
34
+ }
35
+ }
36
+ for (const item of components.removed) {
37
+ lines.push({ key: `comp-rm-${item.name}`, color: 'red', text: ` - ${item.name}` });
38
+ }
39
+ for (const item of components.changed) {
40
+ lines.push({
41
+ key: `comp-h-${item.current.name}`,
42
+ color: 'yellow',
43
+ text: ` ~ ${item.current.name}${item.hasPendingDraftChanges ? ' ⚡ has pending draft changes' : ''}`,
44
+ });
45
+ if (item.changeClassification?.classification === 'breaking') {
46
+ const reasons = item.changeClassification.breakingChanges
47
+ .map((bc) => `${bc.propertyId}: ${bc.reason}`)
48
+ .join(', ');
49
+ lines.push({ key: `comp-b-${item.current.name}`, color: 'red', text: ` ⚠ BREAKING: ${reasons}` });
50
+ }
51
+ const diffLines = computeComponentDiffLines(item.current, item.proposed, item.changeClassification);
52
+ for (const d of diffLines) {
53
+ lines.push({ key: `comp-d-${item.current.name}-${d.key}`, color: d.color, text: ` ${d.text}` });
54
+ }
55
+ // Also enumerate slots + allowedComponents from the proposed side so the
56
+ // reviewer sees the shape they're pushing, not just the delta.
57
+ const proposedSlots = item.proposed['$slots'] ?? {};
58
+ for (const slotName of Object.keys(proposedSlots).sort()) {
59
+ const allowed = proposedSlots[slotName]?.['$allowedComponents'];
60
+ if (Array.isArray(allowed) && allowed.length > 0) {
61
+ const names = allowed.filter((n) => typeof n === 'string');
62
+ const key = `comp-d-${item.current.name}-slot-${slotName}-allow-list`;
63
+ if (!lines.some((l) => l.key === key)) {
64
+ lines.push({
65
+ key,
66
+ color: 'gray',
67
+ text: ` slot ${slotName} allowedComponents: [${names.join(', ')}]`,
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+ for (const item of tokens.new) {
74
+ const raw = item;
75
+ const name = raw.name ?? raw.path ?? 'unknown';
76
+ lines.push({ key: `tok-new-${name}`, color: 'green', text: ` + ${name}` });
77
+ }
78
+ for (const item of tokens.removed) {
79
+ lines.push({ key: `tok-rm-${item.name}`, color: 'red', text: ` - ${item.name}` });
80
+ }
81
+ for (const item of tokens.changed) {
82
+ const tokenName = item.current.name;
83
+ lines.push({
84
+ key: `tok-h-${tokenName}`,
85
+ color: 'yellow',
86
+ text: ` ~ ${tokenName}${item.hasPendingDraftChanges ? ' ⚡ has pending draft changes' : ''}`,
87
+ });
88
+ if (item.changeClassification?.classification === 'breaking') {
89
+ const reasons = item.changeClassification.breakingChanges
90
+ .map((bc) => `${bc.propertyId}: ${bc.reason}`)
91
+ .join(', ');
92
+ lines.push({ key: `tok-b-${tokenName}`, color: 'red', text: ` ⚠ BREAKING: ${reasons}` });
93
+ }
94
+ }
95
+ return lines;
96
+ }
7
97
  export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber, totalSteps, onConfirm, onEdit, onSaveFiles, onQuit, }) {
8
98
  const breakingWithImpact = hasBreakingChangesWithImpact(preview);
9
99
  const [diffExpanded, setDiffExpanded] = useState(false);
@@ -14,56 +104,10 @@ export function WizardPreviewStep({ preview, spaceId, environmentId, stepNumber,
14
104
  const allDiffLines = useMemo(() => {
15
105
  if (!diffExpanded)
16
106
  return [];
17
- const lines = [];
18
- const { components, tokens } = preview;
19
- for (const item of components.new) {
20
- const name = item.key ??
21
- item.$name ??
22
- 'unknown';
23
- lines.push({ key: `comp-new-${name}`, element: _jsxs(Text, { color: "green", children: [" + ", name] }) });
24
- }
25
- for (const item of components.removed) {
26
- lines.push({ key: `comp-rm-${item.name}`, element: _jsxs(Text, { color: "red", children: [" - ", item.name] }) });
27
- }
28
- for (const item of components.changed) {
29
- lines.push({
30
- key: `comp-h-${item.current.name}`,
31
- element: (_jsxs(Text, { color: "yellow", children: [' ', "~ ", item.current.name, item.hasPendingDraftChanges ? _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" }) : null] })),
32
- });
33
- if (item.changeClassification?.classification === 'breaking') {
34
- const reasons = item.changeClassification.breakingChanges
35
- .map((bc) => `${bc.propertyId}: ${bc.reason}`)
36
- .join(', ');
37
- lines.push({ key: `comp-b-${item.current.name}`, element: _jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }) });
38
- }
39
- const diffLines = computeComponentDiffLines(item.current, item.proposed, item.changeClassification);
40
- for (const d of diffLines) {
41
- lines.push({ key: `comp-d-${item.current.name}-${d.key}`, element: _jsxs(Text, { color: d.color, children: [" ", d.text] }) });
42
- }
43
- }
44
- for (const item of tokens.new) {
45
- const name = item.name ??
46
- item.path ??
47
- 'unknown';
48
- lines.push({ key: `tok-new-${name}`, element: _jsxs(Text, { color: "green", children: [" + ", name] }) });
49
- }
50
- for (const item of tokens.removed) {
51
- lines.push({ key: `tok-rm-${item.name}`, element: _jsxs(Text, { color: "red", children: [" - ", item.name] }) });
52
- }
53
- for (const item of tokens.changed) {
54
- const tokenName = item.current.name;
55
- lines.push({
56
- key: `tok-h-${tokenName}`,
57
- element: (_jsxs(Text, { color: "yellow", children: [' ', "~ ", tokenName, item.hasPendingDraftChanges ? _jsx(Text, { color: "yellow", children: " \u26A1 has pending draft changes" }) : null] })),
58
- });
59
- if (item.changeClassification?.classification === 'breaking') {
60
- const reasons = item.changeClassification.breakingChanges
61
- .map((bc) => `${bc.propertyId}: ${bc.reason}`)
62
- .join(', ');
63
- lines.push({ key: `tok-b-${tokenName}`, element: _jsxs(Text, { color: "red", children: [" \u26A0 BREAKING: ", reasons] }) });
64
- }
65
- }
66
- return lines;
107
+ return buildPreviewDiffLines(preview).map((line) => ({
108
+ key: line.key,
109
+ element: (_jsx(Text, { color: line.color === 'gray' ? undefined : line.color, dimColor: line.color === 'gray', children: line.text })),
110
+ }));
67
111
  }, [diffExpanded, preview]);
68
112
  const maxScroll = Math.max(0, allDiffLines.length - viewportHeight);
69
113
  useImmediateInput((input, key) => {
@@ -59,6 +59,7 @@ export function computeComponentDiffLines(current, proposed, changeClassificatio
59
59
  const currentSlots = new Set(current.slots);
60
60
  const proposedSlots = (proposed['$slots'] ?? {});
61
61
  const proposedSlotNames = new Set(Object.keys(proposedSlots));
62
+ const currentSlotAllowed = current.currentSlotAllowed ?? {};
62
63
  for (const name of [...proposedSlotNames].sort()) {
63
64
  if (!currentSlots.has(name)) {
64
65
  lines.push({ key: `slot-${name}-add`, color: 'green', text: `+ slot: ${name}` });
@@ -69,8 +70,55 @@ export function computeComponentDiffLines(current, proposed, changeClassificatio
69
70
  lines.push({ key: `slot-${name}-rm`, color: 'red', text: `- slot: ${name}` });
70
71
  }
71
72
  }
73
+ // $allowedComponents diffs — for each slot present in both sides, or newly-added
74
+ // with a non-empty allowedComponents list.
75
+ for (const name of [...proposedSlotNames].sort()) {
76
+ const nextAllowed = normalizeAllowedComponents(proposedSlots[name]?.['$allowedComponents']);
77
+ const prevAllowed = normalizeAllowedComponents(currentSlotAllowed[name]);
78
+ const prevExists = currentSlots.has(name);
79
+ if (!prevExists) {
80
+ if (nextAllowed.length > 0) {
81
+ lines.push({
82
+ key: `slot-${name}-allow-new`,
83
+ color: 'green',
84
+ text: `+ slot ${name} allowedComponents: [${nextAllowed.join(', ')}]`,
85
+ });
86
+ }
87
+ continue;
88
+ }
89
+ if (arraysEqual(prevAllowed, nextAllowed))
90
+ continue;
91
+ lines.push({
92
+ key: `slot-${name}-allow-old`,
93
+ color: 'red',
94
+ text: `- slot ${name} allowedComponents: [${prevAllowed.join(', ')}]`,
95
+ });
96
+ lines.push({
97
+ key: `slot-${name}-allow-new`,
98
+ color: 'green',
99
+ text: `+ slot ${name} allowedComponents: [${nextAllowed.join(', ')}]`,
100
+ });
101
+ }
72
102
  return lines;
73
103
  }
104
+ function normalizeAllowedComponents(value) {
105
+ if (!Array.isArray(value))
106
+ return [];
107
+ const out = [];
108
+ for (const item of value) {
109
+ if (typeof item === 'string' && item.length > 0)
110
+ out.push(item);
111
+ }
112
+ return out.sort();
113
+ }
114
+ function arraysEqual(a, b) {
115
+ if (a.length !== b.length)
116
+ return false;
117
+ for (let i = 0; i < a.length; i++)
118
+ if (a[i] !== b[i])
119
+ return false;
120
+ return true;
121
+ }
74
122
  function normalizeType(type) {
75
123
  switch (type.toLowerCase()) {
76
124
  case 'boolean':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.12.3",
3
+ "version": "2.12.4-dev-build-262febd.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.12.3"
40
+ "@contentful/experience-design-system-types": "2.12.4-dev-build-262febd.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@tsconfig/node24": "^24.0.3",