@squiz/dxp-cli-next 5.29.1 → 5.30.0-develop.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.
|
@@ -35,7 +35,7 @@ const createDeployCommand = () => {
|
|
|
35
35
|
.addOption(new commander_1.Option('-t, --tenant <string>', 'Tenant ID to deploy to. If not provided will use configured tenant from login').env('SQUIZ_DXP_TENANT_ID'))
|
|
36
36
|
.addOption(new commander_1.Option('--dry-run', 'Run all pre-deployment processes without deploying').default(false))
|
|
37
37
|
.action((options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
|
-
var _a, _b, _c, _d;
|
|
38
|
+
var _a, _b, _c, _d, _e;
|
|
39
39
|
if (options.contentServiceUrl) {
|
|
40
40
|
console.log(`NOTICE: CONTENT_SERVICE_URL is set and will deploy to ${options.contentServiceUrl}`);
|
|
41
41
|
}
|
|
@@ -51,9 +51,17 @@ const createDeployCommand = () => {
|
|
|
51
51
|
try {
|
|
52
52
|
const layout = yield (0, definitions_1.loadLayoutDefinition)(layoutFile);
|
|
53
53
|
if (layout !== undefined) {
|
|
54
|
+
// Pre-deployment validation: check zones match between YAML and template
|
|
55
|
+
const zoneValidationError = validateZoneConsistency(layout);
|
|
56
|
+
if (zoneValidationError) {
|
|
57
|
+
throw new Error(zoneValidationError);
|
|
58
|
+
}
|
|
54
59
|
const response = yield uploadLayout(apiService.client, layout, contentServiceUrl, options.dryRun);
|
|
55
60
|
if (!options.dryRun) {
|
|
56
61
|
exports.logger.info(`Layout "${layout.name}" version ${response.data.version} deployed successfully.`);
|
|
62
|
+
// Log the deployed layout URL (clickable)
|
|
63
|
+
const layoutUrl = `${baseUrl}/organization/${(_e = options.tenant) !== null && _e !== void 0 ? _e : maybeConfig === null || maybeConfig === void 0 ? void 0 : maybeConfig.tenant}/component-service/all-layouts/${encodeURIComponent(layout.name)}`;
|
|
64
|
+
exports.logger.info(`Deployed layout URL: \u001b]8;;${layoutUrl}\u001b\\${layoutUrl}\u001b]8;;\u001b\\`);
|
|
57
65
|
}
|
|
58
66
|
else {
|
|
59
67
|
exports.logger.info(`Layout "${layout.name}" dry run successful.`);
|
|
@@ -82,6 +90,7 @@ const createDeployCommand = () => {
|
|
|
82
90
|
};
|
|
83
91
|
exports.default = createDeployCommand;
|
|
84
92
|
function uploadLayout(client, layout, contentServiceUrl, dryRun) {
|
|
93
|
+
var _a, _b;
|
|
85
94
|
return __awaiter(this, void 0, void 0, function* () {
|
|
86
95
|
try {
|
|
87
96
|
const queryParam = dryRun ? '?_dryRun=true' : '';
|
|
@@ -91,7 +100,11 @@ function uploadLayout(client, layout, contentServiceUrl, dryRun) {
|
|
|
91
100
|
if (response.status === 200) {
|
|
92
101
|
return response;
|
|
93
102
|
}
|
|
94
|
-
|
|
103
|
+
const error = new Error(response.data.message);
|
|
104
|
+
if ((_b = (_a = response.data.details) === null || _a === void 0 ? void 0 : _a.input) === null || _b === void 0 ? void 0 : _b.message) {
|
|
105
|
+
error.message += `: ${response.data.details.input.message}`;
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
95
108
|
}
|
|
96
109
|
catch (error) {
|
|
97
110
|
throw error;
|
|
@@ -106,3 +119,48 @@ function maybeGetApplicationConfig() {
|
|
|
106
119
|
catch (_a) { }
|
|
107
120
|
});
|
|
108
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Validates that zones defined in YAML match zones used in the Handlebars template
|
|
124
|
+
* @param layout The layout definition containing zones and template
|
|
125
|
+
* @returns Error message if validation fails, null if validation passes
|
|
126
|
+
*/
|
|
127
|
+
function validateZoneConsistency(layout) {
|
|
128
|
+
const yamlZones = Object.keys(layout.zones || {});
|
|
129
|
+
const template = layout.template;
|
|
130
|
+
// If no template is provided, skip validation
|
|
131
|
+
if (!template) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
// Extract zone references from Handlebars template
|
|
135
|
+
// Look for patterns like {{#each zones.zoneName}}, {{zones.zoneName}}, {{#if zones.zoneName}}
|
|
136
|
+
const zonePattern = /\{\{(?:#(?:each|if)\s+)?zones\.(\w+)/g;
|
|
137
|
+
const templateZones = new Set();
|
|
138
|
+
let match;
|
|
139
|
+
// Loop through the template and extract the zone names
|
|
140
|
+
while ((match = zonePattern.exec(template)) !== null) {
|
|
141
|
+
templateZones.add(match[1]);
|
|
142
|
+
}
|
|
143
|
+
// Convert the set to an array
|
|
144
|
+
const templateZoneArray = Array.from(templateZones);
|
|
145
|
+
// Find zones defined in YAML but not used in template
|
|
146
|
+
const unusedYamlZones = yamlZones.filter(zone => !templateZones.has(zone));
|
|
147
|
+
// Find zones used in template but not defined in YAML
|
|
148
|
+
const undefinedTemplateZones = templateZoneArray.filter(zone => !yamlZones.includes(zone));
|
|
149
|
+
// Create an array of errors
|
|
150
|
+
const errors = [];
|
|
151
|
+
// Add the unused zones to the errors
|
|
152
|
+
if (unusedYamlZones.length > 0) {
|
|
153
|
+
errors.push(`Zones defined in YAML but not used in template: ${unusedYamlZones.join(', ')}`);
|
|
154
|
+
}
|
|
155
|
+
// Add the undefined zones to the errors
|
|
156
|
+
if (undefinedTemplateZones.length > 0) {
|
|
157
|
+
errors.push(`Zones used in template but not defined in YAML: ${undefinedTemplateZones.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
// If there are errors, return the errors
|
|
160
|
+
if (errors.length > 0) {
|
|
161
|
+
return `Zone consistency validation failed:\n${errors
|
|
162
|
+
.map(err => ` - ${err}`)
|
|
163
|
+
.join('\n')}`;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
@@ -92,8 +92,14 @@ describe('deployCommand', () => {
|
|
|
92
92
|
it('deploys a layout successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
93
93
|
const file = './src/__tests__/layouts/page-layout.yaml';
|
|
94
94
|
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
95
|
-
const mockLayout = {
|
|
96
|
-
|
|
95
|
+
const mockLayout = {
|
|
96
|
+
name: 'test-layout',
|
|
97
|
+
zones: {
|
|
98
|
+
content: { displayName: 'Content', description: 'Main content' },
|
|
99
|
+
},
|
|
100
|
+
template: '<div>{{zones.content}}</div>',
|
|
101
|
+
};
|
|
102
|
+
const mockResponse = { name: 'test-layout', version: '12345' };
|
|
97
103
|
definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
|
|
98
104
|
(0, nock_1.default)(dxpBaseUrl + '/__dxp/service/components-content')
|
|
99
105
|
.post('/page-layout', mockLayout)
|
|
@@ -101,14 +107,21 @@ describe('deployCommand', () => {
|
|
|
101
107
|
const program = (0, deploy_1.default)();
|
|
102
108
|
yield program.parseAsync(createMockArgs({ config: file, dxpBaseUrl }));
|
|
103
109
|
expect(mockLoggerInfoFn).toHaveBeenNthCalledWith(1, `Loading layout data from the file ${file}`);
|
|
104
|
-
expect(mockLoggerInfoFn).toHaveBeenNthCalledWith(2, 'Layout "
|
|
110
|
+
expect(mockLoggerInfoFn).toHaveBeenNthCalledWith(2, 'Layout "test-layout" version 12345 deployed successfully.');
|
|
111
|
+
expect(mockLoggerInfoFn).toHaveBeenNthCalledWith(3, 'Deployed layout URL: \u001b]8;;http://dxp-base-url.com/organization/myTenant/component-service/all-layouts/test-layout\u001b\\http://dxp-base-url.com/organization/myTenant/component-service/all-layouts/test-layout\u001b]8;;\u001b\\');
|
|
105
112
|
}));
|
|
106
113
|
it('deploys a layout with dry-run option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
114
|
const file = './src/__tests__/layout.yaml';
|
|
108
115
|
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
109
116
|
const dryRun = true;
|
|
110
|
-
const mockLayout = {
|
|
111
|
-
|
|
117
|
+
const mockLayout = {
|
|
118
|
+
name: 'test-layout',
|
|
119
|
+
zones: {
|
|
120
|
+
content: { displayName: 'Content', description: 'Main content' },
|
|
121
|
+
},
|
|
122
|
+
template: '<div>{{zones.content}}</div>',
|
|
123
|
+
};
|
|
124
|
+
const mockResponse = { name: 'test-layout', version: '12345' };
|
|
112
125
|
definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
|
|
113
126
|
(0, nock_1.default)(dxpBaseUrl + '/__dxp/service/components-content')
|
|
114
127
|
.post('/page-layout', mockLayout)
|
|
@@ -117,7 +130,7 @@ describe('deployCommand', () => {
|
|
|
117
130
|
const program = (0, deploy_1.default)();
|
|
118
131
|
yield program.parseAsync(createMockArgs({ config: file, dxpBaseUrl, dryRun }));
|
|
119
132
|
expect(mockLoggerInfoFn).toHaveBeenNthCalledWith(1, `Loading layout data from the file ${file}`);
|
|
120
|
-
expect(mockLoggerInfoFn).toHaveBeenNthCalledWith(2, 'Layout "
|
|
133
|
+
expect(mockLoggerInfoFn).toHaveBeenNthCalledWith(2, 'Layout "test-layout" dry run successful.');
|
|
121
134
|
}));
|
|
122
135
|
it('handles InvalidLoginSessionError', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
136
|
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
@@ -159,4 +172,57 @@ describe('deployCommand', () => {
|
|
|
159
172
|
yield program.parseAsync(createMockArgs({ contentServiceUrl }));
|
|
160
173
|
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('An unknown error occurred'));
|
|
161
174
|
}));
|
|
175
|
+
it('should log additional details when layout validation fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
176
|
+
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
177
|
+
(0, nock_1.default)(dxpBaseUrl + '/__dxp/service/components-content')
|
|
178
|
+
.post('/page-layout')
|
|
179
|
+
.reply(400, {
|
|
180
|
+
message: 'Layout validation failed',
|
|
181
|
+
details: {
|
|
182
|
+
input: {
|
|
183
|
+
message: 'ERROR: Validation failed: "version" is an excess property and therefore is not allowed',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const program = (0, deploy_1.default)();
|
|
188
|
+
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
189
|
+
yield program.parseAsync(createMockArgs({ dxpBaseUrl }));
|
|
190
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Layout validation failed: ERROR: Validation failed: "version" is an excess property and therefore is not allowed'));
|
|
191
|
+
}));
|
|
192
|
+
describe('zone consistency validation', () => {
|
|
193
|
+
it('should handle zone consistency validation errors where zones are used but not defined in the layout', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
194
|
+
const file = './src/__tests__/layout.yaml';
|
|
195
|
+
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
196
|
+
// Mock layout with zones that don't match the template
|
|
197
|
+
const mockLayout = {
|
|
198
|
+
name: 'test-layout',
|
|
199
|
+
zones: {
|
|
200
|
+
col1: { displayName: 'Column 1', description: 'The first column' },
|
|
201
|
+
},
|
|
202
|
+
template: '{{zones.col1}} {{zones.col2}}', // col2 is used but not defined in zones
|
|
203
|
+
};
|
|
204
|
+
definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
|
|
205
|
+
const program = (0, deploy_1.default)();
|
|
206
|
+
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
207
|
+
yield program.parseAsync(createMockArgs({ config: file, dxpBaseUrl }));
|
|
208
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones used in template but not defined in YAML: col2/));
|
|
209
|
+
}));
|
|
210
|
+
it('should handle zone consistency validation errors where zones are defined but not used in the template', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
211
|
+
const file = './src/__tests__/layout.yaml';
|
|
212
|
+
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
213
|
+
const mockLayout = {
|
|
214
|
+
name: 'test-layout',
|
|
215
|
+
zones: {
|
|
216
|
+
col1: { displayName: 'Column 1', description: 'The first column' },
|
|
217
|
+
col2: { displayName: 'Column 2', description: 'The second column' }, // col2 is defined but not used in the template
|
|
218
|
+
},
|
|
219
|
+
template: '{{zones.col1}}',
|
|
220
|
+
};
|
|
221
|
+
definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
|
|
222
|
+
const program = (0, deploy_1.default)();
|
|
223
|
+
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
224
|
+
yield program.parseAsync(createMockArgs({ config: file, dxpBaseUrl }));
|
|
225
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones defined in YAML but not used in template: col2/));
|
|
226
|
+
}));
|
|
227
|
+
});
|
|
162
228
|
});
|