@squiz/dxp-cli-next 5.30.0-develop.2 → 5.30.0-develop.3
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 +2 -46
- package/lib/page/layouts/validation/index.d.ts +1 -0
- package/lib/page/layouts/validation/index.js +5 -0
- package/lib/page/layouts/validation/zone-consistency.d.ts +7 -0
- package/lib/page/layouts/validation/zone-consistency.js +49 -0
- package/lib/page/layouts/validation/zone-consistency.spec.d.ts +1 -0
- package/lib/page/layouts/validation/zone-consistency.spec.js +101 -0
- package/lib/page/templates/validation.d.ts +11 -0
- package/lib/page/templates/validation.js +30 -0
- package/lib/page/templates/validation.spec.d.ts +1 -0
- package/lib/page/templates/validation.spec.js +89 -0
- package/lib/page/utils/definitions.d.ts +5 -0
- package/lib/page/utils/definitions.js +21 -1
- package/lib/page/utils/definitions.spec.js +62 -0
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ const ApiService_1 = require("../../../ApiService");
|
|
|
19
19
|
const ApplicationConfig_1 = require("../../../ApplicationConfig");
|
|
20
20
|
const constants_1 = require("../../../constants");
|
|
21
21
|
const definitions_1 = require("../../utils/definitions");
|
|
22
|
+
const validation_1 = require("../validation");
|
|
22
23
|
const dx_logger_lib_1 = require("@squiz/dx-logger-lib");
|
|
23
24
|
exports.logger = (0, dx_logger_lib_1.getLogger)({
|
|
24
25
|
name: 'upload-layout',
|
|
@@ -52,7 +53,7 @@ const createDeployCommand = () => {
|
|
|
52
53
|
const layout = yield (0, definitions_1.loadLayoutDefinition)(layoutFile);
|
|
53
54
|
if (layout !== undefined) {
|
|
54
55
|
// Pre-deployment validation: check zones match between YAML and template
|
|
55
|
-
const zoneValidationError = validateZoneConsistency(layout);
|
|
56
|
+
const zoneValidationError = (0, validation_1.validateZoneConsistency)(layout);
|
|
56
57
|
if (zoneValidationError) {
|
|
57
58
|
throw new Error(zoneValidationError);
|
|
58
59
|
}
|
|
@@ -119,48 +120,3 @@ function maybeGetApplicationConfig() {
|
|
|
119
120
|
catch (_a) { }
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Validates that zones defined in YAML match zones used in the Handlebars template
|
|
124
|
-
* @param layout The layout definition containing zones and template
|
|
125
|
-
* @returns Error message if validation fails, null if validation passes
|
|
126
|
-
*/
|
|
127
|
-
function validateZoneConsistency(layout) {
|
|
128
|
-
const yamlZones = Object.keys(layout.zones || {});
|
|
129
|
-
const template = layout.template;
|
|
130
|
-
// If no template is provided, skip validation
|
|
131
|
-
if (!template) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
// Extract zone references from Handlebars template
|
|
135
|
-
// Look for patterns like {{#each zones.zoneName}}, {{zones.zoneName}}, {{#if zones.zoneName}}
|
|
136
|
-
const zonePattern = /\{\{(?:#(?:each|if)\s+)?zones\.(\w+)/g;
|
|
137
|
-
const templateZones = new Set();
|
|
138
|
-
let match;
|
|
139
|
-
// Loop through the template and extract the zone names
|
|
140
|
-
while ((match = zonePattern.exec(template)) !== null) {
|
|
141
|
-
templateZones.add(match[1]);
|
|
142
|
-
}
|
|
143
|
-
// Convert the set to an array
|
|
144
|
-
const templateZoneArray = Array.from(templateZones);
|
|
145
|
-
// Find zones defined in YAML but not used in template
|
|
146
|
-
const unusedYamlZones = yamlZones.filter(zone => !templateZones.has(zone));
|
|
147
|
-
// Find zones used in template but not defined in YAML
|
|
148
|
-
const undefinedTemplateZones = templateZoneArray.filter(zone => !yamlZones.includes(zone));
|
|
149
|
-
// Create an array of errors
|
|
150
|
-
const errors = [];
|
|
151
|
-
// Add the unused zones to the errors
|
|
152
|
-
if (unusedYamlZones.length > 0) {
|
|
153
|
-
errors.push(`Zones defined in YAML but not used in template: ${unusedYamlZones.join(', ')}`);
|
|
154
|
-
}
|
|
155
|
-
// Add the undefined zones to the errors
|
|
156
|
-
if (undefinedTemplateZones.length > 0) {
|
|
157
|
-
errors.push(`Zones used in template but not defined in YAML: ${undefinedTemplateZones.join(', ')}`);
|
|
158
|
-
}
|
|
159
|
-
// If there are errors, return the errors
|
|
160
|
-
if (errors.length > 0) {
|
|
161
|
-
return `Zone consistency validation failed:\n${errors
|
|
162
|
-
.map(err => ` - ${err}`)
|
|
163
|
-
.join('\n')}`;
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { validateZoneConsistency } from './zone-consistency';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateZoneConsistency = void 0;
|
|
4
|
+
var zone_consistency_1 = require("./zone-consistency");
|
|
5
|
+
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 zones defined in YAML match zones used in the Handlebars template
|
|
4
|
+
* @param layout The layout definition containing zones and template
|
|
5
|
+
* @returns Error message if validation fails, null if validation passes
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateZoneConsistency(layout: LayoutDefinition): string | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateZoneConsistency = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Validates that zones defined in YAML match zones used in the Handlebars template
|
|
6
|
+
* @param layout The layout definition containing zones and template
|
|
7
|
+
* @returns Error message if validation fails, null if validation passes
|
|
8
|
+
*/
|
|
9
|
+
function validateZoneConsistency(layout) {
|
|
10
|
+
const yamlZones = Object.keys(layout.zones || {});
|
|
11
|
+
const template = layout.template;
|
|
12
|
+
// If no template is provided, skip validation
|
|
13
|
+
if (!template) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
// Extract zone references from Handlebars template
|
|
17
|
+
// Look for patterns like {{#each zones.zoneName}}, {{zones.zoneName}}, {{#if zones.zoneName}}
|
|
18
|
+
const zonePattern = /\{\{(?:#(?:each|if)\s+)?zones\.(\w+)/g;
|
|
19
|
+
const templateZones = new Set();
|
|
20
|
+
let match;
|
|
21
|
+
// Loop through the template and extract the zone names
|
|
22
|
+
while ((match = zonePattern.exec(template)) !== null) {
|
|
23
|
+
templateZones.add(match[1]);
|
|
24
|
+
}
|
|
25
|
+
// Convert the set to an array
|
|
26
|
+
const templateZoneArray = Array.from(templateZones);
|
|
27
|
+
// Find zones defined in YAML but not used in template
|
|
28
|
+
const unusedYamlZones = yamlZones.filter(zone => !templateZones.has(zone));
|
|
29
|
+
// Find zones used in template but not defined in YAML
|
|
30
|
+
const undefinedTemplateZones = templateZoneArray.filter(zone => !yamlZones.includes(zone));
|
|
31
|
+
// Create an array of errors
|
|
32
|
+
const errors = [];
|
|
33
|
+
// Add the unused zones to the errors
|
|
34
|
+
if (unusedYamlZones.length > 0) {
|
|
35
|
+
errors.push(`Zones defined in YAML but not used in template: ${unusedYamlZones.join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
// Add the undefined zones to the errors
|
|
38
|
+
if (undefinedTemplateZones.length > 0) {
|
|
39
|
+
errors.push(`Zones used in template but not defined in YAML: ${undefinedTemplateZones.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
// If there are errors, return the errors
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
return `Zone consistency validation failed:\n${errors
|
|
44
|
+
.map(err => ` - ${err}`)
|
|
45
|
+
.join('\n')}`;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
exports.validateZoneConsistency = validateZoneConsistency;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const zone_consistency_1 = require("./zone-consistency");
|
|
4
|
+
describe('validateZoneConsistency', () => {
|
|
5
|
+
const createMockLayout = (zones, template) => ({
|
|
6
|
+
name: 'test-layout',
|
|
7
|
+
displayName: 'Test Layout',
|
|
8
|
+
description: 'A test layout',
|
|
9
|
+
zones,
|
|
10
|
+
template,
|
|
11
|
+
});
|
|
12
|
+
it('should return null for valid zone consistency', () => {
|
|
13
|
+
const layout = createMockLayout({
|
|
14
|
+
header: { displayName: 'Header', description: 'Header zone' },
|
|
15
|
+
content: { displayName: 'Content', description: 'Content zone' },
|
|
16
|
+
}, '<div>{{zones.header}}</div><div>{{zones.content}}</div>');
|
|
17
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
18
|
+
expect(result).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
it('should return null when no template is provided', () => {
|
|
21
|
+
const layout = createMockLayout({
|
|
22
|
+
header: { displayName: 'Header', description: 'Header zone' },
|
|
23
|
+
}, '');
|
|
24
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
25
|
+
expect(result).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
it('should detect zones defined in YAML but not used in template', () => {
|
|
28
|
+
const layout = createMockLayout({
|
|
29
|
+
header: { displayName: 'Header', description: 'Header zone' },
|
|
30
|
+
sidebar: { displayName: 'Sidebar', description: 'Sidebar zone' },
|
|
31
|
+
}, '<div>{{zones.header}}</div>');
|
|
32
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
33
|
+
expect(result).toContain('Zone consistency validation failed');
|
|
34
|
+
expect(result).toContain('Zones defined in YAML but not used in template: sidebar');
|
|
35
|
+
});
|
|
36
|
+
it('should detect zones used in template but not defined in YAML', () => {
|
|
37
|
+
const layout = createMockLayout({
|
|
38
|
+
header: { displayName: 'Header', description: 'Header zone' },
|
|
39
|
+
}, '<div>{{zones.header}}</div><div>{{zones.footer}}</div>');
|
|
40
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
41
|
+
expect(result).toContain('Zone consistency validation failed');
|
|
42
|
+
expect(result).toContain('Zones used in template but not defined in YAML: footer');
|
|
43
|
+
});
|
|
44
|
+
it('should detect both unused and undefined zones', () => {
|
|
45
|
+
const layout = createMockLayout({
|
|
46
|
+
header: { displayName: 'Header', description: 'Header zone' },
|
|
47
|
+
sidebar: { displayName: 'Sidebar', description: 'Sidebar zone' },
|
|
48
|
+
}, '<div>{{zones.header}}</div><div>{{zones.footer}}</div>');
|
|
49
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
50
|
+
expect(result).toContain('Zone consistency validation failed');
|
|
51
|
+
expect(result).toContain('Zones defined in YAML but not used in template: sidebar');
|
|
52
|
+
expect(result).toContain('Zones used in template but not defined in YAML: footer');
|
|
53
|
+
});
|
|
54
|
+
it('should handle Handlebars each loops', () => {
|
|
55
|
+
const layout = createMockLayout({
|
|
56
|
+
items: { displayName: 'Items', description: 'Items zone' },
|
|
57
|
+
}, '<div>{{#each zones.items}}<p>{{this}}</p>{{/each}}</div>');
|
|
58
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
59
|
+
expect(result).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
it('should handle Handlebars if conditions', () => {
|
|
62
|
+
const layout = createMockLayout({
|
|
63
|
+
optional: { displayName: 'Optional', description: 'Optional zone' },
|
|
64
|
+
}, '<div>{{#if zones.optional}}{{zones.optional}}{{/if}}</div>');
|
|
65
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
66
|
+
expect(result).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
it('should handle complex Handlebars patterns', () => {
|
|
69
|
+
const layout = createMockLayout({
|
|
70
|
+
header: { displayName: 'Header', description: 'Header zone' },
|
|
71
|
+
content: { displayName: 'Content', description: 'Content zone' },
|
|
72
|
+
sidebar: { displayName: 'Sidebar', description: 'Sidebar zone' },
|
|
73
|
+
}, `
|
|
74
|
+
<div>{{zones.header}}</div>
|
|
75
|
+
<div>
|
|
76
|
+
{{#if zones.sidebar}}
|
|
77
|
+
<aside>{{zones.sidebar}}</aside>
|
|
78
|
+
{{/if}}
|
|
79
|
+
<main>{{zones.content}}</main>
|
|
80
|
+
</div>
|
|
81
|
+
`);
|
|
82
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
83
|
+
expect(result).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
it('should handle empty zones object', () => {
|
|
86
|
+
const layout = createMockLayout({}, '<div>Static content only</div>');
|
|
87
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
88
|
+
expect(result).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
it('should handle undefined zones property', () => {
|
|
91
|
+
const layout = {
|
|
92
|
+
name: 'test-layout',
|
|
93
|
+
displayName: 'Test Layout',
|
|
94
|
+
description: 'A test layout',
|
|
95
|
+
zones: undefined,
|
|
96
|
+
template: '<div>Static content only</div>',
|
|
97
|
+
};
|
|
98
|
+
const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
|
|
99
|
+
expect(result).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2025, Squiz Australia Pty Ltd.
|
|
4
|
+
* All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Validates a template file and returns detailed results
|
|
8
|
+
* @param content - The template to validate
|
|
9
|
+
* @returns An array of errors
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateTemplateFile(content: string): string[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*!
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright (c) 2025, Squiz Australia Pty Ltd.
|
|
5
|
+
* All rights reserved.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.validateTemplateFile = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* Validates a template file and returns detailed results
|
|
11
|
+
* @param content - The template to validate
|
|
12
|
+
* @returns An array of errors
|
|
13
|
+
*/
|
|
14
|
+
function validateTemplateFile(content) {
|
|
15
|
+
const errors = [];
|
|
16
|
+
// Check for top-level HTML structure tags that should not be in body content
|
|
17
|
+
const topLevelTags = [
|
|
18
|
+
{ name: 'html', regex: /<html(\s|>)/gi },
|
|
19
|
+
{ name: 'head', regex: /<head(\s|>)/gi },
|
|
20
|
+
{ name: 'body', regex: /<body(\s|>)/gi },
|
|
21
|
+
{ name: 'doctype', regex: /<!DOCTYPE[^>]*>/gi },
|
|
22
|
+
];
|
|
23
|
+
for (const tag of topLevelTags) {
|
|
24
|
+
if (tag.regex.test(content)) {
|
|
25
|
+
errors.push(`Template should not contain top-level HTML structure tag: <${tag.name}>. Templates should only contain body content.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return errors;
|
|
29
|
+
}
|
|
30
|
+
exports.validateTemplateFile = validateTemplateFile;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const validation_1 = require("./validation");
|
|
4
|
+
describe('Handlebars Template Validation', () => {
|
|
5
|
+
describe('validateTemplateFile', () => {
|
|
6
|
+
it('should validate an empty template', () => {
|
|
7
|
+
const template = '';
|
|
8
|
+
const errors = (0, validation_1.validateTemplateFile)(template);
|
|
9
|
+
expect(errors).toHaveLength(0);
|
|
10
|
+
});
|
|
11
|
+
it('should validate a simple valid template', () => {
|
|
12
|
+
const template = '<div>{{zones.content}}</div>';
|
|
13
|
+
const errors = (0, validation_1.validateTemplateFile)(template);
|
|
14
|
+
expect(errors).toHaveLength(0);
|
|
15
|
+
});
|
|
16
|
+
it('should detect top-level HTML tags', () => {
|
|
17
|
+
const template = '<html><body><div>{{zones.content}}</div></body></html>';
|
|
18
|
+
const errors = (0, validation_1.validateTemplateFile)(template);
|
|
19
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
20
|
+
expect(errors).toEqual(expect.arrayContaining([
|
|
21
|
+
expect.stringContaining('Template should not contain top-level HTML structure tag: <html>'),
|
|
22
|
+
expect.stringContaining('Template should not contain top-level HTML structure tag: <body>'),
|
|
23
|
+
]));
|
|
24
|
+
});
|
|
25
|
+
it('should detect DOCTYPE declarations', () => {
|
|
26
|
+
const template = '<!DOCTYPE html><div>{{zones.content}}</div>';
|
|
27
|
+
const errors = (0, validation_1.validateTemplateFile)(template);
|
|
28
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
29
|
+
expect(errors).toEqual(expect.arrayContaining([
|
|
30
|
+
expect.stringContaining('Template should not contain top-level HTML structure tag: <doctype>'),
|
|
31
|
+
]));
|
|
32
|
+
});
|
|
33
|
+
it('should detect head tags', () => {
|
|
34
|
+
const template = '<head><title>Test</title></head><div>{{zones.content}}</div>';
|
|
35
|
+
const errors = (0, validation_1.validateTemplateFile)(template);
|
|
36
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
37
|
+
expect(errors).toEqual(expect.arrayContaining([
|
|
38
|
+
expect.stringContaining('Template should not contain top-level HTML structure tag: <head>'),
|
|
39
|
+
]));
|
|
40
|
+
});
|
|
41
|
+
it('should allow body content elements', () => {
|
|
42
|
+
const template = `
|
|
43
|
+
<div class="layout">
|
|
44
|
+
<header>{{zones.header}}</header>
|
|
45
|
+
<main>{{zones.content}}</main>
|
|
46
|
+
<aside>{{zones.sidebar}}</aside>
|
|
47
|
+
<footer>{{zones.footer}}</footer>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
const errors = (0, validation_1.validateTemplateFile)(template);
|
|
51
|
+
expect(errors).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
it('should allow complex Handlebars expressions', () => {
|
|
54
|
+
const template = `
|
|
55
|
+
<div class="layout">
|
|
56
|
+
{{#if zones.header}}
|
|
57
|
+
<header>{{zones.header}}</header>
|
|
58
|
+
{{/if}}
|
|
59
|
+
<main>{{zones.content}}</main>
|
|
60
|
+
{{#each zones.sidebar}}
|
|
61
|
+
<aside>{{this}}</aside>
|
|
62
|
+
{{/each}}
|
|
63
|
+
</div>
|
|
64
|
+
`;
|
|
65
|
+
const errors = (0, validation_1.validateTemplateFile)(template);
|
|
66
|
+
expect(errors).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
it('should allow all valid HTML body elements', () => {
|
|
69
|
+
const template = `
|
|
70
|
+
<div>{{zones.content}}</div>
|
|
71
|
+
<p>{{zones.text}}</p>
|
|
72
|
+
<span>{{zones.inline}}</span>
|
|
73
|
+
<section>{{zones.section}}</section>
|
|
74
|
+
<article>{{zones.article}}</article>
|
|
75
|
+
<nav>{{zones.navigation}}</nav>
|
|
76
|
+
<header>{{zones.header}}</header>
|
|
77
|
+
<footer>{{zones.footer}}</footer>
|
|
78
|
+
<aside>{{zones.sidebar}}</aside>
|
|
79
|
+
<main>{{zones.main}}</main>
|
|
80
|
+
<h1>{{zones.title}}</h1>
|
|
81
|
+
<img src="{{zones.image}}" alt="{{zones.alt}}">
|
|
82
|
+
<a href="{{zones.link}}">{{zones.linkText}}</a>
|
|
83
|
+
<ul>{{#each zones.list}}<li>{{this}}</li>{{/each}}</ul>
|
|
84
|
+
`;
|
|
85
|
+
const errors = (0, validation_1.validateTemplateFile)(template);
|
|
86
|
+
expect(errors).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2025, Squiz Australia Pty Ltd.
|
|
4
|
+
* All rights reserved.
|
|
5
|
+
*/
|
|
1
6
|
import { z } from 'zod';
|
|
2
7
|
export declare function loadLayoutDefinition(layoutFile: string): Promise<LayoutDefinition>;
|
|
3
8
|
export declare function loadLayoutFromFile(layoutFile: string): Promise<InputLayoutDefinition>;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/*!
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright (c) 2025, Squiz Australia Pty Ltd.
|
|
5
|
+
* All rights reserved.
|
|
6
|
+
*/
|
|
2
7
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
8
|
if (k2 === undefined) k2 = k;
|
|
4
9
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -44,10 +49,13 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
44
49
|
};
|
|
45
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
51
|
exports.LayoutDefinition = exports.InputLayoutDefinition = exports.BaseLayoutDefinition = exports.loadLayoutFromFile = exports.loadLayoutDefinition = void 0;
|
|
52
|
+
// External
|
|
47
53
|
const fs = __importStar(require("node:fs/promises"));
|
|
48
54
|
const path = __importStar(require("node:path"));
|
|
49
55
|
const zod_1 = require("zod");
|
|
50
56
|
const yaml_1 = require("yaml");
|
|
57
|
+
// Local
|
|
58
|
+
const validation_1 = require("../templates/validation");
|
|
51
59
|
function loadLayoutDefinition(layoutFile) {
|
|
52
60
|
return __awaiter(this, void 0, void 0, function* () {
|
|
53
61
|
try {
|
|
@@ -83,12 +91,24 @@ function loadLayoutFromFile(layoutFile) {
|
|
|
83
91
|
});
|
|
84
92
|
}
|
|
85
93
|
exports.loadLayoutFromFile = loadLayoutFromFile;
|
|
94
|
+
/**
|
|
95
|
+
* Loads a template file from a file system
|
|
96
|
+
* @param layoutDirectory - The path to the layout directory
|
|
97
|
+
* @param templateFile - The path to the template file
|
|
98
|
+
* @returns The template
|
|
99
|
+
*/
|
|
86
100
|
function loadTemplate(layoutDirectory, templateFile) {
|
|
87
101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
88
102
|
try {
|
|
89
|
-
|
|
103
|
+
const template = yield fs.readFile(path.resolve(layoutDirectory, templateFile), {
|
|
90
104
|
encoding: 'utf-8',
|
|
91
105
|
});
|
|
106
|
+
// Validate the template
|
|
107
|
+
const validationErrors = yield (0, validation_1.validateTemplateFile)(template);
|
|
108
|
+
if (validationErrors.length > 0) {
|
|
109
|
+
throw new Error(validationErrors.join('\n'));
|
|
110
|
+
}
|
|
111
|
+
return template;
|
|
92
112
|
}
|
|
93
113
|
catch (e) {
|
|
94
114
|
throw Error(`Failed loading template file "${templateFile}": ${e.message}`);
|
|
@@ -34,6 +34,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
const fs = __importStar(require("node:fs/promises"));
|
|
36
36
|
const definitions_1 = require("./definitions");
|
|
37
|
+
const validation_1 = require("../templates/validation");
|
|
37
38
|
jest.mock('node:fs/promises');
|
|
38
39
|
describe('loadLayoutDefinition', () => {
|
|
39
40
|
const paintLayoutFileYaml = './some-dir/page-layout.yaml';
|
|
@@ -436,3 +437,64 @@ describe('LayoutDefinitionParse', () => {
|
|
|
436
437
|
expect(layoutDefinition.zones.main.minNodes).toBe(0);
|
|
437
438
|
});
|
|
438
439
|
});
|
|
440
|
+
describe('validateTemplateFile', () => {
|
|
441
|
+
describe('valid templates', () => {
|
|
442
|
+
it('should validate a simple valid template', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
443
|
+
const template = '<div>{{zones.content}}</div>';
|
|
444
|
+
const errors = yield (0, validation_1.validateTemplateFile)(template);
|
|
445
|
+
expect(errors).toHaveLength(0);
|
|
446
|
+
}));
|
|
447
|
+
it('should validate a complex valid template', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
448
|
+
const template = `
|
|
449
|
+
<div class="layout">
|
|
450
|
+
{{#if zones.header}}
|
|
451
|
+
<header>{{zones.header}}</header>
|
|
452
|
+
{{/if}}
|
|
453
|
+
<main>{{zones.content}}</main>
|
|
454
|
+
{{#each zones.sidebar}}
|
|
455
|
+
<aside>{{this}}</aside>
|
|
456
|
+
{{/each}}
|
|
457
|
+
</div>
|
|
458
|
+
`;
|
|
459
|
+
const errors = yield (0, validation_1.validateTemplateFile)(template);
|
|
460
|
+
expect(errors).toHaveLength(0);
|
|
461
|
+
}));
|
|
462
|
+
it('should validate template with options', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
463
|
+
const template = '<div class="{{options.theme}}">{{zones.content}}</div>';
|
|
464
|
+
const errors = yield (0, validation_1.validateTemplateFile)(template);
|
|
465
|
+
expect(errors).toHaveLength(0);
|
|
466
|
+
}));
|
|
467
|
+
it('should allow empty template', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
468
|
+
const template = '';
|
|
469
|
+
const errors = yield (0, validation_1.validateTemplateFile)(template);
|
|
470
|
+
expect(errors).toHaveLength(0);
|
|
471
|
+
}));
|
|
472
|
+
});
|
|
473
|
+
describe('invalid templates', () => {
|
|
474
|
+
it('should detect top-level HTML tags', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
475
|
+
const template = '<html><body>{{zones.content}}</body></html>';
|
|
476
|
+
const errors = yield (0, validation_1.validateTemplateFile)(template);
|
|
477
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
478
|
+
expect(errors).toEqual(expect.arrayContaining([
|
|
479
|
+
expect.stringContaining('Template should not contain top-level HTML structure tag: <html>'),
|
|
480
|
+
expect.stringContaining('Template should not contain top-level HTML structure tag: <body>'),
|
|
481
|
+
]));
|
|
482
|
+
}));
|
|
483
|
+
it('should detect DOCTYPE declarations', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
484
|
+
const template = '<!DOCTYPE html><div>{{zones.content}}</div>';
|
|
485
|
+
const errors = yield (0, validation_1.validateTemplateFile)(template);
|
|
486
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
487
|
+
expect(errors).toEqual(expect.arrayContaining([
|
|
488
|
+
expect.stringContaining('Template should not contain top-level HTML structure tag: <doctype>'),
|
|
489
|
+
]));
|
|
490
|
+
}));
|
|
491
|
+
});
|
|
492
|
+
describe('error handling', () => {
|
|
493
|
+
it('should handle null template gracefully', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
494
|
+
// Test with a null template
|
|
495
|
+
const template = null;
|
|
496
|
+
const errors = yield (0, validation_1.validateTemplateFile)(template);
|
|
497
|
+
expect(errors).toHaveLength(0);
|
|
498
|
+
}));
|
|
499
|
+
});
|
|
500
|
+
});
|