@bigcommerce/stencil-cli 3.6.2 → 3.7.1
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/CHANGELOG.md +27 -0
- package/lib/bundle-validator.js +59 -0
- package/lib/lang-assembler.js +1 -1
- package/lib/lang-helper.js +59 -0
- package/lib/stencil-start.js +63 -3
- package/lib/store-settings-api-client.js +34 -0
- package/lib/utils/frontmatter.js +33 -0
- package/package-lock.json +458 -23893
- package/package.json +3 -2
- package/server/index.js +3 -0
- package/server/plugins/renderer/renderer.module.js +22 -12
- package/server/plugins/router/router.module.js +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
### 3.7.1 (2021-12-20)
|
|
2
|
+
|
|
3
|
+
- fix: strf-9576 Fix graphql queries ([810](https://github.com/bigcommerce/stencil-cli/pull/810))
|
|
4
|
+
- fix: strf-9553 bundled lang.json has lowercase lang keys ([808](https://github.com/bigcommerce/stencil-cli/pull/808))
|
|
5
|
+
|
|
6
|
+
### 3.7.0 (2021-12-08)
|
|
7
|
+
|
|
8
|
+
- fix: strf-9535 Add fallback for shopper language default ([804](https://github.com/bigcommerce/stencil-cli/pull/804))
|
|
9
|
+
- feat: strf-9245 Warn user if shopper default language file is missing or has missing keys ([802](https://github.com/bigcommerce/stencil-cli/pull/802))
|
|
10
|
+
- Bump paper to rc52
|
|
11
|
+
|
|
12
|
+
Note: BREAKING CHANGE!
|
|
13
|
+
In order to get stencil cli working with this version, new Stencil CLI token should be created
|
|
14
|
+
|
|
15
|
+
### 3.6.5 (2021-11-22)
|
|
16
|
+
|
|
17
|
+
- fix: strf-4307 Frontmatter/yaml validation and trailing symbols checks ([798](https://github.com/bigcommerce/stencil-cli/pull/798))
|
|
18
|
+
|
|
19
|
+
### 3.6.4 (2021-11-5)
|
|
20
|
+
|
|
21
|
+
- fix: strf-9474 Removed "git+" prefix from package-lock ([794](https://github.com/bigcommerce/stencil-cli/pull/794))
|
|
22
|
+
|
|
23
|
+
### 3.6.3 (2021-10-22)
|
|
24
|
+
|
|
25
|
+
- Bump paper to rc51
|
|
26
|
+
- fix: strf-8846 Send stencil-cli header to storefront api ([789](https://github.com/bigcommerce/stencil-cli/pull/789))
|
|
27
|
+
|
|
1
28
|
### 3.6.2 (2021-10-06)
|
|
2
29
|
|
|
3
30
|
- feat: Add overwrite option to download command ([785](https://github.com/bigcommerce/stencil-cli/pull/785))
|
package/lib/bundle-validator.js
CHANGED
|
@@ -6,7 +6,10 @@ const fs = require('fs');
|
|
|
6
6
|
const sizeOf = require('image-size');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const Validator = require('ajv');
|
|
9
|
+
const yamlValidator = require('js-yaml');
|
|
9
10
|
|
|
11
|
+
const { recursiveReadDir } = require('./utils/fsUtils');
|
|
12
|
+
const { getFrontmatterContent, interpolateThemeSettings } = require('./utils/frontmatter');
|
|
10
13
|
const ValidatorSchemaTranslations = require('./validator/schema-translations');
|
|
11
14
|
const privateThemeConfigValidationSchema = require('./schemas/privateThemeConfig.json');
|
|
12
15
|
const themeConfigValidationSchema = require('./schemas/themeConfig.json');
|
|
@@ -43,6 +46,7 @@ class BundleValidator {
|
|
|
43
46
|
this._validateThemeConfiguration.bind(this),
|
|
44
47
|
this._validateThemeSchema.bind(this),
|
|
45
48
|
this._validateSchemaTranslations.bind(this),
|
|
49
|
+
this._validateTemplatesFrontmatter.bind(this),
|
|
46
50
|
];
|
|
47
51
|
|
|
48
52
|
if (!this.isPrivate) {
|
|
@@ -371,6 +375,61 @@ class BundleValidator {
|
|
|
371
375
|
cb(null, true);
|
|
372
376
|
});
|
|
373
377
|
}
|
|
378
|
+
|
|
379
|
+
async _validateTemplatesFrontmatter() {
|
|
380
|
+
const config = await this.themeConfig.getRawConfig();
|
|
381
|
+
const filePaths = await recursiveReadDir(path.join(this.themePath, 'templates'), [
|
|
382
|
+
'!*.html',
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
for await (const filePath of filePaths) {
|
|
386
|
+
const fileContent = await fs.promises.readFile(filePath, { encoding: 'utf-8' });
|
|
387
|
+
const frontmatter = getFrontmatterContent(fileContent);
|
|
388
|
+
if (frontmatter) {
|
|
389
|
+
const yaml = interpolateThemeSettings(frontmatter, config.settings);
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const result = yamlValidator.loadAll(yaml);
|
|
393
|
+
this.validateTrailingSymbols(result[0]);
|
|
394
|
+
} catch (e) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Error: ${e.message}, while parsing frontmatter at "${filePath}".`.red,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
validateTrailingSymbols(data) {
|
|
406
|
+
if (_.isObject(data)) {
|
|
407
|
+
return _.every(data, (value) => this.validateTrailingSymbols(value));
|
|
408
|
+
}
|
|
409
|
+
if (_.isArray(data)) {
|
|
410
|
+
return data.every((row) => this.validateTrailingSymbols(row));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return data ? this.hasFrontmatterValidValue(data) : true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
hasFrontmatterValidValue(value) {
|
|
417
|
+
if (this.hasUnallowedTrailingSymbol(value)) {
|
|
418
|
+
throw new Error(`Found unallowed trailing symbol in: "${value}"`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
getUnallowedTrailingSymbols() {
|
|
425
|
+
return [',', ';'];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
hasUnallowedTrailingSymbol(value) {
|
|
429
|
+
const symbols = this.getUnallowedTrailingSymbols();
|
|
430
|
+
const trailingSymbol = value.toString().trim().slice(-1);
|
|
431
|
+
return symbols.includes(trailingSymbol);
|
|
432
|
+
}
|
|
374
433
|
}
|
|
375
434
|
|
|
376
435
|
module.exports = BundleValidator;
|
package/lib/lang-assembler.js
CHANGED
|
@@ -26,7 +26,7 @@ function assemble(callback) {
|
|
|
26
26
|
for (const localeFile of filteredLocaleFiles) {
|
|
27
27
|
const localeName = path.basename(localeFile, '.json');
|
|
28
28
|
|
|
29
|
-
localesToLoad[localeName] = (cb2) => {
|
|
29
|
+
localesToLoad[localeName.toLowerCase()] = (cb2) => {
|
|
30
30
|
const localeFilePath = `${LOCALE_DIRECTORY}/${localeFile}`;
|
|
31
31
|
|
|
32
32
|
fs.readFile(localeFilePath, 'utf-8', cb2);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require('colors');
|
|
2
|
+
const fsUtilsModule = require('./utils/fsUtils');
|
|
3
|
+
|
|
4
|
+
function flattenObject(object, result = {}, parentKey = '') {
|
|
5
|
+
return Object.entries(object).reduce((currentLayer, [key, innerValue]) => {
|
|
6
|
+
const resultKey = parentKey !== '' ? `${parentKey}.${key}` : key;
|
|
7
|
+
if (typeof innerValue === 'object') {
|
|
8
|
+
return flattenObject(innerValue, currentLayer, resultKey);
|
|
9
|
+
}
|
|
10
|
+
// eslint-disable-next-line no-param-reassign
|
|
11
|
+
currentLayer[resultKey] = innerValue;
|
|
12
|
+
return currentLayer;
|
|
13
|
+
}, result);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class LangHelper {
|
|
17
|
+
constructor({ fsUtils = fsUtilsModule, logger = console } = {}) {
|
|
18
|
+
this._fsUtils = fsUtils;
|
|
19
|
+
this._logger = logger;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async checkLangKeysPresence(filesPaths, themeLang) {
|
|
23
|
+
const themeLangFilename = `${themeLang}.json`;
|
|
24
|
+
const defaultLangFilePath = filesPaths.find((filePath) =>
|
|
25
|
+
filePath.includes(themeLangFilename),
|
|
26
|
+
);
|
|
27
|
+
const defaultLangFile = await this._fsUtils.parseJsonFile(defaultLangFilePath);
|
|
28
|
+
const flattenedDefaultLang = flattenObject(defaultLangFile);
|
|
29
|
+
const alreadyWarnedKey = [];
|
|
30
|
+
|
|
31
|
+
for await (const filePath of filesPaths) {
|
|
32
|
+
if (!filePath.includes(themeLangFilename)) {
|
|
33
|
+
const langFile = await this._fsUtils.parseJsonFile(filePath);
|
|
34
|
+
const flattenedLang = flattenObject(langFile);
|
|
35
|
+
const langKeys = Object.keys(flattenedLang);
|
|
36
|
+
for (const langKey of langKeys) {
|
|
37
|
+
if (!alreadyWarnedKey.includes(langKey)) {
|
|
38
|
+
if (!flattenedDefaultLang[langKey]) {
|
|
39
|
+
this._logger.log(
|
|
40
|
+
`${
|
|
41
|
+
'Warning'.yellow
|
|
42
|
+
}: file: ${defaultLangFilePath} doesn't have ${langKey}, which is present in ${filePath}`,
|
|
43
|
+
);
|
|
44
|
+
} else if (flattenedDefaultLang[langKey].trim().length === 0) {
|
|
45
|
+
this._logger.log(
|
|
46
|
+
`${
|
|
47
|
+
'Warning'.yellow
|
|
48
|
+
}: file: ${defaultLangFilePath} has ${langKey}, but it's empty`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
alreadyWarnedKey.push(langKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = LangHelper;
|
package/lib/stencil-start.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const Cycles = require('./Cycles');
|
|
7
7
|
const templateAssemblerModule = require('./template-assembler');
|
|
8
|
-
const { THEME_PATH } = require('../constants');
|
|
8
|
+
const { PACKAGE_INFO, THEME_PATH } = require('../constants');
|
|
9
9
|
const Server = require('../server');
|
|
10
10
|
const StencilConfigManager = require('./StencilConfigManager');
|
|
11
11
|
const ThemeConfig = require('./theme-config');
|
|
@@ -14,11 +14,15 @@ const fsUtilsModule = require('./utils/fsUtils');
|
|
|
14
14
|
const stencilPushUtilsModule = require('./stencil-push.utils');
|
|
15
15
|
const cliCommonModule = require('./cliCommon');
|
|
16
16
|
const themeApiClientModule = require('./theme-api-client');
|
|
17
|
+
const storeSettingsApiClientModule = require('./store-settings-api-client');
|
|
18
|
+
const LangHelper = require('./lang-helper');
|
|
17
19
|
|
|
18
20
|
class StencilStart {
|
|
19
21
|
constructor({
|
|
20
22
|
browserSync = BrowserSync.create(),
|
|
21
23
|
themeApiClient = themeApiClientModule,
|
|
24
|
+
storeSettingsApiClient = storeSettingsApiClientModule,
|
|
25
|
+
langHelper = new LangHelper(),
|
|
22
26
|
fsUtils = fsUtilsModule,
|
|
23
27
|
cliCommon = cliCommonModule,
|
|
24
28
|
stencilConfigManager = new StencilConfigManager(),
|
|
@@ -31,6 +35,8 @@ class StencilStart {
|
|
|
31
35
|
} = {}) {
|
|
32
36
|
this._browserSync = browserSync;
|
|
33
37
|
this._themeApiClient = themeApiClient;
|
|
38
|
+
this._storeSettingsApiClient = storeSettingsApiClient;
|
|
39
|
+
this._langHelper = langHelper;
|
|
34
40
|
this._fsUtils = fsUtils;
|
|
35
41
|
this._cliCommon = cliCommon;
|
|
36
42
|
this._stencilConfigManager = stencilConfigManager;
|
|
@@ -62,6 +68,11 @@ class StencilStart {
|
|
|
62
68
|
storeInfoFromAPI,
|
|
63
69
|
);
|
|
64
70
|
|
|
71
|
+
this._storeSettingsLocale = await this.getStoreSettingsLocale(
|
|
72
|
+
cliOptions,
|
|
73
|
+
updatedStencilConfig,
|
|
74
|
+
);
|
|
75
|
+
|
|
65
76
|
await this.startLocalServer(cliOptions, updatedStencilConfig);
|
|
66
77
|
|
|
67
78
|
this._logger.log(this.getStartUpInfo(updatedStencilConfig));
|
|
@@ -69,14 +80,24 @@ class StencilStart {
|
|
|
69
80
|
await this.startBrowserSync(cliOptions, updatedStencilConfig, browserSyncPort);
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
async getStoreSettingsLocale(cliOptions, stencilConfig) {
|
|
84
|
+
const { accessToken } = stencilConfig;
|
|
85
|
+
const apiHost = cliOptions.apiHost || stencilConfig.apiHost;
|
|
86
|
+
return this._storeSettingsApiClient.getStoreSettingsLocale({
|
|
87
|
+
storeHash: this.storeHash,
|
|
88
|
+
accessToken,
|
|
89
|
+
apiHost,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
async getChannelUrl(stencilConfig, cliOptions) {
|
|
73
94
|
const { accessToken } = stencilConfig;
|
|
74
95
|
const apiHost = cliOptions.apiHost || stencilConfig.apiHost;
|
|
75
|
-
|
|
96
|
+
this.storeHash = await this._themeApiClient.getStoreHash({
|
|
76
97
|
storeUrl: stencilConfig.normalStoreUrl,
|
|
77
98
|
});
|
|
78
99
|
const channels = await this._themeApiClient.getStoreChannels({
|
|
79
|
-
storeHash,
|
|
100
|
+
storeHash: this.storeHash,
|
|
80
101
|
accessToken,
|
|
81
102
|
apiHost,
|
|
82
103
|
});
|
|
@@ -131,6 +152,8 @@ class StencilStart {
|
|
|
131
152
|
variationIndex: this._themeConfigManager.variationIndex || 0,
|
|
132
153
|
useCache: cliOptions.cache,
|
|
133
154
|
themePath: this._themeConfigManager.themePath,
|
|
155
|
+
stencilCliVersion: PACKAGE_INFO.version,
|
|
156
|
+
storeSettingsLocale: this._storeSettingsLocale,
|
|
134
157
|
});
|
|
135
158
|
}
|
|
136
159
|
|
|
@@ -178,6 +201,20 @@ class StencilStart {
|
|
|
178
201
|
}
|
|
179
202
|
});
|
|
180
203
|
|
|
204
|
+
const langsPath = path.join(themePath, 'lang');
|
|
205
|
+
this._browserSync.watch(langsPath, async (event) => {
|
|
206
|
+
try {
|
|
207
|
+
if (event === 'change') {
|
|
208
|
+
await this.checkLangFiles(
|
|
209
|
+
langsPath,
|
|
210
|
+
this._storeSettingsLocale.default_shopper_language,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
this._logger.error(e);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
181
218
|
// tunnel value should be true/false or a string with name
|
|
182
219
|
// https://browsersync.io/docs/options#option-tunnel
|
|
183
220
|
// convert undefined/true -> false/true
|
|
@@ -214,6 +251,8 @@ class StencilStart {
|
|
|
214
251
|
if (this._buildConfigManager.development) {
|
|
215
252
|
this._buildConfigManager.initWorker().development(this._browserSync);
|
|
216
253
|
}
|
|
254
|
+
|
|
255
|
+
await this.checkLangFiles(langsPath, this._storeSettingsLocale.default_shopper_language);
|
|
217
256
|
}
|
|
218
257
|
|
|
219
258
|
/**
|
|
@@ -235,6 +274,27 @@ class StencilStart {
|
|
|
235
274
|
);
|
|
236
275
|
}
|
|
237
276
|
|
|
277
|
+
async checkLangFiles(langsPath, defaultShopperLanguage) {
|
|
278
|
+
const filesPaths = await this._fsUtils.recursiveReadDir(langsPath);
|
|
279
|
+
const isDefaultLanguagePresent = filesPaths.some((file) =>
|
|
280
|
+
file.includes(defaultShopperLanguage),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (!isDefaultLanguagePresent) {
|
|
284
|
+
this._logger.log(
|
|
285
|
+
`${
|
|
286
|
+
'Warning'.yellow
|
|
287
|
+
}: "missing language file for default shopper language: ${defaultShopperLanguage}"`,
|
|
288
|
+
);
|
|
289
|
+
} else {
|
|
290
|
+
try {
|
|
291
|
+
await this._langHelper.checkLangKeysPresence(filesPaths, defaultShopperLanguage);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
this._logger.error(e);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
238
298
|
/**
|
|
239
299
|
* Displays information about your environment and configuration.
|
|
240
300
|
* @param {Object} stencilConfig
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require('colors');
|
|
2
|
+
const NetworkUtils = require('./utils/NetworkUtils');
|
|
3
|
+
|
|
4
|
+
const networkUtils = new NetworkUtils();
|
|
5
|
+
|
|
6
|
+
async function getStoreSettingsLocale({ apiHost, storeHash, accessToken }) {
|
|
7
|
+
try {
|
|
8
|
+
const response = await networkUtils.sendApiRequest({
|
|
9
|
+
url: `${apiHost}/stores/${storeHash}/v3/settings/store/locale`,
|
|
10
|
+
accessToken,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!response.data.data) {
|
|
14
|
+
throw new Error('Received empty store locale in the server response'.red);
|
|
15
|
+
} else if (!response.data.data.default_shopper_language) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Received empty default_shopper_language field in the server response'.red,
|
|
18
|
+
);
|
|
19
|
+
} else if (!response.data.data.shopper_language_selection_method) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'Received empty shopper_language_selection_method field in the server response'.red,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return response.data.data;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
err.name = 'StoreSettingsLocaleError';
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
getStoreSettingsLocale,
|
|
34
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const frontmatterRegex = /---\r?\n(?:.|\s)*?\r?\n---\r?\n/g;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param {String} file
|
|
6
|
+
* @returns {String|null}
|
|
7
|
+
*/
|
|
8
|
+
function getFrontmatterContent(file) {
|
|
9
|
+
const frontmatterMatch = file.match(frontmatterRegex);
|
|
10
|
+
return frontmatterMatch !== null ? frontmatterMatch[0] : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param {String} frontmatter
|
|
16
|
+
* @param {Object} settings
|
|
17
|
+
* @returns {String}
|
|
18
|
+
*/
|
|
19
|
+
function interpolateThemeSettings(frontmatter, settings) {
|
|
20
|
+
for (const [key, val] of Object.entries(settings)) {
|
|
21
|
+
const regex = `{{\\s*?theme_settings\\.${key}\\s*?}}`;
|
|
22
|
+
// eslint-disable-next-line no-param-reassign
|
|
23
|
+
frontmatter = frontmatter.replace(new RegExp(regex, 'g'), val);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return frontmatter;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
frontmatterRegex,
|
|
31
|
+
getFrontmatterContent,
|
|
32
|
+
interpolateThemeSettings,
|
|
33
|
+
};
|