@squiz/dxp-cli-next 5.31.0-develop.4 → 5.31.0-develop.5
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/page/layouts/deploy/deploy.js +43 -8
- package/lib/page/layouts/deploy/deploy.spec.js +110 -19
- package/lib/page/layouts/dev/dev.js +7 -3
- package/lib/page/layouts/dev/dev.spec.js +35 -8
- package/lib/page/layouts/validation/index.d.ts +2 -0
- package/lib/page/layouts/validation/index.js +5 -1
- package/lib/page/layouts/validation/property-consistency.d.ts +7 -0
- package/lib/page/layouts/validation/property-consistency.js +92 -0
- package/lib/page/layouts/validation/property-consistency.spec.d.ts +1 -0
- package/lib/page/layouts/validation/property-consistency.spec.js +305 -0
- package/lib/page/layouts/validation/validateLayoutFormat.d.ts +2 -0
- package/lib/page/layouts/validation/validateLayoutFormat.js +25 -0
- package/lib/page/layouts/validation/validateLayoutFormat.spec.d.ts +1 -0
- package/lib/page/layouts/validation/validateLayoutFormat.spec.js +40 -0
- package/lib/page/layouts/validation/zone-consistency.d.ts +1 -1
- package/lib/page/layouts/validation/zone-consistency.js +10 -9
- package/lib/page/layouts/validation/zone-consistency.spec.js +32 -34
- package/lib/page/utils/definitions.d.ts +346 -49
- package/lib/page/utils/definitions.js +102 -21
- package/lib/page/utils/definitions.spec.js +460 -267
- package/lib/page/utils/normalize.d.ts +8 -0
- package/lib/page/utils/normalize.js +61 -0
- package/lib/page/utils/normalize.spec.d.ts +1 -0
- package/lib/page/utils/normalize.spec.js +315 -0
- package/lib/page/utils/parse-args.d.ts +20 -4
- package/lib/page/utils/parse-args.js +48 -13
- package/lib/page/utils/parse-args.spec.js +159 -21
- package/lib/page/utils/render.d.ts +27 -9
- package/lib/page/utils/render.js +66 -12
- package/lib/page/utils/render.spec.js +14 -14
- package/lib/page/utils/server.d.ts +1 -1
- package/lib/page/utils/server.js +2 -2
- package/lib/page/utils/server.spec.js +13 -13
- 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')
|
|
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,7 +47,11 @@ 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
|
-
|
|
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);
|
|
@@ -57,6 +61,11 @@ const createDeployCommand = () => {
|
|
|
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
|
|
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
|
-
|
|
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).
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
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,12 +28,12 @@ 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',
|
|
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('--
|
|
36
|
+
.option('--properties <items>', 'Layout properties', parse_args_1.parsePropertiesList, {})
|
|
36
37
|
.allowUnknownOption(true)
|
|
37
38
|
.allowExcessArguments(true)
|
|
38
39
|
.action((options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -43,14 +44,17 @@ 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);
|
|
46
48
|
// Confirm for entry property
|
|
47
49
|
const layoutDefinition = Object.assign({}, rawLayoutDefinition);
|
|
50
|
+
// Normalize properties to convert string booleans to actual booleans
|
|
51
|
+
const normalizedProperties = (0, parse_args_1.normalizeLayoutProperties)(options.properties, layoutDefinition);
|
|
48
52
|
exports.logger.info('Starting development server...');
|
|
49
53
|
yield (0, server_1.startDevServer)({
|
|
50
54
|
configPath: path_1.default.resolve(options.config),
|
|
51
55
|
layoutDefinition,
|
|
52
56
|
zoneContent: options.zones,
|
|
53
|
-
|
|
57
|
+
layoutProperties: normalizedProperties,
|
|
54
58
|
stylesheet: options.stylesheet
|
|
55
59
|
? path_1.default.resolve(options.stylesheet)
|
|
56
60
|
: undefined,
|
|
@@ -47,9 +47,9 @@ function createMockArgs(opts) {
|
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
-
if (opts.
|
|
51
|
-
Object.entries(opts.
|
|
52
|
-
args.push(`--
|
|
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(
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
|
|
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* () {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|