gscan 5.3.1 → 5.3.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/app/middlewares/log-request.js +4 -4
- package/app/tpl/layouts/default.hbs +3 -3
- package/bin/cli.js +1 -1
- package/lib/ast-linter/helpers/index.js +2 -2
- package/lib/ast-linter/linter.js +8 -20
- package/lib/ast-linter/rules/internal/scope.js +2 -0
- package/lib/ast-linter/rules/lint-no-unknown-custom-theme-select-value-in-match.js +1 -8
- package/lib/checker.js +2 -4
- package/lib/checks/002-comment-id.js +1 -1
- package/lib/checks/080-helper-usage.js +1 -1
- package/lib/checks/100-custom-template-settings-usage.js +1 -128
- package/lib/checks/110-page-builder-usage.js +1 -130
- package/lib/checks/120-no-unknown-globals.js +0 -1
- package/lib/faker/index.js +5 -5
- package/lib/format.js +7 -7
- package/lib/read-theme.js +33 -31
- package/lib/specs/v1.js +1 -1
- package/lib/utils/check-utils.js +137 -0
- package/lib/utils/labs-enabled-helpers.js +16 -0
- package/lib/utils/score-calculator.js +1 -1
- package/package.json +19 -19
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const randomUUID = require('crypto').randomUUID;
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @TODO:
|
|
6
6
|
* - move middleware to ignition?
|
|
7
7
|
*/
|
|
8
8
|
module.exports = function logRequest(req, res, next) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const startTime = Date.now();
|
|
10
|
+
const requestId = randomUUID();
|
|
11
11
|
|
|
12
12
|
function logResponse() {
|
|
13
13
|
res.responseTime = (Date.now() - startTime) + 'ms';
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
</div>
|
|
59
59
|
<div class="gh-navbar-right">
|
|
60
|
-
<a class="gh-navbar-item" href="https://ghost.org/
|
|
60
|
+
<a class="gh-navbar-item" href="https://docs.ghost.org/themes/" target="blank" rel="noopener">Theme Docs</a>
|
|
61
61
|
<a class="gh-navbar-btn gh-btn" href="https://ghost.org/"><span>Ghost.org</span></a>
|
|
62
62
|
</div>
|
|
63
63
|
</nav>
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
</nav>
|
|
100
100
|
<div class="gh-mobilemenu">
|
|
101
101
|
<a class="gh-navbar-item" href="https://gscan.ghost.org">GScan</a>
|
|
102
|
-
<a class="gh-navbar-item" href="https://ghost.org/
|
|
102
|
+
<a class="gh-navbar-item" href="https://docs.ghost.org/themes/">Theme Docs</a>
|
|
103
103
|
<a class="gh-navbar-item" href="https://ghost.org/">Ghost.org</a>
|
|
104
104
|
</div>
|
|
105
105
|
</header>
|
|
@@ -191,4 +191,4 @@ var dynamicContent = getParameterByName('s');
|
|
|
191
191
|
});
|
|
192
192
|
</script>
|
|
193
193
|
</body>
|
|
194
|
-
</html>
|
|
194
|
+
</html>
|
package/bin/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ process.removeAllListeners('warning');
|
|
|
6
6
|
const prettyCLI = require('@tryghost/pretty-cli');
|
|
7
7
|
const ui = require('@tryghost/pretty-cli').ui;
|
|
8
8
|
const _ = require('lodash');
|
|
9
|
-
const chalk = require('chalk');
|
|
9
|
+
const {default: chalk} = require('chalk');
|
|
10
10
|
const gscan = require('../lib');
|
|
11
11
|
const ghostVersions = require('../lib/utils').versions;
|
|
12
12
|
|
|
@@ -49,8 +49,8 @@ function blockParamIndex(name, options) {
|
|
|
49
49
|
depth < len;
|
|
50
50
|
depth++
|
|
51
51
|
) {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const blockParams = options.blockParams[depth];
|
|
53
|
+
const param = blockParams && blockParams.indexOf(name);
|
|
54
54
|
if (blockParams && param >= 0) {
|
|
55
55
|
return [depth, param];
|
|
56
56
|
}
|
package/lib/ast-linter/linter.js
CHANGED
|
@@ -144,28 +144,16 @@ class Linter {
|
|
|
144
144
|
|
|
145
145
|
scanner.accept(options.parsed.ast);
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
this.partials = scanner.context.partials;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (scanner.context.helpers) {
|
|
152
|
-
this.helpers = scanner.context.helpers.map(p => ({
|
|
153
|
-
name: p.node,
|
|
154
|
-
helperType: p.helperType
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
147
|
+
this.partials = scanner.context.partials;
|
|
157
148
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
149
|
+
this.helpers = scanner.context.helpers.map(p => ({
|
|
150
|
+
name: p.node,
|
|
151
|
+
helperType: p.helperType
|
|
152
|
+
}));
|
|
161
153
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (scanner.context.usedPageProperties) {
|
|
167
|
-
this.usedPageProperties = scanner.context.usedPageProperties;
|
|
168
|
-
}
|
|
154
|
+
this.customThemeSettings = scanner.context.customThemeSettings;
|
|
155
|
+
this.inlinePartials = scanner.context.inlinePartials;
|
|
156
|
+
this.usedPageProperties = scanner.context.usedPageProperties;
|
|
169
157
|
|
|
170
158
|
return messages;
|
|
171
159
|
}
|
|
@@ -51,6 +51,7 @@ const contexts = {
|
|
|
51
51
|
pagination: {}
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
/* v8 ignore start */
|
|
54
55
|
const helpers = { // eslint-disable-line no-unused-vars
|
|
55
56
|
// {{get}} can return multiple different contexts depending on params
|
|
56
57
|
get(node) {
|
|
@@ -91,6 +92,7 @@ const helpers = { // eslint-disable-line no-unused-vars
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
};
|
|
95
|
+
/* v8 ignore stop */
|
|
94
96
|
|
|
95
97
|
function getTemplateContext(fileName) {
|
|
96
98
|
if (fileName.match(/^post(-.*)?\.hbs/)) {
|
|
@@ -17,14 +17,7 @@ function getCustomSettingValue(node) {
|
|
|
17
17
|
module.exports = class NoUnknownCustomThemeSelectValueInMatch extends Rule {
|
|
18
18
|
_checkForCustomThemeSelectValueInMatch(node) {
|
|
19
19
|
if (node.path.original === 'match' && node.params.length === 3) {
|
|
20
|
-
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
let indexSecondParameter = 1; // Case {{match @custom.example "123"}}
|
|
25
|
-
if (node.params.length === 3) {
|
|
26
|
-
indexSecondParameter = 2; // Case {{match @custom.example "!=" "123"}}
|
|
27
|
-
}
|
|
20
|
+
const indexSecondParameter = 2; // Case {{match @custom.example "!=" "123"}}
|
|
28
21
|
const setting = getCustomSettingName(node.params[0]) || getCustomSettingName(node.params[indexSecondParameter]);
|
|
29
22
|
const value = getCustomSettingValue(node.params[0]) || getCustomSettingValue(node.params[indexSecondParameter]);
|
|
30
23
|
|
package/lib/checker.js
CHANGED
|
@@ -6,10 +6,8 @@ const debug = require('@tryghost/debug')('checker');
|
|
|
6
6
|
const errors = require('@tryghost/errors');
|
|
7
7
|
const versions = require('./utils').versions;
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
const labsEnabledHelpers = {
|
|
12
|
-
};
|
|
9
|
+
// See lib/utils/labs-enabled-helpers.js to add/remove labs-flagged helpers
|
|
10
|
+
const labsEnabledHelpers = require('./utils/labs-enabled-helpers');
|
|
13
11
|
|
|
14
12
|
function loadChecks() {
|
|
15
13
|
const checksDir = nodePath.join(__dirname, 'checks');
|
|
@@ -16,7 +16,7 @@ const checkCommentID = function checkCommentID(theme, options) {
|
|
|
16
16
|
});
|
|
17
17
|
_.each(ruleSet, function (check, ruleCode) {
|
|
18
18
|
_.each(theme.files, function (themeFile) {
|
|
19
|
-
|
|
19
|
+
const template = themeFile.file.match(/\.hbs$/);
|
|
20
20
|
|
|
21
21
|
if (template) {
|
|
22
22
|
if (themeFile.content.match(check.regex)) {
|
|
@@ -17,7 +17,7 @@ module.exports = function checkUsage(theme, options) {
|
|
|
17
17
|
});
|
|
18
18
|
_.each(ruleSet, function (check, ruleCode) {
|
|
19
19
|
_.each(theme.files, function (themeFile) {
|
|
20
|
-
|
|
20
|
+
const template = themeFile.file.match(/\.hbs$/);
|
|
21
21
|
const validApi = check.validInAPI ? check.validInAPI.includes(targetApiVersion) : true;
|
|
22
22
|
if (template) {
|
|
23
23
|
if (validApi && themeFile.content.match(check.regex)) {
|
|
@@ -1,132 +1,5 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const
|
|
3
|
-
const {versions} = require('../utils');
|
|
4
|
-
const ASTLinter = require('../ast-linter');
|
|
5
|
-
|
|
6
|
-
function getRules(id, options) {
|
|
7
|
-
const checkVersion = _.get(options, 'checkVersion', versions.default);
|
|
8
|
-
let ruleSet = spec.get([checkVersion]);
|
|
9
|
-
|
|
10
|
-
const ruleRegex = new RegExp('^' + id + '-.*', 'g');
|
|
11
|
-
ruleSet = _.pickBy(ruleSet.rules, function (rule, ruleCode) {
|
|
12
|
-
if (ruleCode.match(ruleRegex)) {
|
|
13
|
-
return rule;
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
return ruleSet;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getLogger({theme, rule, file = null}) {
|
|
21
|
-
return {
|
|
22
|
-
failure: (content) => {
|
|
23
|
-
if (!theme.results.fail[rule.code]) {
|
|
24
|
-
theme.results.fail[rule.code] = {failures: []};
|
|
25
|
-
}
|
|
26
|
-
const failure = {
|
|
27
|
-
...content,
|
|
28
|
-
rule: rule.code
|
|
29
|
-
};
|
|
30
|
-
if (file) {
|
|
31
|
-
failure.ref = file.file;
|
|
32
|
-
}
|
|
33
|
-
theme.results.fail[rule.code].failures.push(failure);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function applyRule(rule, theme) {
|
|
39
|
-
const partialVerificationCache = new Map();
|
|
40
|
-
// The result variable is passed around to keep a state through the full lifecycle
|
|
41
|
-
const result = {};
|
|
42
|
-
try {
|
|
43
|
-
// Check if the rule is enabled (optional)
|
|
44
|
-
if (typeof rule.isEnabled === 'function') {
|
|
45
|
-
if (!rule.isEnabled({theme, log: getLogger({theme, rule}), result})) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
} else if (typeof rule.isEnabled === 'boolean' && !rule.isEnabled) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Initialize the rule (optional)
|
|
53
|
-
if (typeof rule.init === 'function') {
|
|
54
|
-
rule.init({theme, log: getLogger({theme, rule}), result});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Run the main function on each theme file (optional)
|
|
58
|
-
if (typeof rule.eachFile === 'function') {
|
|
59
|
-
_.each(theme.files , function (themeFile) {
|
|
60
|
-
rule.eachFile({file: themeFile, theme, log: getLogger({theme, rule}), result, partialVerificationCache});
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Run the final function
|
|
65
|
-
if (typeof rule.done === 'function') {
|
|
66
|
-
rule.done({theme, log: getLogger({theme, rule}), result});
|
|
67
|
-
}
|
|
68
|
-
} catch (e) {
|
|
69
|
-
// Output something instead of failing silently (should never happen)
|
|
70
|
-
// eslint-disable-next-line
|
|
71
|
-
console.error('gscan failure', e);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function parseWithAST({theme, log, file, rules, callback, partialVerificationCache}){
|
|
76
|
-
const linter = new ASTLinter();
|
|
77
|
-
|
|
78
|
-
// This rule is needed to find partials
|
|
79
|
-
// Partials are needed for a full parsing
|
|
80
|
-
if (!rules['mark-used-partials']) {
|
|
81
|
-
rules['mark-used-partials'] = require(`../ast-linter/rules/mark-used-partials`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function processFile(themeFile) {
|
|
85
|
-
if (themeFile.parsed.error) {
|
|
86
|
-
// Ignore parsing errors, they are handled in 005
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const fileName = themeFile.file;
|
|
91
|
-
// Check if the file is a partial
|
|
92
|
-
const isPartial = fileName.startsWith('partials/');
|
|
93
|
-
// Skip if already cached (for partials only)
|
|
94
|
-
if (isPartial && partialVerificationCache.has(fileName)) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const astResults = linter.verify({
|
|
99
|
-
parsed: themeFile.parsed,
|
|
100
|
-
rules,
|
|
101
|
-
source: themeFile.content,
|
|
102
|
-
moduleId: themeFile.file
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Cache the result for this partial
|
|
106
|
-
if (isPartial) {
|
|
107
|
-
partialVerificationCache.set(fileName, astResults);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (astResults.length) {
|
|
111
|
-
log.failure({
|
|
112
|
-
message: astResults[0].message
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (typeof callback === 'function') {
|
|
117
|
-
callback(linter);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
linter.partials.forEach(({normalizedName}) => {
|
|
121
|
-
const partialFile = theme.files.find(f => f.normalizedFile === `partials/${normalizedName}.hbs`);
|
|
122
|
-
if (partialFile) {
|
|
123
|
-
processFile(partialFile);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return processFile(file);
|
|
129
|
-
}
|
|
2
|
+
const {getRules, applyRule, parseWithAST} = require('../utils/check-utils');
|
|
130
3
|
|
|
131
4
|
const ruleImplementations = {
|
|
132
5
|
'GS100-NO-UNUSED-CUSTOM-THEME-SETTING': {
|
|
@@ -1,134 +1,5 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const
|
|
3
|
-
const {versions} = require('../utils');
|
|
4
|
-
const ASTLinter = require('../ast-linter');
|
|
5
|
-
|
|
6
|
-
function getRules(id, options) {
|
|
7
|
-
const checkVersion = _.get(options, 'checkVersion', versions.default);
|
|
8
|
-
let ruleSet = spec.get([checkVersion]);
|
|
9
|
-
|
|
10
|
-
const ruleRegex = new RegExp('^' + id + '-.*', 'g');
|
|
11
|
-
ruleSet = _.pickBy(ruleSet.rules, function (rule, ruleCode) {
|
|
12
|
-
if (ruleCode.match(ruleRegex)) {
|
|
13
|
-
return rule;
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
return ruleSet;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getLogger({theme, rule, file = null}) {
|
|
21
|
-
return {
|
|
22
|
-
failure: (content) => {
|
|
23
|
-
if (!theme.results.fail[rule.code]) {
|
|
24
|
-
theme.results.fail[rule.code] = {failures: []};
|
|
25
|
-
}
|
|
26
|
-
const failure = {
|
|
27
|
-
...content,
|
|
28
|
-
rule: rule.code
|
|
29
|
-
};
|
|
30
|
-
if (file) {
|
|
31
|
-
failure.ref = file.file;
|
|
32
|
-
}
|
|
33
|
-
theme.results.fail[rule.code].failures.push(failure);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function applyRule(rule, theme) {
|
|
39
|
-
const partialVerificationCache = new Map();
|
|
40
|
-
|
|
41
|
-
// The result variable is passed around to keep a state through the full lifecycle
|
|
42
|
-
const result = {};
|
|
43
|
-
try {
|
|
44
|
-
// Check if the rule is enabled (optional)
|
|
45
|
-
if (typeof rule.isEnabled === 'function') {
|
|
46
|
-
if (!rule.isEnabled({theme, log: getLogger({theme, rule}), result, options: rule.options})) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
} else if (typeof rule.isEnabled === 'boolean' && !rule.isEnabled) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Initialize the rule (optional)
|
|
54
|
-
if (typeof rule.init === 'function') {
|
|
55
|
-
rule.init({theme, log: getLogger({theme, rule}), result});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Run the main function on each theme file (optional)
|
|
59
|
-
if (typeof rule.eachFile === 'function') {
|
|
60
|
-
_.each(theme.files, function (themeFile) {
|
|
61
|
-
rule.eachFile({file: themeFile, theme, log: getLogger({theme, rule}), result, partialVerificationCache});
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Run the final function
|
|
66
|
-
if (typeof rule.done === 'function') {
|
|
67
|
-
rule.done({theme, log: getLogger({theme, rule}), result});
|
|
68
|
-
}
|
|
69
|
-
} catch (e) {
|
|
70
|
-
// Output something instead of failing silently (should never happen)
|
|
71
|
-
// eslint-disable-next-line
|
|
72
|
-
console.error('gscan failure', e);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function parseWithAST({theme, log, file, rules, callback, partialVerificationCache}) {
|
|
77
|
-
const linter = new ASTLinter();
|
|
78
|
-
|
|
79
|
-
// This rule is needed to find partials
|
|
80
|
-
// Partials are needed for a full parsing
|
|
81
|
-
if (!rules['mark-used-partials']) {
|
|
82
|
-
rules['mark-used-partials'] = require(`../ast-linter/rules/mark-used-partials`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function processFile(themeFile) {
|
|
86
|
-
if (themeFile.parsed.error) {
|
|
87
|
-
// Ignore parsing errors, they are handled in 005
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const fileName = themeFile.file;
|
|
92
|
-
// Check if the file is a partial
|
|
93
|
-
const isPartial = fileName.startsWith('partials/');
|
|
94
|
-
// Skip if already cached (for partials only)
|
|
95
|
-
if (isPartial && partialVerificationCache.has(fileName)) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const astResults = linter.verify({
|
|
100
|
-
parsed: themeFile.parsed,
|
|
101
|
-
rules,
|
|
102
|
-
source: themeFile.content,
|
|
103
|
-
moduleId: themeFile.file
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Cache the result for this partial
|
|
107
|
-
if (isPartial) {
|
|
108
|
-
partialVerificationCache.set(fileName, astResults);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (astResults.length) {
|
|
112
|
-
log.failure({
|
|
113
|
-
message: astResults[0].message,
|
|
114
|
-
ref: themeFile.file
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (typeof callback === 'function') {
|
|
119
|
-
callback(linter);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
linter.partials.forEach(({normalizedName}) => {
|
|
123
|
-
const partialFile = theme.files.find(f => f.normalizedFile === `partials/${normalizedName}.hbs`);
|
|
124
|
-
if (partialFile) {
|
|
125
|
-
processFile(partialFile);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return processFile(file);
|
|
131
|
-
}
|
|
2
|
+
const {getRules, applyRule, parseWithAST} = require('../utils/check-utils');
|
|
132
3
|
|
|
133
4
|
const ruleImplementations = {
|
|
134
5
|
'GS110-NO-MISSING-PAGE-BUILDER-USAGE': {
|
package/lib/faker/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const fakeData = function fakeData(themeFile) {
|
|
2
|
+
const tag = {
|
|
3
3
|
id: '345678901',
|
|
4
4
|
name: 'hill',
|
|
5
5
|
description: 'tag for hill',
|
|
@@ -9,7 +9,7 @@ var fakeData = function fakeData(themeFile) {
|
|
|
9
9
|
url: 'http://talltalesofhighhills.com/tag/hill'
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const author = {
|
|
13
13
|
id: '234567890',
|
|
14
14
|
name: 'John McHill',
|
|
15
15
|
slug: 'john-mchill',
|
|
@@ -23,7 +23,7 @@ var fakeData = function fakeData(themeFile) {
|
|
|
23
23
|
url: 'http:///talltalesofhighhills.com/author/john-mchill'
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
const post = {
|
|
27
27
|
id: '123456789',
|
|
28
28
|
title: 'The highs and lows of hills',
|
|
29
29
|
slug: 'the-highs-and-low-of-hills',
|
|
@@ -43,7 +43,7 @@ var fakeData = function fakeData(themeFile) {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
// Initialise data for index/home hbs templates
|
|
46
|
-
|
|
46
|
+
let postsData = {posts: [post], pagination: {}};
|
|
47
47
|
|
|
48
48
|
if (themeFile.file.match(/^post/) || themeFile.file.match(/^page/)) {
|
|
49
49
|
postsData = {post: post};
|
package/lib/format.js
CHANGED
|
@@ -31,13 +31,13 @@ const format = function format(theme, options = {}) {
|
|
|
31
31
|
const checkVersion = _.get(options, 'checkVersion', versions.default);
|
|
32
32
|
const ruleSet = spec.get([checkVersion]);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
const processedCodes = [];
|
|
35
|
+
let hasFatalErrors = false;
|
|
36
|
+
const stats = {
|
|
37
|
+
error: 0,
|
|
38
|
+
warning: 0,
|
|
39
|
+
recommendation: 0
|
|
40
|
+
};
|
|
41
41
|
|
|
42
42
|
theme.results.error = [];
|
|
43
43
|
theme.results.warning = [];
|
package/lib/read-theme.js
CHANGED
|
@@ -24,12 +24,12 @@ const readThemeStructure = function readThemeFiles(themePath, subPath, arr) {
|
|
|
24
24
|
themePath = path.join(themePath, '.');
|
|
25
25
|
subPath = subPath || '';
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const tmpPath = os.tmpdir();
|
|
28
|
+
const inTmp = themePath.substr(0, tmpPath.length) === tmpPath;
|
|
29
29
|
|
|
30
30
|
arr = arr || [];
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
const makeResult = function makeResult(result, subFilePath, ext, symlink) {
|
|
33
33
|
result.push({
|
|
34
34
|
file: subFilePath,
|
|
35
35
|
normalizedFile: normalizePath(subFilePath),
|
|
@@ -163,36 +163,36 @@ const processHelpers = function (theme, themeFile) {
|
|
|
163
163
|
* this function is outsourced in the future, so it can be used by GScan and Ghost. But for now, we don't pre-optimise.
|
|
164
164
|
*/
|
|
165
165
|
const extractCustomTemplates = function extractCustomTemplates(allTemplates) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
const toReturn = [];
|
|
167
|
+
const generateName = function generateName(templateName) {
|
|
168
|
+
let name = templateName;
|
|
169
|
+
|
|
170
|
+
name = name.replace(/^(post-|page-|custom-)/, '');
|
|
171
|
+
name = name.replace(/-/g, ' ');
|
|
172
|
+
name = name.replace(/\b\w/g, function (letter) {
|
|
173
|
+
return letter.toUpperCase();
|
|
174
|
+
});
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
return name.trim();
|
|
177
|
+
};
|
|
178
|
+
const generateFor = function (templateName) {
|
|
179
|
+
if (templateName.match(/^page-/)) {
|
|
180
|
+
return ['page'];
|
|
181
|
+
}
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
if (templateName.match(/^post-/)) {
|
|
184
|
+
return ['post'];
|
|
185
|
+
}
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
return ['page', 'post'];
|
|
188
|
+
};
|
|
189
|
+
const generateSlug = function (templateName) {
|
|
190
|
+
if (templateName.match(/^custom-/)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
return templateName.match(/^(page-|post-)(.*)/)[2];
|
|
195
|
+
};
|
|
196
196
|
|
|
197
197
|
_.each(allTemplates, function (templateName) {
|
|
198
198
|
if (templateName.match(/^(post-|page-|custom-)/) && !templateName.match(/\//)) {
|
|
@@ -223,7 +223,7 @@ const extractTemplates = function extractTemplates(allFiles) {
|
|
|
223
223
|
return templates;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
const tplMatch = entry.file.match(/(.*)\.hbs$/);
|
|
227
227
|
if (tplMatch) {
|
|
228
228
|
templates.push(tplMatch[1]);
|
|
229
229
|
}
|
|
@@ -239,7 +239,7 @@ const extractTemplates = function extractTemplates(allFiles) {
|
|
|
239
239
|
module.exports = function readTheme(themePath) {
|
|
240
240
|
return readThemeStructure(themePath)
|
|
241
241
|
.then(function (themeFiles) {
|
|
242
|
-
|
|
242
|
+
const allTemplates = extractTemplates(themeFiles);
|
|
243
243
|
|
|
244
244
|
return readFiles({
|
|
245
245
|
path: themePath,
|
|
@@ -258,6 +258,8 @@ module.exports = function readTheme(themePath) {
|
|
|
258
258
|
});
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
+
module.exports._private = {readFiles};
|
|
262
|
+
|
|
261
263
|
/**
|
|
262
264
|
* @typedef {Object} Theme
|
|
263
265
|
* @param {string} path
|
package/lib/specs/v1.js
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const spec = require('../specs');
|
|
3
|
+
const {versions} = require('./');
|
|
4
|
+
const ASTLinter = require('../ast-linter');
|
|
5
|
+
|
|
6
|
+
function getRules(id, options) {
|
|
7
|
+
const checkVersion = _.get(options, 'checkVersion', versions.default);
|
|
8
|
+
let ruleSet = spec.get([checkVersion]);
|
|
9
|
+
|
|
10
|
+
const ruleRegex = new RegExp('^' + id + '-.*', 'g');
|
|
11
|
+
ruleSet = _.pickBy(ruleSet.rules, function (rule, ruleCode) {
|
|
12
|
+
if (ruleCode.match(ruleRegex)) {
|
|
13
|
+
return rule;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return ruleSet;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getLogger({theme, rule, file = null}) {
|
|
21
|
+
return {
|
|
22
|
+
failure: (content) => {
|
|
23
|
+
if (!theme.results.fail[rule.code]) {
|
|
24
|
+
theme.results.fail[rule.code] = {failures: []};
|
|
25
|
+
}
|
|
26
|
+
const failure = {
|
|
27
|
+
...content,
|
|
28
|
+
rule: rule.code
|
|
29
|
+
};
|
|
30
|
+
if (file) {
|
|
31
|
+
failure.ref = file.file;
|
|
32
|
+
}
|
|
33
|
+
theme.results.fail[rule.code].failures.push(failure);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applyRule(rule, theme) {
|
|
39
|
+
const partialVerificationCache = new Map();
|
|
40
|
+
// The result variable is passed around to keep a state through the full lifecycle
|
|
41
|
+
const result = {};
|
|
42
|
+
try {
|
|
43
|
+
// Check if the rule is enabled (optional)
|
|
44
|
+
if (typeof rule.isEnabled === 'function') {
|
|
45
|
+
if (!rule.isEnabled({theme, log: getLogger({theme, rule}), result, options: rule.options})) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
} else if (typeof rule.isEnabled === 'boolean' && !rule.isEnabled) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Initialize the rule (optional)
|
|
53
|
+
if (typeof rule.init === 'function') {
|
|
54
|
+
rule.init({theme, log: getLogger({theme, rule}), result});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Run the main function on each theme file (optional)
|
|
58
|
+
if (typeof rule.eachFile === 'function') {
|
|
59
|
+
_.each(theme.files, function (themeFile) {
|
|
60
|
+
rule.eachFile({file: themeFile, theme, log: getLogger({theme, rule}), result, partialVerificationCache});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Run the final function
|
|
65
|
+
if (typeof rule.done === 'function') {
|
|
66
|
+
rule.done({theme, log: getLogger({theme, rule}), result});
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Output something instead of failing silently (should never happen)
|
|
70
|
+
// eslint-disable-next-line
|
|
71
|
+
console.error('gscan failure', e);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseWithAST({theme, log, file, rules, callback, partialVerificationCache}) {
|
|
76
|
+
const linter = new ASTLinter();
|
|
77
|
+
|
|
78
|
+
// This rule is needed to find partials
|
|
79
|
+
// Partials are needed for a full parsing
|
|
80
|
+
if (!rules['mark-used-partials']) {
|
|
81
|
+
rules['mark-used-partials'] = require(`../ast-linter/rules/mark-used-partials`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function processFile(themeFile) {
|
|
85
|
+
if (themeFile.parsed.error) {
|
|
86
|
+
// Ignore parsing errors, they are handled in 005
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fileName = themeFile.file;
|
|
91
|
+
// Check if the file is a partial
|
|
92
|
+
const isPartial = fileName.startsWith('partials/');
|
|
93
|
+
// Skip if already cached (for partials only)
|
|
94
|
+
if (isPartial && partialVerificationCache.has(fileName)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const astResults = linter.verify({
|
|
99
|
+
parsed: themeFile.parsed,
|
|
100
|
+
rules,
|
|
101
|
+
source: themeFile.content,
|
|
102
|
+
moduleId: themeFile.file
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Cache the result for this partial
|
|
106
|
+
if (isPartial) {
|
|
107
|
+
partialVerificationCache.set(fileName, astResults);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (astResults.length) {
|
|
111
|
+
log.failure({
|
|
112
|
+
message: astResults[0].message,
|
|
113
|
+
ref: themeFile.file
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof callback === 'function') {
|
|
118
|
+
callback(linter);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
linter.partials.forEach(({normalizedName}) => {
|
|
122
|
+
const partialFile = theme.files.find(f => f.normalizedFile === `partials/${normalizedName}.hbs`);
|
|
123
|
+
if (partialFile) {
|
|
124
|
+
processFile(partialFile);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return processFile(file);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
getRules,
|
|
134
|
+
getLogger,
|
|
135
|
+
applyRule,
|
|
136
|
+
parseWithAST
|
|
137
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Mapping of helper names to their Ghost labs flag names.
|
|
2
|
+
// When a Ghost feature is behind a labs flag, add the helper here
|
|
3
|
+
// so GScan includes it in knownHelpers when the flag is enabled.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// 1. Add an entry: { helperName: 'labsFlagName' }
|
|
7
|
+
// 2. Ghost passes { labs: { labsFlagName: true } } to GScan
|
|
8
|
+
// 3. GScan adds helperName to the spec's knownHelpers
|
|
9
|
+
// 4. When the helper graduates from labs, remove the entry
|
|
10
|
+
//
|
|
11
|
+
// Example (from when match was behind a labs flag):
|
|
12
|
+
// module.exports = {
|
|
13
|
+
// match: 'matchHelper'
|
|
14
|
+
// };
|
|
15
|
+
module.exports = {
|
|
16
|
+
};
|
|
@@ -19,7 +19,7 @@ const levelWeights = {
|
|
|
19
19
|
* @returns {Object}
|
|
20
20
|
*/
|
|
21
21
|
const calcScore = function calcScore(results, stats) {
|
|
22
|
-
|
|
22
|
+
let maxScore; let actualScore; let balancedScore;
|
|
23
23
|
|
|
24
24
|
maxScore = _.reduce(levelWeights, function (max, weight, level) {
|
|
25
25
|
return max + (weight * stats[level]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gscan",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.3",
|
|
4
4
|
"description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ghost",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"registry": "https://registry.npmjs.org/"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": "^20.
|
|
20
|
+
"node": "^20.19.0 || ^22.13.1 || ^24.0.0"
|
|
21
21
|
},
|
|
22
22
|
"bugs": {
|
|
23
23
|
"url": "https://github.com/TryGhost/gscan/issues"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"start": "node app/index.js",
|
|
34
34
|
"dev": "NODE_ENV=development DEBUG=gscan:* nodemon",
|
|
35
35
|
"lint": "eslint . --ext .js --cache",
|
|
36
|
-
"test": "NODE_ENV=testing
|
|
36
|
+
"test": "NODE_ENV=testing vitest run --coverage",
|
|
37
37
|
"posttest": "yarn lint",
|
|
38
38
|
"preship": "yarn test",
|
|
39
39
|
"ship": "pro-ship",
|
|
@@ -43,19 +43,19 @@
|
|
|
43
43
|
"gscan": "./bin/cli.js"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@sentry/node": "10.
|
|
47
|
-
"@tryghost/config": "2.0.
|
|
48
|
-
"@tryghost/debug": "2.0.
|
|
49
|
-
"@tryghost/errors": "3.0.
|
|
50
|
-
"@tryghost/logging": "4.0.
|
|
46
|
+
"@sentry/node": "10.42.0",
|
|
47
|
+
"@tryghost/config": "2.0.2",
|
|
48
|
+
"@tryghost/debug": "2.0.2",
|
|
49
|
+
"@tryghost/errors": "3.0.2",
|
|
50
|
+
"@tryghost/logging": "4.0.2",
|
|
51
51
|
"@tryghost/nql": "0.12.10",
|
|
52
|
-
"@tryghost/pretty-cli": "3.0.
|
|
53
|
-
"@tryghost/server": "2.0.
|
|
54
|
-
"@tryghost/zip": "3.0.
|
|
52
|
+
"@tryghost/pretty-cli": "3.0.2",
|
|
53
|
+
"@tryghost/server": "2.0.2",
|
|
54
|
+
"@tryghost/zip": "3.0.2",
|
|
55
55
|
"chalk": "5.6.2",
|
|
56
56
|
"express": "5.2.1",
|
|
57
57
|
"express-handlebars": "7.1.3",
|
|
58
|
-
"fs-extra": "11.3.
|
|
58
|
+
"fs-extra": "11.3.4",
|
|
59
59
|
"glob": "13.0.6",
|
|
60
60
|
"lodash": "4.17.23",
|
|
61
61
|
"multer": "2.1.1",
|
|
@@ -63,17 +63,17 @@
|
|
|
63
63
|
"validator": "^13.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@eslint/compat": "2.0.
|
|
66
|
+
"@eslint/compat": "2.0.3",
|
|
67
|
+
"@eslint/eslintrc": "3.3.5",
|
|
67
68
|
"@eslint/js": "10.0.1",
|
|
68
69
|
"@tryghost/pro-ship": "1.0.7",
|
|
69
|
-
"
|
|
70
|
-
"eslint": "10.0.
|
|
71
|
-
"eslint-plugin-ghost": "3.
|
|
72
|
-
"mocha": "11.7.5",
|
|
70
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
71
|
+
"eslint": "10.0.3",
|
|
72
|
+
"eslint-plugin-ghost": "3.5.0",
|
|
73
73
|
"nodemon": "3.1.14",
|
|
74
|
-
"rewire": "9.0.1",
|
|
75
74
|
"should": "13.2.3",
|
|
76
|
-
"sinon": "21.0.
|
|
75
|
+
"sinon": "21.0.2",
|
|
76
|
+
"vitest": "^4.0.18"
|
|
77
77
|
},
|
|
78
78
|
"files": [
|
|
79
79
|
"lib",
|