@squiz/dxp-cli-next 5.31.0-develop.4 → 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.
Files changed (34) hide show
  1. package/lib/page/layouts/deploy/deploy.js +44 -9
  2. package/lib/page/layouts/deploy/deploy.spec.js +110 -19
  3. package/lib/page/layouts/dev/dev.js +18 -4
  4. package/lib/page/layouts/dev/dev.spec.js +117 -8
  5. package/lib/page/layouts/validation/index.d.ts +2 -0
  6. package/lib/page/layouts/validation/index.js +5 -1
  7. package/lib/page/layouts/validation/property-consistency.d.ts +7 -0
  8. package/lib/page/layouts/validation/property-consistency.js +92 -0
  9. package/lib/page/layouts/validation/property-consistency.spec.d.ts +1 -0
  10. package/lib/page/layouts/validation/property-consistency.spec.js +305 -0
  11. package/lib/page/layouts/validation/validateLayoutFormat.d.ts +2 -0
  12. package/lib/page/layouts/validation/validateLayoutFormat.js +27 -0
  13. package/lib/page/layouts/validation/validateLayoutFormat.spec.d.ts +1 -0
  14. package/lib/page/layouts/validation/validateLayoutFormat.spec.js +42 -0
  15. package/lib/page/layouts/validation/zone-consistency.d.ts +1 -1
  16. package/lib/page/layouts/validation/zone-consistency.js +10 -9
  17. package/lib/page/layouts/validation/zone-consistency.spec.js +32 -34
  18. package/lib/page/utils/definitions.d.ts +347 -50
  19. package/lib/page/utils/definitions.js +103 -22
  20. package/lib/page/utils/definitions.spec.js +516 -267
  21. package/lib/page/utils/normalize.d.ts +8 -0
  22. package/lib/page/utils/normalize.js +61 -0
  23. package/lib/page/utils/normalize.spec.d.ts +1 -0
  24. package/lib/page/utils/normalize.spec.js +315 -0
  25. package/lib/page/utils/parse-args.d.ts +20 -4
  26. package/lib/page/utils/parse-args.js +48 -13
  27. package/lib/page/utils/parse-args.spec.js +159 -21
  28. package/lib/page/utils/render.d.ts +27 -9
  29. package/lib/page/utils/render.js +66 -12
  30. package/lib/page/utils/render.spec.js +14 -14
  31. package/lib/page/utils/server.d.ts +1 -1
  32. package/lib/page/utils/server.js +2 -2
  33. package/lib/page/utils/server.spec.js +13 -13
  34. package/package.json +1 -1
@@ -28,7 +28,7 @@ exports.logger = (0, dx_logger_lib_1.getLogger)({
28
28
  const createDeployCommand = () => {
29
29
  const deployCommand = new commander_1.Command()
30
30
  .name('deploy')
31
- .addOption(new commander_1.Option('--config <string>', 'File path to the page layout config file').default('./page-layout.yaml'))
31
+ .addOption(new commander_1.Option('--config <string>', 'File path to the page layout config file'))
32
32
  .addOption(new commander_1.Option('-cu, --content-service-url <string>', 'Override the content service url from login')
33
33
  .env('CONTENT_SERVICE_URL')
34
34
  .hideHelp(true))
@@ -47,16 +47,25 @@ const createDeployCommand = () => {
47
47
  tenantId: (_c = options.tenant) !== null && _c !== void 0 ? _c : maybeConfig === null || maybeConfig === void 0 ? void 0 : maybeConfig.tenant,
48
48
  });
49
49
  const contentServiceUrl = (_d = options.contentServiceUrl) !== null && _d !== void 0 ? _d : new URL('/__dxp/service/components-content', baseUrl).toString();
50
- const layoutFile = options.config;
50
+ let layoutFile = options.config;
51
+ (0, validation_1.validateLayoutFormat)(exports.logger, layoutFile);
52
+ if (!layoutFile) {
53
+ layoutFile = `./${definitions_1.LAYOUT_MANIFEST_FILE}`;
54
+ }
51
55
  exports.logger.info(`Loading layout data from the file ${layoutFile}`);
52
56
  try {
53
57
  const layout = yield (0, definitions_1.loadLayoutDefinition)(layoutFile);
54
58
  if (layout !== undefined) {
55
- // Pre-deployment validation: check zones match between YAML and template
59
+ // Pre-deployment validation: check zones match between manifest and template
56
60
  const zoneValidationError = (0, validation_1.validateZoneConsistency)(layout);
57
61
  if (zoneValidationError) {
58
62
  throw new Error(zoneValidationError);
59
63
  }
64
+ // Validate Handlebars template only refers to properties if manifest.json is used
65
+ const propertyValidationError = (0, validation_1.validatePropertyConsistency)(layout, layoutFile);
66
+ if (propertyValidationError) {
67
+ throw new Error(propertyValidationError);
68
+ }
60
69
  const response = yield uploadLayout(apiService.client, layout, contentServiceUrl, options.dryRun);
61
70
  if (!options.dryRun) {
62
71
  exports.logger.info(`Layout "${layout.name}" version ${response.data.version} deployed successfully.`);
@@ -90,8 +99,34 @@ const createDeployCommand = () => {
90
99
  return deployCommand;
91
100
  };
92
101
  exports.default = createDeployCommand;
102
+ /**
103
+ * Formats backend error responses into user-friendly error messages.
104
+ * Handles both validation errors (400s) and unknown server errors (500s).
105
+ *
106
+ * @param data - The error response data from the backend
107
+ * @param statusCode - The HTTP status code (optional)
108
+ * @returns Formatted error message string
109
+ */
110
+ function formatErrorResponse(data, statusCode) {
111
+ // For 500 errors or unknown errors, show a generic message
112
+ if (statusCode && statusCode >= 500) {
113
+ return data.message || data.data || 'An unknown error occurred';
114
+ }
115
+ // For validation errors (400s), format with details
116
+ let errorMessage = data.message || 'Validation failed';
117
+ // Properties/options validation errors come as an object with field keys
118
+ if (data.details &&
119
+ typeof data.details === 'object' &&
120
+ !Array.isArray(data.details)) {
121
+ const details = Object.entries(data.details)
122
+ .map(([field, error]) => ` - ${field}: ${error.message}`)
123
+ .join('\n');
124
+ errorMessage += `\n${details}`;
125
+ }
126
+ return errorMessage;
127
+ }
93
128
  function uploadLayout(client, layout, contentServiceUrl, dryRun) {
94
- var _a, _b;
129
+ var _a;
95
130
  return __awaiter(this, void 0, void 0, function* () {
96
131
  try {
97
132
  const queryParam = dryRun ? '?_dryRun=true' : '';
@@ -101,13 +136,13 @@ function uploadLayout(client, layout, contentServiceUrl, dryRun) {
101
136
  if (response.status === 200) {
102
137
  return response;
103
138
  }
104
- const error = new Error(response.data.message);
105
- if ((_b = (_a = response.data.details) === null || _a === void 0 ? void 0 : _a.input) === null || _b === void 0 ? void 0 : _b.message) {
106
- error.message += `: ${response.data.details.input.message}`;
107
- }
108
- throw error;
139
+ throw new Error(formatErrorResponse(response.data, response.status));
109
140
  }
110
141
  catch (error) {
142
+ // Extract error details from axios error response
143
+ if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
144
+ throw new Error(formatErrorResponse(error.response.data, error.response.status));
145
+ }
111
146
  throw error;
112
147
  }
113
148
  });
@@ -17,10 +17,12 @@ jest.mock('@squiz/dx-logger-lib', () => ({
17
17
  getLogger: () => {
18
18
  return {
19
19
  info: mockLoggerInfoFn,
20
+ warn: mockLoggerWarnFn,
20
21
  };
21
22
  },
22
23
  }));
23
24
  const mockLoggerInfoFn = jest.fn();
25
+ const mockLoggerWarnFn = jest.fn();
24
26
  const nock_1 = __importDefault(require("nock"));
25
27
  const deploy_1 = __importDefault(require("./deploy"));
26
28
  const definitions_1 = require("../../utils/definitions");
@@ -58,7 +60,7 @@ describe('deployCommand', () => {
58
60
  const program = (0, deploy_1.default)();
59
61
  yield program.parseAsync(createMockArgs({}));
60
62
  const opts = program.opts();
61
- expect(opts.config).toEqual('./page-layout.yaml');
63
+ expect(opts.config).toBeUndefined();
62
64
  expect(opts.tenant).toEqual('myTenant');
63
65
  expect(opts.dryRun).toEqual(false);
64
66
  }));
@@ -90,13 +92,17 @@ describe('deployCommand', () => {
90
92
  expect(logSpy).toHaveBeenCalledWith(`NOTICE: CONTENT_SERVICE_URL is set and will deploy to ${contentServiceUrl}`);
91
93
  }));
92
94
  it('deploys a layout successfully', () => __awaiter(void 0, void 0, void 0, function* () {
93
- const file = './src/__tests__/layouts/page-layout.yaml';
95
+ const file = `./src/__tests__/layouts/${definitions_1.LAYOUT_MANIFEST_FILE}`;
94
96
  const dxpBaseUrl = 'http://dxp-base-url.com';
95
97
  const mockLayout = {
96
98
  name: 'test-layout',
97
- zones: {
98
- content: { displayName: 'Content', description: 'Main content' },
99
- },
99
+ zones: [
100
+ {
101
+ key: 'content',
102
+ displayName: 'Content',
103
+ description: 'Main content',
104
+ },
105
+ ],
100
106
  template: '<div>{{zones.content}}</div>',
101
107
  };
102
108
  const mockResponse = { name: 'test-layout', version: '12345' };
@@ -116,9 +122,13 @@ describe('deployCommand', () => {
116
122
  const dryRun = true;
117
123
  const mockLayout = {
118
124
  name: 'test-layout',
119
- zones: {
120
- content: { displayName: 'Content', description: 'Main content' },
121
- },
125
+ zones: [
126
+ {
127
+ key: 'content',
128
+ displayName: 'Content',
129
+ description: 'Main content',
130
+ },
131
+ ],
122
132
  template: '<div>{{zones.content}}</div>',
123
133
  };
124
134
  const mockResponse = { name: 'test-layout', version: '12345' };
@@ -170,7 +180,7 @@ describe('deployCommand', () => {
170
180
  const program = (0, deploy_1.default)();
171
181
  errorSpy = jest.spyOn(program, 'error').mockImplementation();
172
182
  yield program.parseAsync(createMockArgs({ contentServiceUrl }));
173
- expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('An unknown error occurred'));
183
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Internal server error'));
174
184
  }));
175
185
  it('should log additional details when layout validation fails', () => __awaiter(void 0, void 0, void 0, function* () {
176
186
  const dxpBaseUrl = 'http://dxp-base-url.com';
@@ -187,8 +197,53 @@ describe('deployCommand', () => {
187
197
  const program = (0, deploy_1.default)();
188
198
  errorSpy = jest.spyOn(program, 'error').mockImplementation();
189
199
  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'));
200
+ const errorCall = errorSpy.mock.calls[0][0];
201
+ expect(errorCall).toContain('- input: ERROR: Validation failed: "version" is an excess property and therefore is not allowed');
191
202
  }));
203
+ describe('backend property validation error reporting', () => {
204
+ it('should display clean error when single property validation fails', () => __awaiter(void 0, void 0, void 0, function* () {
205
+ const contentServiceUrl = 'http://localhost:9999';
206
+ (0, nock_1.default)(contentServiceUrl)
207
+ .post('/page-layout')
208
+ .reply(400, {
209
+ message: 'Validation failed',
210
+ details: {
211
+ 'input.properties.myBoolean': {
212
+ message: "Property 'myBoolean' with type 'boolean' can not have values specified",
213
+ },
214
+ },
215
+ });
216
+ const program = (0, deploy_1.default)();
217
+ errorSpy = jest.spyOn(program, 'error').mockImplementation();
218
+ yield program.parseAsync(createMockArgs({ contentServiceUrl }));
219
+ const errorCall = errorSpy.mock.calls[0][0];
220
+ expect(errorCall).toContain('Validation failed');
221
+ expect(errorCall).toContain("- input.properties.myBoolean: Property 'myBoolean' with type 'boolean' can not have values specified");
222
+ }));
223
+ it('should display multiple property validation errors cleanly', () => __awaiter(void 0, void 0, void 0, function* () {
224
+ const contentServiceUrl = 'http://localhost:9999';
225
+ (0, nock_1.default)(contentServiceUrl)
226
+ .post('/page-layout')
227
+ .reply(400, {
228
+ message: 'Validation failed',
229
+ details: {
230
+ 'input.properties.badBoolean': {
231
+ message: "Property 'badBoolean' with type 'boolean' cannot have enum values specified",
232
+ },
233
+ 'input.properties.badProperty.type': {
234
+ message: "Property 'badProperty' has invalid type 'INVALID'. Must be one of: string, boolean",
235
+ },
236
+ },
237
+ });
238
+ const program = (0, deploy_1.default)();
239
+ errorSpy = jest.spyOn(program, 'error').mockImplementation();
240
+ yield program.parseAsync(createMockArgs({ contentServiceUrl }));
241
+ const errorCall = errorSpy.mock.calls[0][0];
242
+ expect(errorCall).toContain('Validation failed');
243
+ expect(errorCall).toContain("- input.properties.badBoolean: Property 'badBoolean' with type 'boolean' cannot have enum values specified");
244
+ expect(errorCall).toContain("- input.properties.badProperty.type: Property 'badProperty' has invalid type 'INVALID'. Must be one of: string, boolean");
245
+ }));
246
+ });
192
247
  describe('zone consistency validation', () => {
193
248
  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
249
  const file = './src/__tests__/layout.yaml';
@@ -196,33 +251,69 @@ describe('deployCommand', () => {
196
251
  // Mock layout with zones that don't match the template
197
252
  const mockLayout = {
198
253
  name: 'test-layout',
199
- zones: {
200
- col1: { displayName: 'Column 1', description: 'The first column' },
201
- },
254
+ zones: [
255
+ {
256
+ key: 'col1',
257
+ displayName: 'Column 1',
258
+ description: 'The first column',
259
+ },
260
+ ],
202
261
  template: '{{zones.col1}} {{zones.col2}}', // col2 is used but not defined in zones
203
262
  };
204
263
  definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
205
264
  const program = (0, deploy_1.default)();
206
265
  errorSpy = jest.spyOn(program, 'error').mockImplementation();
207
266
  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/));
267
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones used in template but not defined in layout definition: col2/));
209
268
  }));
210
269
  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
270
  const file = './src/__tests__/layout.yaml';
212
271
  const dxpBaseUrl = 'http://dxp-base-url.com';
213
272
  const mockLayout = {
214
273
  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
- },
274
+ zones: [
275
+ {
276
+ key: 'col1',
277
+ displayName: 'Column 1',
278
+ description: 'The first column',
279
+ },
280
+ {
281
+ key: 'col2',
282
+ displayName: 'Column 2',
283
+ description: 'The second column',
284
+ }, // col2 is defined but not used in the template
285
+ ],
219
286
  template: '{{zones.col1}}',
220
287
  };
221
288
  definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
222
289
  const program = (0, deploy_1.default)();
223
290
  errorSpy = jest.spyOn(program, 'error').mockImplementation();
224
291
  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/));
292
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones defined in layout definition but not used in template: col2/));
293
+ }));
294
+ });
295
+ describe('property consistency validation', () => {
296
+ 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* () {
297
+ const file = './src/__tests__/manifest.json';
298
+ const dxpBaseUrl = 'http://dxp-base-url.com';
299
+ // Mock layout with properties that don't match the template
300
+ const mockLayout = {
301
+ name: 'test-layout',
302
+ zones: [],
303
+ properties: {
304
+ title: {
305
+ type: 'string',
306
+ title: 'Title',
307
+ description: 'Page title',
308
+ },
309
+ },
310
+ template: '{{properties.title}} {{properties.undefined}}', // undefined is used but not defined in properties
311
+ };
312
+ definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
313
+ const program = (0, deploy_1.default)();
314
+ errorSpy = jest.spyOn(program, 'error').mockImplementation();
315
+ yield program.parseAsync(createMockArgs({ config: file, dxpBaseUrl }));
316
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Property consistency validation failed[\s\S]*Properties used in template but not defined in layout definition: undefined/));
226
317
  }));
227
318
  });
228
319
  });
@@ -19,6 +19,7 @@ const definitions_1 = require("../../utils/definitions");
19
19
  const path_1 = __importDefault(require("path"));
20
20
  const server_1 = require("../../utils/server");
21
21
  const parse_args_1 = require("../../utils/parse-args");
22
+ const validation_1 = require("../validation");
22
23
  exports.logger = (0, dx_logger_lib_1.getLogger)({
23
24
  name: 'layout-dev',
24
25
  format: 'human',
@@ -27,13 +28,13 @@ const createDevCommand = () => {
27
28
  const devCommand = new commander_1.Command()
28
29
  .name('dev')
29
30
  .description('Start a development server for page layouts')
30
- .option('--config <string>', 'File path to the page layout config file', './page-layout.yaml')
31
+ .option('--config <string>', 'File path to the page layout config file', `./${definitions_1.LAYOUT_MANIFEST_FILE}`)
31
32
  .option('--port <number>', 'Port to run the development server on', '4040')
32
33
  .option('--no-open', 'Do not automatically open browser')
33
34
  .option('--stylesheet <path>', 'Path to CSS file to include')
34
35
  .option('--zones <items>', 'Zone content mappings', parse_args_1.parseZonesList, {})
35
- .option('--options <items>', 'Layout options', parse_args_1.parseOptionsList, {})
36
- .allowUnknownOption(true)
36
+ .option('--properties <items>', 'Layout properties', parse_args_1.parsePropertiesList, {})
37
+ .allowUnknownOption(false)
37
38
  .allowExcessArguments(true)
38
39
  .action((options) => __awaiter(void 0, void 0, void 0, function* () {
39
40
  try {
@@ -43,14 +44,27 @@ const createDevCommand = () => {
43
44
  if (!rawLayoutDefinition) {
44
45
  throw new Error(`Failed to load layout definition from ${options.config}`);
45
46
  }
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
+ }
46
58
  // Confirm for entry property
47
59
  const layoutDefinition = Object.assign({}, rawLayoutDefinition);
60
+ // Normalize properties to convert string booleans to actual booleans
61
+ const normalizedProperties = (0, parse_args_1.normalizeLayoutProperties)(options.properties, layoutDefinition);
48
62
  exports.logger.info('Starting development server...');
49
63
  yield (0, server_1.startDevServer)({
50
64
  configPath: path_1.default.resolve(options.config),
51
65
  layoutDefinition,
52
66
  zoneContent: options.zones,
53
- layoutOptions: options.options,
67
+ layoutProperties: normalizedProperties,
54
68
  stylesheet: options.stylesheet
55
69
  ? path_1.default.resolve(options.stylesheet)
56
70
  : undefined,
@@ -47,9 +47,9 @@ function createMockArgs(opts) {
47
47
  });
48
48
  });
49
49
  }
50
- if (opts.layoutOptions) {
51
- Object.entries(opts.layoutOptions).forEach(([optionName, value]) => {
52
- args.push(`--options=${optionName}=${value}`);
50
+ if (opts.layoutProperties) {
51
+ Object.entries(opts.layoutProperties).forEach(([propertyName, value]) => {
52
+ args.push(`--properties=${propertyName}=${value}`);
53
53
  });
54
54
  }
55
55
  return args;
@@ -65,7 +65,7 @@ describe('devCommand', () => {
65
65
  process.argv = args;
66
66
  yield program.parseAsync(args);
67
67
  const opts = program.opts();
68
- expect(opts.config).toEqual('./page-layout.yaml');
68
+ expect(opts.config).toEqual(`./${definitions_1.LAYOUT_MANIFEST_FILE}`);
69
69
  expect(opts.port).toEqual('4040');
70
70
  expect(opts.open).toEqual(true);
71
71
  expect(opts.stylesheet).toBeUndefined();
@@ -121,19 +121,46 @@ describe('devCommand', () => {
121
121
  zoneContent,
122
122
  }));
123
123
  }));
124
- it('parses and passes layout options to the server', () => __awaiter(void 0, void 0, void 0, function* () {
124
+ it('parses and passes layout properties to the server', () => __awaiter(void 0, void 0, void 0, function* () {
125
125
  const mockLayout = { name: 'Test Layout' };
126
126
  definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
127
- const layoutOptions = {
127
+ const layoutProperties = {
128
128
  sizing: 'large',
129
129
  theme: 'dark',
130
130
  };
131
131
  const program = (0, dev_1.default)();
132
- const args = createMockArgs({ layoutOptions });
132
+ const args = createMockArgs({ layoutProperties });
133
133
  process.argv = args;
134
134
  yield program.parseAsync(args);
135
135
  expect(server_1.startDevServer).toHaveBeenCalledWith(expect.objectContaining({
136
- layoutOptions,
136
+ layoutProperties,
137
+ }));
138
+ }));
139
+ it('normalizes boolean properties from strings to actual booleans', () => __awaiter(void 0, void 0, void 0, function* () {
140
+ const mockLayout = {
141
+ name: 'Test Layout',
142
+ properties: {
143
+ showFooter: { type: 'boolean' },
144
+ showHeader: { type: 'boolean' },
145
+ theme: { type: 'string' },
146
+ },
147
+ };
148
+ definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
149
+ const layoutProperties = {
150
+ showFooter: 'true',
151
+ showHeader: 'false',
152
+ theme: 'dark',
153
+ };
154
+ const program = (0, dev_1.default)();
155
+ const args = createMockArgs({ layoutProperties });
156
+ process.argv = args;
157
+ yield program.parseAsync(args);
158
+ expect(server_1.startDevServer).toHaveBeenCalledWith(expect.objectContaining({
159
+ layoutProperties: {
160
+ showFooter: true,
161
+ showHeader: false,
162
+ theme: 'dark', // Remains string
163
+ },
137
164
  }));
138
165
  }));
139
166
  it('handles failure to load layout definition', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -159,4 +186,86 @@ describe('devCommand', () => {
159
186
  yield program.parseAsync(args);
160
187
  expect(mockLoggerErrorFn).toHaveBeenCalledWith(expect.stringContaining('Server start failed'));
161
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
+ });
162
271
  });
@@ -1 +1,3 @@
1
+ export { validateLayoutFormat } from './validateLayoutFormat';
2
+ export { validatePropertyConsistency } from './property-consistency';
1
3
  export { validateZoneConsistency } from './zone-consistency';
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateZoneConsistency = void 0;
3
+ exports.validateZoneConsistency = exports.validatePropertyConsistency = exports.validateLayoutFormat = void 0;
4
+ var validateLayoutFormat_1 = require("./validateLayoutFormat");
5
+ Object.defineProperty(exports, "validateLayoutFormat", { enumerable: true, get: function () { return validateLayoutFormat_1.validateLayoutFormat; } });
6
+ var property_consistency_1 = require("./property-consistency");
7
+ Object.defineProperty(exports, "validatePropertyConsistency", { enumerable: true, get: function () { return property_consistency_1.validatePropertyConsistency; } });
4
8
  var zone_consistency_1 = require("./zone-consistency");
5
9
  Object.defineProperty(exports, "validateZoneConsistency", { enumerable: true, get: function () { return zone_consistency_1.validateZoneConsistency; } });
@@ -0,0 +1,7 @@
1
+ import { LayoutDefinition } from '../../utils/definitions';
2
+ /**
3
+ * Validates that options/properties defined in layout definition match options/properties used in the Handlebars template
4
+ * @param layout The layout definition containing options/properties and template
5
+ * @returns Error message if validation fails, null if validation passes
6
+ */
7
+ export declare function validatePropertyConsistency(layout: LayoutDefinition, filePath: string): string | null;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.validatePropertyConsistency = void 0;
27
+ const path = __importStar(require("node:path"));
28
+ const definitions_1 = require("../../utils/definitions");
29
+ /**
30
+ * Validates that options/properties defined in layout definition match options/properties used in the Handlebars template
31
+ * @param layout The layout definition containing options/properties and template
32
+ * @returns Error message if validation fails, null if validation passes
33
+ */
34
+ function validatePropertyConsistency(layout, filePath) {
35
+ var _a;
36
+ const layoutProperties = (_a = layout.properties) !== null && _a !== void 0 ? _a : {};
37
+ const template = layout.template;
38
+ // If no template or properties are provided, skip validation
39
+ if (!template) {
40
+ return null;
41
+ }
42
+ const uniquePropertyKeys = new Set(Object.keys(layoutProperties));
43
+ if (uniquePropertyKeys.size !== Object.keys(layoutProperties).length) {
44
+ return 'Duplicate property keys found in layout definition';
45
+ }
46
+ // Extract property references from Handlebars template
47
+ // Look for patterns like {{properties.propertyName}}, {{#if properties.propertyName}} {{#ifEq 'value' properties.propertyName}}
48
+ const propertyPattern = /\{\{#*(?:[^\}]+\s)*properties\.(\w+)/g;
49
+ const optionPattern = /\{\{#*(?:[^\}]+\s)*options\.(\w+)/g;
50
+ // Keep track of properties/options used in the Handlebars template
51
+ const templateProperties = getTemplateMatches(template, propertyPattern);
52
+ const templateOptions = getTemplateMatches(template, optionPattern);
53
+ // Create an array of errors
54
+ const errors = [];
55
+ // Check that properties/options are being used correctly based on the layout definition file format
56
+ const fileName = path.basename(filePath);
57
+ const isNewFormat = fileName === definitions_1.LAYOUT_MANIFEST_FILE;
58
+ const templateSetToCheck = isNewFormat ? templateProperties : templateOptions;
59
+ // Check for incorrect option/property usage based on the layout definition file format
60
+ if (isNewFormat) {
61
+ if (templateOptions.size > 0) {
62
+ errors.push('Options are not supported in the Handlebars template for a layout defined with "manifest.json". Please use properties instead.');
63
+ }
64
+ }
65
+ else {
66
+ if (templateProperties.size > 0) {
67
+ errors.push('Properties are only allowed in the Handlebars template for a layout defined with "manifest.json". Please use options instead.');
68
+ }
69
+ }
70
+ // Find options/properties used in template but not defined in layout definition
71
+ const undefinedTemplateProperties = Array.from(templateSetToCheck).filter(property => !Object.keys(layoutProperties).includes(property));
72
+ // Add the undefined options/properties to the errors
73
+ if (undefinedTemplateProperties.length > 0) {
74
+ errors.push(`${isNewFormat ? 'Properties' : 'Options'} used in template but not defined in layout definition: ${undefinedTemplateProperties.join(', ')}`);
75
+ }
76
+ // If there are errors, return the errors
77
+ if (errors.length > 0) {
78
+ return `${isNewFormat ? 'Property' : 'Option'} consistency validation failed:\n${errors
79
+ .map(err => ` - ${err}`)
80
+ .join('\n')}`;
81
+ }
82
+ return null;
83
+ }
84
+ exports.validatePropertyConsistency = validatePropertyConsistency;
85
+ function getTemplateMatches(template, pattern) {
86
+ const matches = new Set();
87
+ let match;
88
+ while ((match = pattern.exec(template)) !== null) {
89
+ matches.add(match[1]);
90
+ }
91
+ return matches;
92
+ }