@contentful/experience-design-system-cli 2.12.4-dev-build-9c819d8.0 → 2.13.1-dev-build-847dead.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 +3 -6
- package/dist/src/analyze/command.js +2 -22
- package/dist/src/analyze/select/command.js +1 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +4 -1
- package/dist/src/analyze/select-agent/command.js +1 -1
- package/dist/src/apply/command.d.ts +1 -5
- package/dist/src/apply/command.js +0 -43
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.js +20 -102
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +0 -10
- package/dist/src/import/tui/steps/WizardPreviewStep.js +51 -95
- package/dist/src/import/tui/steps/preview-diff.js +0 -48
- package/dist/src/session/db.d.ts +0 -9
- package/dist/src/session/db.js +0 -46
- package/dist/src/types.d.ts +2 -76
- package/dist/src/types.js +1 -4
- package/package.json +3 -6
- package/dist/src/analyze/cycle-detection.d.ts +0 -68
- package/dist/src/analyze/cycle-detection.js +0 -285
- package/dist/src/analyze/extract/astro.d.ts +0 -5
- package/dist/src/analyze/extract/astro.js +0 -289
- package/dist/src/analyze/extract/non-authorable-filter.d.ts +0 -16
- package/dist/src/analyze/extract/non-authorable-filter.js +0 -60
- package/dist/src/analyze/extract/pipeline.d.ts +0 -6
- package/dist/src/analyze/extract/pipeline.js +0 -314
- package/dist/src/analyze/extract/react.d.ts +0 -2
- package/dist/src/analyze/extract/react.js +0 -2041
- package/dist/src/analyze/extract/scoring.d.ts +0 -12
- package/dist/src/analyze/extract/scoring.js +0 -108
- package/dist/src/analyze/extract/slot-allowed-components.d.ts +0 -8
- package/dist/src/analyze/extract/slot-allowed-components.js +0 -40
- package/dist/src/analyze/extract/slot-detection.d.ts +0 -35
- package/dist/src/analyze/extract/slot-detection.js +0 -101
- package/dist/src/analyze/extract/source-inspection.d.ts +0 -13
- package/dist/src/analyze/extract/source-inspection.js +0 -189
- package/dist/src/analyze/extract/stencil.d.ts +0 -2
- package/dist/src/analyze/extract/stencil.js +0 -296
- package/dist/src/analyze/extract/svelte.d.ts +0 -5
- package/dist/src/analyze/extract/svelte.js +0 -1626
- package/dist/src/analyze/extract/tsx-shared.d.ts +0 -8
- package/dist/src/analyze/extract/tsx-shared.js +0 -263
- package/dist/src/analyze/extract/validate.d.ts +0 -16
- package/dist/src/analyze/extract/validate.js +0 -94
- package/dist/src/analyze/extract/vue-tsx.d.ts +0 -2
- package/dist/src/analyze/extract/vue-tsx.js +0 -498
- package/dist/src/analyze/extract/vue.d.ts +0 -5
- package/dist/src/analyze/extract/vue.js +0 -653
- package/dist/src/analyze/extract/web-components.d.ts +0 -2
- package/dist/src/analyze/extract/web-components.js +0 -876
- package/dist/src/analyze/pre-classify.d.ts +0 -17
- package/dist/src/analyze/pre-classify.js +0 -211
|
@@ -59,7 +59,6 @@ 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 ?? {};
|
|
63
62
|
for (const name of [...proposedSlotNames].sort()) {
|
|
64
63
|
if (!currentSlots.has(name)) {
|
|
65
64
|
lines.push({ key: `slot-${name}-add`, color: 'green', text: `+ slot: ${name}` });
|
|
@@ -70,55 +69,8 @@ export function computeComponentDiffLines(current, proposed, changeClassificatio
|
|
|
70
69
|
lines.push({ key: `slot-${name}-rm`, color: 'red', text: `- slot: ${name}` });
|
|
71
70
|
}
|
|
72
71
|
}
|
|
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
|
-
}
|
|
102
72
|
return lines;
|
|
103
73
|
}
|
|
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
|
-
}
|
|
122
74
|
function normalizeType(type) {
|
|
123
75
|
switch (type.toLowerCase()) {
|
|
124
76
|
case 'boolean':
|
package/dist/src/session/db.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { RawComponentDefinition } from '../types.js';
|
|
|
3
3
|
import type { CDFComponentEntry, DTCGTokenEntry, DTCGTokenGroup } from '@contentful/experience-design-system-types';
|
|
4
4
|
import type { ToolCall, TokenToolCall } from '../generate/agent-runner.js';
|
|
5
5
|
import type { ComponentTypeSummary } from '@contentful/experience-design-system-types';
|
|
6
|
-
import type { SlotCycle, SlotEdge } from '../analyze/cycle-detection.js';
|
|
7
6
|
export type StepStatus = 'pending' | 'complete' | 'failed' | 'interrupted';
|
|
8
7
|
export type CommandName = 'analyze extract' | 'analyze select' | 'generate components' | 'generate tokens' | 'generate edit' | 'apply preview' | 'apply select' | 'apply push' | 'print components' | 'print tokens' | 'import';
|
|
9
8
|
export interface SessionRow {
|
|
@@ -177,14 +176,6 @@ export declare function lookupCacheByEntity(db: DatabaseSync, entityType: 'compo
|
|
|
177
176
|
export declare function storeCache(db: DatabaseSync, inputHash: string, entityType: 'component' | 'token_set', entityId: string, sourceSessionId: string, humanEdited: boolean, promptHash?: string): void;
|
|
178
177
|
export declare function storeScannedFiles(db: DatabaseSync, sessionId: string, filePaths: string[]): void;
|
|
179
178
|
export declare function loadScannedFiles(db: DatabaseSync, sessionId: string): string[];
|
|
180
|
-
export declare function storeSlotCycles(db: DatabaseSync, sessionId: string, cycles: Array<SlotCycle & {
|
|
181
|
-
suggestedBreak?: SlotEdge | null;
|
|
182
|
-
}>): void;
|
|
183
|
-
export interface StoredSlotCycle extends SlotCycle {
|
|
184
|
-
suggestedBreak: SlotEdge | null;
|
|
185
|
-
}
|
|
186
|
-
export declare function loadSlotCycles(db: DatabaseSync, sessionId: string): StoredSlotCycle[];
|
|
187
|
-
export declare function clearSlotCycles(db: DatabaseSync, sessionId: string): void;
|
|
188
179
|
export declare function markCacheHumanEdited(db: DatabaseSync, entityType: 'component' | 'token_set', entityId: string): void;
|
|
189
180
|
export declare function copyComponentFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string, componentId: string): void;
|
|
190
181
|
export declare function copyTokensFromCache(db: DatabaseSync, sourceSessionId: string, targetSessionId: string): void;
|
package/dist/src/session/db.js
CHANGED
|
@@ -138,15 +138,6 @@ CREATE TABLE IF NOT EXISTS scanned_files (
|
|
|
138
138
|
PRIMARY KEY (session_id, path)
|
|
139
139
|
);
|
|
140
140
|
|
|
141
|
-
CREATE TABLE IF NOT EXISTS slot_cycles (
|
|
142
|
-
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
143
|
-
cycle_index INTEGER NOT NULL,
|
|
144
|
-
path_json TEXT NOT NULL,
|
|
145
|
-
edges_json TEXT NOT NULL,
|
|
146
|
-
suggested_break_json TEXT,
|
|
147
|
-
PRIMARY KEY (session_id, cycle_index)
|
|
148
|
-
);
|
|
149
|
-
|
|
150
141
|
CREATE INDEX IF NOT EXISTS idx_steps_session ON steps(session_id);
|
|
151
142
|
CREATE INDEX IF NOT EXISTS idx_steps_command ON steps(session_id, command);
|
|
152
143
|
CREATE INDEX IF NOT EXISTS idx_raw_components_session ON raw_components(session_id);
|
|
@@ -1397,43 +1388,6 @@ export function loadScannedFiles(db, sessionId) {
|
|
|
1397
1388
|
const rows = db.prepare('SELECT path FROM scanned_files WHERE session_id = ? ORDER BY path').all(sessionId);
|
|
1398
1389
|
return rows.map((r) => r.path);
|
|
1399
1390
|
}
|
|
1400
|
-
// --- Slot-dependency cycle persistence (INTEG-4401) ---
|
|
1401
|
-
//
|
|
1402
|
-
// The `slot_cycles` table caches the result of running the cycle detector
|
|
1403
|
-
// over the extractor's slot output so the TUI can render sidebar badges,
|
|
1404
|
-
// a banner, and an expandable detail panel without re-running the analysis
|
|
1405
|
-
// on every render. Rows are session-scoped and rewritten wholesale on each
|
|
1406
|
-
// re-extract via `storeSlotCycles`.
|
|
1407
|
-
export function storeSlotCycles(db, sessionId, cycles) {
|
|
1408
|
-
db.exec('BEGIN');
|
|
1409
|
-
try {
|
|
1410
|
-
db.prepare('DELETE FROM slot_cycles WHERE session_id = ?').run(sessionId);
|
|
1411
|
-
const insert = db.prepare(`INSERT INTO slot_cycles (session_id, cycle_index, path_json, edges_json, suggested_break_json)
|
|
1412
|
-
VALUES (?, ?, ?, ?, ?)`);
|
|
1413
|
-
for (let i = 0; i < cycles.length; i += 1) {
|
|
1414
|
-
const cycle = cycles[i];
|
|
1415
|
-
insert.run(sessionId, i, JSON.stringify(cycle.path), JSON.stringify(cycle.edges), cycle.suggestedBreak ? JSON.stringify(cycle.suggestedBreak) : null);
|
|
1416
|
-
}
|
|
1417
|
-
db.exec('COMMIT');
|
|
1418
|
-
}
|
|
1419
|
-
catch (e) {
|
|
1420
|
-
db.exec('ROLLBACK');
|
|
1421
|
-
throw e;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
export function loadSlotCycles(db, sessionId) {
|
|
1425
|
-
const rows = db
|
|
1426
|
-
.prepare('SELECT cycle_index, path_json, edges_json, suggested_break_json FROM slot_cycles WHERE session_id = ? ORDER BY cycle_index')
|
|
1427
|
-
.all(sessionId);
|
|
1428
|
-
return rows.map((r) => ({
|
|
1429
|
-
path: JSON.parse(r.path_json),
|
|
1430
|
-
edges: JSON.parse(r.edges_json),
|
|
1431
|
-
suggestedBreak: r.suggested_break_json ? JSON.parse(r.suggested_break_json) : null,
|
|
1432
|
-
}));
|
|
1433
|
-
}
|
|
1434
|
-
export function clearSlotCycles(db, sessionId) {
|
|
1435
|
-
db.prepare('DELETE FROM slot_cycles WHERE session_id = ?').run(sessionId);
|
|
1436
|
-
}
|
|
1437
1391
|
export function markCacheHumanEdited(db, entityType, entityId) {
|
|
1438
1392
|
const now = new Date().toISOString();
|
|
1439
1393
|
db.prepare(`UPDATE generation_cache SET human_edited = 1, updated_at = ? WHERE entity_type = ? AND entity_id = ?`).run(now, entityType, entityId);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import type { DesignTokenType } from '@contentful/experience-design-system-types';
|
|
2
|
-
export type ExtractionValidationIssueCode
|
|
3
|
-
export
|
|
4
|
-
severity: 'error' | 'warning';
|
|
5
|
-
code: ExtractionValidationIssueCode;
|
|
6
|
-
message: string;
|
|
7
|
-
field?: string;
|
|
8
|
-
};
|
|
2
|
+
export type { ExtractionValidationIssueCode, ExtractionValidationIssue, RawPropDefinition, RawSlotDefinition, RawComponentDefinition, ComponentExtractionResult, ExtractorProgress, ExtractorOptions, ComponentExtractor, } from '@contentful/experience-design-system-extraction';
|
|
3
|
+
export { stripScoringFields } from '@contentful/experience-design-system-extraction';
|
|
9
4
|
export interface RawTokenDefinition {
|
|
10
5
|
name: string;
|
|
11
6
|
value: string;
|
|
@@ -13,76 +8,7 @@ export interface RawTokenDefinition {
|
|
|
13
8
|
inferredKind: DesignTokenType | string;
|
|
14
9
|
ambiguous: boolean;
|
|
15
10
|
}
|
|
16
|
-
export interface RawPropDefinition {
|
|
17
|
-
name: string;
|
|
18
|
-
type: string;
|
|
19
|
-
required: boolean;
|
|
20
|
-
category?: 'content' | 'design' | 'state';
|
|
21
|
-
defaultValue?: string;
|
|
22
|
-
allowedValues?: string[];
|
|
23
|
-
description?: string;
|
|
24
|
-
tokenReference?: string;
|
|
25
|
-
/** 1-indexed source line where this prop's declaration begins; relative to RawComponentDefinition.source. */
|
|
26
|
-
sourceStartLine?: number;
|
|
27
|
-
/** 1-indexed inclusive source line where this prop's declaration ends; relative to RawComponentDefinition.source. */
|
|
28
|
-
sourceEndLine?: number;
|
|
29
|
-
}
|
|
30
|
-
export interface RawSlotDefinition {
|
|
31
|
-
name: string;
|
|
32
|
-
isDefault: boolean;
|
|
33
|
-
description?: string;
|
|
34
|
-
allowedComponents?: string[];
|
|
35
|
-
}
|
|
36
|
-
export interface RawComponentDefinition {
|
|
37
|
-
name: string;
|
|
38
|
-
source: string;
|
|
39
|
-
framework: 'react' | 'next' | 'vue' | 'astro' | 'web-component' | 'stencil' | 'svelte';
|
|
40
|
-
props: RawPropDefinition[];
|
|
41
|
-
slots: RawSlotDefinition[];
|
|
42
|
-
/**
|
|
43
|
-
* True when the source file declaring this component calls
|
|
44
|
-
* React.createContext / createContext. Used downstream to filter
|
|
45
|
-
* non-authorable context-provider components.
|
|
46
|
-
*/
|
|
47
|
-
usesCreateContext?: boolean;
|
|
48
|
-
extractionConfidence?: number | null;
|
|
49
|
-
reviewReasons?: string[];
|
|
50
|
-
needsReview?: boolean;
|
|
51
|
-
validationIssues?: ExtractionValidationIssue[];
|
|
52
|
-
/** Absolute path to the source file this component was extracted from. Null for synthesized / multi-file. */
|
|
53
|
-
sourcePath?: string;
|
|
54
|
-
}
|
|
55
|
-
export interface ComponentExtractionResult {
|
|
56
|
-
components: RawComponentDefinition[];
|
|
57
|
-
warnings: string[];
|
|
58
|
-
}
|
|
59
|
-
export type ExtractorProgress = {
|
|
60
|
-
filesProcessed: number;
|
|
61
|
-
componentsFound: number;
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* Optional extraction-time settings forwarded from the CLI through the
|
|
65
|
-
* pipeline to each extractor. Only the Svelte extractor currently consumes
|
|
66
|
-
* any of these; other extractors ignore the value.
|
|
67
|
-
*/
|
|
68
|
-
export interface ExtractorOptions {
|
|
69
|
-
/**
|
|
70
|
-
* Whether the Svelte extractor should run a retry pass for components whose
|
|
71
|
-
* declared Props type couldn't be resolved on the first pass (cross-package
|
|
72
|
-
* extends, path-alias-only types, etc.). See svelte.ts for the policy.
|
|
73
|
-
*/
|
|
74
|
-
resolveUnreachable?: 'auto' | 'always' | 'never';
|
|
75
|
-
/** Absolute project root — used by the retry pass to locate tsconfig.json and node_modules. */
|
|
76
|
-
projectRoot?: string;
|
|
77
|
-
}
|
|
78
|
-
export interface ComponentExtractor {
|
|
79
|
-
name: string;
|
|
80
|
-
fileFilter: (filePath: string) => boolean;
|
|
81
|
-
extract(filePaths: string[], onProgress?: (p: ExtractorProgress) => void, opts?: ExtractorOptions): Promise<ComponentExtractionResult>;
|
|
82
|
-
}
|
|
83
11
|
export interface TokenExtractor {
|
|
84
12
|
name: string;
|
|
85
13
|
extract(projectRoot: string): Promise<RawTokenDefinition[]>;
|
|
86
14
|
}
|
|
87
|
-
/** Strip internal scoring fields before serialising a RawComponentDefinition for display or editing. */
|
|
88
|
-
export declare function stripScoringFields({ extractionConfidence: _c, reviewReasons: _r, needsReview: _n, ...rest }: RawComponentDefinition): Omit<RawComponentDefinition, 'extractionConfidence' | 'reviewReasons' | 'needsReview'>;
|
package/dist/src/types.js
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export function stripScoringFields({ extractionConfidence: _c, reviewReasons: _r, needsReview: _n, ...rest }) {
|
|
3
|
-
return rest;
|
|
4
|
-
}
|
|
1
|
+
export { stripScoringFields } from '@contentful/experience-design-system-extraction';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.1-dev-build-847dead.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,16 +28,13 @@
|
|
|
28
28
|
"skills/"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@vue/compiler-sfc": "^3.5.31",
|
|
32
31
|
"commander": "^13.1.0",
|
|
33
32
|
"ink": "^4.4.1",
|
|
34
33
|
"react": "^18.3.1",
|
|
35
34
|
"react-devtools-core": "^4.19.1",
|
|
36
35
|
"react-dom": "^18.3.1",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"typescript": "^5.9.3",
|
|
40
|
-
"@contentful/experience-design-system-types": "2.12.4-dev-build-9c819d8.0"
|
|
36
|
+
"@contentful/experience-design-system-extraction": "2.13.1-dev-build-847dead.0",
|
|
37
|
+
"@contentful/experience-design-system-types": "2.13.1-dev-build-847dead.0"
|
|
41
38
|
},
|
|
42
39
|
"devDependencies": {
|
|
43
40
|
"@tsconfig/node24": "^24.0.3",
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slot-dependency cycle detection.
|
|
3
|
-
*
|
|
4
|
-
* The Contentful Experience Design System backend rejects component manifests
|
|
5
|
-
* that contain slot-dependency cycles at apply time — a topo-sort in the
|
|
6
|
-
* apply worker refuses to create component types whose slot's
|
|
7
|
-
* `$allowedComponents` transitively point back at themselves. Reference:
|
|
8
|
-
* `experience-design-system-integrations` / services/design-system-sources-apply-worker/src/topo-sort.ts.
|
|
9
|
-
*
|
|
10
|
-
* We surface the same check locally so the wizard can:
|
|
11
|
-
* 1. warn the operator at extract time (soft) with sidebar badges + banner,
|
|
12
|
-
* 2. hard-block the push at manifest-finalization time before any API call.
|
|
13
|
-
*
|
|
14
|
-
* The algorithm is Johnson's — an O((V + E)(C + 1)) enumeration of all
|
|
15
|
-
* elementary (simple) cycles in a directed graph. We need *elementary*
|
|
16
|
-
* cycles rather than just cycle-existence because an operator can have
|
|
17
|
-
* multiple independent cycles they need to address separately, and each
|
|
18
|
-
* gets its own suggested fix.
|
|
19
|
-
*
|
|
20
|
-
* Johnson's algorithm reference: Donald B. Johnson, "Finding all the
|
|
21
|
-
* elementary circuits of a directed graph." SIAM J. Comput., 4(1):77-84, 1975.
|
|
22
|
-
*/
|
|
23
|
-
/** A directed edge in the slot-dependency graph. */
|
|
24
|
-
export interface SlotEdge {
|
|
25
|
-
fromComponent: string;
|
|
26
|
-
slotName: string;
|
|
27
|
-
toComponent: string;
|
|
28
|
-
}
|
|
29
|
-
/** A single elementary cycle, with `path[0] === path[path.length - 1]`. */
|
|
30
|
-
export interface SlotCycle {
|
|
31
|
-
/** Component names visited, with the first repeated at the end. */
|
|
32
|
-
path: string[];
|
|
33
|
-
/** Ordered edges forming the cycle. `edges.length === path.length - 1`. */
|
|
34
|
-
edges: SlotEdge[];
|
|
35
|
-
}
|
|
36
|
-
/** Input shape mirroring the slot definition on CDFComponentEntry. */
|
|
37
|
-
export interface ComponentSlotInfo {
|
|
38
|
-
name: string;
|
|
39
|
-
slots: Array<{
|
|
40
|
-
name: string;
|
|
41
|
-
allowedComponents?: string[];
|
|
42
|
-
}>;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Johnson's algorithm — enumerates all elementary (simple) cycles in the
|
|
46
|
-
* slot-dependency graph. Self-loops are handled as a degenerate first pass
|
|
47
|
-
* because Tarjan's SCC decomposition assigns a self-looping node to its own
|
|
48
|
-
* single-node SCC, which Johnson's inner loop otherwise skips.
|
|
49
|
-
*/
|
|
50
|
-
export declare function findSlotCycles(components: ComponentSlotInfo[]): SlotCycle[];
|
|
51
|
-
/**
|
|
52
|
-
* Pick which edge is the best candidate to remove to break a cycle.
|
|
53
|
-
* Heuristic: prefer removing the edge whose `toComponent` appears most often
|
|
54
|
-
* as a destination across all cycles — that "hub" node is the biggest
|
|
55
|
-
* contributor and removing an inbound edge to it likely breaks multiple
|
|
56
|
-
* cycles at once. Falls back to the first edge in the cycle for
|
|
57
|
-
* deterministic output when all candidates are tied.
|
|
58
|
-
*/
|
|
59
|
-
export declare function suggestCycleBreakEdge(cycle: SlotCycle, allCycles: SlotCycle[]): SlotEdge;
|
|
60
|
-
/**
|
|
61
|
-
* Format a cycle for the compact banner:
|
|
62
|
-
* `CardA → header → CardB → footer → CardA`
|
|
63
|
-
*
|
|
64
|
-
* Truncates at `maxHops` edges (default 8), inserting `…` mid-path so the
|
|
65
|
-
* beginning and end remain visible. A hop is one edge; the returned string
|
|
66
|
-
* for an n-edge cycle contains n+1 component names.
|
|
67
|
-
*/
|
|
68
|
-
export declare function formatCyclePath(cycle: SlotCycle, maxHops?: number): string;
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slot-dependency cycle detection.
|
|
3
|
-
*
|
|
4
|
-
* The Contentful Experience Design System backend rejects component manifests
|
|
5
|
-
* that contain slot-dependency cycles at apply time — a topo-sort in the
|
|
6
|
-
* apply worker refuses to create component types whose slot's
|
|
7
|
-
* `$allowedComponents` transitively point back at themselves. Reference:
|
|
8
|
-
* `experience-design-system-integrations` / services/design-system-sources-apply-worker/src/topo-sort.ts.
|
|
9
|
-
*
|
|
10
|
-
* We surface the same check locally so the wizard can:
|
|
11
|
-
* 1. warn the operator at extract time (soft) with sidebar badges + banner,
|
|
12
|
-
* 2. hard-block the push at manifest-finalization time before any API call.
|
|
13
|
-
*
|
|
14
|
-
* The algorithm is Johnson's — an O((V + E)(C + 1)) enumeration of all
|
|
15
|
-
* elementary (simple) cycles in a directed graph. We need *elementary*
|
|
16
|
-
* cycles rather than just cycle-existence because an operator can have
|
|
17
|
-
* multiple independent cycles they need to address separately, and each
|
|
18
|
-
* gets its own suggested fix.
|
|
19
|
-
*
|
|
20
|
-
* Johnson's algorithm reference: Donald B. Johnson, "Finding all the
|
|
21
|
-
* elementary circuits of a directed graph." SIAM J. Comput., 4(1):77-84, 1975.
|
|
22
|
-
*/
|
|
23
|
-
function buildGraph(components) {
|
|
24
|
-
const nodes = [];
|
|
25
|
-
const seen = new Set();
|
|
26
|
-
for (const c of components) {
|
|
27
|
-
if (seen.has(c.name))
|
|
28
|
-
continue;
|
|
29
|
-
seen.add(c.name);
|
|
30
|
-
nodes.push(c.name);
|
|
31
|
-
}
|
|
32
|
-
const adjacency = new Map();
|
|
33
|
-
for (const node of nodes)
|
|
34
|
-
adjacency.set(node, []);
|
|
35
|
-
for (const comp of components) {
|
|
36
|
-
const outgoing = adjacency.get(comp.name);
|
|
37
|
-
if (!outgoing)
|
|
38
|
-
continue;
|
|
39
|
-
for (const slot of comp.slots) {
|
|
40
|
-
const allowed = slot.allowedComponents ?? [];
|
|
41
|
-
for (const target of allowed) {
|
|
42
|
-
if (!seen.has(target))
|
|
43
|
-
continue; // unknown / external — ignore
|
|
44
|
-
outgoing.push({ target, slotName: slot.name });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { nodes, adjacency };
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Tarjan's strongly-connected-components algorithm — used by Johnson's
|
|
52
|
-
* algorithm to constrain cycle search to one SCC at a time. Returns SCCs in
|
|
53
|
-
* reverse-topological order.
|
|
54
|
-
*/
|
|
55
|
-
function stronglyConnectedComponents(nodes, adjacency) {
|
|
56
|
-
const index = new Map();
|
|
57
|
-
const lowlink = new Map();
|
|
58
|
-
const onStack = new Set();
|
|
59
|
-
const stack = [];
|
|
60
|
-
const sccs = [];
|
|
61
|
-
let counter = 0;
|
|
62
|
-
const nodeSet = new Set(nodes);
|
|
63
|
-
function strongconnect(v) {
|
|
64
|
-
index.set(v, counter);
|
|
65
|
-
lowlink.set(v, counter);
|
|
66
|
-
counter += 1;
|
|
67
|
-
stack.push(v);
|
|
68
|
-
onStack.add(v);
|
|
69
|
-
const neighbours = adjacency.get(v) ?? [];
|
|
70
|
-
for (const { target } of neighbours) {
|
|
71
|
-
if (!nodeSet.has(target))
|
|
72
|
-
continue;
|
|
73
|
-
if (!index.has(target)) {
|
|
74
|
-
strongconnect(target);
|
|
75
|
-
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(target)));
|
|
76
|
-
}
|
|
77
|
-
else if (onStack.has(target)) {
|
|
78
|
-
lowlink.set(v, Math.min(lowlink.get(v), index.get(target)));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (lowlink.get(v) === index.get(v)) {
|
|
82
|
-
const scc = [];
|
|
83
|
-
let w;
|
|
84
|
-
do {
|
|
85
|
-
w = stack.pop();
|
|
86
|
-
onStack.delete(w);
|
|
87
|
-
scc.push(w);
|
|
88
|
-
} while (w !== v);
|
|
89
|
-
sccs.push(scc);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
for (const node of nodes) {
|
|
93
|
-
if (!index.has(node))
|
|
94
|
-
strongconnect(node);
|
|
95
|
-
}
|
|
96
|
-
return sccs;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Given a specific traversal `path` (component names) and the edge multiset
|
|
100
|
-
* we walked to build it, materialize a `SlotCycle`. Because multiple parallel
|
|
101
|
-
* edges may exist between the same two components (different slot names), we
|
|
102
|
-
* carry the exact edges along the search rather than reconstructing them from
|
|
103
|
-
* the node path.
|
|
104
|
-
*/
|
|
105
|
-
function makeCycle(path, edges) {
|
|
106
|
-
return { path: [...path, path[0]], edges: [...edges] };
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Johnson's algorithm — enumerates all elementary (simple) cycles in the
|
|
110
|
-
* slot-dependency graph. Self-loops are handled as a degenerate first pass
|
|
111
|
-
* because Tarjan's SCC decomposition assigns a self-looping node to its own
|
|
112
|
-
* single-node SCC, which Johnson's inner loop otherwise skips.
|
|
113
|
-
*/
|
|
114
|
-
export function findSlotCycles(components) {
|
|
115
|
-
if (components.length === 0)
|
|
116
|
-
return [];
|
|
117
|
-
const { nodes, adjacency } = buildGraph(components);
|
|
118
|
-
if (nodes.length === 0)
|
|
119
|
-
return [];
|
|
120
|
-
const cycles = [];
|
|
121
|
-
// Degenerate case: self-loops. For any component whose slot allows itself,
|
|
122
|
-
// emit a length-1 cycle before invoking Johnson's inner loop — Tarjan's
|
|
123
|
-
// trivial single-node SCC test in Johnson's original paper only counts as
|
|
124
|
-
// an SCC if the node participates in an edge, which we replicate here.
|
|
125
|
-
for (const node of nodes) {
|
|
126
|
-
const outgoing = adjacency.get(node) ?? [];
|
|
127
|
-
for (const edge of outgoing) {
|
|
128
|
-
if (edge.target === node) {
|
|
129
|
-
cycles.push({
|
|
130
|
-
path: [node, node],
|
|
131
|
-
edges: [{ fromComponent: node, slotName: edge.slotName, toComponent: node }],
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Johnson's main algorithm — iterate over remaining nodes in a fixed order,
|
|
137
|
-
// decomposing the subgraph induced by `{node, ...higherOrder}` into SCCs,
|
|
138
|
-
// and running the blocked-list circuit search rooted at the least node of
|
|
139
|
-
// each non-trivial SCC. This is the classic formulation from the paper.
|
|
140
|
-
const nodeOrder = [...nodes].sort();
|
|
141
|
-
let remaining = [...nodeOrder];
|
|
142
|
-
while (remaining.length > 0) {
|
|
143
|
-
const subgraphNodes = remaining;
|
|
144
|
-
const sccs = stronglyConnectedComponents(subgraphNodes, adjacency).filter((scc) => {
|
|
145
|
-
if (scc.length > 1)
|
|
146
|
-
return true;
|
|
147
|
-
// Non-trivial single-node SCC iff the node has a self-loop within the
|
|
148
|
-
// subgraph. We already emitted those above, so exclude here.
|
|
149
|
-
return false;
|
|
150
|
-
});
|
|
151
|
-
if (sccs.length === 0)
|
|
152
|
-
break;
|
|
153
|
-
// Pick the SCC containing the lexicographically-smallest node — mirrors
|
|
154
|
-
// the "least vertex" root selection in Johnson's paper.
|
|
155
|
-
let startNode = null;
|
|
156
|
-
let startScc = null;
|
|
157
|
-
for (const scc of sccs) {
|
|
158
|
-
const min = [...scc].sort()[0];
|
|
159
|
-
if (startNode === null || min < startNode) {
|
|
160
|
-
startNode = min;
|
|
161
|
-
startScc = scc;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (startNode === null || startScc === null)
|
|
165
|
-
break;
|
|
166
|
-
const sccSet = new Set(startScc);
|
|
167
|
-
const blocked = new Set();
|
|
168
|
-
const blockedMap = new Map();
|
|
169
|
-
const pathStack = [];
|
|
170
|
-
const edgeStack = [];
|
|
171
|
-
function unblock(u) {
|
|
172
|
-
blocked.delete(u);
|
|
173
|
-
const dependents = blockedMap.get(u);
|
|
174
|
-
if (!dependents)
|
|
175
|
-
return;
|
|
176
|
-
for (const w of dependents) {
|
|
177
|
-
if (blocked.has(w))
|
|
178
|
-
unblock(w);
|
|
179
|
-
}
|
|
180
|
-
dependents.clear();
|
|
181
|
-
}
|
|
182
|
-
function circuit(v, root) {
|
|
183
|
-
let foundCycle = false;
|
|
184
|
-
pathStack.push(v);
|
|
185
|
-
blocked.add(v);
|
|
186
|
-
const outgoing = adjacency.get(v) ?? [];
|
|
187
|
-
for (const { target: w, slotName } of outgoing) {
|
|
188
|
-
if (!sccSet.has(w))
|
|
189
|
-
continue;
|
|
190
|
-
edgeStack.push({ fromComponent: v, slotName, toComponent: w });
|
|
191
|
-
if (w === root) {
|
|
192
|
-
cycles.push(makeCycle(pathStack, edgeStack));
|
|
193
|
-
foundCycle = true;
|
|
194
|
-
}
|
|
195
|
-
else if (!blocked.has(w)) {
|
|
196
|
-
if (circuit(w, root))
|
|
197
|
-
foundCycle = true;
|
|
198
|
-
}
|
|
199
|
-
edgeStack.pop();
|
|
200
|
-
}
|
|
201
|
-
if (foundCycle) {
|
|
202
|
-
unblock(v);
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
for (const { target: w } of outgoing) {
|
|
206
|
-
if (!sccSet.has(w))
|
|
207
|
-
continue;
|
|
208
|
-
let deps = blockedMap.get(w);
|
|
209
|
-
if (!deps) {
|
|
210
|
-
deps = new Set();
|
|
211
|
-
blockedMap.set(w, deps);
|
|
212
|
-
}
|
|
213
|
-
deps.add(v);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
pathStack.pop();
|
|
217
|
-
return foundCycle;
|
|
218
|
-
}
|
|
219
|
-
circuit(startNode, startNode);
|
|
220
|
-
// Remove `startNode` from consideration and repeat with the reduced
|
|
221
|
-
// subgraph. In classic Johnson's this is done by re-running SCC on the
|
|
222
|
-
// subgraph induced by the remaining nodes.
|
|
223
|
-
remaining = remaining.filter((n) => n !== startNode);
|
|
224
|
-
}
|
|
225
|
-
return cycles;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Pick which edge is the best candidate to remove to break a cycle.
|
|
229
|
-
* Heuristic: prefer removing the edge whose `toComponent` appears most often
|
|
230
|
-
* as a destination across all cycles — that "hub" node is the biggest
|
|
231
|
-
* contributor and removing an inbound edge to it likely breaks multiple
|
|
232
|
-
* cycles at once. Falls back to the first edge in the cycle for
|
|
233
|
-
* deterministic output when all candidates are tied.
|
|
234
|
-
*/
|
|
235
|
-
export function suggestCycleBreakEdge(cycle, allCycles) {
|
|
236
|
-
if (cycle.edges.length === 0) {
|
|
237
|
-
throw new Error('suggestCycleBreakEdge: cycle has no edges');
|
|
238
|
-
}
|
|
239
|
-
const indegree = new Map();
|
|
240
|
-
for (const c of allCycles) {
|
|
241
|
-
for (const edge of c.edges) {
|
|
242
|
-
indegree.set(edge.toComponent, (indegree.get(edge.toComponent) ?? 0) + 1);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
let best = cycle.edges[0];
|
|
246
|
-
let bestScore = indegree.get(best.toComponent) ?? 0;
|
|
247
|
-
for (const edge of cycle.edges) {
|
|
248
|
-
const score = indegree.get(edge.toComponent) ?? 0;
|
|
249
|
-
if (score > bestScore) {
|
|
250
|
-
best = edge;
|
|
251
|
-
bestScore = score;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return best;
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Format a cycle for the compact banner:
|
|
258
|
-
* `CardA → header → CardB → footer → CardA`
|
|
259
|
-
*
|
|
260
|
-
* Truncates at `maxHops` edges (default 8), inserting `…` mid-path so the
|
|
261
|
-
* beginning and end remain visible. A hop is one edge; the returned string
|
|
262
|
-
* for an n-edge cycle contains n+1 component names.
|
|
263
|
-
*/
|
|
264
|
-
export function formatCyclePath(cycle, maxHops = 8) {
|
|
265
|
-
const arrow = ' → ';
|
|
266
|
-
const parts = [];
|
|
267
|
-
// Interleave: component, slot, component, slot, ..., final component
|
|
268
|
-
// path.length === edges.length + 1.
|
|
269
|
-
for (let i = 0; i < cycle.edges.length; i += 1) {
|
|
270
|
-
parts.push(cycle.path[i]);
|
|
271
|
-
parts.push(cycle.edges[i].slotName);
|
|
272
|
-
}
|
|
273
|
-
parts.push(cycle.path[cycle.path.length - 1]);
|
|
274
|
-
if (cycle.edges.length <= maxHops) {
|
|
275
|
-
return parts.join(arrow);
|
|
276
|
-
}
|
|
277
|
-
// Truncated: keep the first `keep` edges' worth of tokens and the final
|
|
278
|
-
// component. Two "tokens per hop" (component + slot) plus the trailing
|
|
279
|
-
// component name.
|
|
280
|
-
const keepHops = Math.max(1, maxHops - 1);
|
|
281
|
-
const keepTokens = keepHops * 2;
|
|
282
|
-
const head = parts.slice(0, keepTokens).join(arrow);
|
|
283
|
-
const tail = parts[parts.length - 1];
|
|
284
|
-
return `${head}${arrow}…${arrow}${tail}`;
|
|
285
|
-
}
|