gscan 6.0.2 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -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
  }
package/lib/checker.js CHANGED
@@ -32,6 +32,7 @@ function loadChecks() {
32
32
  * @param {string} [options.themeName] name of the checked theme
33
33
  * @param {Object=} [options.labs] object containing boolean flags for enabled labs features
34
34
  * @param {boolean} [options.skipChecks] flag to allow reading theme without incurring check costs
35
+ * @param {Object=} [options.limits] zip extraction size limits
35
36
  * @returns {Promise<Object>}
36
37
  */
37
38
  const check = async function checkAll(themePath, options = {}) {
@@ -105,7 +106,7 @@ const checkZip = async function checkZip(path, options) {
105
106
 
106
107
  try {
107
108
  const readZip = require('./read-zip');
108
- ({path: extractedZipPath} = await readZip(zip));
109
+ ({path: extractedZipPath} = await readZip(zip, {limits: options.limits}));
109
110
  return await check(extractedZipPath, Object.assign({themeName: zip.name}, options));
110
111
  } catch (error) {
111
112
  if (!errors.utils.isGhostError(error)) {
@@ -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
  }
@@ -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/read-theme.js CHANGED
@@ -11,6 +11,9 @@ const ignore = [
11
11
  '.DS_Store',
12
12
  '.git',
13
13
  '.svn',
14
+ '.claude',
15
+ 'CLAUDE.md',
16
+ 'AGENTS.md',
14
17
  'Thumbs.db',
15
18
  '.yarn-cache'
16
19
  ];
package/lib/read-zip.js CHANGED
@@ -7,6 +7,10 @@ const {extract} = require('@tryghost/zip');
7
7
  const errors = require('@tryghost/errors');
8
8
  const _ = require('lodash');
9
9
 
10
+ const isKnownZipError = (err) => {
11
+ return errors.utils.isGhostError(err);
12
+ };
13
+
10
14
  const resolveBaseDir = async (zipPath) => {
11
15
  let matches = [];
12
16
 
@@ -25,12 +29,17 @@ const resolveBaseDir = async (zipPath) => {
25
29
  return zipPath;
26
30
  };
27
31
 
28
- const readZip = (zip) => {
32
+ const readZip = (zip, options = {}) => {
29
33
  const tempUuid = randomUUID();
30
34
  const tempPath = os.tmpdir() + '/' + tempUuid;
35
+ const extractOptions = {};
36
+
37
+ if (options.limits) {
38
+ extractOptions.limits = options.limits;
39
+ }
31
40
 
32
41
  debug('Reading Zip', zip.path, 'into', tempPath);
33
- return extract(zip.path, tempPath)
42
+ return extract(zip.path, tempPath, extractOptions)
34
43
  .then(async () => {
35
44
  let resolvedPath = await resolveBaseDir(tempPath);
36
45
  zip.origPath = tempPath;
@@ -41,6 +50,10 @@ const readZip = (zip) => {
41
50
  }).catch((err) => {
42
51
  debug('Zip extraction error', err);
43
52
 
53
+ if (isKnownZipError(err)) {
54
+ throw err;
55
+ }
56
+
44
57
  throw new errors.ValidationError({
45
58
  message: 'Failed to read zip file',
46
59
  help: 'Your zip file might be corrupted, try unzipping and zipping again.',
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/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.0.2",
3
+ "version": "6.2.0",
4
4
  "description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
5
5
  "keywords": [
6
6
  "ghost",
@@ -43,15 +43,15 @@
43
43
  "gscan": "./bin/cli.js"
44
44
  },
45
45
  "dependencies": {
46
- "@sentry/node": "10.52.0",
46
+ "@sentry/node": "10.53.1",
47
47
  "@tryghost/config": "2.2.0",
48
48
  "@tryghost/debug": "2.2.0",
49
- "@tryghost/errors": "3.2.0",
50
- "@tryghost/logging": "4.2.0",
49
+ "@tryghost/errors": "3.2.1",
50
+ "@tryghost/logging": "4.2.1",
51
51
  "@tryghost/nql": "0.12.10",
52
52
  "@tryghost/pretty-cli": "3.2.0",
53
- "@tryghost/server": "2.2.0",
54
- "@tryghost/zip": "3.2.0",
53
+ "@tryghost/server": "2.2.1",
54
+ "@tryghost/zip": "3.3.1",
55
55
  "chalk": "5.6.2",
56
56
  "express": "5.2.1",
57
57
  "express-handlebars": "8.0.1",
@@ -66,16 +66,16 @@
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.6",
71
+ "eslint": "10.4.0",
72
72
  "eslint-plugin-ghost": "3.5.0",
73
73
  "nodemon": "3.1.14",
74
- "vitest": "4.1.5"
74
+ "vitest": "4.1.6"
75
75
  },
76
76
  "resolutions": {
77
77
  "node-loggly-bulk": "4.0.2",
78
- "node-loggly-bulk/axios": "1.15.2",
78
+ "node-loggly-bulk/axios": "1.16.0",
79
79
  "**/handlebars": "4.7.9"
80
80
  },
81
81
  "files": [