@squiz/dxp-cli-next 5.31.0-develop.5 → 5.31.0-develop.6

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.
@@ -56,7 +56,7 @@ const createDeployCommand = () => {
56
56
  try {
57
57
  const layout = yield (0, definitions_1.loadLayoutDefinition)(layoutFile);
58
58
  if (layout !== undefined) {
59
- // Pre-deployment validation: check zones match between YAML and template
59
+ // Pre-deployment validation: check zones match between manifest and template
60
60
  const zoneValidationError = (0, validation_1.validateZoneConsistency)(layout);
61
61
  if (zoneValidationError) {
62
62
  throw new Error(zoneValidationError);
@@ -34,7 +34,7 @@ const createDevCommand = () => {
34
34
  .option('--stylesheet <path>', 'Path to CSS file to include')
35
35
  .option('--zones <items>', 'Zone content mappings', parse_args_1.parseZonesList, {})
36
36
  .option('--properties <items>', 'Layout properties', parse_args_1.parsePropertiesList, {})
37
- .allowUnknownOption(true)
37
+ .allowUnknownOption(false)
38
38
  .allowExcessArguments(true)
39
39
  .action((options) => __awaiter(void 0, void 0, void 0, function* () {
40
40
  try {
@@ -45,6 +45,16 @@ const createDevCommand = () => {
45
45
  throw new Error(`Failed to load layout definition from ${options.config}`);
46
46
  }
47
47
  (0, validation_1.validateLayoutFormat)(exports.logger, options.config);
48
+ // Check zones match between manifest and template
49
+ const zoneValidationError = (0, validation_1.validateZoneConsistency)(rawLayoutDefinition);
50
+ if (zoneValidationError) {
51
+ throw new Error(zoneValidationError);
52
+ }
53
+ // Validate Handlebars template only refers to properties if manifest.json is used
54
+ const propertyValidationError = (0, validation_1.validatePropertyConsistency)(rawLayoutDefinition, options.config);
55
+ if (propertyValidationError) {
56
+ throw new Error(propertyValidationError);
57
+ }
48
58
  // Confirm for entry property
49
59
  const layoutDefinition = Object.assign({}, rawLayoutDefinition);
50
60
  // Normalize properties to convert string booleans to actual booleans
@@ -186,4 +186,86 @@ describe('devCommand', () => {
186
186
  yield program.parseAsync(args);
187
187
  expect(mockLoggerErrorFn).toHaveBeenCalledWith(expect.stringContaining('Server start failed'));
188
188
  }));
189
+ it('should not allow unknown options (e.g. --options instead of --properties)', () => __awaiter(void 0, void 0, void 0, function* () {
190
+ const config = './src/__tests__/layout.yaml';
191
+ const program = (0, dev_1.default)();
192
+ const args = createMockArgs({ config });
193
+ args.push('--options=backgroundColor=red');
194
+ process.argv = args;
195
+ const errorSpy = jest.spyOn(program, 'error').mockImplementation();
196
+ yield program.parseAsync(args);
197
+ expect(errorSpy).toHaveBeenCalledWith("error: unknown option '--options=backgroundColor=red'", { code: 'commander.unknownOption' });
198
+ }));
199
+ describe('zone consistency validation', () => {
200
+ 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* () {
201
+ const config = './src/__tests__/layout.yaml';
202
+ // Mock layout with zones that don't match the template
203
+ const mockLayout = {
204
+ name: 'test-layout',
205
+ zones: [
206
+ {
207
+ key: 'col1',
208
+ displayName: 'Column 1',
209
+ description: 'The first column',
210
+ },
211
+ ],
212
+ template: '{{zones.col1}} {{zones.col2}}', // col2 is used but not defined in zones
213
+ };
214
+ definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
215
+ const program = (0, dev_1.default)();
216
+ const args = createMockArgs({ config });
217
+ process.argv = args;
218
+ yield program.parseAsync(args);
219
+ expect(mockLoggerErrorFn).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones used in template but not defined in layout definition: col2/));
220
+ }));
221
+ 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* () {
222
+ const config = './src/__tests__/layout.yaml';
223
+ const mockLayout = {
224
+ name: 'test-layout',
225
+ zones: [
226
+ {
227
+ key: 'col1',
228
+ displayName: 'Column 1',
229
+ description: 'The first column',
230
+ },
231
+ {
232
+ key: 'col2',
233
+ displayName: 'Column 2',
234
+ description: 'The second column',
235
+ }, // col2 is defined but not used in the template
236
+ ],
237
+ template: '{{zones.col1}}',
238
+ };
239
+ definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
240
+ const program = (0, dev_1.default)();
241
+ const args = createMockArgs({ config });
242
+ process.argv = args;
243
+ yield program.parseAsync(args);
244
+ expect(mockLoggerErrorFn).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones defined in layout definition but not used in template: col2/));
245
+ }));
246
+ });
247
+ describe('property consistency validation', () => {
248
+ it('should handle property consistency validation errors where properties are used but not defined in the layout', () => __awaiter(void 0, void 0, void 0, function* () {
249
+ const config = './src/__tests__/manifest.json';
250
+ // Mock layout with properties that don't match the template
251
+ const mockLayout = {
252
+ name: 'test-layout',
253
+ zones: [],
254
+ properties: {
255
+ title: {
256
+ type: 'string',
257
+ title: 'Title',
258
+ description: 'Page title',
259
+ },
260
+ },
261
+ template: '{{properties.title}} {{properties.undefined}}', // undefined is used but not defined in properties
262
+ };
263
+ definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
264
+ const program = (0, dev_1.default)();
265
+ const args = createMockArgs({ config });
266
+ process.argv = args;
267
+ yield program.parseAsync(args);
268
+ expect(mockLoggerErrorFn).toHaveBeenCalledWith(expect.stringMatching(/Property consistency validation failed[\s\S]*Properties used in template but not defined in layout definition: undefined/));
269
+ }));
270
+ });
189
271
  });
@@ -11,7 +11,8 @@ function validateLayoutFormat(logger, layoutFile) {
11
11
  if (!layoutFile) {
12
12
  logger.warn(`⚠️ DEFAULT CONFIG FILE: "${definitions_1.LAYOUT_MANIFEST_FILE}" has replaced "page-layout.yaml" as the default config file. ` +
13
13
  `Please migrate to "${definitions_1.LAYOUT_MANIFEST_FILE}" using the new layout definition format. ` +
14
- 'Support for "page-layout.yaml" and any other file format will be removed in a future version. ');
14
+ `Support for files other than "${definitions_1.LAYOUT_MANIFEST_FILE}" will be removed in a future version. ` +
15
+ 'For more information, visit https://docs.squiz.net/component-service/latest/layouts/layout-files.html');
15
16
  return;
16
17
  }
17
18
  // Warn that any file other than manifest.json is deprecated
@@ -19,7 +20,8 @@ function validateLayoutFormat(logger, layoutFile) {
19
20
  if (fileName !== definitions_1.LAYOUT_MANIFEST_FILE) {
20
21
  logger.warn(`⚠️ DEPRECATED: The file name "${fileName}" is deprecated. ` +
21
22
  `Please migrate to "${definitions_1.LAYOUT_MANIFEST_FILE}" using the new layout definition format. ` +
22
- 'Support for any other file format will be removed in a future version. ');
23
+ `Support for files other than "${definitions_1.LAYOUT_MANIFEST_FILE}" will be removed in a future version. ` +
24
+ 'For more information, visit https://docs.squiz.net/component-service/latest/layouts/layout-files.html');
23
25
  }
24
26
  }
25
27
  exports.validateLayoutFormat = validateLayoutFormat;
@@ -23,7 +23,8 @@ describe('validateLayoutFormat', () => {
23
23
  expect(mockLogger.warn).toHaveBeenCalledTimes(1);
24
24
  expect(mockLogger.warn).toHaveBeenCalledWith('⚠️ DEPRECATED: The file name "page-layout.yaml" is deprecated. ' +
25
25
  `Please migrate to "${definitions_1.LAYOUT_MANIFEST_FILE}" using the new layout definition format. ` +
26
- 'Support for any other file format will be removed in a future version. ');
26
+ `Support for files other than "${definitions_1.LAYOUT_MANIFEST_FILE}" will be removed in a future version. ` +
27
+ 'For more information, visit https://docs.squiz.net/component-service/latest/layouts/layout-files.html');
27
28
  });
28
29
  it('should extract filename correctly from full path', () => {
29
30
  const layoutFile = '/very/deep/nested/path/to/layout/page-layout.yaml';
@@ -35,6 +36,7 @@ describe('validateLayoutFormat', () => {
35
36
  expect(mockLogger.warn).toHaveBeenCalledTimes(1);
36
37
  expect(mockLogger.warn).toHaveBeenCalledWith('⚠️ DEFAULT CONFIG FILE: "manifest.json" has replaced "page-layout.yaml" as the default config file. ' +
37
38
  `Please migrate to "${definitions_1.LAYOUT_MANIFEST_FILE}" using the new layout definition format. ` +
38
- 'Support for "page-layout.yaml" and any other file format will be removed in a future version. ');
39
+ `Support for files other than "${definitions_1.LAYOUT_MANIFEST_FILE}" will be removed in a future version. ` +
40
+ 'For more information, visit https://docs.squiz.net/component-service/latest/layouts/layout-files.html');
39
41
  });
40
42
  });
@@ -89,7 +89,7 @@ export declare const InputLayoutDefinitionV1: z.ZodObject<z.objectUtil.extendSha
89
89
  values?: string[] | undefined;
90
90
  valueType?: "boolean" | "text" | "string-enum" | undefined;
91
91
  }>>>;
92
- }>, "strip", z.ZodTypeAny, {
92
+ }>, "strict", z.ZodTypeAny, {
93
93
  name: string;
94
94
  entry: string;
95
95
  description: string;
@@ -300,7 +300,7 @@ export declare const InputLayoutDefinition: z.ZodUnion<[z.ZodObject<z.objectUtil
300
300
  values?: string[] | undefined;
301
301
  valueType?: "boolean" | "text" | "string-enum" | undefined;
302
302
  }>>>;
303
- }>, "strip", z.ZodTypeAny, {
303
+ }>, "strict", z.ZodTypeAny, {
304
304
  name: string;
305
305
  entry: string;
306
306
  description: string;
@@ -183,7 +183,7 @@ exports.InputLayoutDefinitionV1 = exports.BaseLayoutDefinition.extend({
183
183
  values: zod_1.z.array(zod_1.z.string()).optional(),
184
184
  }))
185
185
  .optional(),
186
- });
186
+ }).strict();
187
187
  /**
188
188
  * Layout definition used for manifest.json files
189
189
  */
@@ -317,6 +317,40 @@ entry: template.hbs
317
317
 
318
318
  Schema validation failed:
319
319
  - Required at "name""
320
+ `);
321
+ }));
322
+ it('should throw an error if YAML contains properties (instead of options)', () => __awaiter(void 0, void 0, void 0, function* () {
323
+ const yamlContentWithProperties = `
324
+ name: test-layout
325
+ displayName: Test Layout
326
+ description: A test layout
327
+ zones:
328
+ main:
329
+ displayName: Main Zone
330
+ description: Main content area
331
+ properties:
332
+ color:
333
+ title: Color
334
+ description: Color options
335
+ type: string
336
+ enum: ['red', 'blue']
337
+ entry: template.hbs
338
+ `;
339
+ fs.readFile.mockImplementation((filePath) => {
340
+ if (filePath.endsWith('page-layout.yaml')) {
341
+ return yamlContentWithProperties;
342
+ }
343
+ if (filePath.endsWith('template.hbs')) {
344
+ return templateContent;
345
+ }
346
+ throw new Error('File not found');
347
+ });
348
+ yield expect((0, definitions_1.loadLayoutDefinition)(pageLayoutYaml)).rejects
349
+ .toThrowErrorMatchingInlineSnapshot(`
350
+ "Failed loading layout definition: Failed to parse ./some-dir/page-layout.yaml:
351
+
352
+ Schema validation failed:
353
+ - Unrecognized key(s) in object: 'properties'"
320
354
  `);
321
355
  }));
322
356
  it('should throw an error if YAML layout file has invalid YAML', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -548,6 +582,28 @@ describe('LayoutDefinitionParse', () => {
548
582
  expect((_g = layoutDefinition.options) === null || _g === void 0 ? void 0 : _g.withNeither.valueType).toBeUndefined();
549
583
  expect((_h = layoutDefinition.options) === null || _h === void 0 ? void 0 : _h.withNeither.values).toBeUndefined();
550
584
  });
585
+ it('should reject properties (instead of options)', () => {
586
+ expect(() => definitions_1.InputLayoutDefinitionV1.parse({
587
+ name: 'test-layout',
588
+ displayName: 'Test Layout',
589
+ description: 'A test layout',
590
+ entry: 'template.hbs',
591
+ zones: {
592
+ main: {
593
+ displayName: 'Main Zone',
594
+ description: 'Main content area',
595
+ },
596
+ },
597
+ properties: {
598
+ color: {
599
+ title: 'Color',
600
+ description: 'Color options',
601
+ type: 'string',
602
+ enum: ['red', 'blue'],
603
+ },
604
+ },
605
+ })).toThrow();
606
+ });
551
607
  });
552
608
  });
553
609
  describe('V2 format - zones arrays and properties', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/dxp-cli-next",
3
- "version": "5.31.0-develop.5",
3
+ "version": "5.31.0-develop.6",
4
4
  "repository": {
5
5
  "url": "https://gitlab.squiz.net/dxp/dxp-cli-next"
6
6
  },