@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
- throw new Error(response.data.message);
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 = { name: 'Test Layout' };
96
- const mockResponse = { name: 'Test Layout', version: '12345' };
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 "Test Layout" version 12345 deployed successfully.');
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 = { name: 'Test Layout' };
111
- const mockResponse = { name: 'Test Layout', version: '12345' };
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 "Test Layout" dry run successful.');
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/dxp-cli-next",
3
- "version": "5.29.1",
3
+ "version": "5.30.0-develop.2",
4
4
  "repository": {
5
5
  "url": "https://gitlab.squiz.net/dxp/dxp-cli-next"
6
6
  },