@squiz/dxp-cli-next 5.30.0 → 5.31.0-develop.1

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.
@@ -90,8 +90,34 @@ const createDeployCommand = () => {
90
90
  return deployCommand;
91
91
  };
92
92
  exports.default = createDeployCommand;
93
+ /**
94
+ * Formats backend error responses into user-friendly error messages.
95
+ * Handles both validation errors (400s) and unknown server errors (500s).
96
+ *
97
+ * @param data - The error response data from the backend
98
+ * @param statusCode - The HTTP status code (optional)
99
+ * @returns Formatted error message string
100
+ */
101
+ function formatErrorResponse(data, statusCode) {
102
+ // For 500 errors or unknown errors, show a generic message
103
+ if (statusCode && statusCode >= 500) {
104
+ return data.message || data.data || 'An unknown error occurred';
105
+ }
106
+ // For validation errors (400s), format with details
107
+ let errorMessage = data.message || 'Validation failed';
108
+ // Options validation errors come as an object with field keys
109
+ if (data.details &&
110
+ typeof data.details === 'object' &&
111
+ !Array.isArray(data.details)) {
112
+ const details = Object.entries(data.details)
113
+ .map(([field, error]) => ` - ${field}: ${error.message}`)
114
+ .join('\n');
115
+ errorMessage += `\n${details}`;
116
+ }
117
+ return errorMessage;
118
+ }
93
119
  function uploadLayout(client, layout, contentServiceUrl, dryRun) {
94
- var _a, _b;
120
+ var _a;
95
121
  return __awaiter(this, void 0, void 0, function* () {
96
122
  try {
97
123
  const queryParam = dryRun ? '?_dryRun=true' : '';
@@ -101,13 +127,13 @@ function uploadLayout(client, layout, contentServiceUrl, dryRun) {
101
127
  if (response.status === 200) {
102
128
  return response;
103
129
  }
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;
130
+ throw new Error(formatErrorResponse(response.data, response.status));
109
131
  }
110
132
  catch (error) {
133
+ // Extract error details from axios error response
134
+ if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
135
+ throw new Error(formatErrorResponse(error.response.data, error.response.status));
136
+ }
111
137
  throw error;
112
138
  }
113
139
  });
@@ -170,7 +170,7 @@ describe('deployCommand', () => {
170
170
  const program = (0, deploy_1.default)();
171
171
  errorSpy = jest.spyOn(program, 'error').mockImplementation();
172
172
  yield program.parseAsync(createMockArgs({ contentServiceUrl }));
173
- expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('An unknown error occurred'));
173
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Internal server error'));
174
174
  }));
175
175
  it('should log additional details when layout validation fails', () => __awaiter(void 0, void 0, void 0, function* () {
176
176
  const dxpBaseUrl = 'http://dxp-base-url.com';
@@ -187,8 +187,61 @@ describe('deployCommand', () => {
187
187
  const program = (0, deploy_1.default)();
188
188
  errorSpy = jest.spyOn(program, 'error').mockImplementation();
189
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'));
190
+ const errorCall = errorSpy.mock.calls[0][0];
191
+ expect(errorCall).toContain('- input: ERROR: Validation failed: "version" is an excess property and therefore is not allowed');
191
192
  }));
193
+ describe('backend option validation error reporting', () => {
194
+ it('should display clean error when single option validation fails', () => __awaiter(void 0, void 0, void 0, function* () {
195
+ const contentServiceUrl = 'http://localhost:9999';
196
+ (0, nock_1.default)(contentServiceUrl)
197
+ .post('/page-layout')
198
+ .reply(400, {
199
+ message: 'Validation failed',
200
+ details: {
201
+ 'input.options.myBoolean': {
202
+ message: "Option 'myBoolean' with valueType 'boolean' can not have values specified",
203
+ },
204
+ },
205
+ });
206
+ const program = (0, deploy_1.default)();
207
+ errorSpy = jest.spyOn(program, 'error').mockImplementation();
208
+ yield program.parseAsync(createMockArgs({ contentServiceUrl }));
209
+ const errorCall = errorSpy.mock.calls[0][0];
210
+ expect(errorCall).toContain('Validation failed');
211
+ expect(errorCall).toContain("- input.options.myBoolean: Option 'myBoolean' with valueType 'boolean' can not have values specified");
212
+ }));
213
+ it('should display multiple option validation errors cleanly', () => __awaiter(void 0, void 0, void 0, function* () {
214
+ const contentServiceUrl = 'http://localhost:9999';
215
+ (0, nock_1.default)(contentServiceUrl)
216
+ .post('/page-layout')
217
+ .reply(400, {
218
+ message: 'Validation failed',
219
+ details: {
220
+ 'input.options.badBoolean': {
221
+ message: "Option 'badBoolean' with valueType 'boolean' can not have values specified",
222
+ },
223
+ 'input.options.badText': {
224
+ message: "Option 'badText' with valueType 'text' can not have values specified",
225
+ },
226
+ 'input.options.emptyEnum': {
227
+ message: "Option 'emptyEnum' with valueType 'string-enum' must have at least one value defined",
228
+ },
229
+ 'input.options.badOption.valueType': {
230
+ message: "Option 'badOption' has invalid valueType 'INVALID'. Must be one of: string-enum, boolean, text",
231
+ },
232
+ },
233
+ });
234
+ const program = (0, deploy_1.default)();
235
+ errorSpy = jest.spyOn(program, 'error').mockImplementation();
236
+ yield program.parseAsync(createMockArgs({ contentServiceUrl }));
237
+ const errorCall = errorSpy.mock.calls[0][0];
238
+ expect(errorCall).toContain('Validation failed');
239
+ expect(errorCall).toContain("- input.options.badBoolean: Option 'badBoolean' with valueType 'boolean' can not have values specified");
240
+ expect(errorCall).toContain("- input.options.badText: Option 'badText' with valueType 'text' can not have values specified");
241
+ expect(errorCall).toContain("- input.options.emptyEnum: Option 'emptyEnum' with valueType 'string-enum' must have at least one value defined");
242
+ expect(errorCall).toContain("- input.options.badOption.valueType: Option 'badOption' has invalid valueType 'INVALID'. Must be one of: string-enum, boolean, text");
243
+ }));
244
+ });
192
245
  describe('zone consistency validation', () => {
193
246
  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
247
  const file = './src/__tests__/layout.yaml';
@@ -45,12 +45,14 @@ const createDevCommand = () => {
45
45
  }
46
46
  // Confirm for entry property
47
47
  const layoutDefinition = Object.assign({}, rawLayoutDefinition);
48
+ // Normalize options to convert string booleans to actual booleans
49
+ const normalizedOptions = (0, parse_args_1.normalizeLayoutOptions)(options.options, layoutDefinition);
48
50
  exports.logger.info('Starting development server...');
49
51
  yield (0, server_1.startDevServer)({
50
52
  configPath: path_1.default.resolve(options.config),
51
53
  layoutDefinition,
52
54
  zoneContent: options.zones,
53
- layoutOptions: options.options,
55
+ layoutOptions: normalizedOptions,
54
56
  stylesheet: options.stylesheet
55
57
  ? path_1.default.resolve(options.stylesheet)
56
58
  : undefined,
@@ -136,6 +136,33 @@ describe('devCommand', () => {
136
136
  layoutOptions,
137
137
  }));
138
138
  }));
139
+ it('normalizes boolean options from strings to actual booleans', () => __awaiter(void 0, void 0, void 0, function* () {
140
+ const mockLayout = {
141
+ name: 'Test Layout',
142
+ options: {
143
+ showFooter: { valueType: 'boolean' },
144
+ showHeader: { valueType: 'boolean' },
145
+ theme: { valueType: 'string-enum' },
146
+ },
147
+ };
148
+ definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
149
+ const layoutOptions = {
150
+ showFooter: 'true',
151
+ showHeader: 'false',
152
+ theme: 'dark',
153
+ };
154
+ const program = (0, dev_1.default)();
155
+ const args = createMockArgs({ layoutOptions });
156
+ process.argv = args;
157
+ yield program.parseAsync(args);
158
+ expect(server_1.startDevServer).toHaveBeenCalledWith(expect.objectContaining({
159
+ layoutOptions: {
160
+ showFooter: true,
161
+ showHeader: false,
162
+ theme: 'dark', // Remains string
163
+ },
164
+ }));
165
+ }));
139
166
  it('handles failure to load layout definition', () => __awaiter(void 0, void 0, void 0, function* () {
140
167
  const config = './src/__tests__/layout.yaml';
141
168
  definitions_1.loadLayoutDefinition.mockResolvedValue(undefined);
@@ -49,15 +49,18 @@ export declare const BaseLayoutDefinition: z.ZodObject<{
49
49
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
50
50
  displayName: z.ZodString;
51
51
  description: z.ZodString;
52
- values: z.ZodArray<z.ZodString, "many">;
52
+ valueType: z.ZodOptional<z.ZodEnum<["string-enum", "boolean", "text"]>>;
53
+ values: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
53
54
  }, "strip", z.ZodTypeAny, {
54
- values: string[];
55
55
  description: string;
56
56
  displayName: string;
57
+ values?: string[] | undefined;
58
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
57
59
  }, {
58
- values: string[];
59
60
  description: string;
60
61
  displayName: string;
62
+ values?: string[] | undefined;
63
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
61
64
  }>>>;
62
65
  }, "strip", z.ZodTypeAny, {
63
66
  name: string;
@@ -70,9 +73,10 @@ export declare const BaseLayoutDefinition: z.ZodObject<{
70
73
  maxNodes?: number | undefined;
71
74
  }>;
72
75
  options?: Record<string, {
73
- values: string[];
74
76
  description: string;
75
77
  displayName: string;
78
+ values?: string[] | undefined;
79
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
76
80
  }> | undefined;
77
81
  }, {
78
82
  name: string;
@@ -85,9 +89,10 @@ export declare const BaseLayoutDefinition: z.ZodObject<{
85
89
  maxNodes?: number | undefined;
86
90
  }>;
87
91
  options?: Record<string, {
88
- values: string[];
89
92
  description: string;
90
93
  displayName: string;
94
+ values?: string[] | undefined;
95
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
91
96
  }> | undefined;
92
97
  }>;
93
98
  export declare const InputLayoutDefinition: z.ZodObject<z.objectUtil.extendShape<{
@@ -133,15 +138,18 @@ export declare const InputLayoutDefinition: z.ZodObject<z.objectUtil.extendShape
133
138
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
134
139
  displayName: z.ZodString;
135
140
  description: z.ZodString;
136
- values: z.ZodArray<z.ZodString, "many">;
141
+ valueType: z.ZodOptional<z.ZodEnum<["string-enum", "boolean", "text"]>>;
142
+ values: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
137
143
  }, "strip", z.ZodTypeAny, {
138
- values: string[];
139
144
  description: string;
140
145
  displayName: string;
146
+ values?: string[] | undefined;
147
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
141
148
  }, {
142
- values: string[];
143
149
  description: string;
144
150
  displayName: string;
151
+ values?: string[] | undefined;
152
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
145
153
  }>>>;
146
154
  }, {
147
155
  entry: z.ZodString;
@@ -157,9 +165,10 @@ export declare const InputLayoutDefinition: z.ZodObject<z.objectUtil.extendShape
157
165
  maxNodes?: number | undefined;
158
166
  }>;
159
167
  options?: Record<string, {
160
- values: string[];
161
168
  description: string;
162
169
  displayName: string;
170
+ values?: string[] | undefined;
171
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
163
172
  }> | undefined;
164
173
  }, {
165
174
  name: string;
@@ -173,9 +182,10 @@ export declare const InputLayoutDefinition: z.ZodObject<z.objectUtil.extendShape
173
182
  maxNodes?: number | undefined;
174
183
  }>;
175
184
  options?: Record<string, {
176
- values: string[];
177
185
  description: string;
178
186
  displayName: string;
187
+ values?: string[] | undefined;
188
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
179
189
  }> | undefined;
180
190
  }>;
181
191
  export declare const LayoutDefinition: z.ZodObject<z.objectUtil.extendShape<{
@@ -221,15 +231,18 @@ export declare const LayoutDefinition: z.ZodObject<z.objectUtil.extendShape<{
221
231
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
222
232
  displayName: z.ZodString;
223
233
  description: z.ZodString;
224
- values: z.ZodArray<z.ZodString, "many">;
234
+ valueType: z.ZodOptional<z.ZodEnum<["string-enum", "boolean", "text"]>>;
235
+ values: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
225
236
  }, "strip", z.ZodTypeAny, {
226
- values: string[];
227
237
  description: string;
228
238
  displayName: string;
239
+ values?: string[] | undefined;
240
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
229
241
  }, {
230
- values: string[];
231
242
  description: string;
232
243
  displayName: string;
244
+ values?: string[] | undefined;
245
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
233
246
  }>>>;
234
247
  }, {
235
248
  template: z.ZodString;
@@ -245,9 +258,10 @@ export declare const LayoutDefinition: z.ZodObject<z.objectUtil.extendShape<{
245
258
  }>;
246
259
  template: string;
247
260
  options?: Record<string, {
248
- values: string[];
249
261
  description: string;
250
262
  displayName: string;
263
+ values?: string[] | undefined;
264
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
251
265
  }> | undefined;
252
266
  }, {
253
267
  name: string;
@@ -261,9 +275,10 @@ export declare const LayoutDefinition: z.ZodObject<z.objectUtil.extendShape<{
261
275
  }>;
262
276
  template: string;
263
277
  options?: Record<string, {
264
- values: string[];
265
278
  description: string;
266
279
  displayName: string;
280
+ values?: string[] | undefined;
281
+ valueType?: "boolean" | "text" | "string-enum" | undefined;
267
282
  }> | undefined;
268
283
  }>;
269
284
  export declare type InputLayoutDefinition = z.infer<typeof InputLayoutDefinition>;
@@ -56,6 +56,7 @@ const zod_1 = require("zod");
56
56
  const yaml_1 = require("yaml");
57
57
  // Local
58
58
  const validation_1 = require("../templates/validation");
59
+ const render_1 = require("./render");
59
60
  function loadLayoutDefinition(layoutFile) {
60
61
  return __awaiter(this, void 0, void 0, function* () {
61
62
  try {
@@ -149,7 +150,8 @@ exports.BaseLayoutDefinition = zod_1.z.object({
149
150
  .record(zod_1.z.string(), zod_1.z.object({
150
151
  displayName: zod_1.z.string(),
151
152
  description: zod_1.z.string(),
152
- values: zod_1.z.array(zod_1.z.string()),
153
+ valueType: zod_1.z.enum(render_1.LayoutOptionValueTypes).optional(),
154
+ values: zod_1.z.array(zod_1.z.string()).optional(),
153
155
  }))
154
156
  .optional(),
155
157
  });
@@ -88,6 +88,16 @@ entry: template.hbs
88
88
  description: 'Color options',
89
89
  values: ['red', 'blue'],
90
90
  },
91
+ showHeader: {
92
+ displayName: 'Show Header',
93
+ description: 'Toggle header visibility',
94
+ valueType: 'boolean',
95
+ },
96
+ customTitle: {
97
+ displayName: 'Custom Title',
98
+ description: 'Enter a custom title',
99
+ valueType: 'text',
100
+ },
91
101
  },
92
102
  entry: 'template.hbs',
93
103
  });
@@ -178,6 +188,16 @@ entry: template.hbs
178
188
  description: 'Color options',
179
189
  values: ['red', 'blue'],
180
190
  },
191
+ showHeader: {
192
+ displayName: 'Show Header',
193
+ description: 'Toggle header visibility',
194
+ valueType: 'boolean',
195
+ },
196
+ customTitle: {
197
+ displayName: 'Custom Title',
198
+ description: 'Enter a custom title',
199
+ valueType: 'text',
200
+ },
181
201
  },
182
202
  template: templateContent,
183
203
  });
@@ -436,6 +456,114 @@ describe('LayoutDefinitionParse', () => {
436
456
  });
437
457
  expect(layoutDefinition.zones.main.minNodes).toBe(0);
438
458
  });
459
+ describe('option schema validation', () => {
460
+ it('should accept specified valueTypes', () => {
461
+ var _a, _b, _c;
462
+ const layoutDefinition = definitions_1.BaseLayoutDefinition.parse({
463
+ name: 'test-layout',
464
+ displayName: 'Test Layout',
465
+ description: 'A test layout',
466
+ zones: {
467
+ main: {
468
+ displayName: 'Main Zone',
469
+ description: 'Main content area',
470
+ },
471
+ },
472
+ options: {
473
+ theme: {
474
+ displayName: 'Theme',
475
+ description: 'Color theme',
476
+ valueType: 'string-enum',
477
+ values: ['light', 'dark'],
478
+ },
479
+ showSidebar: {
480
+ displayName: 'Show Sidebar',
481
+ description: 'Toggle sidebar',
482
+ valueType: 'boolean',
483
+ },
484
+ customCss: {
485
+ displayName: 'Custom CSS',
486
+ description: 'Enter custom CSS',
487
+ valueType: 'text',
488
+ },
489
+ },
490
+ });
491
+ expect((_a = layoutDefinition.options) === null || _a === void 0 ? void 0 : _a.theme.valueType).toBe('string-enum');
492
+ expect((_b = layoutDefinition.options) === null || _b === void 0 ? void 0 : _b.showSidebar.valueType).toBe('boolean');
493
+ expect((_c = layoutDefinition.options) === null || _c === void 0 ? void 0 : _c.customCss.valueType).toBe('text');
494
+ });
495
+ it('should reject invalid valueType', () => {
496
+ expect(() => definitions_1.BaseLayoutDefinition.parse({
497
+ name: 'test-layout',
498
+ displayName: 'Test Layout',
499
+ description: 'A test layout',
500
+ zones: {
501
+ main: {
502
+ displayName: 'Main Zone',
503
+ description: 'Main content area',
504
+ },
505
+ },
506
+ options: {
507
+ badOption: {
508
+ displayName: 'Bad Option',
509
+ description: 'Invalid valueType',
510
+ valueType: 'INVALID_TYPE',
511
+ },
512
+ },
513
+ })).toThrow();
514
+ });
515
+ it('should allow valueType and values to be optional', () => {
516
+ var _a, _b, _c, _d, _e, _f, _g, _h;
517
+ const layoutDefinition = definitions_1.BaseLayoutDefinition.parse({
518
+ name: 'test-layout',
519
+ displayName: 'Test Layout',
520
+ description: 'A test layout',
521
+ zones: {
522
+ main: {
523
+ displayName: 'Main Zone',
524
+ description: 'Main content area',
525
+ },
526
+ },
527
+ options: {
528
+ withBoth: {
529
+ displayName: 'With Both',
530
+ description: 'Has valueType and values',
531
+ valueType: 'string-enum',
532
+ values: ['option1', 'option2'],
533
+ },
534
+ withValueTypeOnly: {
535
+ displayName: 'With ValueType Only',
536
+ description: 'Has valueType but no values',
537
+ valueType: 'boolean',
538
+ },
539
+ withValuesOnly: {
540
+ displayName: 'With Values Only',
541
+ description: 'Has values but no valueType (legacy)',
542
+ values: ['legacy1', 'legacy2'],
543
+ },
544
+ withNeither: {
545
+ displayName: 'With Neither',
546
+ description: 'Has neither valueType nor values',
547
+ },
548
+ },
549
+ });
550
+ // Schema accepts all combinations - Business logic validation (e.g., boolean/text can't have values) happens on backend and reported in CLI on deploy.
551
+ expect((_a = layoutDefinition.options) === null || _a === void 0 ? void 0 : _a.withBoth.valueType).toBe('string-enum');
552
+ expect((_b = layoutDefinition.options) === null || _b === void 0 ? void 0 : _b.withBoth.values).toEqual([
553
+ 'option1',
554
+ 'option2',
555
+ ]);
556
+ expect((_c = layoutDefinition.options) === null || _c === void 0 ? void 0 : _c.withValueTypeOnly.valueType).toBe('boolean');
557
+ expect((_d = layoutDefinition.options) === null || _d === void 0 ? void 0 : _d.withValueTypeOnly.values).toBeUndefined();
558
+ expect((_e = layoutDefinition.options) === null || _e === void 0 ? void 0 : _e.withValuesOnly.valueType).toBeUndefined();
559
+ expect((_f = layoutDefinition.options) === null || _f === void 0 ? void 0 : _f.withValuesOnly.values).toEqual([
560
+ 'legacy1',
561
+ 'legacy2',
562
+ ]);
563
+ expect((_g = layoutDefinition.options) === null || _g === void 0 ? void 0 : _g.withNeither.valueType).toBeUndefined();
564
+ expect((_h = layoutDefinition.options) === null || _h === void 0 ? void 0 : _h.withNeither.values).toBeUndefined();
565
+ });
566
+ });
439
567
  });
440
568
  describe('validateTemplateFile', () => {
441
569
  describe('valid templates', () => {
@@ -19,10 +19,26 @@ export declare function parseZonesList(value: string, previous: Record<string, s
19
19
  * - Comma-separated list: 'key1=value1,key2=value2' or 'key1:value1,key2:value2'
20
20
  * - Multiple --options flags: later values override earlier ones
21
21
  *
22
+ * Note: All values are returned as strings. Use normalizeLayoutOptions() to convert
23
+ * boolean options to actual boolean types based on the layout definition.
24
+ *
22
25
  * @param value Current value being processed
23
26
  * @param previous Previously processed value (used for multiple flags)
24
- * @returns Record of option names to values
27
+ * @returns Record of option names to string values
25
28
  */
26
29
  export declare function parseOptionsList(value: string, previous: Record<string, string>): {
27
30
  [x: string]: string;
28
31
  };
32
+ /**
33
+ * Normalizes layout options by converting string boolean values to actual booleans
34
+ * based on the layout definition's option types.
35
+ *
36
+ * @param options The parsed options from CLI (all strings)
37
+ * @param layoutDefinition The layout definition with option type information
38
+ * @returns Options with boolean values converted to actual booleans
39
+ */
40
+ export declare function normalizeLayoutOptions(options: Record<string, string>, layoutDefinition: {
41
+ options?: Record<string, {
42
+ valueType?: string;
43
+ }>;
44
+ }): Record<string, string | boolean>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseOptionsList = exports.parseZonesList = void 0;
3
+ exports.normalizeLayoutOptions = exports.parseOptionsList = exports.parseZonesList = void 0;
4
+ const render_1 = require("./render");
4
5
  /**
5
6
  * Parse zones list from command line
6
7
  * Supports multiple formats:
@@ -50,9 +51,12 @@ exports.parseZonesList = parseZonesList;
50
51
  * - Comma-separated list: 'key1=value1,key2=value2' or 'key1:value1,key2:value2'
51
52
  * - Multiple --options flags: later values override earlier ones
52
53
  *
54
+ * Note: All values are returned as strings. Use normalizeLayoutOptions() to convert
55
+ * boolean options to actual boolean types based on the layout definition.
56
+ *
53
57
  * @param value Current value being processed
54
58
  * @param previous Previously processed value (used for multiple flags)
55
- * @returns Record of option names to values
59
+ * @returns Record of option names to string values
56
60
  */
57
61
  function parseOptionsList(value, previous) {
58
62
  const result = Object.assign({}, previous);
@@ -82,3 +86,35 @@ function parseOptionsList(value, previous) {
82
86
  return result;
83
87
  }
84
88
  exports.parseOptionsList = parseOptionsList;
89
+ /**
90
+ * Normalizes layout options by converting string boolean values to actual booleans
91
+ * based on the layout definition's option types.
92
+ *
93
+ * @param options The parsed options from CLI (all strings)
94
+ * @param layoutDefinition The layout definition with option type information
95
+ * @returns Options with boolean values converted to actual booleans
96
+ */
97
+ function normalizeLayoutOptions(options, layoutDefinition) {
98
+ const normalized = Object.assign({}, options);
99
+ if (!layoutDefinition.options) {
100
+ return normalized;
101
+ }
102
+ // Convert string "true"/"false" to actual booleans for boolean-type options
103
+ for (const [key, optionDef] of Object.entries(layoutDefinition.options)) {
104
+ if (optionDef.valueType === render_1.LayoutOptionValueType.boolean &&
105
+ key in normalized) {
106
+ const value = normalized[key];
107
+ if (value === 'true') {
108
+ normalized[key] = true;
109
+ }
110
+ else if (value === 'false') {
111
+ normalized[key] = false;
112
+ }
113
+ else {
114
+ throw new Error(`Invalid boolean value "${value}" for option "${key}". Must be "true" or "false".`);
115
+ }
116
+ }
117
+ }
118
+ return normalized;
119
+ }
120
+ exports.normalizeLayoutOptions = normalizeLayoutOptions;
@@ -144,3 +144,141 @@ describe('parseOptionsList', () => {
144
144
  });
145
145
  });
146
146
  });
147
+ describe('normalizeLayoutOptions', () => {
148
+ it('returns empty object when given no options', () => {
149
+ const result = (0, parse_args_1.normalizeLayoutOptions)({}, {});
150
+ expect(result).toEqual({});
151
+ });
152
+ it('returns options unchanged when layoutDefinition has no options', () => {
153
+ const options = {
154
+ theme: 'dark',
155
+ size: 'large',
156
+ };
157
+ const result = (0, parse_args_1.normalizeLayoutOptions)(options, {});
158
+ expect(result).toEqual(options);
159
+ });
160
+ it('converts string "true" to boolean true for boolean-type options', () => {
161
+ const options = {
162
+ showFooter: 'true',
163
+ };
164
+ const layoutDefinition = {
165
+ options: {
166
+ showFooter: {
167
+ valueType: 'boolean',
168
+ },
169
+ },
170
+ };
171
+ const result = (0, parse_args_1.normalizeLayoutOptions)(options, layoutDefinition);
172
+ expect(result).toEqual({
173
+ showFooter: true,
174
+ });
175
+ expect(typeof result.showFooter).toBe('boolean');
176
+ });
177
+ it('converts string "false" to boolean false for boolean-type options', () => {
178
+ const options = {
179
+ showFooter: 'false',
180
+ };
181
+ const layoutDefinition = {
182
+ options: {
183
+ showFooter: {
184
+ valueType: 'boolean',
185
+ },
186
+ },
187
+ };
188
+ const result = (0, parse_args_1.normalizeLayoutOptions)(options, layoutDefinition);
189
+ expect(result).toEqual({
190
+ showFooter: false,
191
+ });
192
+ expect(typeof result.showFooter).toBe('boolean');
193
+ });
194
+ it('leaves string options unchanged for non-boolean types', () => {
195
+ const options = {
196
+ theme: 'dark',
197
+ customTitle: 'My Title',
198
+ };
199
+ const layoutDefinition = {
200
+ options: {
201
+ theme: {
202
+ valueType: 'string-enum',
203
+ },
204
+ customTitle: {
205
+ valueType: 'text',
206
+ },
207
+ },
208
+ };
209
+ const result = (0, parse_args_1.normalizeLayoutOptions)(options, layoutDefinition);
210
+ expect(result).toEqual(options);
211
+ });
212
+ it('handles mixed boolean and string options', () => {
213
+ const options = {
214
+ showFooter: 'true',
215
+ showHeader: 'false',
216
+ theme: 'dark',
217
+ customTitle: 'Test',
218
+ };
219
+ const layoutDefinition = {
220
+ options: {
221
+ showFooter: { valueType: 'boolean' },
222
+ showHeader: { valueType: 'boolean' },
223
+ theme: { valueType: 'string-enum' },
224
+ customTitle: { valueType: 'text' },
225
+ },
226
+ };
227
+ const result = (0, parse_args_1.normalizeLayoutOptions)(options, layoutDefinition);
228
+ expect(result).toEqual({
229
+ showFooter: true,
230
+ showHeader: false,
231
+ theme: 'dark',
232
+ customTitle: 'Test',
233
+ });
234
+ });
235
+ it('throws error for invalid boolean value', () => {
236
+ const options = {
237
+ showFooter: 'yes',
238
+ };
239
+ const layoutDefinition = {
240
+ options: {
241
+ showFooter: {
242
+ valueType: 'boolean',
243
+ },
244
+ },
245
+ };
246
+ expect(() => (0, parse_args_1.normalizeLayoutOptions)(options, layoutDefinition)).toThrow('Invalid boolean value "yes" for option "showFooter". Must be "true" or "false".');
247
+ });
248
+ it('only normalizes options that exist in layoutDefinition', () => {
249
+ const options = {
250
+ showFooter: 'true',
251
+ unknownOption: 'true',
252
+ };
253
+ const layoutDefinition = {
254
+ options: {
255
+ showFooter: {
256
+ valueType: 'boolean',
257
+ },
258
+ },
259
+ };
260
+ const result = (0, parse_args_1.normalizeLayoutOptions)(options, layoutDefinition);
261
+ expect(result).toEqual({
262
+ showFooter: true,
263
+ unknownOption: 'true', // Left as string because not in layoutDefinition
264
+ });
265
+ });
266
+ it('preserves options not present in layoutDefinition options', () => {
267
+ const options = {
268
+ showFooter: 'true',
269
+ theme: 'dark',
270
+ };
271
+ const layoutDefinition = {
272
+ options: {
273
+ showFooter: {
274
+ valueType: 'boolean',
275
+ },
276
+ },
277
+ };
278
+ const result = (0, parse_args_1.normalizeLayoutOptions)(options, layoutDefinition);
279
+ expect(result).toEqual({
280
+ showFooter: true,
281
+ theme: 'dark', // Preserved even though not in layoutDefinition
282
+ });
283
+ });
284
+ });
@@ -16,18 +16,29 @@ export interface ZoneDefinition {
16
16
  minNodes?: number;
17
17
  maxNodes?: number;
18
18
  }
19
+ /**
20
+ * Layout option value types.
21
+ */
22
+ export declare const LayoutOptionValueType: {
23
+ readonly stringEnum: "string-enum";
24
+ readonly boolean: "boolean";
25
+ readonly text: "text";
26
+ };
27
+ export declare type LayoutOptionValueType = typeof LayoutOptionValueType[keyof typeof LayoutOptionValueType];
28
+ export declare const LayoutOptionValueTypes: readonly ["string-enum", "boolean", "text"];
19
29
  export interface OptionDefinition {
20
30
  displayName: string;
21
31
  description: string;
22
- values: string[];
32
+ valueType?: LayoutOptionValueType;
33
+ values?: string[];
23
34
  }
24
35
  /**
25
36
  * Renders a layout using Handlebars templating
26
37
  *
27
38
  * @param templateContent The Handlebars template string
28
39
  * @param zoneContents Content for each zone as single concatenated strings
29
- * @param layoutOptions Options for the layout
40
+ * @param layoutOptions Options for the layout (can include boolean values for boolean-type options)
30
41
  * @param layoutDefinition Optional layout definition object to pass to the template
31
42
  * @returns The rendered HTML
32
43
  */
33
- export declare function renderLayout(templateContent: string, zoneContents: Record<string, string>, layoutOptions: Record<string, any>, layoutDefinition?: ExtendedLayoutDefinition): Promise<string>;
44
+ export declare function renderLayout(templateContent: string, zoneContents: Record<string, string>, layoutOptions: Record<string, string | boolean>, layoutDefinition?: ExtendedLayoutDefinition): Promise<string>;
@@ -12,14 +12,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.renderLayout = void 0;
15
+ exports.renderLayout = exports.LayoutOptionValueTypes = exports.LayoutOptionValueType = void 0;
16
16
  const handlebars_1 = __importDefault(require("handlebars"));
17
+ /**
18
+ * Layout option value types.
19
+ */
20
+ exports.LayoutOptionValueType = {
21
+ stringEnum: 'string-enum',
22
+ boolean: 'boolean',
23
+ text: 'text',
24
+ };
25
+ exports.LayoutOptionValueTypes = [
26
+ exports.LayoutOptionValueType.stringEnum,
27
+ exports.LayoutOptionValueType.boolean,
28
+ exports.LayoutOptionValueType.text,
29
+ ];
17
30
  /**
18
31
  * Renders a layout using Handlebars templating
19
32
  *
20
33
  * @param templateContent The Handlebars template string
21
34
  * @param zoneContents Content for each zone as single concatenated strings
22
- * @param layoutOptions Options for the layout
35
+ * @param layoutOptions Options for the layout (can include boolean values for boolean-type options)
23
36
  * @param layoutDefinition Optional layout definition object to pass to the template
24
37
  * @returns The rendered HTML
25
38
  */
@@ -68,7 +68,7 @@ describe('renderLayout', () => {
68
68
  main: '<p>Content</p>',
69
69
  };
70
70
  const layoutOptions = {
71
- theme: { selectedValue: 'dark' },
71
+ theme: 'dark',
72
72
  };
73
73
  const result = yield (0, render_1.renderLayout)(templateContent, zoneContents, layoutOptions);
74
74
  expect(result).toContain('<div class="theme-dark">');
@@ -3,7 +3,7 @@ interface DevServerOptions {
3
3
  configPath: string;
4
4
  layoutDefinition: ExtendedLayoutDefinition;
5
5
  zoneContent: Record<string, string[]>;
6
- layoutOptions: Record<string, string>;
6
+ layoutOptions: Record<string, string | boolean>;
7
7
  stylesheet?: string;
8
8
  port: number;
9
9
  openBrowser: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/dxp-cli-next",
3
- "version": "5.30.0",
3
+ "version": "5.31.0-develop.1",
4
4
  "repository": {
5
5
  "url": "https://gitlab.squiz.net/dxp/dxp-cli-next"
6
6
  },