gscan 5.2.5 → 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/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
@@ -153,11 +153,14 @@ function outputResult(result, options) {
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
 
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
  },
@@ -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': {
@@ -400,7 +395,8 @@ rules = {
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/#package-json" target=_blank><code>package.json</code> documentation</a> for further information.`
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>.`
404
400
  },
405
401
  'GS010-PJ-NAME-LC': {
406
402
  level: 'error',
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,7 +403,7 @@ 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.
406
+ The <code>{{@blog.permalinks}}</code> property will therefore no longer be used and should be removed from the theme.<br>
408
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}}'
@@ -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',
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/`;
@@ -35,7 +35,7 @@ let rules = {
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>
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
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}}'
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/`;
@@ -109,7 +109,7 @@ let rules = {
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/`;
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.5",
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
- };