@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 +1 -1
- package/dist/src/apply/command.js +19 -18
- package/dist/src/apply/tui/ServerApplyView.js +2 -5
- package/dist/src/import/orchestrator.js +11 -7
- package/dist/src/import/tui/WizardApp.js +13 -3
- package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
- package/dist/src/import/tui/steps/DoneStep.js +2 -2
- package/dist/src/import/tui/steps/PreviewValidationErrorStep.js +10 -1
- package/dist/src/lib/error-parser.d.ts +38 -0
- package/dist/src/lib/error-parser.js +264 -0
- package/package.json +3 -3
- package/dist/src/apply/error-parser.d.ts +0 -16
- package/dist/src/apply/error-parser.js +0 -117
package/dist/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 '../../
|
|
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
|
|
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
|
|
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
|
|
38
|
-
"@contentful/experience-design-system-types": "2.15.2
|
|
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
|
-
}
|