@contentful/experience-design-system-cli 2.15.2-dev-build-7aee0f8.0 → 2.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.15.2-dev-build-7aee0f8.0",
3
+ "version": "2.15.2",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,6 +4,7 @@ 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';
6
6
  import { ApiError, ImportApiClient } from './api-client.js';
7
+ import { formatApiError, formatEdsiError } from '../lib/error-parser.js';
7
8
  import { openPipelineDb, loadCDFComponents } from '../session/db.js';
8
9
  import { findSlotCycles, suggestCycleBreakEdge, formatCyclePath } from '../analyze/cycle-detection.js';
9
10
  import { isEmptyPreview } from './preview-utils.js';
@@ -289,7 +290,7 @@ function buildApplyOutput(operation, spaceId, environmentId, host) {
289
290
  .map((item) => ({
290
291
  entityType: item.entityType,
291
292
  entityId: item.id,
292
- error: item.error,
293
+ error: formatEdsiError(item.error),
293
294
  })),
294
295
  };
295
296
  }
@@ -435,7 +436,7 @@ export function registerApplyCommand(program) {
435
436
  }
436
437
  catch (e) {
437
438
  if (e instanceof ApiError)
438
- die(`Error: ${e.message}`);
439
+ die(`Error: ${formatApiError(e)}`);
439
440
  throw e;
440
441
  }
441
442
  const { components, tokens, client } = inputs;
@@ -444,7 +445,7 @@ export function registerApplyCommand(program) {
444
445
  }
445
446
  catch (e) {
446
447
  if (e instanceof ApiError)
447
- die(`Error: ${e.message}`);
448
+ die(`Error: ${formatApiError(e)}`);
448
449
  const cause = e instanceof Error && e.cause instanceof Error ? e.cause.message : '';
449
450
  die(`Error: unable to connect to API host${cause ? `: ${cause}` : ''}`);
450
451
  }
@@ -455,7 +456,7 @@ export function registerApplyCommand(program) {
455
456
  }
456
457
  catch (e) {
457
458
  if (e instanceof ApiError)
458
- die(`Error: ${e.message}`);
459
+ die(`Error: ${formatApiError(e)}`);
459
460
  throw e;
460
461
  }
461
462
  const spaceId = opts.spaceId;
@@ -501,7 +502,7 @@ export function registerApplyCommand(program) {
501
502
  }
502
503
  catch (e) {
503
504
  if (e instanceof ApiError)
504
- die(`Error: ${e.message}`);
505
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
505
506
  throw e;
506
507
  }
507
508
  const { components, tokens, client } = inputs;
@@ -511,7 +512,7 @@ export function registerApplyCommand(program) {
511
512
  }
512
513
  catch (e) {
513
514
  if (e instanceof ApiError)
514
- die(`Error: ${e.message}`);
515
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
515
516
  throw e;
516
517
  }
517
518
  const manifest = buildManifest(components, tokens);
@@ -521,7 +522,7 @@ export function registerApplyCommand(program) {
521
522
  }
522
523
  catch (e) {
523
524
  if (e instanceof ApiError)
524
- die(`Error: ${e.message}`);
525
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
525
526
  throw e;
526
527
  }
527
528
  const spaceId = opts.spaceId;
@@ -566,7 +567,7 @@ export function registerApplyCommand(program) {
566
567
  }
567
568
  catch (e) {
568
569
  if (e instanceof ApiError)
569
- die(`Error: ${e.message}`);
570
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
570
571
  throw e;
571
572
  }
572
573
  process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
@@ -575,7 +576,7 @@ export function registerApplyCommand(program) {
575
576
  }
576
577
  catch (e) {
577
578
  if (e instanceof ApiError)
578
- die(`Error: ${e.message}`);
579
+ die(`Error: ${formatApiError(e, opts.verbose)}`);
579
580
  throw e;
580
581
  }
581
582
  const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
@@ -600,7 +601,7 @@ export function registerApplyCommand(program) {
600
601
  spaceId,
601
602
  environmentId,
602
603
  status: 'error',
603
- error: e.message,
604
+ error: formatApiError(e, opts.verbose),
604
605
  }));
605
606
  return;
606
607
  }
@@ -621,7 +622,7 @@ export function registerApplyCommand(program) {
621
622
  spaceId,
622
623
  environmentId,
623
624
  status: 'error',
624
- error: e.message,
625
+ error: formatApiError(e, opts.verbose),
625
626
  }));
626
627
  return;
627
628
  }
@@ -677,7 +678,7 @@ export function registerApplyCommand(program) {
677
678
  }
678
679
  catch (e) {
679
680
  if (e instanceof ApiError)
680
- die(`Error: ${e.message}`);
681
+ die(`Error: ${formatApiError(e)}`);
681
682
  throw e;
682
683
  }
683
684
  const { components, tokens, client } = inputs;
@@ -687,7 +688,7 @@ export function registerApplyCommand(program) {
687
688
  }
688
689
  catch (e) {
689
690
  if (e instanceof ApiError)
690
- die(`Error: ${e.message}`);
691
+ die(`Error: ${formatApiError(e)}`);
691
692
  throw e;
692
693
  }
693
694
  const fullManifest = buildManifest(components, tokens);
@@ -697,7 +698,7 @@ export function registerApplyCommand(program) {
697
698
  }
698
699
  catch (e) {
699
700
  if (e instanceof ApiError)
700
- die(`Error: ${e.message}`);
701
+ die(`Error: ${formatApiError(e)}`);
701
702
  throw e;
702
703
  }
703
704
  const spaceId = opts.spaceId;
@@ -735,7 +736,7 @@ export function registerApplyCommand(program) {
735
736
  }
736
737
  catch (e) {
737
738
  if (e instanceof ApiError)
738
- die(`Error: ${e.message}`);
739
+ die(`Error: ${formatApiError(e)}`);
739
740
  throw e;
740
741
  }
741
742
  process.stderr.write(`Apply operation started: ${operation.sys.id}\n`);
@@ -744,7 +745,7 @@ export function registerApplyCommand(program) {
744
745
  }
745
746
  catch (e) {
746
747
  if (e instanceof ApiError)
747
- die(`Error: ${e.message}`);
748
+ die(`Error: ${formatApiError(e)}`);
748
749
  throw e;
749
750
  }
750
751
  const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
@@ -781,7 +782,7 @@ export function registerApplyCommand(program) {
781
782
  spaceId,
782
783
  environmentId,
783
784
  status: 'error',
784
- error: e.message,
785
+ error: formatApiError(e),
785
786
  }));
786
787
  return;
787
788
  }
@@ -802,7 +803,7 @@ export function registerApplyCommand(program) {
802
803
  spaceId,
803
804
  environmentId,
804
805
  status: 'error',
805
- error: e.message,
806
+ error: formatApiError(e),
806
807
  }));
807
808
  return;
808
809
  }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { ServerPreviewView } from './ServerPreviewView.js';
4
4
  import { buildPostPushUrl } from '../../lib/contentful-urls.js';
5
+ import { formatEdsiError } from '../../lib/error-parser.js';
5
6
  export function ServerPreviewConfirm({ preview, spaceId, environmentId, breakingWithImpact, onConfirm, onCancel, }) {
6
7
  useInput((input, key) => {
7
8
  if (key.return)
@@ -35,9 +36,5 @@ export function ServerApplyDone({ operation, spaceId, environmentId, host }) {
35
36
  return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Import complete \u2014 ", environmentId, " @ ", spaceId] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "green", children: [" \u2713 ", operation.summary.succeeded, " succeeded"] }), operation.summary.failed > 0 && _jsxs(Text, { color: "red", children: [" \u2717 ", operation.summary.failed, " failed"] }), operation.summary.failed === 0 && _jsx(Text, { dimColor: true, children: " All entities imported successfully." }), failures.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Failures:" }), failures.map((item, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: [' ', "\u2717 ", item.entityType, ": ", item.id] }), item.error && _jsxs(Text, { dimColor: true, children: [" ", formatItemError(item.error)] })] }, i)))] })), operation.sys.status === 'succeeded' && operation.summary.succeeded > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " View your design system:" }), _jsxs(Text, { color: "cyan", children: [" ", buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId })] })] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Press Q to exit." })] }));
36
37
  }
37
38
  function formatItemError(error) {
38
- if (!error)
39
- return '';
40
- if (typeof error === 'string')
41
- return error;
42
- return `${error.code}: ${error.message}`;
39
+ return formatEdsiError(error);
43
40
  }
@@ -61,16 +61,20 @@ export function parseCycleComponentNames(report) {
61
61
  return [...new Set(names)];
62
62
  }
63
63
  export function isPreviewValidationError(result) {
64
- return (result.exitCode !== 0 &&
65
- result.stderr.includes(`${PREVIEW_ERROR_PREFIX} 422`) &&
66
- result.stderr.includes(VALIDATION_FAILED_CODE));
64
+ if (result.exitCode === 0 || !result.stderr.includes(`${PREVIEW_ERROR_PREFIX} 422`))
65
+ return false;
66
+ return result.stderr.includes(VALIDATION_FAILED_CODE) || result.stderr.includes('[ValidationFailed]');
67
67
  }
68
68
  export function parseOffendingComponentNames(output) {
69
69
  const jsonStart = output.indexOf('{');
70
- if (jsonStart === -1)
71
- return [];
72
- const errors = parsePreviewValidationErrors(output.slice(jsonStart));
73
- return [...new Set(errors.map((e) => e.componentName))];
70
+ if (jsonStart !== -1) {
71
+ const errors = parsePreviewValidationErrors(output.slice(jsonStart));
72
+ const names = [...new Set(errors.map((error) => error.componentName))];
73
+ if (names.length > 0)
74
+ return names;
75
+ }
76
+ const names = [...output.matchAll(/Component:\s*([^;\n)]+)/g)].map((match) => match[1].trim()).filter(Boolean);
77
+ return [...new Set(names)];
74
78
  }
75
79
  export function buildPushStepResult(args) {
76
80
  const { created, updated, failed, durationMs, stderr, excludedByRetry, totalFailure } = args;
@@ -37,7 +37,7 @@ import { ImportApiClient, ApiError } from '../../apply/api-client.js';
37
37
  import { detectSlotCycles, extractComponentsFromManifest, formatSlotCycleReport } from '../../apply/command.js';
38
38
  import { findSlotCycles } from '../../analyze/cycle-detection.js';
39
39
  import { buildComponentGraph } from '../../analyze/slot-graph.js';
40
- import { parseEdsiError, formatParsedEdsiError } from '../../apply/error-parser.js';
40
+ import { parseEdsiError, formatEdsiError, formatParsedEdsiError } from '../../lib/error-parser.js';
41
41
  import { handlePreview422, applySkipValidationErrors, clearedValidationErrorState } from './wizard-422-helpers.js';
42
42
  import { parseGenerateStderrChunk } from './wizard-generate-progress.js';
43
43
  import { spawnGenerateChild } from './spawn-generate.js';
@@ -927,7 +927,10 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
927
927
  update({
928
928
  step: 'error',
929
929
  errorStep: 'apply preview',
930
- errorMessage: e.message,
930
+ errorMessage: formatParsedEdsiError(parseEdsiError(e.body || e.message), {
931
+ verbose: process.env['EDSI_VERBOSE_ERRORS'] === '1',
932
+ raw: e.body,
933
+ }) || e.message,
931
934
  errorAllowCredentialRetry: true,
932
935
  });
933
936
  return;
@@ -1056,6 +1059,13 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1056
1059
  failed: items.filter((i) => i.entityType === 'DesignToken' && i.status === 'failed').length,
1057
1060
  },
1058
1061
  summary: operation.summary,
1062
+ failures: items
1063
+ .filter((item) => item.status === 'failed')
1064
+ .map((item) => ({
1065
+ entityType: item.entityType,
1066
+ entityId: item.id,
1067
+ message: formatEdsiError(item.error),
1068
+ })),
1059
1069
  };
1060
1070
  }
1061
1071
  else {
@@ -1645,7 +1655,7 @@ export function WizardApp({ initialSpaceId = '', initialEnvironmentId = 'master'
1645
1655
  case 'done': {
1646
1656
  const totalFailed = state.pushResult.componentTypes.failed + state.pushResult.designTokens.failed;
1647
1657
  const teaser = buildRunTeaserLine(state.lastRunId);
1648
- return (_jsx(DoneStep, { componentTypes: state.pushResult.componentTypes, designTokens: state.pushResult.designTokens, summary: state.pushResult.summary, spaceId: state.spaceId, environmentId: state.environmentId, host: state.host, ...(teaser ? { runTeaser: teaser } : {}), onExit: () => process.exit(totalFailed > 0 ? 1 : 0) }));
1658
+ return (_jsx(DoneStep, { componentTypes: state.pushResult.componentTypes, designTokens: state.pushResult.designTokens, summary: state.pushResult.summary, failures: state.pushResult.failures, spaceId: state.spaceId, environmentId: state.environmentId, host: state.host, ...(teaser ? { runTeaser: teaser } : {}), onExit: () => process.exit(totalFailed > 0 ? 1 : 0) }));
1649
1659
  }
1650
1660
  case 'preview-validation-error': {
1651
1661
  return (_jsx(PreviewValidationErrorStep, { errors: state.previewValidationErrors, missingNames: state.previewValidationMissingNames, onEdit: () => {
@@ -17,7 +17,12 @@ type DoneStepProps = {
17
17
  environmentId: string;
18
18
  host?: string;
19
19
  runTeaser?: string;
20
+ failures?: Array<{
21
+ entityType: string;
22
+ entityId: string;
23
+ message: string;
24
+ }>;
20
25
  onExit: () => void;
21
26
  };
22
- export declare function DoneStep({ componentTypes, designTokens, summary, spaceId, environmentId, host, runTeaser, onExit, }: DoneStepProps): React.ReactElement;
27
+ export declare function DoneStep({ componentTypes, designTokens, summary, spaceId, environmentId, host, runTeaser, failures, onExit, }: DoneStepProps): React.ReactElement;
23
28
  export {};
@@ -3,7 +3,7 @@ import { PALETTE } from '../../../analyze/select/tui/theme.js';
3
3
  import { Box, Text } from 'ink';
4
4
  import { useImmediateInput } from '../../../analyze/select/tui/hooks/useImmediateInput.js';
5
5
  import { buildPostPushUrl } from '../../../lib/contentful-urls.js';
6
- export function DoneStep({ componentTypes, designTokens, summary, spaceId, environmentId, host, runTeaser, onExit, }) {
6
+ export function DoneStep({ componentTypes, designTokens, summary, spaceId, environmentId, host, runTeaser, failures = [], onExit, }) {
7
7
  useImmediateInput((input, key) => {
8
8
  if (key.return || input === 'q' || key.escape) {
9
9
  onExit();
@@ -20,5 +20,5 @@ export function DoneStep({ componentTypes, designTokens, summary, spaceId, envir
20
20
  function EntityRows({ entity, label }) {
21
21
  return (_jsxs(_Fragment, { children: [entity.created > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.success, children: "\u2713" }), _jsxs(Text, { children: [entity.created, " ", label, entity.created !== 1 ? 's' : '', " created"] })] })), entity.updated > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.success, children: "\u2713" }), _jsxs(Text, { children: [entity.updated, " ", label, entity.updated !== 1 ? 's' : '', " updated"] })] })), entity.removed > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.success, children: "\u2713" }), _jsxs(Text, { children: [entity.removed, " ", label, entity.removed !== 1 ? 's' : '', " removed"] })] })), entity.failed > 0 && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: PALETTE.error, children: "\u2717" }), _jsxs(Text, { color: PALETTE.error, children: [entity.failed, " ", label, entity.failed !== 1 ? 's' : '', " failed \u2014 check logs above"] })] }))] }));
22
22
  }
23
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [success ? (_jsx(Text, { bold: true, color: PALETTE.success, children: "Done!" })) : (_jsx(Text, { bold: true, color: PALETTE.warning, children: "\u26A0 Finished with errors" })), totalPushed === 0 && totalFailed === 0 && !summary ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Nothing was pushed \u2014 everything was already up to date." }) })) : (_jsxs(Box, { flexDirection: "column", gap: 0, marginTop: 1, children: [_jsx(EntityRows, { entity: componentTypes, label: "Component Type" }), _jsx(EntityRows, { entity: designTokens, label: "Design Token" }), summary && (_jsxs(Box, { gap: 1, marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["Server: ", summary.succeeded, "/", summary.total, " succeeded"] }), summary.failed > 0 && _jsxs(Text, { color: PALETTE.error, children: [", ", summary.failed, " failed"] })] }))] })), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Space:" }), _jsx(Text, { children: spaceId }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { dimColor: true, children: "Environment:" }), _jsx(Text, { children: environmentId })] }), success && totalPushed > 0 && (_jsxs(Box, { flexDirection: "column", gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Your design system is now in Contentful ExO." }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: "View it here:" }), _jsx(Text, { color: PALETTE.info, children: buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId }) })] })] })), runTeaser && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: runTeaser }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Enter / q] Exit" }) })] }));
23
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, paddingY: 1, children: [success ? (_jsx(Text, { bold: true, color: PALETTE.success, children: "Done!" })) : (_jsx(Text, { bold: true, color: PALETTE.warning, children: "\u26A0 Finished with errors" })), totalPushed === 0 && totalFailed === 0 && !summary ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Nothing was pushed \u2014 everything was already up to date." }) })) : (_jsxs(Box, { flexDirection: "column", gap: 0, marginTop: 1, children: [_jsx(EntityRows, { entity: componentTypes, label: "Component Type" }), _jsx(EntityRows, { entity: designTokens, label: "Design Token" }), summary && (_jsxs(Box, { gap: 1, marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["Server: ", summary.succeeded, "/", summary.total, " succeeded"] }), summary.failed > 0 && _jsxs(Text, { color: PALETTE.error, children: [", ", summary.failed, " failed"] })] }))] })), failures.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.error, children: "Failure details" }), failures.map((failure) => (_jsxs(Text, { color: PALETTE.error, children: [failure.entityType, " ", failure.entityId, ": ", failure.message] }, `${failure.entityType}:${failure.entityId}`)))] })), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Space:" }), _jsx(Text, { children: spaceId }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { dimColor: true, children: "Environment:" }), _jsx(Text, { children: environmentId })] }), success && totalPushed > 0 && (_jsxs(Box, { flexDirection: "column", gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Your design system is now in Contentful ExO." }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { dimColor: true, children: "View it here:" }), _jsx(Text, { color: PALETTE.info, children: buildPostPushUrl({ host: host ?? 'api.contentful.com', spaceId, environmentId }) })] })] })), runTeaser && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: runTeaser }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Enter / q] Exit" }) })] }));
24
24
  }
@@ -1,9 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { GateStep } from './GateStep.js';
3
+ import { formatParsedEdsiError } from '../../../lib/error-parser.js';
3
4
  export function PreviewValidationErrorStep({ errors, missingNames, onEdit, onSkip, onQuit, }) {
4
5
  const uniqueNames = [...new Set(errors.map((e) => e.componentName))];
5
6
  const matchedNames = uniqueNames.filter((n) => !missingNames.includes(n));
6
- const errorLines = errors.map((e) => ` ${e.componentName}: ${e.message}`).join('\n');
7
+ const errorLines = errors
8
+ .map((error) => formatParsedEdsiError({
9
+ code: null,
10
+ message: '',
11
+ cycle: null,
12
+ raw: false,
13
+ diagnostics: [{ message: error.message, component: error.componentName, path: error.path }],
14
+ }))
15
+ .join('\n');
7
16
  const missingNote = missingNames.length > 0
8
17
  ? `\n\nNote: ${missingNames.length} component name${missingNames.length === 1 ? '' : 's'} from the server (${missingNames.join(', ')}) ${missingNames.length === 1 ? 'does' : 'do'} not match anything in this session — they cannot be edited or skipped from here.`
9
18
  : '';
@@ -0,0 +1,38 @@
1
+ export interface ParsedEdsiError {
2
+ /** Server-side error code, if we could extract one. */
3
+ code: string | null;
4
+ /** Human-readable message, stripped of log/trace decoration. */
5
+ message: string;
6
+ /** Cycle participants, when `code === 'TopoSortCycleError'`. */
7
+ cycle: string[] | null;
8
+ /** True when the message survived cleaning as-is (no parseable structure). */
9
+ raw: boolean;
10
+ /** Validation details supplied by the API, normalized for terminal output. */
11
+ diagnostics?: ErrorDiagnostic[];
12
+ }
13
+ export interface ErrorDiagnostic {
14
+ message: string;
15
+ component?: string;
16
+ /**
17
+ * A null path means that the API supplied a location but it contained no
18
+ * usable segments. An undefined path means the API did not provide one.
19
+ */
20
+ path?: string | null;
21
+ /** The API field that supplied the diagnostic message, for verbose output. */
22
+ messageSource?: 'message' | 'details' | 'error' | 'name';
23
+ }
24
+ export interface ApiErrorLike {
25
+ body?: string;
26
+ message: string;
27
+ }
28
+ export declare function stripLambdaLogPrefix(body: string): string;
29
+ export declare function parseEdsiError(rawInput: string | undefined | null): ParsedEdsiError;
30
+ export declare function formatParsedEdsiError(parsed: ParsedEdsiError, opts?: {
31
+ verbose?: boolean;
32
+ raw?: string;
33
+ }): string;
34
+ export declare function formatEdsiError(raw: unknown, opts?: {
35
+ verbose?: boolean;
36
+ raw?: string;
37
+ }): string;
38
+ export declare function formatApiError(error: ApiErrorLike, verbose?: boolean): string;
@@ -0,0 +1,264 @@
1
+ const LAMBDA_LOG_PREFIX_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\s+[0-9a-f-]+\s+ERROR\s+(?:\[dd\.[^\]]*\]\s*)?/;
2
+ const DD_TAG_RE = /\[dd\.(?:trace_id|span_id)=[^\]]*\]\s*/g;
3
+ export function stripLambdaLogPrefix(body) {
4
+ let out = body.replace(LAMBDA_LOG_PREFIX_RE, '');
5
+ out = out.replace(DD_TAG_RE, '');
6
+ return out.trim();
7
+ }
8
+ function parseObjectLiteralTail(body) {
9
+ const braceStart = body.lastIndexOf('{');
10
+ if (braceStart === -1)
11
+ return null;
12
+ const tail = body.slice(braceStart);
13
+ const codeMatch = tail.match(/code:\s*['"]([^'"]+)['"]/);
14
+ const cycleMatch = tail.match(/cycle:\s*\[\s*([^\]]*)\s*\]/);
15
+ if (!codeMatch && !cycleMatch)
16
+ return null;
17
+ const code = codeMatch ? codeMatch[1] : null;
18
+ let cycle = null;
19
+ if (cycleMatch) {
20
+ cycle = cycleMatch[1]
21
+ .split(',')
22
+ .map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
23
+ .filter((s) => s.length > 0);
24
+ if (cycle.length === 0)
25
+ cycle = null;
26
+ }
27
+ return { code, cycle };
28
+ }
29
+ function asRecord(value) {
30
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
31
+ ? value
32
+ : null;
33
+ }
34
+ function displayValue(value) {
35
+ if (typeof value === 'string')
36
+ return value;
37
+ if (typeof value === 'number' || typeof value === 'boolean')
38
+ return String(value);
39
+ if (value === null || value === undefined)
40
+ return null;
41
+ try {
42
+ return JSON.stringify(value);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function isPathSegment(value) {
49
+ return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value));
50
+ }
51
+ function formatPath(value) {
52
+ if (isPathSegment(value))
53
+ return String(value) ? { path: String(value) } : { path: null };
54
+ if (Array.isArray(value)) {
55
+ const parts = value.filter(isPathSegment);
56
+ return parts.length > 0 ? { path: parts.join(' › ') } : { path: null };
57
+ }
58
+ return {};
59
+ }
60
+ function componentFromPath(path) {
61
+ if (!path)
62
+ return undefined;
63
+ const manifestMatch = path.match(/manifest:components\/([^/›]+)/);
64
+ if (manifestMatch?.[1])
65
+ return manifestMatch[1];
66
+ const parts = path
67
+ .split(/[›/]/)
68
+ .map((part) => part.trim())
69
+ .filter(Boolean);
70
+ const componentsIndex = parts.findIndex((part) => part === 'components');
71
+ return componentsIndex === -1 ? undefined : parts[componentsIndex + 1];
72
+ }
73
+ function diagnosticMessage(error) {
74
+ const candidates = [
75
+ ['message', error.message],
76
+ ['details', error.details],
77
+ ['error', error.error],
78
+ ['name', error.name],
79
+ ];
80
+ for (const [messageSource, value] of candidates) {
81
+ const message = displayValue(value);
82
+ if (message !== null)
83
+ return { message, messageSource };
84
+ }
85
+ return { message: 'Validation failed' };
86
+ }
87
+ function parseValidationDiagnostics(details) {
88
+ if (!Array.isArray(details.errors))
89
+ return [];
90
+ const diagnostics = [];
91
+ for (const rawError of details.errors) {
92
+ const error = asRecord(rawError);
93
+ if (!error)
94
+ continue;
95
+ const location = formatPath(error.path);
96
+ const component = displayValue(error.component ?? error.componentName ?? error.componentId) ?? componentFromPath(location.path);
97
+ diagnostics.push({ ...diagnosticMessage(error), ...location, ...(component ? { component } : {}) });
98
+ }
99
+ return diagnostics;
100
+ }
101
+ function parseJsonBody(body) {
102
+ let parsed;
103
+ try {
104
+ parsed = JSON.parse(body);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ const p = asRecord(parsed);
110
+ if (!p)
111
+ return null;
112
+ const details = asRecord(p.details) ?? {};
113
+ const pick = (k) => (p[k] !== undefined ? p[k] : details[k]);
114
+ const codeRaw = pick('code') ?? asRecord(p.sys)?.id;
115
+ const messageRaw = pick('message');
116
+ const cycleRaw = pick('cycle');
117
+ const out = {};
118
+ if (typeof codeRaw === 'string')
119
+ out.code = codeRaw;
120
+ if (typeof messageRaw === 'string')
121
+ out.message = messageRaw;
122
+ if (Array.isArray(cycleRaw)) {
123
+ const strs = cycleRaw.filter((x) => typeof x === 'string');
124
+ if (strs.length > 0)
125
+ out.cycle = strs;
126
+ }
127
+ const diagnostics = parseValidationDiagnostics(details);
128
+ if (diagnostics.length > 0)
129
+ out.diagnostics = diagnostics;
130
+ return out;
131
+ }
132
+ function parseBindingDiagnostics(body) {
133
+ if (!/binding configurations are invalid|Pointer path does not exist/i.test(body))
134
+ return null;
135
+ const locationMatches = [...body.matchAll(/(?:^|\.\s)([A-Za-z0-9_$-]+(?:\s*›\s*[A-Za-z0-9_$-]+){2,})/g)];
136
+ const location = locationMatches.at(-1)?.[1];
137
+ const pointerMessage = body.match(/Pointer path does not exist for '\[object Object\]':\s*(.+?)(?=\.\s*Default\s*›|$)/i)?.[1];
138
+ const graphQlMessage = body.match(/return GraphQL validation error:\s*(.+?)(?=\.\s*Default\s*›|$)/i)?.[1];
139
+ const heading = body.match(/^(.*?)(?=\.?\s*Pointer path does not exist|\.?\s*Default\s*›)/i)?.[1]?.trim();
140
+ const diagnostics = [];
141
+ if (pointerMessage)
142
+ diagnostics.push({ message: pointerMessage.trim(), ...(location ? { path: location } : {}) });
143
+ if (graphQlMessage)
144
+ diagnostics.push({ message: graphQlMessage.trim(), ...(location ? { path: location } : {}) });
145
+ return {
146
+ code: 'BindingValidationFailed',
147
+ message: heading || 'One or more binding configurations are invalid.',
148
+ ...(diagnostics.length > 0 ? { diagnostics } : {}),
149
+ };
150
+ }
151
+ // ApiError.message shape: `${phasePrefix}\n${body}` where phasePrefix looks
152
+ // like `apply failed: 400`, `preview failed: 422`, `poll failed: 500`. We
153
+ // only strip the prefix line when the first line matches this shape —
154
+ // otherwise a raw body that happens to contain a newline (e.g. a Lambda log
155
+ // spill with a multi-line object literal) gets truncated mid-structure.
156
+ const API_ERROR_PREFIX_RE = /^(?:apply|preview|poll) failed: \d+$/;
157
+ export function parseEdsiError(rawInput) {
158
+ if (!rawInput)
159
+ return { code: null, message: '', cycle: null, raw: true };
160
+ const nlIndex = rawInput.indexOf('\n');
161
+ let body = rawInput;
162
+ let prefix = '';
163
+ if (nlIndex !== -1) {
164
+ const firstLine = rawInput.slice(0, nlIndex);
165
+ if (API_ERROR_PREFIX_RE.test(firstLine)) {
166
+ prefix = firstLine;
167
+ body = rawInput.slice(nlIndex + 1);
168
+ }
169
+ }
170
+ const cleaned = stripLambdaLogPrefix(body);
171
+ const json = parseJsonBody(cleaned) ?? parseJsonBody(body);
172
+ const bindingFromJson = typeof json?.message === 'string' ? parseBindingDiagnostics(json.message) : null;
173
+ if (bindingFromJson) {
174
+ return {
175
+ code: bindingFromJson.code ?? json?.code ?? null,
176
+ message: bindingFromJson.message ?? cleaned,
177
+ cycle: null,
178
+ raw: false,
179
+ ...(bindingFromJson.diagnostics ? { diagnostics: bindingFromJson.diagnostics } : {}),
180
+ };
181
+ }
182
+ if (json && (json.code || json.message || json.cycle)) {
183
+ return {
184
+ code: json.code ?? null,
185
+ message: json.message ?? (cleaned || prefix),
186
+ cycle: json.cycle ?? null,
187
+ raw: false,
188
+ ...(json.diagnostics ? { diagnostics: json.diagnostics } : {}),
189
+ };
190
+ }
191
+ const binding = parseBindingDiagnostics(cleaned);
192
+ if (binding) {
193
+ return {
194
+ code: binding.code ?? null,
195
+ message: binding.message ?? cleaned,
196
+ cycle: null,
197
+ raw: false,
198
+ ...(binding.diagnostics ? { diagnostics: binding.diagnostics } : {}),
199
+ };
200
+ }
201
+ const literal = parseObjectLiteralTail(cleaned);
202
+ if (literal && (literal.code || literal.cycle)) {
203
+ const braceStart = cleaned.lastIndexOf('{');
204
+ const head = braceStart === -1 ? cleaned : cleaned.slice(0, braceStart).trim();
205
+ return {
206
+ code: literal.code ?? null,
207
+ message: head || cleaned,
208
+ cycle: literal.cycle ?? null,
209
+ raw: false,
210
+ };
211
+ }
212
+ return { code: null, message: cleaned || prefix || rawInput, cycle: null, raw: true };
213
+ }
214
+ export function formatParsedEdsiError(parsed, opts = {}) {
215
+ const lines = [];
216
+ if (parsed.code) {
217
+ lines.push(`[${parsed.code}]`);
218
+ }
219
+ if (parsed.message) {
220
+ lines.push(parsed.message);
221
+ }
222
+ for (const diagnostic of parsed.diagnostics ?? []) {
223
+ const context = [
224
+ diagnostic.component ? `Component: ${diagnostic.component}` : null,
225
+ diagnostic.path ? `Path: ${diagnostic.path}` : null,
226
+ ].filter(Boolean);
227
+ lines.push(`- ${diagnostic.message}${context.length > 0 ? ` (${context.join('; ')})` : ''}`);
228
+ if (diagnostic.path === null) {
229
+ lines.push(' Location: not provided by the server. Review the submitted manifest; no component or field was identified.');
230
+ }
231
+ if (opts.verbose && diagnostic.messageSource) {
232
+ lines.push(` Message field: ${diagnostic.messageSource}`);
233
+ }
234
+ }
235
+ if (parsed.code === 'BindingValidationFailed') {
236
+ const location = parsed.diagnostics?.find((diagnostic) => diagnostic.path)?.path;
237
+ lines.push(location
238
+ ? `Next action: review the binding at ${location} and correct its pointer or GraphQL selection.`
239
+ : 'Next action: review the binding pointer and GraphQL selection.');
240
+ }
241
+ if (parsed.cycle && parsed.cycle.length > 0) {
242
+ lines.push(`Cycle: ${parsed.cycle.join(' → ')} → ${parsed.cycle[0]}`);
243
+ lines.push('Break the cycle by removing at least one $allowedComponents entry.');
244
+ }
245
+ if (opts.verbose && opts.raw) {
246
+ lines.push('');
247
+ lines.push('--- raw ---');
248
+ lines.push(opts.raw);
249
+ }
250
+ return lines.filter(Boolean).join('\n');
251
+ }
252
+ export function formatEdsiError(raw, opts = {}) {
253
+ const input = typeof raw === 'string' ? raw : displayValue(raw);
254
+ if (!input)
255
+ return '';
256
+ return formatParsedEdsiError(parseEdsiError(input), { ...opts, raw: opts.raw ?? input });
257
+ }
258
+ export function formatApiError(error, verbose = false) {
259
+ const formatted = formatEdsiError(error.body || error.message, { verbose, raw: error.body }) || error.message;
260
+ const phase = error.message.split('\n', 1)[0];
261
+ return /^(?:apply|preview|poll) failed: \d+$/.test(phase) && formatted !== phase
262
+ ? `${phase}\n${formatted}`
263
+ : formatted;
264
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.15.2-dev-build-7aee0f8.0",
3
+ "version": "2.15.2",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,8 +34,8 @@
34
34
  "react": "^18.3.1",
35
35
  "react-devtools-core": "^4.19.1",
36
36
  "react-dom": "^18.3.1",
37
- "@contentful/experience-design-system-extraction": "2.15.2-dev-build-7aee0f8.0",
38
- "@contentful/experience-design-system-types": "2.15.2-dev-build-7aee0f8.0"
37
+ "@contentful/experience-design-system-extraction": "2.15.2",
38
+ "@contentful/experience-design-system-types": "2.15.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@tsconfig/node24": "^24.0.3",
@@ -1,16 +0,0 @@
1
- export interface ParsedEdsiError {
2
- /** Server-side error code, if we could extract one. */
3
- code: string | null;
4
- /** Human-readable message, stripped of log/trace decoration. */
5
- message: string;
6
- /** Cycle participants, when `code === 'TopoSortCycleError'`. */
7
- cycle: string[] | null;
8
- /** True when the message survived cleaning as-is (no parseable structure). */
9
- raw: boolean;
10
- }
11
- export declare function stripLambdaLogPrefix(body: string): string;
12
- export declare function parseEdsiError(rawInput: string | undefined | null): ParsedEdsiError;
13
- export declare function formatParsedEdsiError(parsed: ParsedEdsiError, opts?: {
14
- verbose?: boolean;
15
- raw?: string;
16
- }): string;
@@ -1,117 +0,0 @@
1
- const LAMBDA_LOG_PREFIX_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\s+[0-9a-f-]+\s+ERROR\s+(?:\[dd\.[^\]]*\]\s*)?/;
2
- const DD_TAG_RE = /\[dd\.(?:trace_id|span_id)=[^\]]*\]\s*/g;
3
- export function stripLambdaLogPrefix(body) {
4
- let out = body.replace(LAMBDA_LOG_PREFIX_RE, '');
5
- out = out.replace(DD_TAG_RE, '');
6
- return out.trim();
7
- }
8
- function parseObjectLiteralTail(body) {
9
- const braceStart = body.lastIndexOf('{');
10
- if (braceStart === -1)
11
- return null;
12
- const tail = body.slice(braceStart);
13
- const codeMatch = tail.match(/code:\s*['"]([^'"]+)['"]/);
14
- const cycleMatch = tail.match(/cycle:\s*\[\s*([^\]]*)\s*\]/);
15
- if (!codeMatch && !cycleMatch)
16
- return null;
17
- const code = codeMatch ? codeMatch[1] : null;
18
- let cycle = null;
19
- if (cycleMatch) {
20
- cycle = cycleMatch[1]
21
- .split(',')
22
- .map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
23
- .filter((s) => s.length > 0);
24
- if (cycle.length === 0)
25
- cycle = null;
26
- }
27
- return { code, cycle };
28
- }
29
- function parseJsonBody(body) {
30
- let parsed;
31
- try {
32
- parsed = JSON.parse(body);
33
- }
34
- catch {
35
- return null;
36
- }
37
- if (!parsed || typeof parsed !== 'object')
38
- return null;
39
- const p = parsed;
40
- const details = (p.details && typeof p.details === 'object' ? p.details : {});
41
- const pick = (k) => (p[k] !== undefined ? p[k] : details[k]);
42
- const codeRaw = pick('code');
43
- const messageRaw = pick('message');
44
- const cycleRaw = pick('cycle');
45
- const out = {};
46
- if (typeof codeRaw === 'string')
47
- out.code = codeRaw;
48
- if (typeof messageRaw === 'string')
49
- out.message = messageRaw;
50
- if (Array.isArray(cycleRaw)) {
51
- const strs = cycleRaw.filter((x) => typeof x === 'string');
52
- if (strs.length > 0)
53
- out.cycle = strs;
54
- }
55
- return out;
56
- }
57
- // ApiError.message shape: `${phasePrefix}\n${body}` where phasePrefix looks
58
- // like `apply failed: 400`, `preview failed: 422`, `poll failed: 500`. We
59
- // only strip the prefix line when the first line matches this shape —
60
- // otherwise a raw body that happens to contain a newline (e.g. a Lambda log
61
- // spill with a multi-line object literal) gets truncated mid-structure.
62
- const API_ERROR_PREFIX_RE = /^(?:apply|preview|poll) failed: \d+$/;
63
- export function parseEdsiError(rawInput) {
64
- if (!rawInput)
65
- return { code: null, message: '', cycle: null, raw: true };
66
- const nlIndex = rawInput.indexOf('\n');
67
- let body = rawInput;
68
- let prefix = '';
69
- if (nlIndex !== -1) {
70
- const firstLine = rawInput.slice(0, nlIndex);
71
- if (API_ERROR_PREFIX_RE.test(firstLine)) {
72
- prefix = firstLine;
73
- body = rawInput.slice(nlIndex + 1);
74
- }
75
- }
76
- const cleaned = stripLambdaLogPrefix(body);
77
- const json = parseJsonBody(cleaned) ?? parseJsonBody(body);
78
- if (json && (json.code || json.message || json.cycle)) {
79
- return {
80
- code: json.code ?? null,
81
- message: json.message ?? (cleaned || prefix),
82
- cycle: json.cycle ?? null,
83
- raw: false,
84
- };
85
- }
86
- const literal = parseObjectLiteralTail(cleaned);
87
- if (literal && (literal.code || literal.cycle)) {
88
- const braceStart = cleaned.lastIndexOf('{');
89
- const head = braceStart === -1 ? cleaned : cleaned.slice(0, braceStart).trim();
90
- return {
91
- code: literal.code ?? null,
92
- message: head || cleaned,
93
- cycle: literal.cycle ?? null,
94
- raw: false,
95
- };
96
- }
97
- return { code: null, message: cleaned || prefix || rawInput, cycle: null, raw: true };
98
- }
99
- export function formatParsedEdsiError(parsed, opts = {}) {
100
- const lines = [];
101
- if (parsed.code) {
102
- lines.push(`[${parsed.code}]`);
103
- }
104
- if (parsed.message) {
105
- lines.push(parsed.message);
106
- }
107
- if (parsed.cycle && parsed.cycle.length > 0) {
108
- lines.push(`Cycle: ${parsed.cycle.join(' → ')} → ${parsed.cycle[0]}`);
109
- lines.push('Break the cycle by removing at least one $allowedComponents entry.');
110
- }
111
- if (opts.verbose && opts.raw) {
112
- lines.push('');
113
- lines.push('--- raw ---');
114
- lines.push(opts.raw);
115
- }
116
- return lines.filter(Boolean).join('\n');
117
- }