@squiz/dxp-cli-next 5.30.0 → 5.31.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.
- package/lib/cdp/instance/activate/activate.js +1 -1
- package/lib/cdp/instance/activate/activate.spec.js +4 -4
- package/lib/page/layouts/deploy/deploy.js +32 -6
- package/lib/page/layouts/deploy/deploy.spec.js +55 -2
- package/lib/page/layouts/dev/dev.js +3 -1
- package/lib/page/layouts/dev/dev.spec.js +27 -0
- package/lib/page/utils/definitions.d.ts +30 -15
- package/lib/page/utils/definitions.js +3 -1
- package/lib/page/utils/definitions.spec.js +128 -0
- package/lib/page/utils/parse-args.d.ts +17 -1
- package/lib/page/utils/parse-args.js +38 -2
- package/lib/page/utils/parse-args.spec.js +138 -0
- package/lib/page/utils/render.d.ts +14 -3
- package/lib/page/utils/render.js +15 -2
- package/lib/page/utils/render.spec.js +1 -1
- package/lib/page/utils/server.d.ts +1 -1
- package/package.json +1 -1
|
@@ -77,7 +77,7 @@ const createActivateCommand = () => {
|
|
|
77
77
|
}
|
|
78
78
|
(0, utils_1.logDebug)(`PUT ${apiUrl}`);
|
|
79
79
|
const activateInstanceAndDeploySchemaResponse = (yield apiService.client
|
|
80
|
-
.put(apiUrl, {
|
|
80
|
+
.put(apiUrl, undefined, {
|
|
81
81
|
headers: Object.assign({ 'Content-Type': 'application/json' }, (!hasRegion && { 'x-dxp-tenant': tenant })),
|
|
82
82
|
})
|
|
83
83
|
.catch((err) => {
|
|
@@ -102,7 +102,7 @@ describe('cdpInstanceCommand', () => {
|
|
|
102
102
|
axios_1.default.create.mockReturnValue(mockedAxiosInstance);
|
|
103
103
|
const program = (0, activate_1.default)();
|
|
104
104
|
yield program.parseAsync((0, utils_1.createMockActivateArgs)(region));
|
|
105
|
-
expect(mockedAxiosInstance.put).toHaveBeenCalledWith(`http://localhost:9999/__dxp/${region}/scv-deploy/${mockTenant}`, {
|
|
105
|
+
expect(mockedAxiosInstance.put).toHaveBeenCalledWith(`http://localhost:9999/__dxp/${region}/scv-deploy/${mockTenant}`, undefined, {
|
|
106
106
|
headers: {
|
|
107
107
|
'Content-Type': 'application/json',
|
|
108
108
|
},
|
|
@@ -138,7 +138,7 @@ describe('cdpInstanceCommand', () => {
|
|
|
138
138
|
axios_1.default.create.mockReturnValue(mockedAxios);
|
|
139
139
|
const program = (0, activate_1.default)();
|
|
140
140
|
yield program.parseAsync((0, utils_1.createMockActivateArgs)(region));
|
|
141
|
-
expect(mockedAxios.put).toHaveBeenCalledWith(`http://localhost:9999/__dxp/${region}/scv-deploy/${mockTenant}`, {
|
|
141
|
+
expect(mockedAxios.put).toHaveBeenCalledWith(`http://localhost:9999/__dxp/${region}/scv-deploy/${mockTenant}`, undefined, {
|
|
142
142
|
headers: {
|
|
143
143
|
'Content-Type': 'application/json',
|
|
144
144
|
},
|
|
@@ -207,7 +207,7 @@ describe('cdpInstanceCommand', () => {
|
|
|
207
207
|
expect(`${mockDomain}${mockPath}`).toEqual(mockDomainWithPath);
|
|
208
208
|
const program = (0, activate_1.default)();
|
|
209
209
|
yield program.parseAsync(createMockArgs());
|
|
210
|
-
expect(mockedAxiosInstance.put).toHaveBeenCalledWith('http://localhost:9999/__dxp/service/scv-deploy', {
|
|
210
|
+
expect(mockedAxiosInstance.put).toHaveBeenCalledWith('http://localhost:9999/__dxp/service/scv-deploy', undefined, {
|
|
211
211
|
headers: {
|
|
212
212
|
'Content-Type': 'application/json',
|
|
213
213
|
'x-dxp-tenant': 'myTenant',
|
|
@@ -248,7 +248,7 @@ describe('cdpInstanceCommand', () => {
|
|
|
248
248
|
expect(`${mockDomain}${mockPath}`).toEqual(mockDomainWithPath);
|
|
249
249
|
const program = (0, activate_1.default)();
|
|
250
250
|
yield program.parseAsync(createMockArgs());
|
|
251
|
-
expect(mockedAxios.put).toHaveBeenCalledWith('http://localhost:9999/__dxp/service/scv-deploy', {
|
|
251
|
+
expect(mockedAxios.put).toHaveBeenCalledWith('http://localhost:9999/__dxp/service/scv-deploy', undefined, {
|
|
252
252
|
headers: {
|
|
253
253
|
'Content-Type': 'application/json',
|
|
254
254
|
'x-dxp-tenant': 'myTenant',
|
|
@@ -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
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
44
|
+
export declare function renderLayout(templateContent: string, zoneContents: Record<string, string>, layoutOptions: Record<string, string | boolean>, layoutDefinition?: ExtendedLayoutDefinition): Promise<string>;
|
package/lib/page/utils/render.js
CHANGED
|
@@ -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:
|
|
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;
|