@contentful/experience-design-system-cli 2.12.3-dev-build-d991763.0 → 2.12.4-dev-build-fd7d551.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/react.js +61 -3
- package/dist/src/analyze/extract/slot-allowed-components.d.ts +8 -0
- package/dist/src/analyze/extract/slot-allowed-components.js +39 -0
- package/dist/src/analyze/extract/svelte.js +70 -4
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +10 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -51
- package/dist/src/import/tui/steps/preview-diff.js +48 -0
- package/package.json +2 -2
package/dist/package.json
CHANGED
|
@@ -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']);
|
|
@@ -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
|
-
|
|
1773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
"version": "2.12.4-dev-build-fd7d551.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.
|
|
40
|
+
"@contentful/experience-design-system-types": "2.12.4-dev-build-fd7d551.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@tsconfig/node24": "^24.0.3",
|