gscan 6.2.0 → 6.3.0
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.
|
@@ -6,7 +6,7 @@ module.exports = class NoUnknownCustomThemeSettings extends Rule {
|
|
|
6
6
|
const name = node.parts[1];
|
|
7
7
|
if (!this.isValidCustomThemeSettingReference(name)) {
|
|
8
8
|
this.log({
|
|
9
|
-
message: `
|
|
9
|
+
message: `Unknown {{@custom.${name}}} on line ${node.loc && node.loc.start.line}: ${this.sourceForNode(node)}`,
|
|
10
10
|
line: node.loc && node.loc.start.line,
|
|
11
11
|
column: node.loc && node.loc.start.column,
|
|
12
12
|
source: this.sourceForNode(node)
|
|
@@ -26,12 +26,12 @@ const ruleImplementations = {
|
|
|
26
26
|
const config = Object.keys(theme.customSettings);
|
|
27
27
|
const notUsedVariable = config.filter(x => !result.customThemeSettings.has(x));
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
notUsedVariable.forEach((name) => {
|
|
30
30
|
log.failure({
|
|
31
|
-
message: `
|
|
31
|
+
message: `config.custom.${name} is declared but never referenced from a template`,
|
|
32
32
|
ref: 'package.json'
|
|
33
33
|
});
|
|
34
|
-
}
|
|
34
|
+
});
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const spec = require('../specs');
|
|
4
|
+
const {versions, normalizePath} = require('../utils');
|
|
5
|
+
|
|
6
|
+
const ruleCode = 'GS130-NO-RECURSIVE-LAYOUT';
|
|
7
|
+
const layoutPattern = /{{!<\s+([A-Za-z0-9._/-]+)\s*}}/;
|
|
8
|
+
|
|
9
|
+
function ensureExtension(layoutPath) {
|
|
10
|
+
if (path.posix.extname(layoutPath)) {
|
|
11
|
+
return layoutPath;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return `${layoutPath}.hbs`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveLayoutPath(sourceFile, layoutName) {
|
|
18
|
+
const source = normalizePath(sourceFile);
|
|
19
|
+
const layout = ensureExtension(normalizePath(layoutName));
|
|
20
|
+
|
|
21
|
+
// Ghost resolves layouts with path.resolve(templateDir, layout), so a leading
|
|
22
|
+
// slash is filesystem-absolute and never resolves to a theme file
|
|
23
|
+
if (layout.startsWith('/')) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return path.posix.normalize(path.posix.join(path.posix.dirname(source), layout));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getLocation(source, index) {
|
|
31
|
+
const lines = source.slice(0, index).split('\n');
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
line: lines.length,
|
|
35
|
+
column: lines[lines.length - 1].length
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getLayoutReference(themeFile) {
|
|
40
|
+
const source = normalizePath(themeFile.normalizedFile || themeFile.file);
|
|
41
|
+
const content = themeFile.content || '';
|
|
42
|
+
const match = content.match(layoutPattern);
|
|
43
|
+
|
|
44
|
+
if (!match) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const target = resolveLayoutPath(source, match[1]);
|
|
49
|
+
|
|
50
|
+
if (!target) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
source,
|
|
56
|
+
target,
|
|
57
|
+
location: getLocation(content, match.index)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildInheritanceGraph(theme) {
|
|
62
|
+
const hbsFiles = theme.files.filter(file => file.ext === '.hbs');
|
|
63
|
+
const existingFiles = new Set(hbsFiles.map(file => normalizePath(file.normalizedFile || file.file)));
|
|
64
|
+
const graph = new Map();
|
|
65
|
+
|
|
66
|
+
hbsFiles.forEach((themeFile) => {
|
|
67
|
+
const reference = getLayoutReference(themeFile);
|
|
68
|
+
|
|
69
|
+
if (!reference || !existingFiles.has(reference.target)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
graph.set(reference.source, reference);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return graph;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getCyclePath(stack, target) {
|
|
80
|
+
const targetIndex = stack.indexOf(target);
|
|
81
|
+
return stack.slice(targetIndex).concat(target).join(' -> ');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getRecursiveLayoutFailures(graph) {
|
|
85
|
+
const visited = new Set();
|
|
86
|
+
const visiting = new Set();
|
|
87
|
+
const stack = [];
|
|
88
|
+
const reportedCycles = new Set();
|
|
89
|
+
const failures = [];
|
|
90
|
+
|
|
91
|
+
function visit(file) {
|
|
92
|
+
visiting.add(file);
|
|
93
|
+
stack.push(file);
|
|
94
|
+
|
|
95
|
+
const reference = graph.get(file);
|
|
96
|
+
|
|
97
|
+
if (!reference) {
|
|
98
|
+
stack.pop();
|
|
99
|
+
visiting.delete(file);
|
|
100
|
+
visited.add(file);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (visiting.has(reference.target)) {
|
|
105
|
+
const cyclePath = getCyclePath(stack, reference.target);
|
|
106
|
+
|
|
107
|
+
if (!reportedCycles.has(cyclePath)) {
|
|
108
|
+
reportedCycles.add(cyclePath);
|
|
109
|
+
|
|
110
|
+
failures.push({
|
|
111
|
+
ref: reference.source,
|
|
112
|
+
line: reference.location.line,
|
|
113
|
+
column: reference.location.column,
|
|
114
|
+
message: `Recursive layout inheritance detected: ${cyclePath}`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} else if (!visited.has(reference.target)) {
|
|
118
|
+
visit(reference.target);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
stack.pop();
|
|
122
|
+
visiting.delete(file);
|
|
123
|
+
visited.add(file);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
graph.forEach((_references, file) => {
|
|
127
|
+
if (!visited.has(file)) {
|
|
128
|
+
visit(file);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return failures;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const checkTemplateInheritance = function checkTemplateInheritance(theme, options) {
|
|
136
|
+
const checkVersion = _.get(options, 'checkVersion', versions.default);
|
|
137
|
+
const ruleSet = spec.get([checkVersion]);
|
|
138
|
+
|
|
139
|
+
if (!ruleSet.rules[ruleCode]) {
|
|
140
|
+
return theme;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const failures = getRecursiveLayoutFailures(buildInheritanceGraph(theme));
|
|
144
|
+
|
|
145
|
+
if (failures.length > 0) {
|
|
146
|
+
theme.results.fail[ruleCode] = {failures};
|
|
147
|
+
} else {
|
|
148
|
+
theme.results.pass.push(ruleCode);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return theme;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
module.exports = checkTemplateInheritance;
|
package/lib/specs/v4.js
CHANGED
|
@@ -172,9 +172,9 @@ let rules = {
|
|
|
172
172
|
},
|
|
173
173
|
'GS090-NO-UNKNOWN-CUSTOM-THEME-SETTINGS': {
|
|
174
174
|
level: 'error',
|
|
175
|
-
rule: 'An unknown custom theme setting has been used.',
|
|
176
175
|
fatal: false,
|
|
177
|
-
|
|
176
|
+
rule: 'Remove or correct the unknown <code>{{@custom}}</code> setting',
|
|
177
|
+
details: oneLineTrim`Every <code>{{@custom.<i>name</i>}}</code> reference in a template needs a matching entry in <code>config.custom</code> in <code>package.json</code>. An unrecognised setting renders as empty. See the <a href="${docsBaseUrl}custom-settings/" target=_blank>custom settings documentation</a> for details.`
|
|
178
178
|
},
|
|
179
179
|
'GS090-NO-UNKNOWN-CUSTOM-THEME-SELECT-VALUE-IN-MATCH': {
|
|
180
180
|
level: 'error',
|
|
@@ -209,8 +209,8 @@ let rules = {
|
|
|
209
209
|
|
|
210
210
|
'GS100-NO-UNUSED-CUSTOM-THEME-SETTING': {
|
|
211
211
|
level: 'error',
|
|
212
|
-
rule: '
|
|
213
|
-
details: oneLineTrim`
|
|
212
|
+
rule: 'Use or remove the unused <code>config.custom</code> setting',
|
|
213
|
+
details: oneLineTrim`A setting declared in <code>config.custom</code> in <code>package.json</code> appears in the admin Customize panel but has no effect unless referenced from a template with <code>{{@custom.<i>name</i>}}</code>. Either use the setting from a template or drop it from <code>package.json</code>. See the <a href="${docsBaseUrl}custom-settings/" target=_blank>custom settings documentation</a> for details.`
|
|
214
214
|
},
|
|
215
215
|
|
|
216
216
|
'GS050-CSS-KGCO': cssCardRule('callout', 'kg-callout-card'),
|
package/lib/specs/v6.js
CHANGED
|
@@ -48,6 +48,12 @@ let rules = {
|
|
|
48
48
|
details: 'AMP support was removed in Ghost 6.0. Remove AMP templates and use responsive design instead.',
|
|
49
49
|
// Matches <html amp> or <html ⚡>, with or without other attributes mixed in
|
|
50
50
|
regex: /<html\s+(?:amp|⚡)(?:\s|>)|<html\s+[^>]*\s(?:amp|⚡)(?:\s|>)/i
|
|
51
|
+
},
|
|
52
|
+
'GS130-NO-RECURSIVE-LAYOUT': {
|
|
53
|
+
level: 'error',
|
|
54
|
+
fatal: true,
|
|
55
|
+
rule: 'Templates must not recursively inherit layouts',
|
|
56
|
+
details: oneLineTrim`Remove recursive layout inheritance such as <code>{{!< default}}</code> from <code>default.hbs</code>. A template cannot inherit from itself, directly or through another layout, because it can cause rendering to recurse indefinitely.`
|
|
51
57
|
}
|
|
52
58
|
};
|
|
53
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gscan",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ghost",
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
"gscan": "./bin/cli.js"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@sentry/node": "10.
|
|
47
|
-
"@tryghost/config": "2.2.
|
|
48
|
-
"@tryghost/debug": "2.2.
|
|
49
|
-
"@tryghost/errors": "3.2.
|
|
50
|
-
"@tryghost/logging": "
|
|
51
|
-
"@tryghost/nql": "0.12.
|
|
52
|
-
"@tryghost/pretty-cli": "3.2.
|
|
53
|
-
"@tryghost/server": "
|
|
54
|
-
"@tryghost/zip": "3.3.
|
|
46
|
+
"@sentry/node": "10.56.0",
|
|
47
|
+
"@tryghost/config": "2.2.2",
|
|
48
|
+
"@tryghost/debug": "2.2.2",
|
|
49
|
+
"@tryghost/errors": "3.2.3",
|
|
50
|
+
"@tryghost/logging": "5.0.2",
|
|
51
|
+
"@tryghost/nql": "0.12.11",
|
|
52
|
+
"@tryghost/pretty-cli": "3.2.2",
|
|
53
|
+
"@tryghost/server": "3.0.2",
|
|
54
|
+
"@tryghost/zip": "3.3.3",
|
|
55
55
|
"chalk": "5.6.2",
|
|
56
56
|
"express": "5.2.1",
|
|
57
57
|
"express-handlebars": "8.0.1",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"handlebars": "4.7.9",
|
|
60
60
|
"lodash": "4.18.1",
|
|
61
61
|
"multer": "2.1.1",
|
|
62
|
-
"semver": "7.8.
|
|
62
|
+
"semver": "7.8.2",
|
|
63
63
|
"validator": "^13.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
@@ -67,16 +67,11 @@
|
|
|
67
67
|
"@eslint/eslintrc": "3.3.5",
|
|
68
68
|
"@eslint/js": "10.0.1",
|
|
69
69
|
"@tryghost/pro-ship": "1.0.10",
|
|
70
|
-
"@vitest/coverage-v8": "4.1.
|
|
71
|
-
"eslint": "10.4.
|
|
70
|
+
"@vitest/coverage-v8": "4.1.8",
|
|
71
|
+
"eslint": "10.4.1",
|
|
72
72
|
"eslint-plugin-ghost": "3.5.0",
|
|
73
73
|
"nodemon": "3.1.14",
|
|
74
|
-
"vitest": "4.1.
|
|
75
|
-
},
|
|
76
|
-
"resolutions": {
|
|
77
|
-
"node-loggly-bulk": "4.0.2",
|
|
78
|
-
"node-loggly-bulk/axios": "1.16.0",
|
|
79
|
-
"**/handlebars": "4.7.9"
|
|
74
|
+
"vitest": "4.1.8"
|
|
80
75
|
},
|
|
81
76
|
"files": [
|
|
82
77
|
"lib",
|