gscan 5.2.4 → 5.3.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.
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2025 Ghost Foundation
1
+ Copyright (c) 2013-2026 Ghost Foundation
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
package/README.md CHANGED
@@ -114,4 +114,4 @@ When developing new rules or testing gscan following tools are great to have in
114
114
 
115
115
  # Copyright & License
116
116
 
117
- Copyright (c) 2013-2025 Ghost Foundation - Released under the [MIT license](LICENSE). Ghost and the Ghost Logo are trademarks of Ghost Foundation Ltd. Please see our [trademark policy](https://ghost.org/trademark/) for info on acceptable usage.
117
+ Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). Ghost and the Ghost Logo are trademarks of Ghost Foundation Ltd. Please see our [trademark policy](https://ghost.org/trademark/) for info on acceptable usage.
package/app/index.js CHANGED
@@ -5,35 +5,70 @@ const sentry = require('@sentry/node');
5
5
  // Require rest of the modules
6
6
  const express = require('express');
7
7
  const debug = require('@tryghost/debug')('app');
8
- const hbs = require('express-hbs');
8
+ const {create} = require('express-handlebars');
9
9
  const multer = require('multer');
10
10
  const server = require('@tryghost/server');
11
11
  const config = require('@tryghost/config');
12
12
  const errors = require('@tryghost/errors');
13
13
  const gscan = require('../lib');
14
14
  const fs = require('fs-extra');
15
+ const path = require('path');
15
16
  const logRequest = require('./middlewares/log-request');
16
17
  const uploadValidation = require('./middlewares/upload-validation');
17
18
  const ghostVer = require('./ghost-version');
18
- const pkgJson = require('../package.json');
19
19
  const ghostVersions = require('../lib/utils').versions;
20
20
  const upload = multer({dest: __dirname + '/uploads/'});
21
21
  const app = express();
22
- const scanHbs = hbs.create();
22
+ const viewsDir = path.join(__dirname, 'tpl');
23
+ const scanHbs = create({
24
+ extname: '.hbs',
25
+ partialsDir: path.join(viewsDir, 'partials'),
26
+ layoutsDir: path.join(viewsDir, 'layouts'),
27
+ defaultLayout: 'default',
28
+ helpers: {
29
+ block(name, options) {
30
+ const root = options.data && options.data.root;
31
+
32
+ if (!root) {
33
+ return typeof options.fn === 'function' ? options.fn(this) : '';
34
+ }
35
+
36
+ const blockCache = root.blockCache || {};
37
+ let val = blockCache[name];
38
+
39
+ if (val === undefined && typeof options.fn === 'function') {
40
+ val = options.fn(this);
41
+ }
42
+
43
+ if (Array.isArray(val)) {
44
+ val = val.join('\n');
45
+ }
46
+
47
+ return val;
48
+ },
49
+ contentFor(name, options) {
50
+ const root = options.data && options.data.root;
51
+
52
+ if (!root) {
53
+ return '';
54
+ }
55
+
56
+ const blockCache = root.blockCache || (root.blockCache = {});
57
+ const block = blockCache[name] || (blockCache[name] = []);
58
+ block.push(options.fn(this));
59
+ return '';
60
+ }
61
+ }
62
+ });
23
63
 
24
64
  // Configure express
25
65
  app.set('x-powered-by', false);
26
66
  app.set('query parser', false);
27
67
 
28
- app.engine('hbs', scanHbs.express4({
29
- partialsDir: __dirname + '/tpl/partials',
30
- layoutsDir: __dirname + '/tpl/layouts',
31
- defaultLayout: __dirname + '/tpl/layouts/default',
32
- templateOptions: {data: {version: pkgJson.version}}
33
- }));
68
+ app.engine('hbs', scanHbs.engine);
34
69
 
35
70
  app.set('view engine', 'hbs');
36
- app.set('views', __dirname + '/tpl');
71
+ app.set('views', viewsDir);
37
72
 
38
73
  app.use(logRequest);
39
74
  app.use(express.static(__dirname + '/public'));
@@ -58,25 +93,29 @@ app.post('/',
58
93
  debug('Uploaded: ' + zip.name + ' to ' + zip.path);
59
94
  debug('Version to check: ' + options.checkVersion);
60
95
 
96
+ let checkError;
97
+
61
98
  gscan.checkZip(zip, options)
62
99
  .then(function processResult(theme) {
63
100
  debug('Checked: ' + zip.name);
64
101
  res.theme = theme;
65
-
102
+ }).catch(function (error) {
103
+ checkError = error;
104
+ }).finally(function () {
66
105
  debug('attempting to remove: ' + req.file.path);
67
106
  fs.remove(req.file.path)
68
- .then(function () {
69
- debug('Calling next');
70
- return next();
107
+ .catch(function (removeError) {
108
+ debug('failed to remove uploaded file', removeError);
71
109
  })
72
- .catch(function () {
73
- // NOTE: transform to `.finally(...) once package is compatible with node >=10
110
+ .then(function () {
111
+ if (checkError) {
112
+ debug('Calling next with error');
113
+ return next(checkError);
114
+ }
115
+
74
116
  debug('Calling next');
75
117
  return next();
76
118
  });
77
- }).catch(function (error) {
78
- debug('Calling next with error');
79
- return next(error);
80
119
  });
81
120
  },
82
121
  function doRender(req, res) {
@@ -1,4 +1,4 @@
1
- var uuid = require('uuid'),
1
+ var randomUUID = require('crypto').randomUUID,
2
2
  logging = require('@tryghost/logging');
3
3
 
4
4
  /**
@@ -7,7 +7,7 @@ var uuid = require('uuid'),
7
7
  */
8
8
  module.exports = function logRequest(req, res, next) {
9
9
  var startTime = Date.now(),
10
- requestId = uuid.v1();
10
+ requestId = randomUUID();
11
11
 
12
12
  function logResponse() {
13
13
  res.responseTime = (Date.now() - startTime) + 'ms';
package/bin/cli.js CHANGED
@@ -146,18 +146,21 @@ function outputResult(result, options) {
146
146
  ui.log(message);
147
147
  });
148
148
  } else {
149
- ui.log(`${chalk.bold('Affected Files:')} ${_.map(result.failures, 'ref')}`);
149
+ ui.log(`${chalk.bold('Affected Files:')} ${_.uniq(_.map(result.failures, 'ref')).join(', ')}`);
150
150
  }
151
151
  }
152
152
 
153
153
  ui.log(''); // extra line-break
154
154
  }
155
155
 
156
+ function formatCount(word, count) {
157
+ return `${count} ${count === 1 ? word : `${word}s`}`;
158
+ }
159
+
156
160
  function getSummary(theme, options) {
157
161
  let summaryText = '';
158
162
  const errorCount = theme.results.error.length;
159
163
  const warnCount = theme.results.warning.length;
160
- const pluralize = require('pluralize');
161
164
  const checkSymbol = '\u2713';
162
165
 
163
166
  if (errorCount === 0 && warnCount === 0) {
@@ -170,7 +173,7 @@ function getSummary(theme, options) {
170
173
  summaryText = `Your theme has`;
171
174
 
172
175
  if (!_.isEmpty(theme.results.error)) {
173
- summaryText += chalk.red.bold(` ${pluralize('error', theme.results.error.length, true)}`);
176
+ summaryText += chalk.red.bold(` ${formatCount('error', theme.results.error.length)}`);
174
177
  }
175
178
 
176
179
  if (!_.isEmpty(theme.results.error) && !_.isEmpty(theme.results.warning)) {
@@ -178,7 +181,7 @@ function getSummary(theme, options) {
178
181
  }
179
182
 
180
183
  if (!_.isEmpty(theme.results.warning)) {
181
- summaryText += chalk.yellow.bold(` ${pluralize('warning', theme.results.warning.length, true)}`);
184
+ summaryText += chalk.yellow.bold(` ${formatCount('warning', theme.results.warning.length)}`);
182
185
  }
183
186
 
184
187
  summaryText += '!';
@@ -215,11 +215,7 @@ class Scope {
215
215
  let matchedFrame = null;
216
216
 
217
217
  if (this.frames && this.frames.length) {
218
- matchedFrame = this.frames.find((frame) => {
219
- if (frame.nodeName === context) {
220
- return true;
221
- }
222
- });
218
+ matchedFrame = this.frames.find(frame => frame.nodeName === context);
223
219
  }
224
220
 
225
221
  return matchedFrame && matchedFrame.node;
package/lib/checker.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const _ = require('lodash');
2
- const requireDir = require('require-dir');
2
+ const nodeFs = require('fs');
3
+ const nodePath = require('path');
3
4
  const debug = require('@tryghost/debug')('checker');
4
5
 
5
6
  const errors = require('@tryghost/errors');
@@ -10,6 +11,18 @@ const versions = require('./utils').versions;
10
11
  const labsEnabledHelpers = {
11
12
  };
12
13
 
14
+ function loadChecks() {
15
+ const checksDir = nodePath.join(__dirname, 'checks');
16
+
17
+ return nodeFs.readdirSync(checksDir)
18
+ .filter(fileName => fileName.endsWith('.js'))
19
+ .reduce((checks, fileName) => {
20
+ const checkName = nodePath.basename(fileName, '.js');
21
+ checks[checkName] = require(nodePath.join(checksDir, fileName));
22
+ return checks;
23
+ }, {});
24
+ }
25
+
13
26
  /**
14
27
  * Check theme
15
28
  *
@@ -25,7 +38,7 @@ const labsEnabledHelpers = {
25
38
  */
26
39
  const check = async function checkAll(themePath, options = {}) {
27
40
  // Require checks late to avoid loading all until used
28
- const checks = options.skipChecks === true ? [] : requireDir('./checks');
41
+ const checks = options.skipChecks === true ? {} : loadChecks();
29
42
  const passedVersion = _.get(options, 'checkVersion', versions.default);
30
43
  let version = passedVersion;
31
44
 
@@ -90,18 +103,12 @@ const checkZip = async function checkZip(path, options) {
90
103
  zip = _.clone(path);
91
104
  }
92
105
 
106
+ let extractedZipPath;
107
+
93
108
  try {
94
109
  const readZip = require('./read-zip');
95
- const {path: extractedZipPath} = await readZip(zip);
96
- const theme = await check(extractedZipPath, Object.assign({themeName: zip.name}, options));
97
-
98
- if (options.keepExtractedDir) {
99
- return theme;
100
- } else {
101
- const fs = require('fs-extra');
102
- await fs.remove(zip.origPath);
103
- return theme;
104
- }
110
+ ({path: extractedZipPath} = await readZip(zip));
111
+ return await check(extractedZipPath, Object.assign({themeName: zip.name}, options));
105
112
  } catch (error) {
106
113
  if (!errors.utils.isGhostError(error)) {
107
114
  throw new errors.ValidationError({
@@ -114,6 +121,11 @@ const checkZip = async function checkZip(path, options) {
114
121
  }
115
122
 
116
123
  throw error;
124
+ } finally {
125
+ if (!options.keepExtractedDir && zip.origPath) {
126
+ const fs = require('fs-extra');
127
+ await fs.remove(zip.origPath);
128
+ }
117
129
  }
118
130
  };
119
131
 
@@ -34,9 +34,11 @@ function processFileFunction(files, failures, rules, partialVerificationCache) {
34
34
  });
35
35
 
36
36
  if (astResults.length) {
37
- failures.push({
38
- ref: themeFile.file,
39
- message: astResults[0].message
37
+ astResults.forEach((result) => {
38
+ failures.push({
39
+ ref: themeFile.file,
40
+ message: `${result.message} (L${result.line})`
41
+ });
40
42
  });
41
43
  }
42
44
 
package/lib/read-zip.js CHANGED
@@ -1,30 +1,32 @@
1
1
  const debug = require('@tryghost/debug')('zip');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
- const glob = require('glob');
4
+ const {randomUUID} = require('crypto');
5
+ const {glob} = require('glob');
5
6
  const {extract} = require('@tryghost/zip');
6
7
  const errors = require('@tryghost/errors');
7
- const uuid = require('uuid');
8
8
  const _ = require('lodash');
9
9
 
10
- const resolveBaseDir = (zipPath) => {
11
- return new Promise((resolve) => {
12
- glob('**/index.hbs', {cwd: zipPath, nosort: true}, function (err, matches) {
13
- var matchedPath;
10
+ const resolveBaseDir = async (zipPath) => {
11
+ let matches = [];
14
12
 
15
- if (!err && !_.isEmpty(matches)) {
16
- debug('Found matches', matches);
17
- matchedPath = matches[0].replace(/index\.hbs$/, '');
18
- zipPath = path.join(zipPath, matchedPath).replace(/\/$/, '');
19
- }
13
+ try {
14
+ matches = await glob('**/index.hbs', {cwd: zipPath, nosort: true});
15
+ } catch (err) {
16
+ debug('Glob match error while resolving zip base dir', err);
17
+ }
20
18
 
21
- return resolve(zipPath);
22
- });
23
- });
19
+ if (!_.isEmpty(matches)) {
20
+ debug('Found matches', matches);
21
+ const matchedPath = matches[0].replace(/index\.hbs$/, '');
22
+ zipPath = path.join(zipPath, matchedPath).replace(/\/$/, '');
23
+ }
24
+
25
+ return zipPath;
24
26
  };
25
27
 
26
28
  const readZip = (zip) => {
27
- const tempUuid = uuid.v4();
29
+ const tempUuid = randomUUID();
28
30
  const tempPath = os.tmpdir() + '/' + tempUuid;
29
31
 
30
32
  debug('Reading Zip', zip.path, 'into', tempPath);
package/lib/specs/v1.js CHANGED
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * This file contains details of the theme API spec, in a format that can be used by GScan
5
5
  */
6
- const oneLineTrim = require('common-tags/lib/oneLineTrim');
6
+ const oneLineTrim = require('../utils/one-line-trim');
7
7
  const docsBaseUrl = `https://ghost.org/docs/themes/`;
8
- let knownHelpers, templates, rules, ruleNext; // eslint-disable-line no-unused-vars
8
+ let knownHelpers, templates, rules, ruleNext;
9
9
 
10
10
  knownHelpers = [
11
11
  // Ghost
@@ -99,8 +99,7 @@ rules = {
99
99
  details: oneLineTrim`The <code>{{image}}</code> helper was replaced with the <code>{{img_url}}</code> helper.<br>
100
100
  Depending on the context of the <code>{{img_url}}</code> helper you would need to use e. g. <br><br><code>{{#post}}<br>&nbsp;&nbsp;&nbsp;&nbsp;{{img_url feature_image}}<br>{{/post}}</code><br><br>to render the feature image of the blog post.<br>
101
101
  <br><b>If you are using <code>{{if image}}</code></b>, then you have to replace it with e.g. <code>{{if feature_image}}.</code>
102
- <br><br>Find more information about the <code>{{img_url}}</code> helper <a href="${docsBaseUrl}helpers/img_url/" target=_blank>here</a> and
103
- read more about Ghost's usage of contexts <a href="${docsBaseUrl}contexts/" target=_blank>here</a>.`,
102
+ <br><br>Find more information about the <code>{{img_url}}</code> helper <a href="${docsBaseUrl}helpers/img_url/" target=_blank>here</a> and read more about Ghost's usage of contexts <a href="${docsBaseUrl}contexts/" target=_blank>here</a>.`,
104
103
  regex: /{{\s*?image\b[\w\s='"]*?}}/g,
105
104
  helper: '{{image}}'
106
105
  },
@@ -323,7 +322,7 @@ rules = {
323
322
  rule: 'Replace <code>{{@blog.posts_per_page}}</code> with <code>{{@config.posts_per_page}}</code>',
324
323
  details: oneLineTrim`The global <code>{{@blog.posts_per_page}}</code> property was replaced with <code>{{@config.posts_per_page}}</code>.<br>
325
324
  Read <a href="${docsBaseUrl}helpers/config/" target=_blank>here</a> about the attribute and
326
- check <a href="${docsBaseUrl}structure/#packagejson" target=_blank>here</a> where you can customise the posts per page setting, as this is now adjustable in your theme.`,
325
+ check <a href="${docsBaseUrl}structure/#package-json" target=_blank>here</a> where you can customise the posts per page setting, as this is now adjustable in your theme.`,
327
326
  regex: /{{\s*?@blog\.posts_per_page\s*?}}/g,
328
327
  helper: '{{@blog.posts_per_page}}'
329
328
  },
@@ -365,22 +364,18 @@ rules = {
365
364
  level: 'error',
366
365
  rule: 'Replace <code>{{id}}</code> with <code>{{comment_id}}</code> in Disqus embeds.',
367
366
  fatal: true,
368
- details: oneLineTrim`The output of <code>{{id}}</code> has changed in v1.0.0 from an incremental ID to an ObjectID.
369
- This results in Disqus comments not loading in Ghost v1.0.0 posts which were imported from earlier versions.
370
- To resolve this, we've added a <code>{{comment_id}}</code> helper that will output the old ID
371
- for posts that have been imported from earlier versions, and ID for new posts.
372
- The Disqus embed must be updated from <code>this.page.identifier = 'ghost-{{id}}';</code> to
373
- <code>this.page.identifier = 'ghost-{{comment_id}}';</code> to ensure Disqus continues to work.`,
367
+ details: oneLineTrim`The output of <code>{{id}}</code> has changed in v1.0.0 from an incremental ID to an ObjectID.<br>
368
+ This results in Disqus comments not loading in Ghost v1.0.0 posts which were imported from earlier versions.<br>
369
+ To resolve this, we've added a <code>{{comment_id}}</code> helper that will output the old ID for posts that have been imported from earlier versions, and ID for new posts.<br>
370
+ The Disqus embed must be updated from <code>this.page.identifier = 'ghost-{{id}}';</code> to <code>this.page.identifier = 'ghost-{{comment_id}}';</code> to ensure Disqus continues to work.`,
374
371
  regex: /(page\.|disqus_)identifier\s?=\s?['"].*?({{\s*?id\s*?}}).*?['"];?/g
375
372
  },
376
373
  'GS002-ID-HELPER': {
377
374
  level: 'recommendation',
378
375
  rule: 'The output of <code>{{id}}</code> changed in Ghost v1.0.0, you may need to use <code>{{comment_id}}</code> instead.',
379
- details: oneLineTrim`The output of <code>{{id}}</code> has changed in v1.0.0 from an incremental ID to an ObjectID.
380
- In v1.0.0 we added a <code>{{comment_id}}</code> helper that will output the old ID
381
- for posts that have been imported from earlier versions, and ID for new posts.
382
- If you need the old ID to be output on imported posts, then you will need to use
383
- <code>{{comment_id}}</code> rather than <code>{{id}}</code>.`,
376
+ details: oneLineTrim`The output of <code>{{id}}</code> has changed in v1.0.0 from an incremental ID to an ObjectID.<br>
377
+ In v1.0.0 we added a <code>{{comment_id}}</code> helper that will output the old ID for posts that have been imported from earlier versions, and ID for new posts.<br>
378
+ If you need the old ID to be output on imported posts, then you will need to use <code>{{comment_id}}</code> rather than <code>{{id}}</code>.`,
384
379
  regex: /({{\s*?id\s*?}})/g
385
380
  },
386
381
  'GS005-TPL-ERR': {
@@ -394,65 +389,65 @@ rules = {
394
389
  level: 'error',
395
390
  rule: '<code>package.json</code> file should be present',
396
391
  details: oneLineTrim`You should provide a <code>package.json</code> file for your theme.<br>
397
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> to see which properties are required and which are recommended.`
392
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> to see which properties are required and which are recommended.`
398
393
  },
399
394
  'GS010-PJ-PARSE': {
400
395
  level: 'error',
401
396
  rule: '<code>package.json</code> file can be parsed',
402
397
  details: oneLineTrim`Your <code>package.json</code> file couldn't be parsed. This is mostly caused by a missing or unnecessary <code>','</code> or the wrong usage of <code>'""'</code>.<br>
403
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.<br>
404
- A good reference for your <code>package.json</code> file is always the latest version of <a href="https://github.com/TryGhost/Casper/blob/master/package.json" target=_blank>Casper</a>.`
398
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.<br>
399
+ A good reference for your <code>package.json</code> file is always the latest version of <a href="https://github.com/TryGhost/Casper/blob/master/package.json" target=_blank>Casper</a>.`
405
400
  },
406
401
  'GS010-PJ-NAME-LC': {
407
402
  level: 'error',
408
403
  rule: '<code>package.json</code> property <code>"name"</code> must be lowercase',
409
404
  details: oneLineTrim`The property <code>"name"</code> in your <code>package.json</code> file must be lowercase.<br>
410
405
  Good examples are: <code>"my-theme"</code> or <code>"theme"</code> rather than <code>"My Theme"</code> or <code>"Theme"</code>.<br>
411
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
406
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
412
407
  },
413
408
  'GS010-PJ-NAME-HY': {
414
409
  level: 'error',
415
410
  rule: '<code>package.json</code> property <code>"name"</code> must be hyphenated',
416
411
  details: oneLineTrim`The property <code>"name"</code> in your <code>package.json</code> file must be hyphenated.<br>
417
412
  Please use <code>"my-theme"</code> rather than <code>"My Theme"</code> or <code>"my theme"</code>.<br>
418
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
413
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
419
414
  },
420
415
  'GS010-PJ-NAME-REQ': {
421
416
  level: 'error',
422
417
  rule: '<code>package.json</code> property <code>"name"</code> is required',
423
418
  details: oneLineTrim`Please add the property <code>"name"</code> to your <code>package.json</code>. E.g. <code>{"name": "my-theme"}</code>.<br>
424
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> to see which properties are required and which are recommended.`
419
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> to see which properties are required and which are recommended.`
425
420
  },
426
421
  'GS010-PJ-VERSION-SEM': {
427
422
  level: 'error',
428
423
  rule: '<code>package.json</code> property <code>"version"</code> must be semver compliant',
429
424
  details: oneLineTrim`The property <code>"version"</code> in your <code>package.json</code> file must be semver compliant. E.g. <code>{"version": "1.0.0"}</code>.<br>
430
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
425
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
431
426
  },
432
427
  'GS010-PJ-VERSION-REQ': {
433
428
  level: 'error',
434
429
  rule: '<code>package.json</code> property <code>"version"</code> is required',
435
430
  details: oneLineTrim`Please add the property <code>"version"</code> to your <code>package.json</code>. E.g. <code>{"version": "1.0.0"}</code>.<br>
436
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> to see which properties are required and which are recommended.`
431
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> to see which properties are required and which are recommended.`
437
432
  },
438
433
  'GS010-PJ-AUT-EM-VAL': {
439
434
  level: 'error',
440
435
  rule: '<code>package.json</code> property <code>"author.email"</code> must be valid',
441
436
  details: oneLineTrim`The property <code>"author.email"</code> in your <code>package.json</code> file must a valid email. E.g. <code>{"author": {"email": "hello@example.com"}}</code>.<br>
442
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
437
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
443
438
  },
444
439
  'GS010-PJ-CONF-PPP': {
445
440
  level: 'recommendation',
446
441
  rule: '<code>package.json</code> property <code>"config.posts_per_page"</code> is recommended. Otherwise, it falls back to 5',
447
442
  details: oneLineTrim`Please add <code>"posts_per_page"</code> to your <code>package.json</code>. E.g. <code>{"config": { "posts_per_page": 5}}</code>.<br>
448
443
  If no <code>"posts_per_page"</code> property is provided, Ghost will use its default setting of 5 posts per page.<br>
449
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
444
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
450
445
  },
451
446
  'GS010-PJ-CONF-PPP-INT': {
452
447
  level: 'error',
453
448
  rule: '<code>package.json</code> property <code>"config.posts_per_page"</code> must be a number above 0',
454
449
  details: oneLineTrim`The property <code>"config.posts_per_page"</code> in your <code>package.json</code> file must be a number greater than zero. E.g. <code>{"config": { "posts_per_page": 5}}</code>.<br>
455
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
450
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
456
451
  },
457
452
  'GS010-PJ-AUT-EM-REQ': {
458
453
  level: 'error',
@@ -461,7 +456,7 @@ rules = {
461
456
  The email is required so that themes which are distributed (either free or paid) have a method of contacting the author so users can get support and more importantly so that>
462
457
  Ghost can reach out about breaking changes and security updates.<br>
463
458
  The <code>package.json</code> file is <strong>NOT</strong> accessible when uploaded to a blog so if the theme is only uploaded to a single blog, no one will see this email address.<br>
464
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> to see which properties are required and which are recommended.`
459
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> to see which properties are required and which are recommended.`
465
460
  },
466
461
  'GS020-INDEX-REQ': {
467
462
  level: 'error',
@@ -476,14 +471,14 @@ rules = {
476
471
  rule: 'A template file called <code>post.hbs</code> must be present',
477
472
  fatal: true,
478
473
  details: oneLineTrim`Your theme must have a template file called <code>index.hbs</code>.<br>
479
- Read <a href="${docsBaseUrl}structure/#templates" target=_blank>here</a> more about the required template structure and <code>post.hbs</code> in <a href="${docsBaseUrl}structure/#posthbs" target=_blank>particular</a>.`,
474
+ Read <a href="${docsBaseUrl}structure/#templates" target=_blank>here</a> more about the required template structure and <code>post.hbs</code> in <a href="${docsBaseUrl}structure/#post-hbs" target=_blank>particular</a>.`,
480
475
  path: 'post.hbs'
481
476
  },
482
477
  'GS020-DEF-REC': {
483
478
  level: 'recommendation',
484
479
  rule: 'Provide a default layout template called default.hbs',
485
480
  details: oneLineTrim`It is recommended that your theme has a template file called <code>default.hbs</code>.<br>
486
- Read <a href="${docsBaseUrl}structure/#templates" target=_blank>here</a> more about the recommended template structure and <code>default.hbs</code> in <a href="${docsBaseUrl}structure/#defaulthbs" target=_blank>particular</a>.`,
481
+ Read <a href="${docsBaseUrl}structure/#templates" target=_blank>here</a> more about the recommended template structure and <code>default.hbs</code> in <a href="${docsBaseUrl}structure/#default-hbs" target=_blank>particular</a>.`,
487
482
  path: 'default.hbs'
488
483
  },
489
484
  'GS030-ASSET-REQ': {
package/lib/specs/v2.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const _ = require('lodash');
2
- const oneLineTrim = require('common-tags/lib/oneLineTrim');
2
+ const oneLineTrim = require('../utils/one-line-trim');
3
3
  const previousSpec = require('./v1');
4
4
  const ghostVersions = require('../utils').versions;
5
5
  const docsBaseUrl = `https://ghost.org/docs/themes/`;
@@ -68,8 +68,7 @@ let rules = {
68
68
  'GS001-DEPR-AUTHBL': {
69
69
  level: 'error',
70
70
  rule: 'Replace <code>{{#author}}</code> with <code>{{#primary_author}}</code> or <code>{{#foreach authors}}...{{/foreach}}</code>',
71
- details: oneLineTrim`The usage of <code>{{#author}}</code> block helper outside of <code>author.hbs</code> is deprecated and
72
- should be replaced with <code>{{#primary_author}}</code> or <code>{{#foreach authors}}...{{/foreach}}</code>.<br>
71
+ details: oneLineTrim`The usage of <code>{{#author}}</code> block helper outside of <code>author.hbs</code> is deprecated and should be replaced with <code>{{#primary_author}}</code> or <code>{{#foreach authors}}...{{/foreach}}</code>.<br>
73
72
  Find more information about the <code>{{authors}}</code> helper <a href="${docsBaseUrl}helpers/authors/" target=_blank>here</a>.`,
74
73
  regex: /{{\s*?#author\s*?}}/g,
75
74
  notValidIn: 'author.hbs',
@@ -395,7 +394,7 @@ let rules = {
395
394
  level: 'error',
396
395
  rule: 'The <code>{{@blog}}</code> helper should be replaced with <code>{{@site}}</code>',
397
396
  details: oneLineTrim`With the introduction of the Content API <code>{{@blog}}</code> became deprecated in favour of <code>{{@site}}</code>.<br>
398
- The <code>{{@blog}}</code> helper will be removed in next version of Ghost and should not be used.
397
+ The <code>{{@blog}}</code> helper will be removed in next version of Ghost and should not be used.<br>
399
398
  Find more information about the <code>@site</code> property <a href="${docsBaseUrl}helpers/site/" target=_blank>here</a>.`,
400
399
  regex: /{{\s*?@blog\.[a-zA-Z0-9_]+\s*?}}/g,
401
400
  helper: '{{@blog}}'
@@ -404,8 +403,8 @@ let rules = {
404
403
  level: 'error',
405
404
  rule: '<code>{{@blog.permalinks}}</code> was removed',
406
405
  details: oneLineTrim`With the introduction of Dynamic Routing, you can define multiple permalinks.<br>
407
- The <code>{{@blog.permalinks}}</code> property will therefore no longer be used and should be removed from the theme.
408
- Find more information about Ghost data helpers <a href="${docsBaseUrl}/helpers/#data-helpers" target=_blank>here</a>.`,
406
+ The <code>{{@blog.permalinks}}</code> property will therefore no longer be used and should be removed from the theme.<br>
407
+ Find more information about Ghost data helpers <a href="${docsBaseUrl}helpers/data/" target=_blank>here</a>.`,
409
408
  regex: /{{\s*?@blog\.permalinks\s*?}}/g,
410
409
  helper: '{{@blog.permalinks}}'
411
410
  },
@@ -413,7 +412,7 @@ let rules = {
413
412
  level: 'error',
414
413
  rule: '<code>{{@site.permalinks}}</code> was removed',
415
414
  details: oneLineTrim`With the introduction of Dynamic Routing, you can define multiple permalinks.<br>
416
- The <code>{{@site.permalinks}}</code> property will therefore no longer be used and should be removed from the theme.
415
+ The <code>{{@site.permalinks}}</code> property will therefore no longer be used and should be removed from the theme.<br>
417
416
  Find more information about the <code>@site</code> property <a href="${docsBaseUrl}helpers/site/" target=_blank>here</a>.`,
418
417
  regex: /{{\s*?@site\.permalinks\s*?}}/g,
419
418
  helper: '{{@site.permalinks}}'
@@ -422,7 +421,7 @@ let rules = {
422
421
  level: 'error',
423
422
  rule: 'Replace <code>{{@site.ghost_head}}</code> with <code>{{ghost_head}}</code>',
424
423
  details: oneLineTrim`The usage of <code>{{@site.ghost_head}}</code> is deprecated and should be replaced with <code>{{ghost_head}}</code>.<br>
425
- The <code>{{@site.ghost_head}}</code> property will therefore no longer be used and should be removed from the theme.
424
+ The <code>{{@site.ghost_head}}</code> property will therefore no longer be used and should be removed from the theme.<br>
426
425
  Find more information about the <code>{{ghost_head}}</code> property <a href="${docsBaseUrl}helpers/ghost_head_foot/" target=_blank>here</a>.`,
427
426
  regex: /{{\s*?@site\.ghost_head\s*?}}/g,
428
427
  helper: '{{@site.ghost_head}}'
@@ -431,7 +430,7 @@ let rules = {
431
430
  level: 'error',
432
431
  rule: 'Replace <code>{{@site.ghost_foot}}</code> with <code>{{ghost_foot}}</code>',
433
432
  details: oneLineTrim`The usage of <code>{{@site.ghost_foot}}</code> is deprecated and should be replaced with <code>{{ghost_foot}}</code>.<br>
434
- The <code>{{@site.ghost_foot}}</code> property will therefore no longer be used and should be removed from the theme.
433
+ The <code>{{@site.ghost_foot}}</code> property will therefore no longer be used and should be removed from the theme.<br>
435
434
  Find more information about the <code>{{ghost_foot}}</code> property <a href="${docsBaseUrl}helpers/ghost_head_foot/" target=_blank>here</a>.`,
436
435
  regex: /{{\s*?@site\.ghost_foot\s*?}}/g,
437
436
  helper: '{{@site.ghost_foot}}'
@@ -440,7 +439,7 @@ let rules = {
440
439
  level: 'error',
441
440
  rule: 'The <code>{{lang}}</code> helper should be replaced with <code>{{@site.lang}}</code>',
442
441
  details: oneLineTrim`The <code>{{lang}}</code> helper is a duplicate of <code>{{@site.lang}}</code>. Using <code>{{@site.lang}}</code> is preferred.<br>
443
- The <code>{{lang}}</code> helper will be removed in next version of Ghost and should not be used.
442
+ The <code>{{lang}}</code> helper will be removed in next version of Ghost and should not be used.<br>
444
443
  Find more information about the <code>@site.lang</code> property <a href="${docsBaseUrl}helpers/site/" target=_blank>here</a>.`,
445
444
  regex: /{{\s*?lang\s*?}}/g,
446
445
  helper: '{{lang}}'
@@ -448,8 +447,8 @@ let rules = {
448
447
  'GS001-DEPR-CSS-KGMD': {
449
448
  level: 'warning',
450
449
  rule: `<code>.kg-card-markdown</code> doesn't exist in current version of Ghost, ensure your theme works without it`,
451
- details: oneLineTrim`The <code>.kg-card-markdown</code> CSS class is deprecated and will no longer be used in Ghost.
452
- It's recommended to add your own wrapper around the <code>{{content}}</code> helper and target that instead if needed.
450
+ details: oneLineTrim`The <code>.kg-card-markdown</code> CSS class is deprecated and will no longer be used in Ghost.<br>
451
+ It's recommended to add your own wrapper around the <code>{{content}}</code> helper and target that instead if needed.<br>
453
452
  Find out more about required theme changes for the Koenig editor <a href="${docsBaseUrl}content/" target=_blank>here</a>.`,
454
453
  regex: /\.kg-card-markdown/g,
455
454
  className: '.kg-card-markdown',
@@ -843,7 +842,7 @@ let rules = {
843
842
  level: 'warning',
844
843
  rule: '<code>package.json</code> property <code>keywords</code> should contain <code>ghost-theme</code>',
845
844
  details: oneLineTrim`The property <code>keywords</code> in your <code>package.json</code> file must contain <code>ghost-theme</code>. E.g. <code>{"keywords": ["ghost-theme"]}</code>.<br>
846
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
845
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
847
846
  }
848
847
  };
849
848
 
package/lib/specs/v3.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const _ = require('lodash');
2
- const oneLineTrim = require('common-tags/lib/oneLineTrim');
2
+ const oneLineTrim = require('../utils/one-line-trim');
3
3
  const previousSpec = require('./v2');
4
4
  const ghostVersions = require('../utils').versions;
5
5
  const docsBaseUrl = `https://ghost.org/docs/themes/`;
@@ -22,21 +22,21 @@ let rules = {
22
22
  rule: '<code>package.json</code> property <code>"engines.ghost-api"</code> is recommended. Otherwise, it falls back to "v3"',
23
23
  details: oneLineTrim`Please add <code>"ghost-api"</code> to your <code>package.json</code>. E.g. <code>{"engines": {"ghost-api": "v3"}}</code>.<br>
24
24
  If no <code>"ghost-api"</code> property is provided, Ghost will use its default setting of "v3" Ghost API.<br>
25
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
25
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
26
26
  },
27
27
  'GS010-PJ-GHOST-API-V01': {
28
28
  level: 'error',
29
29
  rule: '<code>package.json</code> property <code>"engines.ghost-api"</code> is incompatible with current version of Ghost API and will fall back to "v3"',
30
30
  details: oneLineTrim`Please change <code>"ghost-api"</code> in your <code>package.json</code> to higher version. E.g. <code>{"engines": {"ghost-api": "v3"}}</code>.<br>
31
31
  If <code>"ghost-api"</code> property is left at "v0.1", Ghost will use its default setting of "v3".<br>
32
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
32
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
33
33
  },
34
34
  'GS001-DEPR-ESC': {
35
35
  level: 'error',
36
36
  rule: 'Replace <code>{{error.code}}</code> with <code>{{error.statusCode}}</code>',
37
37
  details: oneLineTrim`The usage of <code>{{error.code}}</code> is deprecated and should be replaced with <code>{{error.statusCode}}</code>.<br>
38
- When in <code>error</code> context, e. g. in the <code>error.hbs</code> template, replace <code>{{code}}</code> with <code>{{statusCode}}</code>.<br>
39
- Find more information about the <code>error.hbs</code> template <a href="${docsBaseUrl}structure/#errorhbs" target=_blank>here</a>.`,
38
+ When in <code>error</code> context, e.g. in the <code>error.hbs</code> template, replace <code>{{code}}</code> with <code>{{statusCode}}</code>.<br>
39
+ Find more information about the <code>error.hbs</code> template <a href="${docsBaseUrl}structure/#error-hbs" target=_blank>here</a>.`,
40
40
  regex: /{{\s*?(?:error\.)?(code)\s*?}}/g,
41
41
  helper: '{{error.code}}'
42
42
  },
package/lib/specs/v4.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const _ = require('lodash');
2
- const oneLineTrim = require('common-tags/lib/oneLineTrim');
2
+ const oneLineTrim = require('../utils/one-line-trim');
3
3
  const previousSpec = require('./v3');
4
4
  const ghostVersions = require('../utils').versions;
5
5
  const docsBaseUrl = `https://ghost.org/docs/themes/`;
@@ -35,81 +35,81 @@ let rules = {
35
35
  rule: '<code>package.json</code> property <code>"engines.ghost-api"</code> is deprecated.',
36
36
  details: oneLineTrim`Remove <code>"ghost-api"</code> from your <code>package.json</code>.<br>
37
37
  The <code>ghost-api</code> support will be removed in next major version of Ghost and should not be used.
38
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
38
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
39
39
  },
40
40
  'GS010-PJ-GHOST-API-V01': {
41
41
  level: 'error',
42
42
  rule: '<code>package.json</code> property <code>"engines.ghost-api"</code> is incompatible with current version of Ghost API and will fall back to "v4"',
43
43
  details: oneLineTrim`Change <code>"ghost-api"</code> in your <code>package.json</code> to higher version. E.g. <code>{"engines": {"ghost-api": "v4"}}</code>.<br>
44
44
  If <code>"ghost-api"</code> property is left at "v0.1", Ghost will use its default setting of "v4".<br>
45
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
45
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
46
46
  },
47
47
  'GS010-PJ-GHOST-API-V2': {
48
48
  level: 'warning',
49
49
  rule: '<code>package.json</code> property <code>"engines.ghost-api"</code> is using a deprecated version of Ghost API',
50
50
  details: oneLineTrim`Change <code>"ghost-api"</code> in your <code>package.json</code> to higher version. E.g. <code>{"engines": {"ghost-api": "v4"}}</code>.<br>
51
51
  If <code>"ghost-api"</code> property is left at "v2", it will stop working with next major version upgrade and default to v5.<br>
52
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
52
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
53
53
  },
54
54
  'GS010-PJ-CUST-THEME-TOTAL-SETTINGS': {
55
55
  level: 'error',
56
56
  rule: '<code>package.json</code> property <code>"config.custom"</code> contains too many settings',
57
57
  details: oneLineTrim`Remove key from <code>"config.custom"</code> in your <code>package.json</code> to have less than or exactly 20 settings.<br>
58
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
58
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
59
59
  },
60
60
  'GS010-PJ-CUST-THEME-SETTINGS-CASE': {
61
61
  level: 'error',
62
62
  rule: '<code>package.json</code> property <code>"config.custom"</code> contains a property that isn\'t snake-cased',
63
63
  details: oneLineTrim`Rewrite all property in <code>"config.custom"</code> in your <code>package.json</code> in snake case.<br>
64
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
64
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
65
65
  },
66
66
  'GS010-PJ-CUST-THEME-SETTINGS-TYPE': {
67
67
  level: 'error',
68
68
  rule: '<code>package.json</code> objects defined in <code>"config.custom"</code> should have a known <code>"type"</code>.',
69
69
  details: oneLineTrim`Only use the following types: <code>"select"</code>, <code>"boolean"</code>, <code>"color"</code>, <code>"image"</code>, <code>"text"</code>.<br>
70
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
70
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
71
71
  },
72
72
  'GS010-PJ-CUST-THEME-SETTINGS-GROUP': {
73
73
  level: 'recommendation',
74
74
  rule: '<code>package.json</code> objects defined in <code>"config.custom"</code> should have a known <code>"group"</code>.',
75
75
  details: oneLineTrim`Only use the following groups: <code>"post"</code>, <code>"homepage"</code>.<br>
76
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
76
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
77
77
  },
78
78
  'GS010-PJ-CUST-THEME-SETTINGS-SELECT-OPTIONS': {
79
79
  level: 'error',
80
80
  rule: '<code>package.json</code> objects defined in <code>"config.custom"</code> of type <code>"select"</code> need to have at least 2 <code>"options"</code>.',
81
81
  details: oneLineTrim`Make sure there is at least 2 <code>"options"</code> in each <code>"select"</code> custom theme property.<br>
82
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
82
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
83
83
  },
84
84
  'GS010-PJ-CUST-THEME-SETTINGS-SELECT-DEFAULT': {
85
85
  level: 'error',
86
86
  rule: '<code>package.json</code> objects defined in <code>"config.custom"</code> of type <code>"select"</code> need to have a valid <code>"default"</code>.',
87
87
  details: oneLineTrim`Make sure the <code>"default"</code> property matches a value in <code>"options"</code> of the same <code>"select"</code>.<br>
88
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
88
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
89
89
  },
90
90
  'GS010-PJ-CUST-THEME-SETTINGS-BOOLEAN-DEFAULT': {
91
91
  level: 'error',
92
92
  rule: '<code>package.json</code> objects defined in <code>"config.custom"</code> of type <code>"boolean"</code> need to have a valid <code>"default"</code>.',
93
93
  details: oneLineTrim`Make sure the <code>"default"</code> property is either <code>true</code> or <code>false</code>.<br>
94
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
94
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
95
95
  },
96
96
  'GS010-PJ-CUST-THEME-SETTINGS-COLOR-DEFAULT': {
97
97
  level: 'error',
98
98
  rule: '<code>package.json</code> objects defined in <code>"config.custom"</code> of type <code>"color"</code> need to have a valid <code>"default"</code>.',
99
99
  details: oneLineTrim`Make sure the <code>"default"</code> property is a valid 6-hexadecimal-digit color code like <code>#15171a</code>.<br>
100
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
100
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
101
101
  },
102
102
  'GS010-PJ-CUST-THEME-SETTINGS-IMAGE-DEFAULT': {
103
103
  level: 'error',
104
104
  rule: '<code>package.json</code> objects defined in <code>"config.custom"</code> of type <code>"image"</code> can\'t have a <code>"default"</code> value.',
105
105
  details: oneLineTrim`Make sure the <code>"default"</code> property is either <code>null</code>, an empty string <code>''</code> or isn't present.<br>
106
- Check the <a href="${docsBaseUrl}structure/#packagejson" target=_blank><code>package.json</code> documentation</a> for further information.`
106
+ Check the <a href="${docsBaseUrl}structure/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
107
107
  },
108
108
  'GS001-DEPR-LABS-MEMBERS': {
109
109
  level: 'warning',
110
110
  rule: 'The <code>{{@labs.members}}</code> helper should not be used.',
111
111
  details: oneLineTrim`Remove <code>{{@labs.members}}</code> from the theme.<br>
112
- The <code>{{@labs.members}}</code> helper will always return <code>true</code> in Ghost v4 and will be removed from Ghost v5, at which point it will return <code>null</code> and evaluate to <code>false</code>.
112
+ The <code>{{@labs.members}}</code> helper will always return <code>true</code> in Ghost v4 and will be removed from Ghost v5, at which point it will return <code>null</code> and evaluate to <code>false</code>.<br>
113
113
  Find more information about the <code>@labs</code> property <a href="${docsBaseUrl}helpers/labs/" target=_blank>here</a>.`,
114
114
  regex: /@labs\.members/g,
115
115
  helper: '{{@labs.members}}'
@@ -126,7 +126,7 @@ let rules = {
126
126
  'GS080-CARD-LAST4': {
127
127
  level: 'warning',
128
128
  rule: 'The <code>default_payment_card_last4</code> field now coalesces to <code>****</code> in Ghost 4.x instead of null.',
129
- details: oneLineTrim`The <code>default_payment_card_last4</code> field no longer outputs a falsy(null) value in case of missing card details starting from Ghost 4.x and instead coalesces to <code>****</code>
129
+ details: oneLineTrim`The <code>default_payment_card_last4</code> field no longer outputs a falsy(null) value in case of missing card details starting from Ghost 4.x and instead coalesces to <code>****</code><br>
130
130
  Find more information about the <code>default_payment_card_last4</code> attribute <a href="${docsBaseUrl}members/#subscription-attributes" target=_blank>here</a>.`,
131
131
  regex: /default_payment_card_last4/g,
132
132
  helper: '{{default_payment_card_last4}}',
@@ -144,7 +144,7 @@ let rules = {
144
144
  'GS001-DEPR-CURR-SYM': {
145
145
  level: 'warning',
146
146
  rule: 'Replace <code>{{[#].currency_symbol}}</code> with <code>{{price currency=currency}}</code>.',
147
- details: oneLineTrim`The hardcoded <code>currency_symbol</code> attribute was removed in favour of passing the currency to updated <code>{{price}}</code> helper.
147
+ details: oneLineTrim`The hardcoded <code>currency_symbol</code> attribute was removed in favour of passing the currency to updated <code>{{price}}</code> helper.<br>
148
148
  Find more information about the updated <code>{{price}}</code> helper <a href="${docsBaseUrl}members/#the-price-helper" target=_blank>here</a>.`,
149
149
  helper: '{{[#].currency_symbol}}',
150
150
  regex: /currency_symbol/g
@@ -153,7 +153,7 @@ let rules = {
153
153
  level: 'warning',
154
154
  rule: 'Replace <code>{{@site.lang}}</code> with <code>{{@site.locale}}</code>',
155
155
  details: oneLineTrim`Replace <code>{{@site.lang}}</code> helper with <code>{{@site.locale}}</code>.<br>
156
- The <code>{{@site.lang}}</code> helper will be removed in next version of Ghost and should not be used.
156
+ The <code>{{@site.lang}}</code> helper will be removed in next version of Ghost and should not be used.<br>
157
157
  Find more information about the <code>@site</code> property <a href="${docsBaseUrl}helpers/site/" target=_blank>here</a>.`,
158
158
  regex: /@site\.lang/g,
159
159
  helper: '{{@site.lang}}'
@@ -214,17 +214,17 @@ let rules = {
214
214
  },
215
215
 
216
216
  'GS050-CSS-KGCO': cssCardRule('callout', 'kg-callout-card'),
217
- 'GS050-CSS-KGCOE': cssCardRule('callout', 'kg-callout-card-emoji'),
218
- 'GS050-CSS-KGCOT': cssCardRule('callout', 'kg-callout-card-text'),
219
- 'GS050-CSS-KGCOBGGY': cssCardRule('callout', 'kg-callout-card-background-grey'),
220
- 'GS050-CSS-KGCOBGW': cssCardRule('callout', 'kg-callout-card-background-white'),
221
- 'GS050-CSS-KGCOBGB': cssCardRule('callout', 'kg-callout-card-background-blue'),
222
- 'GS050-CSS-KGCOBGGN': cssCardRule('callout', 'kg-callout-card-background-green'),
223
- 'GS050-CSS-KGCOBGY': cssCardRule('callout', 'kg-callout-card-background-yellow'),
224
- 'GS050-CSS-KGCOBGR': cssCardRule('callout', 'kg-callout-card-background-red'),
225
- 'GS050-CSS-KGCOBGPK': cssCardRule('callout', 'kg-callout-card-background-pink'),
226
- 'GS050-CSS-KGCOBGPE': cssCardRule('callout', 'kg-callout-card-background-purple'),
227
- 'GS050-CSS-KGCOBGA': cssCardRule('callout', 'kg-callout-card-background-accent'),
217
+ 'GS050-CSS-KGCOE': cssCardRule('callout', 'kg-callout-emoji'),
218
+ 'GS050-CSS-KGCOT': cssCardRule('callout', 'kg-callout-text'),
219
+ 'GS050-CSS-KGCOBGGY': cssCardRule('callout', 'kg-callout-card-grey'),
220
+ 'GS050-CSS-KGCOBGW': cssCardRule('callout', 'kg-callout-card-white'),
221
+ 'GS050-CSS-KGCOBGB': cssCardRule('callout', 'kg-callout-card-blue'),
222
+ 'GS050-CSS-KGCOBGGN': cssCardRule('callout', 'kg-callout-card-green'),
223
+ 'GS050-CSS-KGCOBGY': cssCardRule('callout', 'kg-callout-card-yellow'),
224
+ 'GS050-CSS-KGCOBGR': cssCardRule('callout', 'kg-callout-card-red'),
225
+ 'GS050-CSS-KGCOBGPK': cssCardRule('callout', 'kg-callout-card-pink'),
226
+ 'GS050-CSS-KGCOBGPE': cssCardRule('callout', 'kg-callout-card-purple'),
227
+ 'GS050-CSS-KGCOBGA': cssCardRule('callout', 'kg-callout-card-accent'),
228
228
 
229
229
  'GS050-CSS-KG-NFT': cssCardRule('nft', 'kg-nft-card'),
230
230
  'GS050-CSS-KG-NFTCO': cssCardRule('nft', 'kg-nft-card-container'),
@@ -232,7 +232,7 @@ let rules = {
232
232
  'GS050-CSS-KG-NFTIMG': cssCardRule('nft', 'kg-nft-image'),
233
233
  'GS050-CSS-KG-NFTHD': cssCardRule('nft', 'kg-nft-header'),
234
234
  'GS050-CSS-KG-NFTTIT': cssCardRule('nft', 'kg-nft-title'),
235
- 'GS050-CSS-KG-NFTLG': cssCardRule('nft', 'kg-nft-logo'),
235
+ 'GS050-CSS-KG-NFTLG': cssCardRule('nft', 'kg-nft-opensea-logo'),
236
236
  'GS050-CSS-KG-NFTCTR': cssCardRule('nft', 'kg-nft-creator'),
237
237
  'GS050-CSS-KG-NFTDSC': cssCardRule('nft', 'kg-nft-description'),
238
238
 
@@ -264,10 +264,7 @@ let rules = {
264
264
  'GS050-CSS-KGVIDCNT': cssCardRule('video', 'kg-video-container'),
265
265
  'GS050-CSS-KGVIDOVL': cssCardRule('video', 'kg-video-overlay'),
266
266
  'GS050-CSS-KGVIDLGPLICO': cssCardRule('video', 'kg-video-large-play-icon'),
267
- 'GS050-CSS-KGVIDTHUMB': cssCardRule('video', 'kg-video-thumbnail'),
268
- 'GS050-CSS-KGVIDTHUMBPL': cssCardRule('video', 'kg-video-thumbnail.placeholder'),
269
267
  'GS050-CSS-KGVIDPLCNT': cssCardRule('video', 'kg-video-player-container'),
270
- 'GS050-CSS-KGVIDTI': cssCardRule('video', 'kg-video-title'),
271
268
  'GS050-CSS-KGVIDPL': cssCardRule('video', 'kg-video-player'),
272
269
  'GS050-CSS-KGVIDCURRTM': cssCardRule('video', 'kg-video-current-time'),
273
270
  'GS050-CSS-KGVIDTM': cssCardRule('video', 'kg-video-time'),
package/lib/specs/v5.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const _ = require('lodash');
2
- const oneLineTrim = require('common-tags/lib/oneLineTrim');
2
+ const oneLineTrim = require('../utils/one-line-trim');
3
3
  const previousSpec = require('./v4');
4
4
  const ghostVersions = require('../utils').versions;
5
5
  const docsBaseUrl = `https://ghost.org/docs/themes/`;
@@ -23,7 +23,7 @@ let rules = {
23
23
  level: 'warning',
24
24
  rule: 'Remove <code>"engines.ghost-api"</code> from <code>package.json</code>',
25
25
  details: oneLineTrim`The <code>"ghost-api"</code> version is no longer used and can be removed.<br>
26
- Find more information about the <code>package.json</code> file <a href="${docsBaseUrl}structure/#packagejson" target=_blank>here</a>.`
26
+ Find more information about the <code>package.json</code> file <a href="${docsBaseUrl}structure/#package-json" target=_blank>here</a>.`
27
27
  },
28
28
  'GS010-PJ-GHOST-CARD-ASSETS-NOT-PRESENT': {
29
29
  level: 'warning',
@@ -650,7 +650,7 @@ let rules = {
650
650
  rule: 'Remove uses of <code>{{@blog.permalinks}}</code>',
651
651
  details: oneLineTrim`With the introduction of Dynamic Routing, you can define multiple permalinks.<br>
652
652
  The <code>{{@blog.permalinks}}</code> property will therefore no longer be used and should be removed from the theme.
653
- Find more information about Ghost data helpers <a href="${docsBaseUrl}/helpers/#data-helpers" target=_blank>here</a>.`,
653
+ Find more information about Ghost data helpers <a href="${docsBaseUrl}helpers/data/" target=_blank>here</a>.`,
654
654
  regex: /{{\s*?@blog\.permalinks\s*?}}/g,
655
655
  helper: '{{@blog.permalinks}}'
656
656
  },
package/lib/specs/v6.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const _ = require('lodash');
2
- const oneLineTrim = require('common-tags/lib/oneLineTrim');
2
+ const oneLineTrim = require('../utils/one-line-trim');
3
3
  const previousSpec = require('./v5');
4
4
  const docsBaseUrl = `https://docs.ghost.org/themes/`;
5
5
 
@@ -0,0 +1,10 @@
1
+ function oneLineTrim(strings, ...values) {
2
+ const combined = strings.reduce((result, str, index) => {
3
+ const value = index < values.length ? values[index] : '';
4
+ return result + str + value;
5
+ }, '');
6
+
7
+ return combined.replace(/(?:\n\s*)/g, '').trim();
8
+ }
9
+
10
+ module.exports = oneLineTrim;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gscan",
3
- "version": "5.2.4",
3
+ "version": "5.3.0",
4
4
  "description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
5
5
  "keywords": [
6
6
  "ghost",
@@ -17,7 +17,7 @@
17
17
  "registry": "https://registry.npmjs.org/"
18
18
  },
19
19
  "engines": {
20
- "node": "^14.18.0 || ^16.13.0 || ^18.12.1 || ^20.11.1 || ^22.13.1"
20
+ "node": "^20.11.1 || ^22.13.1 || ^24.0.0"
21
21
  },
22
22
  "bugs": {
23
23
  "url": "https://github.com/TryGhost/gscan/issues"
@@ -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 mocha test/*.test.js",
36
+ "test": "NODE_ENV=testing c8 --check-coverage --branches 95 --functions 95 --lines 95 --statements 95 mocha test/*.test.js",
37
37
  "posttest": "yarn lint",
38
38
  "preship": "yarn test",
39
39
  "ship": "pro-ship",
@@ -43,38 +43,35 @@
43
43
  "gscan": "./bin/cli.js"
44
44
  },
45
45
  "dependencies": {
46
- "@sentry/node": "^9.0.0",
47
- "@tryghost/config": "^0.2.18",
48
- "@tryghost/debug": "^0.1.26",
49
- "@tryghost/errors": "^1.2.26",
50
- "@tryghost/logging": "^2.4.7",
51
- "@tryghost/nql": "^0.12.5",
52
- "@tryghost/pretty-cli": "^1.2.38",
53
- "@tryghost/server": "^0.1.37",
54
- "@tryghost/zip": "^1.1.42",
55
- "chalk": "^4.1.2",
56
- "common-tags": "^1.8.2",
57
- "express": "^4.18.2",
58
- "express-hbs": "^2.4.2",
59
- "fs-extra": "^11.1.1",
60
- "glob": "^8.1.0",
61
- "lodash": "^4.17.21",
62
- "multer": "^2.0.0",
63
- "pluralize": "^8.0.0",
64
- "require-dir": "^1.2.0",
65
- "semver": "^7.5.4",
66
- "uuid": "^9.0.1",
46
+ "@sentry/node": "10.40.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",
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",
55
+ "chalk": "5.6.2",
56
+ "express": "5.2.1",
57
+ "express-handlebars": "7.1.3",
58
+ "fs-extra": "11.3.3",
59
+ "glob": "13.0.6",
60
+ "lodash": "4.17.23",
61
+ "multer": "2.1.0",
62
+ "semver": "7.7.4",
67
63
  "validator": "^13.0.0"
68
64
  },
69
65
  "devDependencies": {
66
+ "@eslint/compat": "2.0.2",
67
+ "@eslint/js": "10.0.1",
70
68
  "@tryghost/pro-ship": "1.0.7",
71
- "c8": "8.0.1",
72
- "eslint": "8.1.0",
73
- "eslint-plugin-ghost": "2.1.0",
69
+ "c8": "11.0.0",
70
+ "eslint": "10.0.2",
71
+ "eslint-plugin-ghost": "3.4.4",
74
72
  "mocha": "11.7.5",
75
- "node-fetch": "3.3.2",
76
- "nodemon": "2.0.7",
77
- "rewire": "6.0.0",
73
+ "nodemon": "3.1.14",
74
+ "rewire": "9.0.1",
78
75
  "should": "13.2.3",
79
76
  "sinon": "21.0.1"
80
77
  },
@@ -82,14 +79,5 @@
82
79
  "lib",
83
80
  "bin",
84
81
  "app"
85
- ],
86
- "renovate": {
87
- "extends": [
88
- "@tryghost:quietJS",
89
- "@tryghost:automergeDevDependencies"
90
- ],
91
- "ignoreDeps": [
92
- "validator"
93
- ]
94
- }
82
+ ]
95
83
  }
@@ -1,7 +0,0 @@
1
- /* eslint-env node */
2
- module.exports = {
3
- plugins: ['ghost'],
4
- extends: [
5
- 'plugin:ghost/browser'
6
- ]
7
- };