@contentful/experience-design-system-cli 2.7.0 → 2.7.1-dev-build-02830eb.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.
Files changed (34) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/analyze/select/tui/App.js +54 -475
  3. package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +3 -5
  4. package/dist/src/analyze/select/tui/components/ComponentDetail.js +3 -3
  5. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +1 -1
  6. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +2 -10
  7. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +1 -1
  8. package/dist/src/analyze/select/tui/components/HelpOverlay.js +2 -7
  9. package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +7 -5
  10. package/dist/src/analyze/select/tui/components/JsonEditor.js +15 -146
  11. package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +1 -1
  12. package/dist/src/analyze/select/tui/components/QuitDialog.js +2 -10
  13. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -2
  14. package/dist/src/analyze/select/tui/components/StatusBar.d.ts +0 -2
  15. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +5 -0
  16. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +7 -4
  17. package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +0 -23
  18. package/dist/src/analyze/select/tui/hooks/useKeymap.js +1 -67
  19. package/dist/src/analyze/select/tui/hooks/useRawMode.d.ts +6 -0
  20. package/dist/src/analyze/select/tui/hooks/useRawMode.js +16 -0
  21. package/dist/src/analyze/select/tui/hooks/useSideEffects.d.ts +17 -0
  22. package/dist/src/analyze/select/tui/hooks/useSideEffects.js +226 -0
  23. package/dist/src/analyze/select/tui/hooks/useUndo.js +15 -21
  24. package/dist/src/analyze/select/tui/inputToAction.d.ts +13 -0
  25. package/dist/src/analyze/select/tui/inputToAction.js +68 -0
  26. package/dist/src/analyze/select/tui/state.d.ts +136 -0
  27. package/dist/src/analyze/select/tui/state.js +385 -0
  28. package/dist/src/analyze/select/tui/utils.d.ts +5 -0
  29. package/dist/src/analyze/select/tui/utils.js +11 -0
  30. package/dist/src/analyze/tui/AnalyzeView.js +2 -0
  31. package/dist/src/import/tui/WizardApp.js +2 -0
  32. package/dist/src/import/tui/steps/GenerateReviewStep.js +4 -11
  33. package/dist/src/print/validate/tui/ValidateView.js +2 -0
  34. package/package.json +2 -2
@@ -0,0 +1,226 @@
1
+ import { useEffect, useLayoutEffect, useRef } from 'react';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { openPipelineDb, storeRawComponents, loadCDFComponents } from '../../../../session/db.js';
4
+ import { ImportApiClient } from '../../../../apply/api-client.js';
5
+ import { readTokensFromPath } from '../../../../apply/manifest.js';
6
+ import { buildManifest } from '@contentful/experience-design-system-types';
7
+ /**
8
+ * All async side effects in one place.
9
+ * Detects transitions by comparing state to prevState — no pending* counter signals needed.
10
+ */
11
+ export function useSideEffects(state, dispatch, services) {
12
+ const prevRef = useRef(state);
13
+ // Per-instance timer ref — avoids module-level state shared between test instances
14
+ const previewTimerRef = useRef(null);
15
+ // useLayoutEffect: fires synchronously after commit, before next render.
16
+ // This guarantees transitions are detected even if two renders fire in rapid
17
+ // succession — prevRef.current is updated before the next render reads it.
18
+ useLayoutEffect(() => {
19
+ const prev = prevRef.current;
20
+ prevRef.current = state;
21
+ // ── Session persistence: save whenever session components change ──────
22
+ if (state.session && state.paths && prev.session?.components !== state.session.components) {
23
+ void services.saveState(state.session);
24
+ }
25
+ // ── Draft persist: mode left editing (but not via discard — discard clears the draft) ──
26
+ if (prev.mode.type === 'editing' && state.mode.type !== 'editing') {
27
+ const { componentId } = prev.mode;
28
+ const draft = state.draftsByComponentId[componentId]; // undefined if discarded
29
+ if (draft && state.session && state.paths) {
30
+ void persistDraft(componentId, draft, state, dispatch, services);
31
+ }
32
+ }
33
+ // ── Finalize: mode just became finalized ──────────────────────────────
34
+ if (prev.mode.type !== 'finalized' && state.mode.type === 'finalized') {
35
+ if (state.session && state.paths) {
36
+ void persistFinalize(state, dispatch, services);
37
+ }
38
+ }
39
+ // ── Preview refresh: session components changed ───────────────────────
40
+ if (state.session?.components !== prev.session?.components && state.session) {
41
+ schedulePreviewRefresh(state, dispatch, services.sessionId, previewTimerRef);
42
+ }
43
+ });
44
+ // ── Source code lazy loading ──────────────────────────────────────────────
45
+ useEffect(() => {
46
+ if (!state.selectedId || !state.session)
47
+ return;
48
+ if (state.sourceCodeById[state.selectedId] !== undefined)
49
+ return;
50
+ const component = state.session.components.find((c) => c.id === state.selectedId);
51
+ if (!component)
52
+ return;
53
+ const id = state.selectedId;
54
+ readFile(component.resolvedSourcePath, 'utf8')
55
+ .then((code) => dispatch({ type: 'SOURCE_LOADED', componentId: id, code }))
56
+ .catch(() => dispatch({ type: 'SOURCE_LOADED', componentId: id, code: '' }));
57
+ }, [state.selectedId]);
58
+ // ── SIGINT ────────────────────────────────────────────────────────────────
59
+ useEffect(() => {
60
+ const handler = () => {
61
+ if (Object.keys(state.draftsByComponentId).length > 0) {
62
+ dispatch({ type: 'OPEN_DIALOG', which: 'quit' });
63
+ }
64
+ else {
65
+ process.exit(1);
66
+ }
67
+ };
68
+ process.on('SIGINT', handler);
69
+ return () => {
70
+ process.off('SIGINT', handler);
71
+ };
72
+ }, [state.draftsByComponentId]);
73
+ // ── Auto-clear errors ─────────────────────────────────────────────────────
74
+ useEffect(() => {
75
+ if (!state.saveError)
76
+ return;
77
+ const t = setTimeout(() => dispatch({ type: 'CLEAR_ERRORS' }), 3000);
78
+ return () => clearTimeout(t);
79
+ }, [state.saveError]);
80
+ // ── Seed preview from env (non-live mode) ─────────────────────────────────
81
+ useEffect(() => {
82
+ if (!state.session || state.previewResponse)
83
+ return;
84
+ const raw = process.env['EDS_PREVIEW_COUNTS'];
85
+ if (!raw)
86
+ return;
87
+ try {
88
+ const counts = JSON.parse(raw);
89
+ const make = (n) => Array(n).fill({});
90
+ dispatch({
91
+ type: 'PREVIEW_SUCCESS',
92
+ response: {
93
+ components: {
94
+ new: make(counts.compNew ?? 0),
95
+ changed: make(counts.compChanged ?? 0),
96
+ removed: make(counts.compRemoved ?? 0),
97
+ unchanged: make(counts.compUnchanged ?? 0),
98
+ },
99
+ tokens: {
100
+ new: make(counts.tokNew ?? 0),
101
+ changed: make(counts.tokChanged ?? 0),
102
+ removed: make(counts.tokRemoved ?? 0),
103
+ unchanged: make(counts.tokUnchanged ?? 0),
104
+ },
105
+ },
106
+ annotations: {},
107
+ });
108
+ }
109
+ catch { }
110
+ }, [state.session]);
111
+ }
112
+ // ── Helpers ───────────────────────────────────────────────────────────────────
113
+ async function persistDraft(componentId, draft, state, dispatch, services) {
114
+ try {
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ const parsed = JSON.parse(draft);
117
+ const component = state.session.components.find((c) => c.id === componentId);
118
+ const currentStatus = component?.status;
119
+ const newStatus = currentStatus === 'needs-review' ? 'reviewed' : (currentStatus ?? 'reviewed');
120
+ const updatedComponents = state.session.components.map((c) => c.id === componentId ? { ...c, editedProposal: parsed, status: newStatus } : c);
121
+ await services.saveState({ ...state.session, components: updatedComponents });
122
+ await services.appendEvent({ type: 'draft_saved', payload: { componentId } });
123
+ const db = openPipelineDb();
124
+ try {
125
+ storeRawComponents(db, services.sessionId, updatedComponents.map((c) => c.editedProposal), {
126
+ status: 'generated',
127
+ preserveCDF: true,
128
+ });
129
+ }
130
+ finally {
131
+ db.close();
132
+ }
133
+ dispatch({ type: 'DRAFT_PERSIST_DONE', componentId, updatedComponents });
134
+ }
135
+ catch {
136
+ dispatch({ type: 'DRAFT_PERSIST_DONE', componentId, updatedComponents: state.session.components });
137
+ }
138
+ }
139
+ async function persistFinalize(state, dispatch, services) {
140
+ if (state.mode.type !== 'finalized')
141
+ return;
142
+ const { accepted, rejected, excluded } = state.mode;
143
+ try {
144
+ await services.appendEvent({ type: 'finalized', payload: { accepted, rejected, excluded } });
145
+ const acceptedNames = new Set(state.session.components.filter((c) => c.status === 'accepted').map((c) => c.name));
146
+ const db = openPipelineDb();
147
+ try {
148
+ storeRawComponents(db, services.sessionId, state.session.components.map((c) => c.editedProposal), { status: 'extracted', preserveCDF: true });
149
+ if (acceptedNames.size > 0) {
150
+ db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${[...acceptedNames].map(() => '?').join(',')})`).run(services.sessionId, ...acceptedNames);
151
+ }
152
+ }
153
+ finally {
154
+ db.close();
155
+ }
156
+ process.stdout.write(JSON.stringify({ status: 'finalized', sessionDir: state.paths.sessionDir, accepted, rejected, excluded }, null, 2) + '\n');
157
+ }
158
+ catch (err) {
159
+ dispatch({ type: 'SAVE_ERROR', message: String(err) });
160
+ }
161
+ }
162
+ function schedulePreviewRefresh(state, dispatch, sessionId, timerRef) {
163
+ if (timerRef.current)
164
+ clearTimeout(timerRef.current);
165
+ timerRef.current = setTimeout(() => {
166
+ timerRef.current = null;
167
+ // Sync session to DB
168
+ const db = openPipelineDb();
169
+ try {
170
+ db.prepare(`UPDATE raw_components SET status = 'extracted' WHERE session_id = ?`).run(sessionId);
171
+ const acceptedNames = state
172
+ .session.components.filter((c) => c.status === 'accepted' || c.status === 'reviewed')
173
+ .map((c) => c.name);
174
+ if (acceptedNames.length > 0) {
175
+ db.prepare(`UPDATE raw_components SET status = 'generated' WHERE session_id = ? AND name IN (${acceptedNames.map(() => '?').join(',')})`).run(sessionId, ...acceptedNames);
176
+ }
177
+ }
178
+ finally {
179
+ db.close();
180
+ }
181
+ const cmaToken = process.env['EDS_CMA_TOKEN'];
182
+ const spaceId = process.env['EDS_SPACE_ID'];
183
+ const environmentId = process.env['EDS_ENVIRONMENT_ID'];
184
+ const tokensPath = process.env['EDS_TOKENS_PATH'];
185
+ if (!cmaToken || !spaceId || !environmentId)
186
+ return;
187
+ dispatch({ type: 'PREVIEW_START' });
188
+ void (async () => {
189
+ try {
190
+ const pdb = openPipelineDb();
191
+ let components = [];
192
+ try {
193
+ components = loadCDFComponents(pdb, sessionId);
194
+ }
195
+ finally {
196
+ pdb.close();
197
+ }
198
+ let tokens = [];
199
+ if (tokensPath)
200
+ tokens = await readTokensFromPath('tokens', tokensPath);
201
+ const manifest = buildManifest(components, tokens);
202
+ if (!manifest.componentsManifest)
203
+ manifest.componentsManifest = {};
204
+ const client = new ImportApiClient({ cmaToken, spaceId, environmentId });
205
+ const preview = await client.previewImport(manifest);
206
+ const annotations = {};
207
+ for (const item of preview.components.new) {
208
+ const name = item.name ?? '';
209
+ if (name)
210
+ annotations[name] = 'new';
211
+ }
212
+ for (const item of preview.components.removed)
213
+ annotations[item.name] = 'removed';
214
+ for (const item of preview.components.changed) {
215
+ annotations[item.current.name] =
216
+ item.changeClassification?.classification === 'breaking' ? 'breaking' : 'changed';
217
+ }
218
+ dispatch({ type: 'PREVIEW_SUCCESS', response: preview, annotations });
219
+ }
220
+ catch (err) {
221
+ const msg = err instanceof Error ? err.message : String(err);
222
+ dispatch({ type: 'PREVIEW_ERROR', message: msg.includes('token') ? 'Preview request failed' : msg });
223
+ }
224
+ })();
225
+ }, 500);
226
+ }
@@ -1,26 +1,20 @@
1
1
  import { useState } from 'react';
2
+ // Single-state implementation — push and undo are atomic single setState calls,
3
+ // eliminating the double-render that caused flash in the JsonEditor.
2
4
  export function useUndo(initial, maxSize = 50) {
3
- const [stack, setStack] = useState([]);
4
- const [current, setCurrent] = useState(initial);
5
+ const [state, setState] = useState({ stack: [], current: initial });
5
6
  return {
6
- current,
7
- push: (next) => {
8
- setStack((prev) => {
9
- const newStack = [...prev, current];
10
- return newStack.length > maxSize ? newStack.slice(1) : newStack;
11
- });
12
- setCurrent(next);
13
- },
14
- undo: () => {
15
- setStack((prev) => {
16
- if (prev.length === 0)
17
- return prev;
18
- const newStack = prev.slice(0, -1);
19
- setCurrent(prev[prev.length - 1]);
20
- return newStack;
21
- });
22
- },
23
- canUndo: stack.length > 0,
24
- clear: () => setStack([]),
7
+ current: state.current,
8
+ push: (next) => setState(({ stack, current }) => {
9
+ const newStack = [...stack, current];
10
+ return { stack: newStack.length > maxSize ? newStack.slice(1) : newStack, current: next };
11
+ }),
12
+ undo: () => setState(({ stack, current }) => {
13
+ if (stack.length === 0)
14
+ return { stack, current };
15
+ return { stack: stack.slice(0, -1), current: stack[stack.length - 1] };
16
+ }),
17
+ canUndo: state.stack.length > 0,
18
+ clear: () => setState(({ current }) => ({ stack: [], current })),
25
19
  };
26
20
  }
@@ -0,0 +1,13 @@
1
+ import type { AppAction, AppState, applyEditorKey } from './state.js';
2
+ export type Key = Parameters<typeof applyEditorKey>[2] & {
3
+ tab: boolean;
4
+ pageUp: boolean;
5
+ pageDown: boolean;
6
+ shift: boolean;
7
+ };
8
+ /**
9
+ * Pure function: maps a keypress to an AppAction (or null).
10
+ * No hooks. No closures over stale React state.
11
+ * Branching is on mode — the committed state from the reducer.
12
+ */
13
+ export declare function inputToAction(input: string, key: Key, state: AppState, visibleCount: number, terminalWidth: number): AppAction | null;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pure function: maps a keypress to an AppAction (or null).
3
+ * No hooks. No closures over stale React state.
4
+ * Branching is on mode — the committed state from the reducer.
5
+ */
6
+ export function inputToAction(input, key, state, visibleCount, terminalWidth) {
7
+ const { mode } = state;
8
+ // ── Dialog ────────────────────────────────────────────────────────────────
9
+ if (mode.type === 'dialog') {
10
+ if (input === 'y' || key.return) {
11
+ if (mode.which === 'finalize')
12
+ return { type: 'FINALIZE_CONFIRM' };
13
+ if (mode.which === 'quit')
14
+ return { type: 'QUIT_CONFIRM' };
15
+ return { type: 'CLOSE_DIALOG' };
16
+ }
17
+ if (input === 'n' || key.escape)
18
+ return { type: 'CLOSE_DIALOG' };
19
+ return null;
20
+ }
21
+ // ── Editing ───────────────────────────────────────────────────────────────
22
+ if (mode.type === 'editing') {
23
+ if (key.ctrl && input === 's')
24
+ return { type: 'EDITOR_VALIDATE' };
25
+ if (key.escape)
26
+ return { type: 'DRAFT_DISCARD' };
27
+ // All other keys go to the editor
28
+ return { type: 'EDITOR_KEY', input, key, visibleHeight: visibleCount - 4 };
29
+ }
30
+ // ── Finalized ─────────────────────────────────────────────────────────────
31
+ if (mode.type === 'finalized') {
32
+ if (key.return || input === 'q' || key.escape)
33
+ process.exit(0);
34
+ return null;
35
+ }
36
+ // ── Browsing ──────────────────────────────────────────────────────────────
37
+ if (input === 'q')
38
+ return { type: 'OPEN_DIALOG', which: 'quit' };
39
+ if (input === '?')
40
+ return { type: 'OPEN_DIALOG', which: 'help' };
41
+ if (input === 'F')
42
+ return { type: 'OPEN_DIALOG', which: 'finalize' };
43
+ if (key.tab)
44
+ return { type: 'TOGGLE_FOCUS' };
45
+ if (input === 'a')
46
+ return { type: 'ACCEPT' };
47
+ if (input === 'r')
48
+ return { type: 'REJECT' };
49
+ if (input === 'e')
50
+ return { type: 'ENTER_EDIT' };
51
+ if (input === 's')
52
+ return { type: 'TOGGLE_SOURCE', terminalWidth };
53
+ if (input === 'A')
54
+ return { type: 'APPROVE_ALL' };
55
+ if (mode.sidebarFocused) {
56
+ if (key.upArrow || input === 'k')
57
+ return { type: 'SIDEBAR_UP', visibleCount };
58
+ if (key.downArrow || input === 'j')
59
+ return { type: 'SIDEBAR_DOWN', visibleCount };
60
+ }
61
+ else {
62
+ if (key.upArrow || input === 'k')
63
+ return { type: 'SCROLL_UP' };
64
+ if (key.downArrow || input === 'j')
65
+ return { type: 'SCROLL_DOWN' };
66
+ }
67
+ return null;
68
+ }
@@ -0,0 +1,136 @@
1
+ import type { PreviewAnnotation, ReviewComponentRecord, ReviewComponentSummary, ReviewSessionSnapshot } from '../types.js';
2
+ import type { ServerPreviewResponse } from '@contentful/experience-design-system-types';
3
+ export type AppMode = {
4
+ type: 'browsing';
5
+ sidebarFocused: boolean;
6
+ sourceVisible: boolean;
7
+ } | {
8
+ type: 'editing';
9
+ componentId: string;
10
+ } | {
11
+ type: 'dialog';
12
+ which: 'help' | 'finalize' | 'quit';
13
+ } | {
14
+ type: 'finalized';
15
+ accepted: number;
16
+ rejected: number;
17
+ excluded: number;
18
+ };
19
+ export type EditorCursor = {
20
+ lines: string[];
21
+ cursorRow: number;
22
+ cursorCol: number;
23
+ };
24
+ export type EditorState = {
25
+ cursor: EditorCursor;
26
+ undoStack: EditorCursor[];
27
+ scrollRow: number;
28
+ validationError: string | null;
29
+ };
30
+ export declare function applyEditorKey(e: EditorState, input: string, key: {
31
+ ctrl: boolean;
32
+ meta: boolean;
33
+ return: boolean;
34
+ backspace: boolean;
35
+ delete: boolean;
36
+ escape: boolean;
37
+ upArrow: boolean;
38
+ downArrow: boolean;
39
+ leftArrow: boolean;
40
+ rightArrow: boolean;
41
+ }, visibleHeight: number): EditorState | null;
42
+ export type AppState = {
43
+ mode: AppMode;
44
+ session: ReviewSessionSnapshot | null;
45
+ paths: {
46
+ sessionDir: string;
47
+ statePath: string;
48
+ eventsPath: string;
49
+ } | null;
50
+ selectedId: string | null;
51
+ sortedIds: string[];
52
+ sessionSummary: ReviewComponentSummary[];
53
+ sidebarScrollOffset: number;
54
+ jsonScrollOffset: number;
55
+ draftsByComponentId: Record<string, string>;
56
+ editor: EditorState | null;
57
+ sourceCodeById: Record<string, string>;
58
+ previewAnnotations: Record<string, PreviewAnnotation>;
59
+ previewResponse: ServerPreviewResponse | null;
60
+ previewLoading: boolean;
61
+ previewError: string | null;
62
+ saveError: string | null;
63
+ };
64
+ export declare const initialState: AppState;
65
+ export type AppAction = {
66
+ type: 'SESSION_LOADED';
67
+ session: ReviewSessionSnapshot;
68
+ paths: AppState['paths'];
69
+ } | {
70
+ type: 'SELECT';
71
+ id: string;
72
+ } | {
73
+ type: 'SIDEBAR_UP';
74
+ visibleCount: number;
75
+ } | {
76
+ type: 'SIDEBAR_DOWN';
77
+ visibleCount: number;
78
+ } | {
79
+ type: 'ACCEPT';
80
+ } | {
81
+ type: 'REJECT';
82
+ } | {
83
+ type: 'APPROVE_ALL';
84
+ } | {
85
+ type: 'ENTER_EDIT';
86
+ } | {
87
+ type: 'EDITOR_KEY';
88
+ input: string;
89
+ key: Parameters<typeof applyEditorKey>[2];
90
+ visibleHeight: number;
91
+ } | {
92
+ type: 'EDITOR_VALIDATE';
93
+ } | {
94
+ type: 'DRAFT_DISCARD';
95
+ } | {
96
+ type: 'TOGGLE_FOCUS';
97
+ } | {
98
+ type: 'SCROLL_UP';
99
+ } | {
100
+ type: 'SCROLL_DOWN';
101
+ } | {
102
+ type: 'TOGGLE_SOURCE';
103
+ terminalWidth: number;
104
+ } | {
105
+ type: 'OPEN_DIALOG';
106
+ which: 'help' | 'finalize' | 'quit';
107
+ } | {
108
+ type: 'CLOSE_DIALOG';
109
+ } | {
110
+ type: 'FINALIZE_CONFIRM';
111
+ } | {
112
+ type: 'QUIT_CONFIRM';
113
+ } | {
114
+ type: 'SOURCE_LOADED';
115
+ componentId: string;
116
+ code: string;
117
+ } | {
118
+ type: 'PREVIEW_START';
119
+ } | {
120
+ type: 'PREVIEW_SUCCESS';
121
+ response: ServerPreviewResponse;
122
+ annotations: Record<string, PreviewAnnotation>;
123
+ } | {
124
+ type: 'PREVIEW_ERROR';
125
+ message: string;
126
+ } | {
127
+ type: 'SAVE_ERROR';
128
+ message: string;
129
+ } | {
130
+ type: 'CLEAR_ERRORS';
131
+ } | {
132
+ type: 'DRAFT_PERSIST_DONE';
133
+ componentId: string;
134
+ updatedComponents: ReviewComponentRecord[];
135
+ };
136
+ export declare function reducer(state: AppState, action: AppAction): AppState;