gscan 6.2.1 → 6.4.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.
@@ -14,6 +14,7 @@ module.exports = {
14
14
  'GS090-NO-PRICE-DATA-MONTHLY-YEARLY': require('./lint-no-price-data-monthly-yearly'),
15
15
  'GS090-NO-LIMIT-ALL-IN-GET-HELPER': require('./lint-no-limit-all-in-get-helper'),
16
16
  'GS090-NO-LIMIT-OVER-100-IN-GET-HELPER': require('./lint-no-limit-over-100-in-get-helper'),
17
+ 'GS090-NO-INVALID-CONDITIONAL-ARGUMENTS': require('./lint-no-multi-param-conditionals'),
17
18
  'GS110-NO-UNKNOWN-PAGE-BUILDER-USAGE': require('./lint-no-unknown-page-properties'),
18
19
  'GS120-NO-UNKNOWN-GLOBALS': require('./lint-no-unknown-globals'),
19
20
  'no-multi-param-conditionals': require('./lint-no-multi-param-conditionals'),
@@ -9,7 +9,7 @@ const message = 'The {{img_url}} helper should not be used as a parameter to {{#
9
9
  module.exports = class NoMultiParamConditionals extends Rule {
10
10
  _checkForImgUrlParam(node) {
11
11
  const isConditional = node.path.original === 'if' || node.path.original === 'unless';
12
- const hasImgUrlParam = isConditional && node.params[0].original === 'img_url';
12
+ const hasImgUrlParam = isConditional && node.params[0] && node.params[0].original === 'img_url';
13
13
  let fix;
14
14
 
15
15
  if (isConditional && hasImgUrlParam) {
@@ -1,22 +1,24 @@
1
1
  // https://github.com/TryGhost/gscan/issues/85
2
2
 
3
3
  const Rule = require('./base');
4
- const message = 'Multiple params are not supported in an {{if}} or {{unless}} statement.';
4
+ const message = 'The {{#if}} and {{#unless}} helpers require exactly one argument.';
5
5
 
6
6
  // valid:
7
7
  // {{#if foo}}
8
8
  // {{#unless foo}}
9
9
 
10
10
  // invalid:
11
+ // {{#if}}
11
12
  // {{#if foo bar}}
13
+ // {{#unless}}
12
14
  // {{#unless foo bar}}
13
15
 
14
16
  module.exports = class NoMultiParamConditionals extends Rule {
15
17
  _checkForMultipleParams(node) {
16
18
  const isConditional = node.path.original === 'if' || node.path.original === 'unless';
17
- const hasTooManyParams = node.params.length > 1;
19
+ const hasInvalidParamCount = node.params.length !== 1;
18
20
 
19
- if (isConditional && hasTooManyParams) {
21
+ if (isConditional && hasInvalidParamCount) {
20
22
  this.log({
21
23
  message,
22
24
  line: node.loc && node.loc.start.line,
@@ -0,0 +1,154 @@
1
+ const _ = require('lodash');
2
+ const path = require('path');
3
+ const spec = require('../specs');
4
+ const {versions, normalizePath} = require('../utils');
5
+
6
+ const ruleCode = 'GS130-NO-RECURSIVE-LAYOUT';
7
+ const layoutPattern = /{{!<\s+([A-Za-z0-9._/-]+)\s*}}/;
8
+
9
+ function ensureExtension(layoutPath) {
10
+ if (path.posix.extname(layoutPath)) {
11
+ return layoutPath;
12
+ }
13
+
14
+ return `${layoutPath}.hbs`;
15
+ }
16
+
17
+ function resolveLayoutPath(sourceFile, layoutName) {
18
+ const source = normalizePath(sourceFile);
19
+ const layout = ensureExtension(normalizePath(layoutName));
20
+
21
+ // Ghost resolves layouts with path.resolve(templateDir, layout), so a leading
22
+ // slash is filesystem-absolute and never resolves to a theme file
23
+ if (layout.startsWith('/')) {
24
+ return null;
25
+ }
26
+
27
+ return path.posix.normalize(path.posix.join(path.posix.dirname(source), layout));
28
+ }
29
+
30
+ function getLocation(source, index) {
31
+ const lines = source.slice(0, index).split('\n');
32
+
33
+ return {
34
+ line: lines.length,
35
+ column: lines[lines.length - 1].length
36
+ };
37
+ }
38
+
39
+ function getLayoutReference(themeFile) {
40
+ const source = normalizePath(themeFile.normalizedFile || themeFile.file);
41
+ const content = themeFile.content || '';
42
+ const match = content.match(layoutPattern);
43
+
44
+ if (!match) {
45
+ return;
46
+ }
47
+
48
+ const target = resolveLayoutPath(source, match[1]);
49
+
50
+ if (!target) {
51
+ return;
52
+ }
53
+
54
+ return {
55
+ source,
56
+ target,
57
+ location: getLocation(content, match.index)
58
+ };
59
+ }
60
+
61
+ function buildInheritanceGraph(theme) {
62
+ const hbsFiles = theme.files.filter(file => file.ext === '.hbs');
63
+ const existingFiles = new Set(hbsFiles.map(file => normalizePath(file.normalizedFile || file.file)));
64
+ const graph = new Map();
65
+
66
+ hbsFiles.forEach((themeFile) => {
67
+ const reference = getLayoutReference(themeFile);
68
+
69
+ if (!reference || !existingFiles.has(reference.target)) {
70
+ return;
71
+ }
72
+
73
+ graph.set(reference.source, reference);
74
+ });
75
+
76
+ return graph;
77
+ }
78
+
79
+ function getCyclePath(stack, target) {
80
+ const targetIndex = stack.indexOf(target);
81
+ return stack.slice(targetIndex).concat(target).join(' -> ');
82
+ }
83
+
84
+ function getRecursiveLayoutFailures(graph) {
85
+ const visited = new Set();
86
+ const visiting = new Set();
87
+ const stack = [];
88
+ const reportedCycles = new Set();
89
+ const failures = [];
90
+
91
+ function visit(file) {
92
+ visiting.add(file);
93
+ stack.push(file);
94
+
95
+ const reference = graph.get(file);
96
+
97
+ if (!reference) {
98
+ stack.pop();
99
+ visiting.delete(file);
100
+ visited.add(file);
101
+ return;
102
+ }
103
+
104
+ if (visiting.has(reference.target)) {
105
+ const cyclePath = getCyclePath(stack, reference.target);
106
+
107
+ if (!reportedCycles.has(cyclePath)) {
108
+ reportedCycles.add(cyclePath);
109
+
110
+ failures.push({
111
+ ref: reference.source,
112
+ line: reference.location.line,
113
+ column: reference.location.column,
114
+ message: `Recursive layout inheritance detected: ${cyclePath}`
115
+ });
116
+ }
117
+ } else if (!visited.has(reference.target)) {
118
+ visit(reference.target);
119
+ }
120
+
121
+ stack.pop();
122
+ visiting.delete(file);
123
+ visited.add(file);
124
+ }
125
+
126
+ graph.forEach((_references, file) => {
127
+ if (!visited.has(file)) {
128
+ visit(file);
129
+ }
130
+ });
131
+
132
+ return failures;
133
+ }
134
+
135
+ const checkTemplateInheritance = function checkTemplateInheritance(theme, options) {
136
+ const checkVersion = _.get(options, 'checkVersion', versions.default);
137
+ const ruleSet = spec.get([checkVersion]);
138
+
139
+ if (!ruleSet.rules[ruleCode]) {
140
+ return theme;
141
+ }
142
+
143
+ const failures = getRecursiveLayoutFailures(buildInheritanceGraph(theme));
144
+
145
+ if (failures.length > 0) {
146
+ theme.results.fail[ruleCode] = {failures};
147
+ } else {
148
+ theme.results.pass.push(ruleCode);
149
+ }
150
+
151
+ return theme;
152
+ };
153
+
154
+ module.exports = checkTemplateInheritance;
package/lib/specs/v6.js CHANGED
@@ -11,6 +11,12 @@ const previousRules = previousSpec.rules;
11
11
  let knownHelpers = ['split', 'json', 'color_to_rgba', 'contrast_text_color', 'raw', 'search', 'social_accounts'];
12
12
  let templates = [];
13
13
  let rules = {
14
+ 'GS090-NO-INVALID-CONDITIONAL-ARGUMENTS': {
15
+ level: 'error',
16
+ fatal: true,
17
+ rule: 'Use exactly one argument in <code>{{#if}}</code> and <code>{{#unless}}</code> helpers',
18
+ details: oneLineTrim`The <code>{{#if}}</code> and <code>{{#unless}}</code> helpers only support one argument. To compare values, use a supported helper such as <code>{{#match}}</code>, for example <code>{{#match statusCode 404}}</code>.`
19
+ },
14
20
  'GS090-NO-LIMIT-ALL-IN-GET-HELPER': {
15
21
  level: 'warning',
16
22
  rule: 'Using <code>limit="all"</code> in <code>{{#get}}</code> helper is not supported',
@@ -48,6 +54,12 @@ let rules = {
48
54
  details: 'AMP support was removed in Ghost 6.0. Remove AMP templates and use responsive design instead.',
49
55
  // Matches <html amp> or <html ⚡>, with or without other attributes mixed in
50
56
  regex: /<html\s+(?:amp|⚡)(?:\s|>)|<html\s+[^>]*\s(?:amp|⚡)(?:\s|>)/i
57
+ },
58
+ 'GS130-NO-RECURSIVE-LAYOUT': {
59
+ level: 'error',
60
+ fatal: true,
61
+ rule: 'Templates must not recursively inherit layouts',
62
+ details: oneLineTrim`Remove recursive layout inheritance such as <code>{{!&lt; default}}</code> from <code>default.hbs</code>. A template cannot inherit from itself, directly or through another layout, because it can cause rendering to recurse indefinitely.`
51
63
  }
52
64
  };
53
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gscan",
3
- "version": "6.2.1",
3
+ "version": "6.4.0",
4
4
  "description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
5
5
  "keywords": [
6
6
  "ghost",
@@ -43,23 +43,23 @@
43
43
  "gscan": "./bin/cli.js"
44
44
  },
45
45
  "dependencies": {
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
- "@tryghost/nql": "0.12.10",
52
- "@tryghost/pretty-cli": "3.2.1",
53
- "@tryghost/server": "3.0.1",
54
- "@tryghost/zip": "3.3.2",
46
+ "@sentry/node": "10.58.0",
47
+ "@tryghost/config": "2.3.0",
48
+ "@tryghost/debug": "2.3.0",
49
+ "@tryghost/errors": "3.3.0",
50
+ "@tryghost/logging": "5.1.0",
51
+ "@tryghost/nql": "0.13.1",
52
+ "@tryghost/pretty-cli": "3.3.0",
53
+ "@tryghost/server": "3.1.0",
54
+ "@tryghost/zip": "3.4.0",
55
55
  "chalk": "5.6.2",
56
56
  "express": "5.2.1",
57
57
  "express-handlebars": "8.0.1",
58
58
  "glob": "13.0.6",
59
59
  "handlebars": "4.7.9",
60
60
  "lodash": "4.18.1",
61
- "multer": "2.1.1",
62
- "semver": "7.8.1",
61
+ "multer": "2.2.0",
62
+ "semver": "7.8.4",
63
63
  "validator": "^13.0.0"
64
64
  },
65
65
  "devDependencies": {
@@ -67,11 +67,11 @@
67
67
  "@eslint/eslintrc": "3.3.5",
68
68
  "@eslint/js": "10.0.1",
69
69
  "@tryghost/pro-ship": "1.0.10",
70
- "@vitest/coverage-v8": "4.1.7",
71
- "eslint": "10.4.0",
70
+ "@vitest/coverage-v8": "4.1.9",
71
+ "eslint": "10.5.0",
72
72
  "eslint-plugin-ghost": "3.5.0",
73
73
  "nodemon": "3.1.14",
74
- "vitest": "4.1.7"
74
+ "vitest": "4.1.9"
75
75
  },
76
76
  "files": [
77
77
  "lib",