gscan 4.37.0 → 4.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ast-linter/linter.js +7 -1
- package/lib/ast-linter/rules/base.js +6 -0
- package/lib/ast-linter/rules/index.js +1 -0
- package/lib/ast-linter/rules/lint-no-unknown-page-properties.js +21 -0
- package/lib/ast-linter/rules/mark-used-page-properties.js +15 -0
- package/lib/checker.js +7 -2
- package/lib/checks/010-package-json.js +1 -1
- package/lib/checks/090-template-syntax.js +3 -2
- package/lib/checks/110-page-builder-usage.js +185 -0
- package/lib/specs/canary.js +15 -0
- package/lib/specs/index.js +1 -1
- package/package.json +4 -4
package/lib/ast-linter/linter.js
CHANGED
|
@@ -20,6 +20,7 @@ class Linter {
|
|
|
20
20
|
this.helpers = [];
|
|
21
21
|
this.inlinePartials = [];
|
|
22
22
|
this.customThemeSettings = [];
|
|
23
|
+
this.usedPageProperties = [];
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -57,7 +58,8 @@ class Linter {
|
|
|
57
58
|
partials: [],
|
|
58
59
|
helpers: [],
|
|
59
60
|
inlinePartials: [],
|
|
60
|
-
customThemeSettings: []
|
|
61
|
+
customThemeSettings: [],
|
|
62
|
+
usedPageProperties: []
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
Scanner.prototype = new Handlebars.Visitor();
|
|
@@ -161,6 +163,10 @@ class Linter {
|
|
|
161
163
|
this.inlinePartials = scanner.context.inlinePartials;
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
if (scanner.context.usedPageProperties) {
|
|
167
|
+
this.usedPageProperties = scanner.context.usedPageProperties;
|
|
168
|
+
}
|
|
169
|
+
|
|
164
170
|
return messages;
|
|
165
171
|
}
|
|
166
172
|
}
|
|
@@ -9,6 +9,8 @@ module.exports = class BaseRule {
|
|
|
9
9
|
this.helpers = options.helpers;
|
|
10
10
|
this.inlinePartials = options.inlinePartials || [];
|
|
11
11
|
this.customThemeSettings = options.customThemeSettings;
|
|
12
|
+
// TODO: remove hardcoded list of known page builder properties once we have a way to get them from the spec
|
|
13
|
+
this.knownPageBuilderProperties = options.knownPageBuilderProperties || ['show_title_and_feature_image'];
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
getVisitor({fileName} = {}) {
|
|
@@ -160,6 +162,10 @@ module.exports = class BaseRule {
|
|
|
160
162
|
return this.helpers && this.helpers.includes(nodeName);
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
isValidPageBuilderProperty(property) {
|
|
166
|
+
return this.knownPageBuilderProperties && this.knownPageBuilderProperties.includes(property);
|
|
167
|
+
}
|
|
168
|
+
|
|
163
169
|
isValidCustomThemeSettingReference(name) {
|
|
164
170
|
return this.customThemeSettings && !!this.customThemeSettings[name];
|
|
165
171
|
}
|
|
@@ -12,6 +12,7 @@ module.exports = {
|
|
|
12
12
|
'GS090-NO-PRICE-DATA-CURRENCY-GLOBAL': require('./lint-no-price-data-currency-global'),
|
|
13
13
|
'GS090-NO-PRICE-DATA-CURRENCY-CONTEXT': require('./lint-no-price-data-currency-context'),
|
|
14
14
|
'GS090-NO-PRICE-DATA-MONTHLY-YEARLY': require('./lint-no-price-data-monthly-yearly'),
|
|
15
|
+
'GS110-NO-UNKNOWN-PAGE-BUILDER-USAGE': require('./lint-no-unknown-page-properties'),
|
|
15
16
|
'no-multi-param-conditionals': require('./lint-no-multi-param-conditionals'),
|
|
16
17
|
'no-nested-async-helpers': require('./lint-no-nested-async-helpers'),
|
|
17
18
|
'no-prev-next-post-outside-post-context': require('./lint-no-prev-next-post-outside-post-context'),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const Rule = require('./base');
|
|
2
|
+
const {logNode} = require('../helpers');
|
|
3
|
+
|
|
4
|
+
module.exports = class NoUnknownPageProperties extends Rule {
|
|
5
|
+
_checkForUnknownPageProperty(node) {
|
|
6
|
+
if (node.data && node.parts && node.parts[0] === 'page' && (node.parts.length > 2 || !this.isValidPageBuilderProperty(node.parts[1]))) {
|
|
7
|
+
this.log({
|
|
8
|
+
message: `${logNode(node)} is not a known @page property`,
|
|
9
|
+
line: node.loc && node.loc.start.line,
|
|
10
|
+
column: node.loc && node.loc.start.column,
|
|
11
|
+
source: this.sourceForNode(node)
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
visitor() {
|
|
17
|
+
return {
|
|
18
|
+
PathExpression: this._checkForUnknownPageProperty.bind(this)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const Rule = require('./base');
|
|
2
|
+
|
|
3
|
+
module.exports = class MarkUsedPageProperties extends Rule {
|
|
4
|
+
_markUsedPageProperties(node) {
|
|
5
|
+
if (node.data && node.parts && node.parts.length === 2 && node.parts[0] === 'page') {
|
|
6
|
+
this.scanner.context.usedPageProperties.push(node.parts[1]);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
visitor() {
|
|
11
|
+
return {
|
|
12
|
+
PathExpression: this._markUsedPageProperties.bind(this)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
};
|
package/lib/checker.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const Promise = require('bluebird');
|
|
2
2
|
const _ = require('lodash');
|
|
3
3
|
const requireDir = require('require-dir');
|
|
4
|
+
const debug = require('@tryghost/debug')('checker');
|
|
5
|
+
|
|
4
6
|
const errors = require('@tryghost/errors');
|
|
5
7
|
const versions = require('./utils').versions;
|
|
6
8
|
|
|
@@ -48,8 +50,11 @@ const check = function checkAll(themePath, options = {}) {
|
|
|
48
50
|
// set the major version to check
|
|
49
51
|
theme.checkedVersion = versions[version].major;
|
|
50
52
|
|
|
51
|
-
return Promise.reduce(_.values(checks), function (themeToCheck, checkFunction) {
|
|
52
|
-
|
|
53
|
+
return Promise.reduce(_.values(checks), async function (themeToCheck, checkFunction) {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const result = await checkFunction(themeToCheck, options, themePath);
|
|
56
|
+
debug(checkFunction.name, 'took', Date.now() - now, 'ms');
|
|
57
|
+
return result;
|
|
53
58
|
}, theme);
|
|
54
59
|
})
|
|
55
60
|
.catch((error) => {
|
|
@@ -104,7 +104,7 @@ _private.validatePackageJSONFields = function validatePackageJSONFields(packageJ
|
|
|
104
104
|
markFailed('nameIsLowerCase');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
if (packageJSON.name && !packageJSON.name.match(/^[a-z0-9]
|
|
107
|
+
if (packageJSON.name && !packageJSON.name.match(/^([a-z0-9]+-)*[a-z0-9]+$/gi)) {
|
|
108
108
|
markFailed('nameIsHyphenated');
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -48,7 +48,7 @@ function getCustomThemeSettings(theme) {
|
|
|
48
48
|
return customThemeSettingsConfig;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const
|
|
51
|
+
const checkTemplateSyntax = function checkTemplateSyntax(theme, options) {
|
|
52
52
|
const checkVersion = _.get(options, 'checkVersion', versions.default);
|
|
53
53
|
const ruleSet = spec.get([checkVersion]);
|
|
54
54
|
const customThemeSettings = getCustomThemeSettings(theme);
|
|
@@ -73,6 +73,7 @@ const checkTemplatesCompile = function checkTemplatesCompile(theme, options) {
|
|
|
73
73
|
|
|
74
74
|
const linter = new ASTLinter({
|
|
75
75
|
partials: theme.partials,
|
|
76
|
+
inlinePartials: [],
|
|
76
77
|
helpers: [],
|
|
77
78
|
customThemeSettings: customThemeSettings
|
|
78
79
|
});
|
|
@@ -95,4 +96,4 @@ const checkTemplatesCompile = function checkTemplatesCompile(theme, options) {
|
|
|
95
96
|
return theme;
|
|
96
97
|
};
|
|
97
98
|
|
|
98
|
-
module.exports =
|
|
99
|
+
module.exports = checkTemplateSyntax;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const spec = require('../specs');
|
|
3
|
+
const {versions, normalizePath} = 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
|
+
// The result variable is passed around to keep a state through the full lifecycle
|
|
40
|
+
const result = {};
|
|
41
|
+
try {
|
|
42
|
+
// Check if the rule is enabled (optional)
|
|
43
|
+
if (typeof rule.isEnabled === 'function') {
|
|
44
|
+
if (!rule.isEnabled({theme, log: getLogger({theme, rule}), result, options: rule.options})) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
} else if (typeof rule.isEnabled === 'boolean' && !rule.isEnabled) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initialize the rule (optional)
|
|
52
|
+
if (typeof rule.init === 'function') {
|
|
53
|
+
rule.init({theme, log: getLogger({theme, rule}), result});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Run the main function on each theme file (optional)
|
|
57
|
+
if (typeof rule.eachFile === 'function') {
|
|
58
|
+
_.each(theme.files, function (themeFile) {
|
|
59
|
+
rule.eachFile({file: themeFile, theme, log: getLogger({theme, rule}), result});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Run the final function
|
|
64
|
+
if (typeof rule.done === 'function') {
|
|
65
|
+
rule.done({theme, log: getLogger({theme, rule}), result});
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// Output something instead of failing silently (should never happen)
|
|
69
|
+
// eslint-disable-next-line
|
|
70
|
+
console.error('gscan failure', e);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseWithAST({theme, log, file, rules, callback}) {
|
|
75
|
+
const linter = new ASTLinter();
|
|
76
|
+
|
|
77
|
+
// // This rule is needed to find partials
|
|
78
|
+
// // Partials are needed for a full parsing
|
|
79
|
+
if (!rules['mark-used-partials']) {
|
|
80
|
+
rules['mark-used-partials'] = require(`../ast-linter/rules/mark-used-partials`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function processFile(themeFile) {
|
|
84
|
+
if (themeFile.parsed.error) {
|
|
85
|
+
// Ignore parsing errors, they are handled in 005
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const astResults = linter.verify({
|
|
90
|
+
parsed: themeFile.parsed,
|
|
91
|
+
rules,
|
|
92
|
+
source: themeFile.content,
|
|
93
|
+
moduleId: themeFile.file
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (astResults.length) {
|
|
97
|
+
log.failure({
|
|
98
|
+
message: astResults[0].message
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof callback === 'function') {
|
|
103
|
+
callback(linter);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
linter.partials.forEach(({node: partial}) => {
|
|
107
|
+
const partialFile = theme.files.find(f => normalizePath(f.file) === `partials/${normalizePath(partial)}.hbs`);
|
|
108
|
+
if (partialFile) {
|
|
109
|
+
processFile(partialFile);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return processFile(file);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ruleImplementations = {
|
|
118
|
+
'GS110-NO-MISSING-PAGE-BUILDER-USAGE': {
|
|
119
|
+
isEnabled: ({options}) => {
|
|
120
|
+
// TODO: change to `isEnabled: true` when removing labs flag
|
|
121
|
+
return options && options.labs && options.labs.pageImprovements;
|
|
122
|
+
},
|
|
123
|
+
init: ({result}) => {
|
|
124
|
+
result.pageBuilderProperties = new Set();
|
|
125
|
+
},
|
|
126
|
+
eachFile: ({file, theme, log, result}) => {
|
|
127
|
+
const templateTest = file.file.match(/(?<!partials\/.+?)\.hbs$/);
|
|
128
|
+
|
|
129
|
+
if (templateTest) {
|
|
130
|
+
parseWithAST({file, theme, rules: {
|
|
131
|
+
'mark-used-page-properties': require(`../ast-linter/rules/mark-used-page-properties`)
|
|
132
|
+
}, log, callback: (linter) => {
|
|
133
|
+
linter.usedPageProperties.forEach((variable) => {
|
|
134
|
+
result.pageBuilderProperties.add(variable);
|
|
135
|
+
});
|
|
136
|
+
}});
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
done: ({log, result}) => {
|
|
140
|
+
// TODO: get this from the spec rather than hard-coding to account for version changes
|
|
141
|
+
const knownPageBuilderProperties = ['show_title_and_feature_image'];
|
|
142
|
+
const notUsedProperties = knownPageBuilderProperties.filter(x => !result.pageBuilderProperties.has(x));
|
|
143
|
+
|
|
144
|
+
notUsedProperties.forEach((property) => {
|
|
145
|
+
log.failure({
|
|
146
|
+
ref: `@page.${property}`
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
'GS110-NO-UNKNOWN-PAGE-BUILDER-USAGE': {
|
|
152
|
+
isEnabled: ({options}) => {
|
|
153
|
+
// TODO: change to `isEnabled: true` when removing labs flag
|
|
154
|
+
return options && options.labs && options.labs.pageImprovements;
|
|
155
|
+
},
|
|
156
|
+
eachFile: ({file, theme, log}) => {
|
|
157
|
+
const templateTest = file.file.match(/(?<!partials\/.+?)\.hbs$/);
|
|
158
|
+
|
|
159
|
+
if (templateTest) {
|
|
160
|
+
parseWithAST({
|
|
161
|
+
file, theme, rules: {
|
|
162
|
+
'no-unknown-page-properties': require(`../ast-linter/rules/lint-no-unknown-page-properties`)
|
|
163
|
+
}, log, callback: () => {}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
function checkUsage(theme, options) {
|
|
171
|
+
const rules = getRules('GS110', options);
|
|
172
|
+
|
|
173
|
+
_.each(rules, function (check, ruleCode) {
|
|
174
|
+
applyRule({
|
|
175
|
+
code: ruleCode,
|
|
176
|
+
...check,
|
|
177
|
+
...ruleImplementations[ruleCode],
|
|
178
|
+
options
|
|
179
|
+
}, theme);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return theme;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = checkUsage;
|
package/lib/specs/canary.js
CHANGED
|
@@ -712,6 +712,21 @@ let rules = {
|
|
|
712
712
|
rule: '<code>package.json</code> property <code>config.custom</code> contains an entry with a <code>description</code> that is too long',
|
|
713
713
|
details: oneLineTrim`<code>config.custom</code> entry <code>description</code> should be less than <code>100</code> characters so that it is displayed correctly.<br />
|
|
714
714
|
Check the <a href="${docsBaseUrl}custom-settings" target=_blank><code>config.custom</code> documentation</a> for further information.`
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
'GS110-NO-MISSING-PAGE-BUILDER-USAGE': {
|
|
718
|
+
level: 'warning',
|
|
719
|
+
rule: 'Not all page builder features are being used',
|
|
720
|
+
// TODO: get proper docs link
|
|
721
|
+
details: oneLineTrim`Some page builder features used via the <code>{{@page}}</code> global are not being used.
|
|
722
|
+
Check the <a href="${docsBaseUrl}page-builder" target=_blank>page builder documentation</a> for further information.`
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
'GS110-NO-UNKNOWN-PAGE-BUILDER-USAGE': {
|
|
726
|
+
level: 'error',
|
|
727
|
+
fatal: true,
|
|
728
|
+
rule: 'Unsupported page builder feature used',
|
|
729
|
+
details: oneLineTrim`A page builder feature used via the <code>{{@page}}</code> global was detected but it's not supported by this version of Ghost.`
|
|
715
730
|
}
|
|
716
731
|
};
|
|
717
732
|
|
package/lib/specs/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gscan",
|
|
3
|
-
"version": "4.37.
|
|
3
|
+
"version": "4.37.2",
|
|
4
4
|
"description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ghost",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"gscan": "./bin/cli.js"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@sentry/node": "7.
|
|
43
|
+
"@sentry/node": "7.60.0",
|
|
44
44
|
"@tryghost/config": "0.2.17",
|
|
45
45
|
"@tryghost/debug": "0.1.25",
|
|
46
46
|
"@tryghost/errors": "1.2.25",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"multer": "1.4.4",
|
|
60
60
|
"pluralize": "8.0.0",
|
|
61
61
|
"require-dir": "1.2.0",
|
|
62
|
-
"semver": "7.5.
|
|
62
|
+
"semver": "7.5.4",
|
|
63
63
|
"uuid": "9.0.0",
|
|
64
64
|
"validator": "13.0.0"
|
|
65
65
|
},
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"eslint-plugin-ghost": "2.1.0",
|
|
69
69
|
"istanbul": "0.4.5",
|
|
70
70
|
"mocha": "10.0.0",
|
|
71
|
-
"node-fetch": "3.3.
|
|
71
|
+
"node-fetch": "3.3.2",
|
|
72
72
|
"nodemon": "2.0.7",
|
|
73
73
|
"rewire": "6.0.0",
|
|
74
74
|
"should": "13.2.3",
|