gscan 6.1.0 → 6.2.1

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.
@@ -9,7 +9,7 @@ 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
12
+ // Keep in sync with `pageBuilderProperties` in lib/specs/v5.js.
13
13
  this.knownPageBuilderProperties = options.knownPageBuilderProperties || ['show_title_and_feature_image'];
14
14
  }
15
15
 
@@ -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: `Missing Custom Theme Setting: "${name}"`,
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)
@@ -19,7 +19,8 @@ module.exports = class NoUnknownPartials extends Rule {
19
19
  }
20
20
  if (node.type === 'PartialStatement') {
21
21
  return this.log({
22
- message: `Inlined dynamic partials like <code>{{> (dynamicPartial) }}</code> can result in page errors if the partial does not exist, use block dynamic partials instead.`,
22
+ code: 'GS005-NO-INLINE-DYNAMIC-PARTIAL',
23
+ message: `Inline dynamic partial on line ${logObject.line}: ${logObject.source}`,
23
24
  ...logObject
24
25
  });
25
26
  }
@@ -43,9 +43,11 @@ function processFileFunction(files, failures, theme, partialsFound) {
43
43
  });
44
44
 
45
45
  if (astResults.length) {
46
+ const first = astResults[0];
46
47
  failures.push({
47
48
  ref: themeFile.file,
48
- message: astResults[0].message
49
+ message: first.message,
50
+ ...(first.code ? {code: first.code} : {})
49
51
  });
50
52
  }
51
53
 
@@ -86,6 +88,8 @@ function processFileFunction(files, failures, theme, partialsFound) {
86
88
  };
87
89
  }
88
90
 
91
+ const DEFAULT_CODE = 'GS005-TPL-ERR';
92
+
89
93
  const checkTemplatesCompile = function checkTemplatesCompile(theme, options) {
90
94
  const failures = [];
91
95
  const checkVersion = _.get(options, 'checkVersion', versions.default);
@@ -96,35 +100,34 @@ const checkTemplatesCompile = function checkTemplatesCompile(theme, options) {
96
100
  // Reset theme.helpers to make sure we only get helpers that are used
97
101
  theme.helpers = {};
98
102
 
99
- // CASE: 001-deprecations checks only needs `rules` that start with `GS001-DEPR-`
100
- const ruleRegex = /GS005-.*/g;
101
-
102
103
  const rulesToCheck = _.pickBy(ruleSet.rules, function (rule, ruleCode) {
103
- if (ruleCode.match(ruleRegex)) {
104
- return rule;
105
- }
104
+ return /^GS005-/.test(ruleCode);
106
105
  });
107
106
 
108
107
  const processFile = processFileFunction(theme.files, failures, theme, partialsFound);
109
108
 
110
- _.each(rulesToCheck, function (check, ruleCode) {
111
- const linter = new ASTLinter({
112
- partials: theme.partials,
113
- helpers: ruleSet.knownHelpers
114
- });
109
+ const linter = new ASTLinter({
110
+ partials: theme.partials,
111
+ helpers: ruleSet.knownHelpers
112
+ });
115
113
 
116
- _.each(theme.files, function (themeFile) {
117
- let templateTest = themeFile.file.match(/(?<!partials\/.+?)\.hbs$/);
114
+ _.each(theme.files, function (themeFile) {
115
+ if (themeFile.normalizedFile.endsWith('.hbs') && !themeFile.normalizedFile.startsWith('partials/')) {
116
+ processFile(linter, themeFile);
117
+ }
118
+ });
118
119
 
119
- if (templateTest) {
120
- processFile(linter, themeFile);
121
- }
122
- });
120
+ theme.partials = Object.keys(partialsFound);
123
121
 
124
- theme.partials = Object.keys(partialsFound);
122
+ // Route each failure to its target rule code (default for parse errors,
123
+ // missing partials, and missing helpers — they're all GS005-TPL-ERR).
124
+ // The lint rules attach a `code` field to flag the rule they belong to.
125
+ const failuresByCode = _.groupBy(failures, f => f.code || DEFAULT_CODE);
125
126
 
126
- if (failures.length > 0) {
127
- theme.results.fail[ruleCode] = {failures: failures};
127
+ _.each(rulesToCheck, function (rule, ruleCode) {
128
+ const matchingFailures = (failuresByCode[ruleCode] || []).map(f => _.omit(f, 'code'));
129
+ if (matchingFailures.length > 0) {
130
+ theme.results.fail[ruleCode] = {failures: matchingFailures};
128
131
  } else {
129
132
  theme.results.pass.push(ruleCode);
130
133
  }
@@ -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
- if (notUsedVariable.length > 0) {
29
+ notUsedVariable.forEach((name) => {
30
30
  log.failure({
31
- message: `Found unused variables: ${notUsedVariable.map(x => '@custom.' + x).join(', ')}`,
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
  };
@@ -1,6 +1,26 @@
1
1
  const _ = require('lodash');
2
+ const spec = require('../specs');
3
+ const {versions} = require('../utils');
2
4
  const {getRules, applyRule, parseWithAST} = require('../utils/check-utils');
3
5
 
6
+ function findPageTemplateRef(theme) {
7
+ if (!theme || !Array.isArray(theme.files)) {
8
+ return 'page.hbs';
9
+ }
10
+
11
+ const pageHbs = theme.files.find(f => f.normalizedFile === 'page.hbs');
12
+ if (pageHbs) {
13
+ return pageHbs.normalizedFile;
14
+ }
15
+
16
+ const pageOverride = theme.files.find(f => /^page-[^/]+\.hbs$/.test(f.normalizedFile));
17
+ if (pageOverride) {
18
+ return pageOverride.normalizedFile;
19
+ }
20
+
21
+ return 'page.hbs';
22
+ }
23
+
4
24
  const ruleImplementations = {
5
25
  'GS110-NO-MISSING-PAGE-BUILDER-USAGE': {
6
26
  isEnabled: true,
@@ -20,15 +40,21 @@ const ruleImplementations = {
20
40
  }});
21
41
  }
22
42
  },
23
- done: ({log, result}) => {
24
- // TODO: get this from the spec rather than hard-coding to account for version changes
25
- const knownPageBuilderProperties = ['show_title_and_feature_image'];
43
+ done: ({theme, log, result, options}) => {
44
+ const checkVersion = _.get(options, 'checkVersion', versions.default);
45
+ const knownPageBuilderProperties = spec.get([checkVersion]).pageBuilderProperties || [];
26
46
  const notUsedProperties = knownPageBuilderProperties.filter(x => !result.pageBuilderProperties.has(x));
27
47
 
48
+ if (!notUsedProperties.length) {
49
+ return;
50
+ }
51
+
52
+ const ref = findPageTemplateRef(theme);
53
+
28
54
  notUsedProperties.forEach((property) => {
29
55
  log.failure({
30
- ref: `page.hbs`,
31
- message: `@page.${property} is not used`
56
+ ref,
57
+ message: `{{@page.${property}}} is not used`
32
58
  });
33
59
  });
34
60
  }
package/lib/specs/v1.js CHANGED
@@ -385,6 +385,14 @@ rules = {
385
385
  details: oneLineTrim`Oops! You seemed to have used invalid Handlebars syntax. This mostly happens when you use a helper that is not supported.<br>
386
386
  See the full list of available helpers <a href="${docsBaseUrl}helpers/" target=_blank>here</a>.`
387
387
  },
388
+ 'GS005-NO-INLINE-DYNAMIC-PARTIAL': {
389
+ level: 'error',
390
+ fatal: true,
391
+ rule: 'Use the block form for dynamic partials',
392
+ details: oneLineTrim`Inline dynamic partials (e.g. <code>{{> (dynamicPartial)}}</code>) throw a page error if the named partial doesn't exist. Use the block form, which falls back to the inner content when the partial is missing:<br>
393
+ <code>&#123;&#123;#&gt; (dynamicPartial)&#125;&#125;fallback markup&#123;&#123;/undefined&#125;&#125;</code><br>
394
+ See the <a href="${docsBaseUrl}helpers/utility/partials/#dynamic-partials" target=_blank>partials documentation</a> for details.`
395
+ },
388
396
  'GS010-PJ-REQ': {
389
397
  level: 'error',
390
398
  rule: '<code>package.json</code> file should be present',
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
- details: oneLineTrim`The custom theme setting should all be defined in the package.json <code>config.custom</code> object.`
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: 'A custom theme setting defined in <code>package.json</code> hasn\'t been used in any theme file.',
213
- details: oneLineTrim`Custom theme settings defined in <code>package.json</code> must be used at least once in the theme templates.`
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/v5.js CHANGED
@@ -726,17 +726,14 @@ let rules = {
726
726
  Check the <a href="${docsBaseUrl}custom-settings" target=_blank><code>config.custom</code> documentation</a> for further information.`
727
727
  },
728
728
  'GS110-NO-MISSING-PAGE-BUILDER-USAGE': {
729
- level: 'error',
730
- rule: 'Not all page features are being used',
731
- details: oneLineTrim`<b>This error only applies to pages created with the Beta editor.</b> Some page features used by Ghost via the <code>{{@page}}</code> global are not implemented in this theme.&nbsp;
732
- Find more information about the <code>{{@page}}</code> global <a href="${docsBaseUrl}helpers/page/" target=_blank>here</a>.`
729
+ level: 'warning',
730
+ rule: 'Support the <code>{{@page.show_title_and_feature_image}}</code> editor setting',
731
+ details: oneLineTrim`Pages have a toggle that lets editors hide a page's title and feature image on a per-page basis. Gate any relevant markup in page templates with <code>{{#if @page.show_title_and_feature_image}}</code> for the toggle to take effect.`
733
732
  },
734
733
  'GS110-NO-UNKNOWN-PAGE-BUILDER-USAGE': {
735
- level: 'error',
736
- fatal: true,
737
- rule: 'Unsupported page builder feature used',
738
- details: oneLineTrim`A page feature used via the <code>{{@page}}</code> global was detected but is not supported by this version of Ghost. Please upgrade to the latest version for full access.&nbsp;
739
- You can find more information about the <code>{{@page}}</code> global <a href="${docsBaseUrl}helpers/page/" target=_blank>here</a>.`
734
+ level: 'warning',
735
+ rule: 'Remove or correct the unknown <code>{{@page}}</code> property',
736
+ details: oneLineTrim`Ghost currently supports only <code>{{@page.show_title_and_feature_image}}</code>. Remove the reference or correct the spelling — see the <a href="${docsBaseUrl}helpers/page/" target=_blank><code>{{@page}}</code> documentation</a> for details.`
740
737
  },
741
738
  'GS120-NO-UNKNOWN-GLOBALS': {
742
739
  level: 'error',
@@ -776,6 +773,7 @@ module.exports = {
776
773
  knownHelpers: knownHelpers,
777
774
  templates: templates,
778
775
  rules: rules,
776
+ pageBuilderProperties: ['show_title_and_feature_image'],
779
777
  /**
780
778
  * Copy of Ghost defaults for https://github.com/TryGhost/Ghost/blob/e25f1df0ae551c447da0d319bae06eadf9665444/core/frontend/services/theme-engine/config/defaults.json
781
779
  */
package/lib/specs/v6.js CHANGED
@@ -61,5 +61,6 @@ module.exports = {
61
61
  knownHelpers: knownHelpers,
62
62
  templates: templates,
63
63
  rules: rules,
64
+ pageBuilderProperties: previousSpec.pageBuilderProperties,
64
65
  defaultPackageJSON: previousSpec.defaultPackageJSON
65
66
  };
@@ -51,19 +51,19 @@ function applyRule(rule, theme) {
51
51
 
52
52
  // Initialize the rule (optional)
53
53
  if (typeof rule.init === 'function') {
54
- rule.init({theme, log: getLogger({theme, rule}), result});
54
+ rule.init({theme, log: getLogger({theme, rule}), result, options: rule.options});
55
55
  }
56
56
 
57
57
  // Run the main function on each theme file (optional)
58
58
  if (typeof rule.eachFile === 'function') {
59
59
  _.each(theme.files, function (themeFile) {
60
- rule.eachFile({file: themeFile, theme, log: getLogger({theme, rule}), result, partialVerificationCache});
60
+ rule.eachFile({file: themeFile, theme, log: getLogger({theme, rule}), result, partialVerificationCache, options: rule.options});
61
61
  });
62
62
  }
63
63
 
64
64
  // Run the final function
65
65
  if (typeof rule.done === 'function') {
66
- rule.done({theme, log: getLogger({theme, rule}), result});
66
+ rule.done({theme, log: getLogger({theme, rule}), result, options: rule.options});
67
67
  }
68
68
  } catch (e) {
69
69
  // Output something instead of failing silently (should never happen)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gscan",
3
- "version": "6.1.0",
3
+ "version": "6.2.1",
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.52.0",
47
- "@tryghost/config": "2.2.0",
48
- "@tryghost/debug": "2.2.0",
49
- "@tryghost/errors": "3.2.1",
50
- "@tryghost/logging": "4.2.1",
46
+ "@sentry/node": "10.53.1",
47
+ "@tryghost/config": "2.2.1",
48
+ "@tryghost/debug": "2.2.1",
49
+ "@tryghost/errors": "3.2.2",
50
+ "@tryghost/logging": "5.0.1",
51
51
  "@tryghost/nql": "0.12.10",
52
- "@tryghost/pretty-cli": "3.2.0",
53
- "@tryghost/server": "2.2.1",
54
- "@tryghost/zip": "3.3.1",
52
+ "@tryghost/pretty-cli": "3.2.1",
53
+ "@tryghost/server": "3.0.1",
54
+ "@tryghost/zip": "3.3.2",
55
55
  "chalk": "5.6.2",
56
56
  "express": "5.2.1",
57
57
  "express-handlebars": "8.0.1",
@@ -59,24 +59,19 @@
59
59
  "handlebars": "4.7.9",
60
60
  "lodash": "4.18.1",
61
61
  "multer": "2.1.1",
62
- "semver": "7.8.0",
62
+ "semver": "7.8.1",
63
63
  "validator": "^13.0.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@eslint/compat": "2.1.0",
67
67
  "@eslint/eslintrc": "3.3.5",
68
68
  "@eslint/js": "10.0.1",
69
- "@tryghost/pro-ship": "1.0.8",
70
- "@vitest/coverage-v8": "4.1.5",
71
- "eslint": "10.3.0",
69
+ "@tryghost/pro-ship": "1.0.10",
70
+ "@vitest/coverage-v8": "4.1.7",
71
+ "eslint": "10.4.0",
72
72
  "eslint-plugin-ghost": "3.5.0",
73
73
  "nodemon": "3.1.14",
74
- "vitest": "4.1.5"
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.7"
80
75
  },
81
76
  "files": [
82
77
  "lib",