@contentful/experience-design-system-cli 2.5.2 → 2.5.3-dev-build-b2e98f1.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/README.md +14 -0
- package/dist/package.json +1 -1
- package/dist/src/analyze/command.js +19 -15
- package/dist/src/analyze/extract/non-authorable-filter.d.ts +16 -0
- package/dist/src/analyze/extract/non-authorable-filter.js +60 -0
- package/dist/src/analyze/extract/react.js +11 -0
- package/dist/src/analyze/tui/AnalyzeView.d.ts +0 -4
- package/dist/src/analyze/tui/AnalyzeView.js +1 -1
- package/dist/src/apply/api-client.js +2 -6
- package/dist/src/apply/command.js +2 -4
- package/dist/src/types.d.ts +6 -0
- package/package.json +2 -2
- package/skills/generate-components.md +2 -2
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ analyze extract → analyze select-agent → generate components →
|
|
|
12
12
|
|
|
13
13
|
`analyze select-agent` uses an AI agent to decide which extracted components belong in Contentful Experience Orchestration. You can substitute it with `analyze select` for manual/pattern-based selection.
|
|
14
14
|
|
|
15
|
+
**Determinism boundary.** `analyze extract` is fully deterministic: ts-morph AST parsing produces the same component list and prop shape on every run, then a deterministic pre-classifier and a structural non-authorable filter (see below) shape the output. AI enters the pipeline at `analyze select-agent` and `generate components`, where coding agents make per-component decisions. This split keeps the extracted artifact reproducible and inspectable — if an extracted component looks wrong, the cause is in the rules, not in agent variability.
|
|
16
|
+
|
|
15
17
|
All intermediate data flows through a local SQLite session database (`~/.contentful/experience-design-system-cli/pipeline.db`). No JSON files are written between steps — each command reads its inputs from the session and writes its outputs back to it. Use `print` to export session data to JSON files on demand (e.g. for inspection or manual validation).
|
|
16
18
|
|
|
17
19
|
---
|
|
@@ -91,6 +93,18 @@ Scans `.tsx`, `.ts`, `.jsx`, `.js`, `.vue`, and `.astro` files. Ignores `node_mo
|
|
|
91
93
|
|
|
92
94
|
Writes extracted components to the session database and prints `session=<id>` to stdout. In an interactive terminal, a scrollable TUI displays the extraction summary (including a warning for any components with 0 props and 0 slots); press `q` or `Enter` to exit.
|
|
93
95
|
|
|
96
|
+
#### Non-authorable component filter
|
|
97
|
+
|
|
98
|
+
Before storing components, `analyze extract` runs a deterministic filter that drops infrastructure components which have no authoring surface (Context providers, analytics shims, security utilities, layout helpers). The filter uses prop-shape signals only — no component-name or source-path patterns — so it works regardless of how a host repo organizes its design system. A component is dropped if **any** of:
|
|
99
|
+
|
|
100
|
+
1. Zero props and zero slots.
|
|
101
|
+
2. Source calls `createContext()` and the component has a prop literally named `value`.
|
|
102
|
+
3. Source calls `createContext()` and the component has zero props.
|
|
103
|
+
4. Source calls `createContext()` and the component has exactly one non-handler prop.
|
|
104
|
+
5. Every prop is a handler or ref (function-typed, `EventHandler`, `Dispatch`, `SetStateAction`, `Ref<>`, name starts with `on`/`set`, or named `ref`/`innerRef`).
|
|
105
|
+
|
|
106
|
+
Each dropped component is reported as a warning (`Skipped non-authorable component: <Name> (<reason>)`) so the operator can audit. The rule set was selected via Monte-Carlo evaluation against a hand-labelled corpus to maximize precision (zero false positives) over recall — components that look like normal authoring surface but are actually infrastructure are deferred to the AI selection stage rather than dropped here.
|
|
107
|
+
|
|
94
108
|
---
|
|
95
109
|
|
|
96
110
|
### `analyze select`
|
package/dist/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import { registerAnalyzeEditCommand } from './select/command.js';
|
|
|
8
8
|
import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
|
|
9
9
|
import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents } from '../session/db.js';
|
|
10
10
|
import { preClassifyComponent } from './pre-classify.js';
|
|
11
|
+
import { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
|
|
11
12
|
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.ts', '.tsx', '.vue']);
|
|
12
13
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
13
14
|
'.git',
|
|
@@ -121,23 +122,32 @@ export function registerAnalyzeCommand(program) {
|
|
|
121
122
|
});
|
|
122
123
|
const stepId = createStep(db, sessionId, 'analyze extract', { project: projectRoot });
|
|
123
124
|
const classifiedComponents = extraction.components.map(preClassifyComponent);
|
|
124
|
-
|
|
125
|
+
const filteredComponents = [];
|
|
126
|
+
const filterWarnings = [];
|
|
127
|
+
for (const component of classifiedComponents) {
|
|
128
|
+
const verdict = isNonAuthorableComponent(component);
|
|
129
|
+
if (verdict.skip) {
|
|
130
|
+
filterWarnings.push(`Skipped non-authorable component: ${component.name} (${verdict.reason})`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
filteredComponents.push(component);
|
|
134
|
+
}
|
|
135
|
+
storeRawComponents(db, sessionId, filteredComponents);
|
|
125
136
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
126
137
|
db.close();
|
|
127
|
-
const
|
|
138
|
+
const allWarnings = [...extraction.warnings, ...filterWarnings];
|
|
128
139
|
const analyzeResult = {
|
|
129
140
|
sourceDirectory,
|
|
130
141
|
sessionId,
|
|
131
142
|
fileCount: sourceFiles.length,
|
|
132
|
-
components:
|
|
143
|
+
components: filteredComponents.map((c) => ({
|
|
133
144
|
name: c.name,
|
|
134
145
|
framework: c.framework,
|
|
135
146
|
propCount: c.props.length,
|
|
136
147
|
slotCount: c.slots.length,
|
|
137
|
-
warnings:
|
|
148
|
+
warnings: allWarnings.filter((w) => w.startsWith(c.name + ':')),
|
|
138
149
|
})),
|
|
139
|
-
totalWarnings:
|
|
140
|
-
zeroPropComponents: zeroPropComponents.map((c) => ({ name: c.name, source: c.source })),
|
|
150
|
+
totalWarnings: allWarnings.length,
|
|
141
151
|
};
|
|
142
152
|
if (process.stdout.isTTY) {
|
|
143
153
|
const { waitUntilExit } = render(createElement(AnalyzeView, {
|
|
@@ -153,15 +163,9 @@ export function registerAnalyzeCommand(program) {
|
|
|
153
163
|
`Scanned ${pluralize(sourceFiles.length, 'source file')} in ${sourceDirectory}`,
|
|
154
164
|
`Extracted ${pluralize(extraction.components.length, 'component')}`,
|
|
155
165
|
];
|
|
156
|
-
if (
|
|
157
|
-
summaryLines.push(`
|
|
158
|
-
summaryLines.push(...
|
|
159
|
-
summaryLines.push('These may be Storybook stories, context providers, or SSR utilities.');
|
|
160
|
-
summaryLines.push("Review them in 'analyze select' before generating.");
|
|
161
|
-
}
|
|
162
|
-
if (extraction.warnings.length > 0) {
|
|
163
|
-
summaryLines.push(`Warnings (${extraction.warnings.length}):`);
|
|
164
|
-
summaryLines.push(...extraction.warnings.map((w) => `- ${w}`));
|
|
166
|
+
if (allWarnings.length > 0) {
|
|
167
|
+
summaryLines.push(`Warnings (${allWarnings.length}):`);
|
|
168
|
+
summaryLines.push(...allWarnings.map((w) => `- ${w}`));
|
|
165
169
|
}
|
|
166
170
|
else {
|
|
167
171
|
summaryLines.push('Warnings: none');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RawComponentDefinition } from '../../types.js';
|
|
2
|
+
export interface NonAuthorableResult {
|
|
3
|
+
skip: boolean;
|
|
4
|
+
reason?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Decides whether a component is non-authorable infrastructure (Context.Provider,
|
|
8
|
+
* analytics shim, layout-only utility, etc.) and should be filtered out of the
|
|
9
|
+
* analyze TUI before authoring-token generation.
|
|
10
|
+
*
|
|
11
|
+
* Uses prop-shape signals only — no component-name or source-path patterns.
|
|
12
|
+
* A design system can live anywhere under any naming convention; relying on
|
|
13
|
+
* suffixes like `*Provider` or paths like `src/lib/` would silently fail in
|
|
14
|
+
* other repos.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isNonAuthorableComponent(component: RawComponentDefinition): NonAuthorableResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const HANDLER_TYPE_PATTERN = /=>|EventHandler|Dispatch<|SetStateAction/;
|
|
2
|
+
const REF_TYPE_PATTERN = /Ref<|RefObject<|MutableRefObject/;
|
|
3
|
+
function isHandlerOrRefProp(prop) {
|
|
4
|
+
if (HANDLER_TYPE_PATTERN.test(prop.type))
|
|
5
|
+
return true;
|
|
6
|
+
if (REF_TYPE_PATTERN.test(prop.type))
|
|
7
|
+
return true;
|
|
8
|
+
if (/^on[A-Z]/.test(prop.name) || /^set[A-Z]/.test(prop.name))
|
|
9
|
+
return true;
|
|
10
|
+
if (prop.name === 'ref' || prop.name === 'innerRef')
|
|
11
|
+
return true;
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Decides whether a component is non-authorable infrastructure (Context.Provider,
|
|
16
|
+
* analytics shim, layout-only utility, etc.) and should be filtered out of the
|
|
17
|
+
* analyze TUI before authoring-token generation.
|
|
18
|
+
*
|
|
19
|
+
* Uses prop-shape signals only — no component-name or source-path patterns.
|
|
20
|
+
* A design system can live anywhere under any naming convention; relying on
|
|
21
|
+
* suffixes like `*Provider` or paths like `src/lib/` would silently fail in
|
|
22
|
+
* other repos.
|
|
23
|
+
*/
|
|
24
|
+
export function isNonAuthorableComponent(component) {
|
|
25
|
+
const { props, slots, usesCreateContext } = component;
|
|
26
|
+
// R1: zero props AND zero slots — nothing for an editor to author.
|
|
27
|
+
// Catches analytics scripts, GTM tags, layout fixers, security tokens, etc.
|
|
28
|
+
if (props.length === 0 && slots.length === 0) {
|
|
29
|
+
return { skip: true, reason: 'component has no props and no slots' };
|
|
30
|
+
}
|
|
31
|
+
// R2: createContext source + prop literally named `value` — canonical
|
|
32
|
+
// `<Context.Provider value={...}>` call site.
|
|
33
|
+
if (usesCreateContext && props.some((p) => p.name === 'value')) {
|
|
34
|
+
return {
|
|
35
|
+
skip: true,
|
|
36
|
+
reason: 'source uses createContext and component exposes a Context.Provider value prop',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// R3: createContext source + zero props — Provider wrapper that hard-codes
|
|
40
|
+
// the context value internally (e.g. FontProvider, BottomSheetProvider).
|
|
41
|
+
if (usesCreateContext && props.length === 0) {
|
|
42
|
+
return { skip: true, reason: 'source uses createContext and component has no props' };
|
|
43
|
+
}
|
|
44
|
+
// R4: createContext source + exactly one non-handler prop — Provider that
|
|
45
|
+
// takes the context value as its sole data prop, named after the data
|
|
46
|
+
// (e.g. `LocaleProvider({ locale })`, `NavigationProvider({ navItems })`).
|
|
47
|
+
if (usesCreateContext && props.length === 1 && !isHandlerOrRefProp(props[0])) {
|
|
48
|
+
return {
|
|
49
|
+
skip: true,
|
|
50
|
+
reason: 'source uses createContext and component has a single non-handler prop',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// R5: every prop is a handler/ref — pure data plumbing, no authoring surface.
|
|
54
|
+
// Catches components like `OsanoCookiePlaceholder({ onBannerLoaded })` or
|
|
55
|
+
// `FeedbackCard({ setShowModal })`.
|
|
56
|
+
if (props.length > 0 && props.every(isHandlerOrRefProp)) {
|
|
57
|
+
return { skip: true, reason: 'every prop is a handler or ref' };
|
|
58
|
+
}
|
|
59
|
+
return { skip: false };
|
|
60
|
+
}
|
|
@@ -233,6 +233,14 @@ function getBoundedImportedJsxDomSurface(tagNameNode) {
|
|
|
233
233
|
function isStencilFile(sourceFile) {
|
|
234
234
|
return sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === '@stencil/core');
|
|
235
235
|
}
|
|
236
|
+
function sourceFileUsesCreateContext(sourceFile) {
|
|
237
|
+
// Match React.createContext(...) and createContext(...) call expressions
|
|
238
|
+
return sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => {
|
|
239
|
+
const expr = call.getExpression();
|
|
240
|
+
const text = expr.getText();
|
|
241
|
+
return text === 'createContext' || text === 'React.createContext';
|
|
242
|
+
});
|
|
243
|
+
}
|
|
236
244
|
function isNextJsComponent(filePath, exportedNames) {
|
|
237
245
|
const normalized = filePath.replace(/\\/g, '/');
|
|
238
246
|
const isAppRouterFile = /\/app\/.*\/(page|layout)\.[jt]sx?$/.test(normalized);
|
|
@@ -1634,6 +1642,7 @@ export async function extractReactComponents(filePaths) {
|
|
|
1634
1642
|
function extractFromSourceFile(sourceFile, isNext) {
|
|
1635
1643
|
const components = [];
|
|
1636
1644
|
const exported = sourceFile.getExportedDeclarations();
|
|
1645
|
+
const usesCreateContext = sourceFileUsesCreateContext(sourceFile);
|
|
1637
1646
|
for (const [exportKey, declarations] of exported) {
|
|
1638
1647
|
let name = exportKey;
|
|
1639
1648
|
if (exportKey === 'default') {
|
|
@@ -1668,6 +1677,7 @@ function extractFromSourceFile(sourceFile, isNext) {
|
|
|
1668
1677
|
framework: isNext ? 'next' : 'react',
|
|
1669
1678
|
props: [],
|
|
1670
1679
|
slots: [],
|
|
1680
|
+
...(usesCreateContext && { usesCreateContext: true }),
|
|
1671
1681
|
});
|
|
1672
1682
|
continue;
|
|
1673
1683
|
}
|
|
@@ -1752,6 +1762,7 @@ function extractFromSourceFile(sourceFile, isNext) {
|
|
|
1752
1762
|
framework: isNext ? 'next' : 'react',
|
|
1753
1763
|
props: propsAfterSlotExpansion,
|
|
1754
1764
|
slots: finalSlots,
|
|
1765
|
+
...(usesCreateContext && { usesCreateContext: true }),
|
|
1755
1766
|
});
|
|
1756
1767
|
}
|
|
1757
1768
|
return components;
|
|
@@ -31,7 +31,7 @@ export function AnalyzeView({ result, onExit }) {
|
|
|
31
31
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TopBar, { subcommand: "analyze", hints: [
|
|
32
32
|
{ key: '?', label: 'help' },
|
|
33
33
|
{ key: 'q', label: 'quit' },
|
|
34
|
-
] }), _jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { children: 'Scanned ' + result.fileCount + ' source files in ' + result.sourceDirectory }), _jsx(Text, { children: 'Extracted ' + result.components.length + ' components' }), _jsx(Text, { dimColor: true, children: 'Session: ' + result.sessionId }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, children: "Components" }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { children: " " }), result.components.slice(scrollOffset).map((component) => (_jsxs(Box, { children: [component.warnings.length > 0 && _jsx(Text, { color: "yellow", children: "\u26A0 " }), component.warnings.length === 0 && _jsx(Text, { children: " " }), _jsx(Text, { children: truncateName(component.name).padEnd(20) }), _jsx(Text, { dimColor: true, children: component.framework.padEnd(10) }), _jsx(Text, { children: (component.propCount + ' props').padEnd(10) }), _jsx(Text, { children: component.slotCount + ' ' + (component.slotCount === 1 ? 'slot' : 'slots') }), component.warnings.length > 0 && (_jsx(Text, { color: "yellow", children: ' ' + component.warnings.length + ' warning' + (component.warnings.length === 1 ? '' : 's') }))] }, component.name))), result.
|
|
34
|
+
] }), _jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { children: 'Scanned ' + result.fileCount + ' source files in ' + result.sourceDirectory }), _jsx(Text, { children: 'Extracted ' + result.components.length + ' components' }), _jsx(Text, { dimColor: true, children: 'Session: ' + result.sessionId }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, children: "Components" }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { children: " " }), result.components.slice(scrollOffset).map((component) => (_jsxs(Box, { children: [component.warnings.length > 0 && _jsx(Text, { color: "yellow", children: "\u26A0 " }), component.warnings.length === 0 && _jsx(Text, { children: " " }), _jsx(Text, { children: truncateName(component.name).padEnd(20) }), _jsx(Text, { dimColor: true, children: component.framework.padEnd(10) }), _jsx(Text, { children: (component.propCount + ' props').padEnd(10) }), _jsx(Text, { children: component.slotCount + ' ' + (component.slotCount === 1 ? 'slot' : 'slots') }), component.warnings.length > 0 && (_jsx(Text, { color: "yellow", children: ' ' + component.warnings.length + ' warning' + (component.warnings.length === 1 ? '' : 's') }))] }, component.name))), result.totalWarnings > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { bold: true, color: "yellow", children: 'Warnings (' + result.totalWarnings + ')' }), _jsx(Text, { dimColor: true, children: '─'.repeat(70) }), _jsx(Text, { children: " " }), result.components
|
|
35
35
|
.filter((c) => c.warnings.length > 0)
|
|
36
36
|
.flatMap((c) => c.warnings.map((w) => ({ component: c.name, warning: w })))
|
|
37
37
|
.map((w, i) => (_jsx(Text, { color: "yellow", children: ' ⚠ ' + w.component + ': ' + w.warning }, i)))] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: 'Run: analyze select --session ' + result.sessionId }), _jsx(Text, { children: " " })] }), _jsx(Box, { borderStyle: "single", paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter or q to exit" }) })] }));
|
|
@@ -48,12 +48,8 @@ export class ImportApiClient {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
async validateToken() {
|
|
51
|
-
// /users/me is the canonical
|
|
52
|
-
//
|
|
53
|
-
// doesn't false-positive 401 for tokens that are entitled to call import endpoints
|
|
54
|
-
// but lack the role assignments public CMA's SpacesController/EnvironmentsController
|
|
55
|
-
// require. Per-space/per-org authorization is enforced by the design-systems API itself
|
|
56
|
-
// on the actual preview/apply call.
|
|
51
|
+
// /users/me is the canonical token-validity endpoint — avoids space-membership
|
|
52
|
+
// false positives that don't apply to the design-systems API authorization path.
|
|
57
53
|
const url = `${this.host}/users/me`;
|
|
58
54
|
const res = await request(url, { token: this.token });
|
|
59
55
|
if (res.status === 401) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createElement } from 'react';
|
|
2
|
-
import { render } from 'ink';
|
|
1
|
+
import { createElement, useState } from 'react';
|
|
2
|
+
import { render, useInput } from 'ink';
|
|
3
3
|
import { access, readFile, readdir, stat } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { validateCDF, flattenDTCG, validateDTCG, buildManifest, buildFilteredManifest, } from '@contentful/experience-design-system-types';
|
|
@@ -7,8 +7,6 @@ import { ApiError, ImportApiClient } from './api-client.js';
|
|
|
7
7
|
import { openPipelineDb, loadCDFComponents } from '../session/db.js';
|
|
8
8
|
import { ServerPreviewApp, ServerPreviewConfirm, ServerApplyProgress, ServerApplyDone } from './tui/ServerApplyView.js';
|
|
9
9
|
import { SelectView, makeSelectKey } from './tui/SelectView.js';
|
|
10
|
-
import { useState } from 'react';
|
|
11
|
-
import { useInput } from 'ink';
|
|
12
10
|
function die(message) {
|
|
13
11
|
process.stderr.write(`${message}\n`);
|
|
14
12
|
process.exit(1);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface RawComponentDefinition {
|
|
|
28
28
|
framework: 'react' | 'next' | 'vue' | 'astro' | 'web-component' | 'stencil';
|
|
29
29
|
props: RawPropDefinition[];
|
|
30
30
|
slots: RawSlotDefinition[];
|
|
31
|
+
/**
|
|
32
|
+
* True when the source file declaring this component calls
|
|
33
|
+
* React.createContext / createContext. Used downstream to filter
|
|
34
|
+
* non-authorable context-provider components.
|
|
35
|
+
*/
|
|
36
|
+
usesCreateContext?: boolean;
|
|
31
37
|
}
|
|
32
38
|
export interface ComponentExtractionResult {
|
|
33
39
|
components: RawComponentDefinition[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3-dev-build-b2e98f1.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"react-dom": "^18.3.1",
|
|
33
33
|
"ts-morph": "^27.0.2",
|
|
34
34
|
"typescript": "^5.9.3",
|
|
35
|
-
"@contentful/experience-design-system-types": "2.5.
|
|
35
|
+
"@contentful/experience-design-system-types": "2.5.3-dev-build-b2e98f1.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@tsconfig/node24": "^24.0.3",
|
|
@@ -109,7 +109,7 @@ Exactly **6** valid types:
|
|
|
109
109
|
| `token` | Design-token-linked prop — requires `token_kind` |
|
|
110
110
|
| `boolean` | Boolean toggle props (visible, disabled, enabled, etc.) |
|
|
111
111
|
|
|
112
|
-
> **IMPORTANT: No `number` type.** The
|
|
112
|
+
> **IMPORTANT: No `number` type.** The design-systems API only supports the `String` design property variant for numeric values. All numeric props must use `cdf_type: "string"` with the number as a string default (e.g. `"0"`, `"100"`). Boolean props can now use `cdf_type: "boolean"` directly.
|
|
113
113
|
|
|
114
114
|
> **Avoid `link` type for simple URL props.** Props named `href`, `url`, or holding plain URL strings → `cdf_type: "string"`, `cdf_category: "content"`. Reserve `link` for props that hold a reference to another Contentful entry.
|
|
115
115
|
|
|
@@ -337,7 +337,7 @@ Before emitting any tool calls, verify:
|
|
|
337
337
|
7. `required` values are JSON booleans, not strings
|
|
338
338
|
8. Framework internals (`ref`, event handlers, test IDs) are excluded — `className`, `style`, and `styles` are classified as `string` design props; discrete positional/geometric props (`top`, `bottom`, `left`, `right`, `rotation`, etc.) are also classified
|
|
339
339
|
9. No `cdf_type: "link"` used — `link` is reserved and rejected by the CLI parser
|
|
340
|
-
10. No `cdf_type: "number"` used — this is not a
|
|
340
|
+
10. No `cdf_type: "number"` used — this is not a supported type; use `"string"` with numeric defaults. `cdf_type: "boolean"` IS valid — use it for boolean toggle props.
|
|
341
341
|
|
|
342
342
|
After the run completes, the developer can validate the pipeline output with:
|
|
343
343
|
|