gscan 5.3.2 → 5.3.4

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.
@@ -1,13 +1,13 @@
1
- var randomUUID = require('crypto').randomUUID,
2
- logging = require('@tryghost/logging');
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
- var startTime = Date.now(),
10
- requestId = randomUUID();
9
+ const startTime = Date.now();
10
+ const requestId = randomUUID();
11
11
 
12
12
  function logResponse() {
13
13
  res.responseTime = (Date.now() - startTime) + 'ms';
package/bin/cli.js CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Remove all Node warnings before doing anything else
4
- process.removeAllListeners('warning');
5
-
6
3
  const prettyCLI = require('@tryghost/pretty-cli');
7
4
  const ui = require('@tryghost/pretty-cli').ui;
8
5
  const _ = require('lodash');
@@ -10,6 +7,51 @@ const {default: chalk} = require('chalk');
10
7
  const gscan = require('../lib');
11
8
  const ghostVersions = require('../lib/utils').versions;
12
9
 
10
+ /**
11
+ * @typedef {object} CliArgv
12
+ * @property {string} themePath
13
+ * @property {boolean} [zip]
14
+ * @property {boolean} [v1]
15
+ * @property {boolean} [v2]
16
+ * @property {boolean} [v3]
17
+ * @property {boolean} [v4]
18
+ * @property {boolean} [v5]
19
+ * @property {boolean} [v6]
20
+ * @property {boolean} [canary]
21
+ * @property {boolean} [fatal]
22
+ * @property {boolean} [verbose]
23
+ * @property {string[]} [labs]
24
+ */
25
+
26
+ /**
27
+ * @typedef {object} CheckOptions
28
+ * @property {'cli'} format
29
+ * @property {string} [checkVersion]
30
+ * @property {boolean} [verbose]
31
+ * @property {boolean} [onlyFatalErrors]
32
+ * @property {Record<string, true>} [labs]
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} ResultFailure
37
+ * @property {string} ref
38
+ * @property {string} [message]
39
+ */
40
+
41
+ /**
42
+ * @typedef {object} FormattedResult
43
+ * @property {'error' | 'warning' | 'recommendation' | 'feature'} level
44
+ * @property {string} rule
45
+ * @property {string} details
46
+ * @property {ResultFailure[]} [failures]
47
+ */
48
+
49
+ /**
50
+ * @typedef {object} FormattedTheme
51
+ * @property {string} checkedVersion
52
+ * @property {{error: FormattedResult[], warning: FormattedResult[], recommendation: FormattedResult[]}} results
53
+ */
54
+
13
55
  const levels = {
14
56
  error: chalk.red,
15
57
  warning: chalk.yellow,
@@ -17,115 +59,68 @@ const levels = {
17
59
  feature: chalk.green
18
60
  };
19
61
 
20
- const cliOptions = {
21
- format: 'cli'
22
- };
62
+ /**
63
+ * @param {CliArgv} argv
64
+ * @returns {CheckOptions}
65
+ */
66
+ function resolveOptions(argv) {
67
+ /** @type {CheckOptions} */
68
+ const options = {format: 'cli'};
23
69
 
24
- prettyCLI
25
- .configure({
26
- name: 'gscan'
27
- })
28
- .groupOrder([
29
- 'Sources:',
30
- 'Utilities:',
31
- 'Commands:',
32
- 'Arguments:',
33
- 'Required Options:',
34
- 'Options:',
35
- 'Global Options:'
36
- ])
37
- .positional('<themePath>', {
38
- paramsDesc: 'Theme folder or .zip file path',
39
- mustExist: true
40
- })
41
- .boolean('-z, --zip', {
42
- desc: 'Theme path points to a zip file'
43
- })
44
- .boolean('-1, --v1', {
45
- desc: 'Check theme for Ghost 1.0 compatibility'
46
- })
47
- .boolean('-2, --v2', {
48
- desc: 'Check theme for Ghost 2.0 compatibility'
49
- })
50
- .boolean('-3, --v3', {
51
- desc: 'Check theme for Ghost 3.0 compatibility'
52
- })
53
- .boolean('-4, --v4', {
54
- desc: 'Check theme for Ghost 4.0 compatibility'
55
- })
56
- .boolean('-5, --v5', {
57
- desc: 'Check theme for Ghost 5.0 compatibility'
58
- })
59
- .boolean('-6, --v6', {
60
- desc: 'Check theme for Ghost 6.0 compatibility'
61
- })
62
- .boolean('-c, --canary', {
63
- desc: 'Check theme for Ghost 6.0 compatibility (alias for --v6)'
64
- })
65
- .boolean('-f, --fatal', {
66
- desc: 'Only show fatal errors that prevent upgrading Ghost'
67
- })
68
- .boolean('--verbose', {
69
- desc: 'Output check details'
70
- })
71
- .array('--labs', {
72
- desc: 'a list of labs flags'
73
- })
74
- .parseAndExit()
75
- .then((argv) => {
76
- if (argv.v1) {
77
- cliOptions.checkVersion = 'v1';
78
- } else if (argv.v2) {
79
- cliOptions.checkVersion = 'v2';
80
- } else if (argv.v3) {
81
- cliOptions.checkVersion = 'v3';
82
- } else if (argv.v4) {
83
- cliOptions.checkVersion = 'v4';
84
- } else if (argv.v5) {
85
- cliOptions.checkVersion = 'v5';
86
- } else if (argv.v6) {
87
- cliOptions.checkVersion = 'v6';
88
- } else if (argv.canary) {
89
- cliOptions.checkVersion = ghostVersions.canary;
90
- } else {
91
- cliOptions.checkVersion = ghostVersions.default;
92
- }
70
+ if (argv.canary) {
71
+ options.checkVersion = ghostVersions.canary;
72
+ } else {
73
+ const versionKey = Object.keys(ghostVersions).find(k => k.startsWith('v') && argv[k]);
74
+ options.checkVersion = versionKey || ghostVersions.default;
75
+ }
93
76
 
94
- cliOptions.verbose = argv.verbose;
95
- cliOptions.onlyFatalErrors = argv.fatal;
77
+ options.verbose = argv.verbose;
78
+ options.onlyFatalErrors = argv.fatal;
96
79
 
97
- if (argv.labs) {
98
- cliOptions.labs = {};
80
+ if (argv.labs) {
81
+ options.labs = {};
99
82
 
100
- argv.labs.forEach((flag) => {
101
- cliOptions.labs[flag] = true;
102
- });
103
- }
83
+ argv.labs.forEach((flag) => {
84
+ options.labs[flag] = true;
85
+ });
86
+ }
104
87
 
105
- if (cliOptions.onlyFatalErrors) {
106
- ui.log(chalk.bold('\nChecking theme compatibility (fatal issues only)...'));
107
- } else {
108
- ui.log(chalk.bold('\nChecking theme compatibility...'));
109
- }
88
+ return options;
89
+ }
110
90
 
91
+ /**
92
+ * @param {CliArgv} argv
93
+ * @param {CheckOptions} options
94
+ * @returns {Promise<void>}
95
+ */
96
+ async function runCheck(argv, options) {
97
+ if (options.onlyFatalErrors) {
98
+ ui.log(chalk.bold('\nChecking theme compatibility (fatal issues only)...'));
99
+ } else {
100
+ ui.log(chalk.bold('\nChecking theme compatibility...'));
101
+ }
102
+
103
+ try {
104
+ const theme = argv.zip
105
+ ? await gscan.checkZip(argv.themePath, options)
106
+ : await gscan.check(argv.themePath, options);
107
+ outputResults(theme, options);
108
+ } catch (err) {
111
109
  if (argv.zip) {
112
- gscan.checkZip(argv.themePath, cliOptions)
113
- .then(theme => outputResults(theme, cliOptions))
114
- .catch((error) => {
115
- ui.log(error);
116
- });
110
+ ui.log(err);
117
111
  } else {
118
- gscan.check(argv.themePath, cliOptions)
119
- .then(theme => outputResults(theme, cliOptions))
120
- .catch((err) => {
121
- ui.log(err.message);
122
- if (err.code === 'ENOTDIR') {
123
- ui.log('Did you mean to add the -z flag to read a zip file?');
124
- }
125
- });
112
+ ui.log(err.message);
113
+ if (err.code === 'ENOTDIR') {
114
+ ui.log('Did you mean to add the -z flag to read a zip file?');
115
+ }
126
116
  }
127
- });
117
+ }
118
+ }
128
119
 
120
+ /**
121
+ * @param {FormattedResult} result
122
+ * @param {CheckOptions} options
123
+ */
129
124
  function outputResult(result, options) {
130
125
  ui.log(levels[result.level](`- ${_.capitalize(result.level)}:`), result.rule);
131
126
 
@@ -153,10 +148,20 @@ function outputResult(result, options) {
153
148
  ui.log(''); // extra line-break
154
149
  }
155
150
 
151
+ /**
152
+ * @param {string} word
153
+ * @param {number} count
154
+ * @returns {string}
155
+ */
156
156
  function formatCount(word, count) {
157
157
  return `${count} ${count === 1 ? word : `${word}s`}`;
158
158
  }
159
159
 
160
+ /**
161
+ * @param {FormattedTheme} theme
162
+ * @param {CheckOptions} options
163
+ * @returns {string}
164
+ */
160
165
  function getSummary(theme, options) {
161
166
  let summaryText = '';
162
167
  const errorCount = theme.results.error.length;
@@ -195,15 +200,20 @@ function getSummary(theme, options) {
195
200
  return summaryText;
196
201
  }
197
202
 
203
+ /**
204
+ * @param {*} theme - Raw theme object; mutated into FormattedTheme by gscan.format()
205
+ * @param {CheckOptions} options
206
+ */
198
207
  function outputResults(theme, options) {
199
208
  try {
209
+ /** @type {FormattedTheme} */
200
210
  theme = gscan.format(theme, options);
201
211
  } catch (err) {
202
212
  ui.log.error('Error formating result, some results may be missing.');
203
213
  ui.log.error(err);
204
214
  }
205
215
 
206
- let errorCount = theme.results.error.length;
216
+ const errorCount = theme.results.error.length;
207
217
 
208
218
  ui.log('\n' + getSummary(theme, options));
209
219
 
@@ -229,7 +239,7 @@ function outputResults(theme, options) {
229
239
  _.each(theme.results.recommendation, rule => outputResult(rule, options));
230
240
  }
231
241
 
232
- ui.log(`\nGet more help at ${chalk.cyan.underline('https://ghost.org/docs/themes/')}`);
242
+ ui.log(`\nGet more help at ${chalk.cyan.underline('https://docs.ghost.org/themes/')}`);
233
243
  ui.log(`You can also check theme compatibility at ${chalk.cyan.underline('https://gscan.ghost.org/')}`);
234
244
 
235
245
  // The CLI feature is mainly used to run gscan programatically in tests within themes.
@@ -244,3 +254,69 @@ function outputResults(theme, options) {
244
254
  process.exit(0);
245
255
  }
246
256
  }
257
+
258
+ function main() {
259
+ prettyCLI
260
+ .configure({
261
+ name: 'gscan'
262
+ })
263
+ .groupOrder([
264
+ 'Sources:',
265
+ 'Utilities:',
266
+ 'Commands:',
267
+ 'Arguments:',
268
+ 'Required Options:',
269
+ 'Options:',
270
+ 'Global Options:'
271
+ ])
272
+ .positional('<themePath>', {
273
+ paramsDesc: 'Theme folder or .zip file path',
274
+ mustExist: true
275
+ })
276
+ .boolean('-z, --zip', {
277
+ desc: 'Theme path points to a zip file'
278
+ })
279
+ .boolean('-1, --v1', {
280
+ desc: 'Check theme for Ghost 1.0 compatibility'
281
+ })
282
+ .boolean('-2, --v2', {
283
+ desc: 'Check theme for Ghost 2.0 compatibility'
284
+ })
285
+ .boolean('-3, --v3', {
286
+ desc: 'Check theme for Ghost 3.0 compatibility'
287
+ })
288
+ .boolean('-4, --v4', {
289
+ desc: 'Check theme for Ghost 4.0 compatibility'
290
+ })
291
+ .boolean('-5, --v5', {
292
+ desc: 'Check theme for Ghost 5.0 compatibility'
293
+ })
294
+ .boolean('-6, --v6', {
295
+ desc: 'Check theme for Ghost 6.0 compatibility'
296
+ })
297
+ .boolean('-c, --canary', {
298
+ desc: 'Check theme for Ghost 6.0 compatibility (alias for --v6)'
299
+ })
300
+ .boolean('-f, --fatal', {
301
+ desc: 'Only show fatal errors that prevent upgrading Ghost'
302
+ })
303
+ .boolean('--verbose', {
304
+ desc: 'Output check details'
305
+ })
306
+ .array('--labs', {
307
+ desc: 'a list of labs flags'
308
+ })
309
+ .parseAndExit()
310
+ .then((argv) => {
311
+ const options = resolveOptions(argv);
312
+ runCheck(argv, options);
313
+ });
314
+ }
315
+
316
+ module.exports = {formatCount, getSummary, outputResult, outputResults, resolveOptions, runCheck};
317
+
318
+ if (require.main === module) {
319
+ // Remove all Node warnings only when run as a CLI, not when imported as a module
320
+ process.removeAllListeners('warning');
321
+ main();
322
+ }
@@ -49,8 +49,8 @@ function blockParamIndex(name, options) {
49
49
  depth < len;
50
50
  depth++
51
51
  ) {
52
- let blockParams = options.blockParams[depth],
53
- param = blockParams && blockParams.indexOf(name);
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
  }
@@ -144,28 +144,16 @@ class Linter {
144
144
 
145
145
  scanner.accept(options.parsed.ast);
146
146
 
147
- if (scanner.context.partials) {
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
- if (scanner.context.customThemeSettings) {
159
- this.customThemeSettings = scanner.context.customThemeSettings;
160
- }
149
+ this.helpers = scanner.context.helpers.map(p => ({
150
+ name: p.node,
151
+ helperType: p.helperType
152
+ }));
161
153
 
162
- if (scanner.context.inlinePartials) {
163
- this.inlinePartials = scanner.context.inlinePartials;
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
- if (node.params.length < 2 || node.params.length > 3) {
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
- // An object containing helpers as keys and their labs flag as values
10
- // E.g. match: 'matchHelper'
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
- var template = themeFile.file.match(/\.hbs$/);
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
- var template = themeFile.file.match(/\.hbs$/);
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 spec = require('../specs');
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 spec = require('../specs');
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': {
@@ -49,7 +49,6 @@ function processFileFunction(files, failures, theme, partialsFound) {
49
49
  });
50
50
  }
51
51
 
52
- theme.helpers = theme.helpers || {};
53
52
  linter.helpers.forEach((helper) => {
54
53
  if (!theme.helpers[helper.name]) {
55
54
  theme.helpers[helper.name] = [];
@@ -1,5 +1,5 @@
1
- var fakeData = function fakeData(themeFile) {
2
- var tag = {
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
- var author = {
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
- var post = {
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
- var postsData = {posts: [post], pagination: {}};
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
- var processedCodes = [],
35
- hasFatalErrors = false,
36
- stats = {
37
- error: 0,
38
- warning: 0,
39
- recommendation: 0
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
- var tmpPath = os.tmpdir(),
28
- inTmp = themePath.substr(0, tmpPath.length) === tmpPath;
27
+ const tmpPath = os.tmpdir();
28
+ const inTmp = themePath.substr(0, tmpPath.length) === tmpPath;
29
29
 
30
30
  arr = arr || [];
31
31
 
32
- var makeResult = function makeResult(result, subFilePath, ext, symlink) {
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
- var toReturn = [],
167
- generateName = function generateName(templateName) {
168
- var 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
- });
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
- return name.trim();
177
- },
178
- generateFor = function (templateName) {
179
- if (templateName.match(/^page-/)) {
180
- return ['page'];
181
- }
176
+ return name.trim();
177
+ };
178
+ const generateFor = function (templateName) {
179
+ if (templateName.match(/^page-/)) {
180
+ return ['page'];
181
+ }
182
182
 
183
- if (templateName.match(/^post-/)) {
184
- return ['post'];
185
- }
183
+ if (templateName.match(/^post-/)) {
184
+ return ['post'];
185
+ }
186
186
 
187
- return ['page', 'post'];
188
- },
189
- generateSlug = function (templateName) {
190
- if (templateName.match(/^custom-/)) {
191
- return null;
192
- }
187
+ return ['page', 'post'];
188
+ };
189
+ const generateSlug = function (templateName) {
190
+ if (templateName.match(/^custom-/)) {
191
+ return null;
192
+ }
193
193
 
194
- return templateName.match(/^(page-|post-)(.*)/)[2];
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
- var tplMatch = entry.file.match(/(.*)\.hbs$/);
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
- var allTemplates = extractTemplates(themeFiles);
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
@@ -5,7 +5,7 @@
5
5
  */
6
6
  const oneLineTrim = require('../utils/one-line-trim');
7
7
  const docsBaseUrl = `https://ghost.org/docs/themes/`;
8
- let knownHelpers, templates, rules, ruleNext;
8
+ let knownHelpers; let templates; let rules; let ruleNext;
9
9
 
10
10
  knownHelpers = [
11
11
  // Ghost
@@ -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
- var maxScore, actualScore, balancedScore;
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.2",
3
+ "version": "5.3.4",
4
4
  "description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
5
5
  "keywords": [
6
6
  "ghost",
@@ -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 c8 --check-coverage --branches 95 --functions 95 --lines 95 --statements 95 mocha test/*.test.js",
36
+ "test": "NODE_ENV=testing vitest run --coverage",
37
37
  "posttest": "yarn lint",
38
38
  "preship": "yarn test",
39
39
  "ship": "pro-ship",
@@ -44,14 +44,14 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@sentry/node": "10.42.0",
47
- "@tryghost/config": "2.0.0",
48
- "@tryghost/debug": "2.0.0",
49
- "@tryghost/errors": "3.0.0",
50
- "@tryghost/logging": "4.0.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.0",
53
- "@tryghost/server": "2.0.0",
54
- "@tryghost/zip": "3.0.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",
@@ -64,16 +64,20 @@
64
64
  },
65
65
  "devDependencies": {
66
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
- "c8": "11.0.0",
70
- "eslint": "10.0.2",
70
+ "@vitest/coverage-v8": "4.0.18",
71
+ "eslint": "10.0.3",
71
72
  "eslint-plugin-ghost": "3.5.0",
72
- "mocha": "11.7.5",
73
73
  "nodemon": "3.1.14",
74
- "rewire": "9.0.1",
75
74
  "should": "13.2.3",
76
- "sinon": "21.0.2"
75
+ "sinon": "21.0.2",
76
+ "vitest": "4.0.18"
77
+ },
78
+ "resolutions": {
79
+ "node-loggly-bulk": "4.0.2",
80
+ "node-loggly-bulk/axios": "1.13.6"
77
81
  },
78
82
  "files": [
79
83
  "lib",