gscan 4.29.2 → 4.30.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/tpl/index.hbs CHANGED
@@ -22,8 +22,8 @@
22
22
  <span class="gh-input-icon select-arrow">{{> icon-arrow-up}}</span>
23
23
  <span class="gh-input-icon select-arrow active">{{> icon-arrow-down}}</span>
24
24
  <select class="gh-input gh-select" name="version" id="version">
25
- <option value="v5">{{ghostVersions.v5.major}}</option>
26
- <option value="v4" selected>{{ghostVersions.v4.major}}</option>
25
+ <option value="v5" selected>{{ghostVersions.v5.major}}</option>
26
+ <option value="v4">{{ghostVersions.v4.major}}</option>
27
27
  <option value="v3">{{ghostVersions.v3.major}}</option>
28
28
  <option value="v2">{{ghostVersions.v2.major}}</option>
29
29
  <option value="v1">{{ghostVersions.v1.major}}</option>
@@ -7,7 +7,11 @@ module.exports = {
7
7
  'GS090-NO-PRODUCT-DATA-HELPER': require('./lint-no-product-data-helper'),
8
8
  'GS090-NO-PRODUCTS-DATA-HELPER': require('./lint-no-products-data-helper'),
9
9
  'GS090-NO-MEMBER-PRODUCTS-DATA-HELPER': require('./lint-no-member-products-data-helper'),
10
- 'GS090-NO-PRICE-DATA-HELPER': require('./lint-no-price-data-helper'),
10
+ 'GS090-NO-TIER-PRICE-AS-OBJECT': require('./lint-no-tier-price-as-object'),
11
+ 'GS090-NO-TIER-BENEFIT-AS-OBJECT': require('./lint-no-tier-benefit-as-object'),
12
+ 'GS090-NO-PRICE-DATA-CURRENCY-GLOBAL': require('./lint-no-price-data-currency-global'),
13
+ 'GS090-NO-PRICE-DATA-CURRENCY-CONTEXT': require('./lint-no-price-data-currency-context'),
14
+ 'GS090-NO-PRICE-DATA-MONTHLY-YEARLY': require('./lint-no-price-data-monthly-yearly'),
11
15
  'no-multi-param-conditionals': require('./lint-no-multi-param-conditionals'),
12
16
  'no-nested-async-helpers': require('./lint-no-nested-async-helpers'),
13
17
  'no-prev-next-post-outside-post-context': require('./lint-no-prev-next-post-outside-post-context'),
@@ -204,6 +204,20 @@ class Scope {
204
204
  return found;
205
205
  }
206
206
 
207
+ getParentContextNode(context) {
208
+ let matchedFrame = null;
209
+
210
+ if (this.frames && this.frames.length) {
211
+ matchedFrame = this.frames.find((frame) => {
212
+ if (frame.nodeName === context) {
213
+ return true;
214
+ }
215
+ });
216
+ }
217
+
218
+ return matchedFrame && matchedFrame.node;
219
+ }
220
+
207
221
  isLocal(node) {
208
222
  // @foo MustacheStatements are referencing globals rather than locals
209
223
  if (node.type === 'MustacheStatement' && node.path.data) {
@@ -0,0 +1,26 @@
1
+ const Rule = require('./base');
2
+ const _ = require('lodash');
3
+
4
+ module.exports = class NoPriceDataCurrencyContext extends Rule {
5
+ _findPriceData(node) {
6
+ if (node.data && node.parts && node.parts.length === 2 && node.parts[0] === 'price' && node.parts[1] === 'currency') {
7
+ const foreachNode = this.scope.getParentContextNode('foreach');
8
+ const foreachObject = _.get(foreachNode, 'params[0].original');
9
+
10
+ if (['@member.subscriptions', 'tiers'].includes(foreachObject)) {
11
+ this.log(({
12
+ message: `{{@price.currency}} should be replaced with {{currency}}`,
13
+ line: node.loc && node.loc.start.line,
14
+ column: node.loc && node.loc.start.column,
15
+ source: this.sourceForNode(node)
16
+ }));
17
+ }
18
+ }
19
+ }
20
+
21
+ visitor() {
22
+ return {
23
+ PathExpression: this._findPriceData.bind(this)
24
+ };
25
+ }
26
+ };
@@ -0,0 +1,26 @@
1
+ const Rule = require('./base');
2
+ const _ = require('lodash');
3
+
4
+ module.exports = class NoPriceDataCurrencyGlobal extends Rule {
5
+ _findPriceData(node) {
6
+ if (node.data && node.parts && node.parts.length === 2 && node.parts[0] === 'price' && node.parts[1] === 'currency') {
7
+ const foreachNode = this.scope.getParentContextNode('foreach');
8
+ const foreachObject = _.get(foreachNode, 'params[0].original');
9
+
10
+ if (!['@member.subscriptions', 'tiers'].includes(foreachObject)) {
11
+ this.log(({
12
+ message: `{{@price.currency}} should be replaced with {{currency}} and a context`,
13
+ line: node.loc && node.loc.start.line,
14
+ column: node.loc && node.loc.start.column,
15
+ source: this.sourceForNode(node)
16
+ }));
17
+ }
18
+ }
19
+ }
20
+
21
+ visitor() {
22
+ return {
23
+ PathExpression: this._findPriceData.bind(this)
24
+ };
25
+ }
26
+ };
@@ -0,0 +1,20 @@
1
+ const Rule = require('./base');
2
+
3
+ module.exports = class NoPriceDataMonthlyYearly extends Rule {
4
+ _findPriceData(node) {
5
+ if (node.data && node.parts && node.parts.length === 2 && node.parts[0] === 'price' && ['monthly', 'yearly'].includes(node.parts[1])) {
6
+ this.log(({
7
+ message: `{{@price.monthly}} and {{@price.yearly}} should be replaced with {{#get "tiers"}}`,
8
+ line: node.loc && node.loc.start.line,
9
+ column: node.loc && node.loc.start.column,
10
+ source: this.sourceForNode(node)
11
+ }));
12
+ }
13
+ }
14
+
15
+ visitor() {
16
+ return {
17
+ PathExpression: this._findPriceData.bind(this)
18
+ };
19
+ }
20
+ };
@@ -0,0 +1,30 @@
1
+ const Rule = require('./base');
2
+ const {getNodeName, logNode} = require('../helpers');
3
+ const _ = require('lodash');
4
+
5
+ module.exports = class NoTierBenefitAsObject extends Rule {
6
+ _checkForHelperName(node) {
7
+ const nodeName = getNodeName(node);
8
+ const isMatchingHelper = (nodeName.includes('name'));
9
+ const foreachNode = this.scope.getParentContextNode('foreach');
10
+ const getNode = this.scope.getParentContextNode('get');
11
+
12
+ const hasForeachBenefits = _.get(foreachNode, 'params[0].original') === 'benefits';
13
+ const hasGetTiers = _.get(getNode, 'params[0].original') === 'tiers';
14
+
15
+ if (hasForeachBenefits && hasGetTiers && isMatchingHelper) {
16
+ this.log({
17
+ message: `${logNode(node)} should not be used`,
18
+ line: node.loc && node.loc.start.line,
19
+ column: node.loc && node.loc.start.column,
20
+ source: this.sourceForNode(node)
21
+ });
22
+ }
23
+ }
24
+
25
+ visitor() {
26
+ return {
27
+ MustacheStatement: this._checkForHelperName.bind(this)
28
+ };
29
+ }
30
+ };
@@ -1,12 +1,15 @@
1
+ const _ = require('lodash');
1
2
  const Rule = require('./base');
2
3
  const {getNodeName, logNode} = require('../helpers');
3
4
 
4
- module.exports = class NoPriceDataHelper extends Rule {
5
+ module.exports = class NoMonthlyYearlyPriceObjects extends Rule {
5
6
  _checkForHelperName(node) {
6
7
  const nodeName = getNodeName(node);
7
- const isMatchingHelper = (nodeName.startsWith('@price.'));
8
+ const isMatchingHelper = (nodeName.includes('monthly_price.')) || (nodeName.includes('yearly_price.'));
9
+ const getNode = this.scope.getParentContextNode('get');
10
+ const hasGetTiers = _.get(getNode, 'params[0].original') === 'tiers';
8
11
 
9
- if (isMatchingHelper) {
12
+ if (isMatchingHelper && hasGetTiers) {
10
13
  this.log({
11
14
  message: `${logNode(node)} should not be used`,
12
15
  line: node.loc && node.loc.start.line,
@@ -55,7 +55,10 @@ const v4PackageJSONValidationRules = _.extend({},
55
55
  );
56
56
 
57
57
  const canaryPackageJSONConditionalRules = {};
58
- const canaryPackageJSONValidationRules = _.extend({});
58
+ const canaryPackageJSONValidationRules = _.extend({},
59
+ {isNotPresentCardAssets: 'GS010-PJ-GHOST-CARD-ASSETS-NOT-PRESENT'},
60
+ canaryPackageJSONConditionalRules
61
+ );
59
62
 
60
63
  _private.validatePackageJSONFields = function validatePackageJSONFields(packageJSON, theme, packageJSONValidationRules) {
61
64
  let failed = [];
@@ -138,6 +141,11 @@ _private.validatePackageJSONFields = function validatePackageJSONFields(packageJ
138
141
  }
139
142
  }
140
143
 
144
+ // @TODO: validate that the card_assets config is valid
145
+ if (!packageJSON.config || (_.isEmpty(packageJSON.config.card_assets) && !_.isBoolean(packageJSON.config.card_assets))) {
146
+ markFailed('isNotPresentCardAssets');
147
+ }
148
+
141
149
  if (packageJSON.config && packageJSON.config.custom) {
142
150
  const customSettingsKeys = Object.keys(packageJSON.config.custom);
143
151
 
package/lib/format.js CHANGED
@@ -1,30 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const spec = require('./specs');
3
- const levelWeights = {
4
- error: 10,
5
- warning: 3,
6
- recommendation: 1
7
- };
8
3
  const versions = require('./utils').versions;
9
-
10
- const calcScore = function calcScore(results, stats) {
11
- var maxScore, actualScore, balancedScore;
12
-
13
- maxScore = _.reduce(levelWeights, function (max, weight, level) {
14
- return max + (weight * stats[level]);
15
- }, 0);
16
-
17
- actualScore = _.reduce(levelWeights, function (max, weight, level) {
18
- return max - (weight * _.size(results[level]));
19
- }, maxScore);
20
-
21
- balancedScore = _.ceil((100 / maxScore) * actualScore);
22
-
23
- return {
24
- value: balancedScore,
25
- level: _.size(results.error) > 0 ? 'error' : balancedScore < 60 ? 'warning' : 'passing'
26
- };
27
- };
4
+ const calcScore = require('./utils/score-calculator');
28
5
 
29
6
  const formatCLI = (theme) => {
30
7
  _.each(theme.results, (results) => {
@@ -21,10 +21,16 @@ let rules = {
21
21
  // New rules
22
22
  'GS010-PJ-GHOST-API-PRESENT': {
23
23
  level: 'warning',
24
- rule: 'Remove <code>"engines.ghost-api"</code> from <code>package.json</code>.',
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
26
  Find more information about the <code>package.json</code> file <a href="${docsBaseUrl}structure/#packagejson" target=_blank>here</a>.`
27
27
  },
28
+ 'GS010-PJ-GHOST-CARD-ASSETS-NOT-PRESENT': {
29
+ level: 'warning',
30
+ rule: '<code>"card_assets"</code> will default to <code>true</code> in <code>package.json</code>',
31
+ details: oneLineTrim`The <code>"card_assets"</code> property is enabled by default if not explicitly set.<br>
32
+ Find more information about the <code>card_assets</code> property <a href="${docsBaseUrl}content/#editor-cards" target=_blank>here</a>.`
33
+ },
28
34
  'GS090-NO-AUTHOR-HELPER-IN-POST-CONTEXT': {
29
35
  level: 'error',
30
36
  fatal: true,
@@ -69,14 +75,46 @@ let rules = {
69
75
  Find more information about the <code>{{@member.subscriptions}}</code> property <a href="${docsBaseUrl}members/#member-subscriptions" target=_blank>here</a>.`,
70
76
  helper: '{{@member.products}}'
71
77
  },
72
- 'GS090-NO-PRICE-DATA-HELPER': {
78
+ 'GS090-NO-PRICE-DATA-CURRENCY-GLOBAL': {
79
+ level: 'error',
80
+ fatal: true,
81
+ rule: 'Replace <code>@price.currency</code> with <code>{{#get "tiers"}}</code> and <code>{{currency}}</code> or <code>{{#foreach @member.subscriptions}}</code> and <code>{{plan.currency}}</code>',
82
+ details: oneLineTrim`There is no longer a global <code>@price</code> object. You need to use either <code>{{#get "tiers"}}</code> to fetch all tiers and use the <code>{{currency}}</code> property of a tier<br>
83
+ or use <code>{{#foreach @member.subscriptions}}</code> to fetch an individual member's subscriptions, and use the <code>{{plan.currency}}</code> property from the subscription.<br>
84
+ Find more information about the <code>{{price}}</code> helper <a href="${docsBaseUrl}members/price/" target=_blank>here</a>.`
85
+ },
86
+ 'GS090-NO-PRICE-DATA-CURRENCY-CONTEXT': {
87
+ level: 'error',
88
+ fatal: true,
89
+ rule: 'Replace <code>{{@price.currency}}</code> with <code>{{currency}}</code> or <code>{{plan.currency}}</code>',
90
+ details: oneLineTrim`There is no longer a global <code>@price</code> object. Instead the <code>{{currency}}</code> property can be used inside <code>{{#get "tiers"}}</code><br>
91
+ or <code>{{plan.currency}}</code> can be used inside <code>{{#foreach @member.subscriptions}}</code><br>
92
+ Find more information about the <code>{{price}}</code> helper <a href="${docsBaseUrl}members/price/" target=_blank>here</a>.`
93
+ },
94
+ 'GS090-NO-PRICE-DATA-MONTHLY-YEARLY': {
95
+ level: 'error',
96
+ fatal: true,
97
+ rule: 'Replace <code>{{@price.monthly}}</code> and <code>{{@price.yearly}}</code> with <code>{{#get "tiers"}}</code> and <code>{{monthly_price}}</code> or <code>{{yearly_price}}</code>',
98
+ details: oneLineTrim`There is no longer a global <code>@price</code> object. You need to use <code>{{#get "tiers"}}</code> to fetch all the tiers and get access to the <code>{{monthly_price}}</code> or <code>{{yearly_price}}</code> for each tier<br>
99
+ Find more information about the <code>{{price}}</code> helper <a href="${docsBaseUrl}helpers/price/" target=_blank>here</a>.`
100
+ },
101
+ 'GS090-NO-TIER-PRICE-AS-OBJECT': {
102
+ level: 'error',
103
+ fatal: true,
104
+ rule: 'Remove usage of <code>{{monthly_price.*}}</code> and <code>{{yearly_price.*}}.</code>',
105
+ details: oneLineTrim`The usage of <code>{{monthly_price.*}}</code> and <code>{{yearly_price.*}} is no longer supported.</code><br>
106
+ ${tierDesc}<br>
107
+ Find more information about the <code>{{#get "tiers"}}</code> <a href="${docsBaseUrl}helpers/tiers/" target=_blank>here</a>.`,
108
+ helper: '{{#get "tiers"}}'
109
+ },
110
+ 'GS090-NO-TIER-BENEFIT-AS-OBJECT': {
73
111
  level: 'error',
74
112
  fatal: true,
75
- rule: 'Replace <code>{{@price}}</code> with <code>{{price}}</code> and <code>{{@member.subscriptions}}</code>',
76
- details: oneLineTrim`The <code>{{@price}}</code> data helper was removed in favor of <code>{{price}}</code> and <code>{{@member.subscriptions}}</code><br>
113
+ rule: 'Remove usage of <code>{{name}}</code> for tier benefits.</code>',
114
+ details: oneLineTrim`The usage of <code>{{name}}</code> for tier benefits is no longer supported.</code><br>
77
115
  ${tierDesc}<br>
78
- Find more information about the <code>{{price}}</code> helper <a href="${docsBaseUrl}members/#the-price-helper" target=_blank>here</a>.`,
79
- helper: '{{@price}}'
116
+ Find more information about the <code>{{#get "tiers"}}</code> <a href="${docsBaseUrl}helpers/tiers/" target=_blank>here</a>.`,
117
+ helper: '{{#get "tiers"}}'
80
118
  },
81
119
 
82
120
  // Updated v1 & v2 rules
@@ -685,8 +723,8 @@ let rules = {
685
723
  level: 'error',
686
724
  fatal: true,
687
725
  rule: 'Replace <code>{{[#].currency_symbol}}</code> with <code>{{price currency=currency}}</code>.',
688
- details: oneLineTrim`The hardcoded <code>currency_symbol</code> attribute is no longer supported in favour of passing the currency to updated <code>{{price}}</code> helper.
689
- Find more information about the updated <code>{{price}}</code> helper <a href="${docsBaseUrl}members/#the-price-helper" target=_blank>here</a>.`,
726
+ details: oneLineTrim`The <code>currency_symbol</code> attribute is no longer supported in favour of passing the currency to updated <code>{{price}}</code> helper.<br>
727
+ Find more information about the updated <code>{{price}}</code> helper <a href="${docsBaseUrl}helpers/price" target=_blank>here</a>.`,
690
728
  helper: '{{[#].currency_symbol}}',
691
729
  regex: /currency_symbol/g
692
730
  }
@@ -0,0 +1,40 @@
1
+ const _ = require('lodash');
2
+
3
+ const levelWeights = {
4
+ error: 10,
5
+ warning: 3,
6
+ recommendation: 1
7
+ };
8
+
9
+ /**
10
+ *
11
+ * @param {Object} results
12
+ * @param {Object} results.error
13
+ * @param {Object} results.warning
14
+ * @param {Object} results.recommendation
15
+ * @param {Object} stats
16
+ * @param {Object} stats.error
17
+ * @param {Object} stats.warning
18
+ * @param {Object} stats.recommendation
19
+ * @returns {Object}
20
+ */
21
+ const calcScore = function calcScore(results, stats) {
22
+ var maxScore, actualScore, balancedScore;
23
+
24
+ maxScore = _.reduce(levelWeights, function (max, weight, level) {
25
+ return max + (weight * stats[level]);
26
+ }, 0);
27
+
28
+ actualScore = _.reduce(levelWeights, function (max, weight, level) {
29
+ return max - (weight * _.size(results[level]));
30
+ }, maxScore);
31
+
32
+ balancedScore = _.floor((100 / maxScore) * actualScore);
33
+
34
+ return {
35
+ value: balancedScore,
36
+ level: _.size(results.error) > 0 ? 'error' : balancedScore < 60 ? 'warning' : 'passing'
37
+ };
38
+ };
39
+
40
+ module.exports = calcScore;
@@ -19,5 +19,5 @@
19
19
  "major": "5.x",
20
20
  "docs": "5.0.0"
21
21
  },
22
- "default": "v4"
22
+ "default": "v5"
23
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gscan",
3
- "version": "4.29.2",
3
+ "version": "4.30.0",
4
4
  "description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
5
5
  "keywords": [
6
6
  "ghost",
@@ -45,7 +45,7 @@
45
45
  "@tryghost/debug": "0.1.11",
46
46
  "@tryghost/errors": "1.2.12",
47
47
  "@tryghost/logging": "2.1.8",
48
- "@tryghost/pretty-cli": "1.2.27",
48
+ "@tryghost/pretty-cli": "1.2.28",
49
49
  "@tryghost/server": "0.1.4",
50
50
  "@tryghost/zip": "1.1.25",
51
51
  "bluebird": "3.7.2",