@bigcommerce/stencil-cli 4.1.0 → 5.1.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/CHANGELOG.md +26 -0
- package/README.md +15 -1
- package/bin/stencil-debug.js +2 -0
- package/lib/StencilConfigManager.js +21 -3
- package/lib/bundle-validator.js +1 -1
- package/lib/schemas/themeSchema.json +2 -1
- package/lib/stencil-bundle.js +139 -80
- package/lib/stencil-init.js +2 -11
- package/lib/stencil-push.js +1 -0
- package/lib/stencil-push.utils.js +45 -1
- package/lib/template-assembler.js +65 -5
- package/lib/theme-api-client.js +8 -2
- package/package-lock.json +1282 -1008
- package/package.json +12 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
### Draft
|
|
2
2
|
|
|
3
|
+
### 5.1.0 (2022-07-14)
|
|
4
|
+
|
|
5
|
+
- fix: STRF-9883 Interm fix to grab 1st variation ([951](https://github.com/bigcommerce/stencil-cli/pull/951))
|
|
6
|
+
- feat(storefront): bctheme-1063 modify stencil to start locally with components ui library ([895](https://github.com/bigcommerce/stencil-cli/pull/895))
|
|
7
|
+
- feat(storefront): bctheme-1064 Modify Stencil bundle for using Components UI Library ([904](https://github.com/bigcommerce/stencil-cli/pull/904))
|
|
8
|
+
- feat: paypal-1514 add sort type to schema ([960](https://github.com/bigcommerce/stencil-cli/pull/960))
|
|
9
|
+
- feat(storefront): strf-9923 CLI should not ask users for which API hostname to use ([955](https://github.com/bigcommerce/stencil-cli/pull/955))
|
|
10
|
+
- fix(storefront): bctheme-1138 fix error on import components with @ symbol in the path ([953](https://github.com/bigcommerce/stencil-cli/pull/953))
|
|
11
|
+
- fix(storefront): bctheme-1145 Enable using of external lib nested into organization for templates ([954](https://github.com/bigcommerce/stencil-cli/pull/954))
|
|
12
|
+
- feat(config): add ability to specify the secret file name ([957](https://github.com/bigcommerce/stencil-cli/pull/957))
|
|
13
|
+
|
|
14
|
+
### 5.0.0 (2022-06-07)
|
|
15
|
+
|
|
16
|
+
- fix: STRF-9835 Bump paper ([947](https://github.com/bigcommerce/stencil-cli/pull/947))
|
|
17
|
+
- feat: strf-9791 Drop Node 12 Support ([945](https://github.com/bigcommerce/stencil-cli/pull/945))
|
|
18
|
+
- feat: STRF-9807 Dockerize stencil cli ([938](https://github.com/bigcommerce/stencil-cli/pull/938))
|
|
19
|
+
- merc-8592 Fix stencil CLI Push resulting in intermittent failures ([935](https://github.com/bigcommerce/stencil-cli/pull/935))
|
|
20
|
+
- feat: strf-9923 CLI should not ask users for which API hostname to use ([955](https://github.com/bigcommerce/stencil-cli/pull/955))
|
|
21
|
+
|
|
22
|
+
### 4.1.1 (2022-05-24)
|
|
23
|
+
|
|
24
|
+
- fix: strf-9781 Bump dev packages for linting ([932](https://github.com/bigcommerce/stencil-cli/pull/932))
|
|
25
|
+
- fix: STRF-9781 Bump axios, @ocktokit/rest and browser-sync ([931](https://github.com/bigcommerce/stencil-cli/pull/931))
|
|
26
|
+
- fix: STRF-9781 Bump async and confidence ([929](https://github.com/bigcommerce/stencil-cli/pull/929))
|
|
27
|
+
- fix: STRF-9781 Bump simple-git ([928](https://github.com/bigcommerce/stencil-cli/pull/928))
|
|
28
|
+
|
|
3
29
|
### 4.1.0 (2022-04-28)
|
|
4
30
|
|
|
5
31
|
- feat: strf-9749 Update Stencil Cli to use node-sass latest by default ([923](https://github.com/bigcommerce/stencil-cli/pull/923))
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The BigCommerce server emulator for local theme development.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
_Note: Stencil requires the Node.js runtime environment,
|
|
11
|
-
version
|
|
11
|
+
version 14.x is supported.
|
|
12
12
|
We do not yet have support for versions greater than Node 14._
|
|
13
13
|
|
|
14
14
|
Run `npm install -g @bigcommerce/stencil-cli`.
|
|
@@ -95,6 +95,20 @@ If you need any help or experience any bugs, please create a GitHub issue in thi
|
|
|
95
95
|
If you would like to improve this project check out the [Contributing Guide](./CONTRIBUTING.md). Also, you can find
|
|
96
96
|
the implementation details there.
|
|
97
97
|
|
|
98
|
+
## Running in docker
|
|
99
|
+
|
|
100
|
+
There is possibility to run stencil-cli in docker.
|
|
101
|
+
Here are steps to have this functionality.
|
|
102
|
+
First of all, build a docker image, let's name it `bc/stencil-cli`
|
|
103
|
+
Run `docker build . -t bc/stencil-cli` in stencil-cli repo directory.
|
|
104
|
+
|
|
105
|
+
Then, after the image is built, you want to run some commands against your theme.
|
|
106
|
+
|
|
107
|
+
For example
|
|
108
|
+
`docker run -p 3005:3005 -v /bigcommerce/cornerstone:/usr/src/app -it bc/stencil-cli stencil init`, where `3005` is port number definded in `config.stencil.json` and `/bigcommerce/cornerstone` path to where theme is located.
|
|
109
|
+
|
|
110
|
+
`docker run -p 3005:3005 -v /bigcommerce/cornerstone:/usr/src/app -it bc/stencil-cli stencil start` and so on
|
|
111
|
+
|
|
98
112
|
## License
|
|
99
113
|
|
|
100
114
|
Copyright (c) 2015-present, BigCommerce Inc.
|
package/bin/stencil-debug.js
CHANGED
|
@@ -50,9 +50,8 @@ class StencilConfigManager {
|
|
|
50
50
|
const generalConfig = this._fs.existsSync(this.configPath)
|
|
51
51
|
? await this._fsUtils.parseJsonFile(this.configPath)
|
|
52
52
|
: null;
|
|
53
|
-
const secretsConfig = this.
|
|
54
|
-
|
|
55
|
-
: null;
|
|
53
|
+
const secretsConfig = await this._getSecretsConfig(generalConfig);
|
|
54
|
+
|
|
56
55
|
if (generalConfig || secretsConfig) {
|
|
57
56
|
const parsedConfig = { ...generalConfig, ...secretsConfig };
|
|
58
57
|
return this._validateStencilConfig(parsedConfig, ignoreMissingFields);
|
|
@@ -93,6 +92,25 @@ class StencilConfigManager {
|
|
|
93
92
|
);
|
|
94
93
|
}
|
|
95
94
|
|
|
95
|
+
/**
|
|
96
|
+
* @private
|
|
97
|
+
* @param {object | null} config
|
|
98
|
+
* @returns {Promise<object | null>}
|
|
99
|
+
*/
|
|
100
|
+
async _getSecretsConfig(generalConfig) {
|
|
101
|
+
if (generalConfig && generalConfig.secretsFileName) {
|
|
102
|
+
const secretsPath = path.join(this.themePath, generalConfig.secretsFileName);
|
|
103
|
+
|
|
104
|
+
if (this._fs.existsSync(secretsPath)) {
|
|
105
|
+
return this._fsUtils.parseJsonFile(secretsPath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this._fs.existsSync(this.secretsPath)
|
|
110
|
+
? this._fsUtils.parseJsonFile(this.secretsPath)
|
|
111
|
+
: null;
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
/**
|
|
97
115
|
* @private
|
|
98
116
|
* @param {object} config
|
package/lib/bundle-validator.js
CHANGED
|
@@ -410,7 +410,7 @@ class BundleValidator {
|
|
|
410
410
|
async _validateCssFiles() {
|
|
411
411
|
const assetsPath = path.join(this.themePath, 'assets');
|
|
412
412
|
const stylesPath = path.join(this.themePath, 'assets/scss');
|
|
413
|
-
const rawConfig = await this.themeConfig.
|
|
413
|
+
const rawConfig = await this.themeConfig.getConfig();
|
|
414
414
|
const cssFiles = await this.getCssFiles();
|
|
415
415
|
|
|
416
416
|
for (const file of cssFiles) {
|
package/lib/stencil-bundle.js
CHANGED
|
@@ -84,6 +84,8 @@ class Bundle {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
this.tasks = tasks;
|
|
87
|
+
|
|
88
|
+
this._getExternalLibs = this._getExternalLibs.bind(this);
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
/**
|
|
@@ -151,51 +153,105 @@ class Bundle {
|
|
|
151
153
|
};
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
/**
|
|
157
|
+
* helps to find any node modules dependencies with
|
|
158
|
+
* ui templates and returns its list
|
|
159
|
+
*
|
|
160
|
+
* @private
|
|
161
|
+
* @param {string} content
|
|
162
|
+
* @returns {Array}
|
|
163
|
+
*/
|
|
164
|
+
async _getExternalLibs(templatePath) {
|
|
165
|
+
const content = await fs.promises.readFile(templatePath, { encoding: 'utf-8' });
|
|
166
|
+
const externalPathRegex = /{{2}>\s*(['"]external)[^{]*?}{2}/g;
|
|
167
|
+
const externalTemplatesImports = content.match(externalPathRegex);
|
|
156
168
|
|
|
157
|
-
|
|
158
|
-
if (readdirError) {
|
|
159
|
-
return callback(readdirError);
|
|
160
|
-
}
|
|
169
|
+
if (!externalTemplatesImports) return [];
|
|
161
170
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
file.replace(this.templatesPath + path.sep, '').replace(/\.html$/, ''),
|
|
165
|
-
);
|
|
166
|
-
});
|
|
171
|
+
return externalTemplatesImports.map((templateImport) => {
|
|
172
|
+
const [, importPath] = templateAssembler.partialRegex.exec(templateImport);
|
|
167
173
|
|
|
168
|
-
|
|
169
|
-
partials,
|
|
170
|
-
templateAssembler.assembleAndBundle.bind(null, this.templatesPath),
|
|
171
|
-
(err, results) => {
|
|
172
|
-
const ret = {};
|
|
174
|
+
templateAssembler.partialRegex.lastIndex = 0;
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
return importPath
|
|
177
|
+
.split('/templates/')[0]
|
|
178
|
+
.slice(templateAssembler.packageMarker.length + 1);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
177
181
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
async assembleTemplatesTask(callback) {
|
|
183
|
+
console.log('Template Parsing Started...');
|
|
184
|
+
const internalTemplatesList = await recursiveReadDir(this.templatesPath, ['!*.html']);
|
|
185
|
+
let externalLibs;
|
|
181
186
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
187
|
+
try {
|
|
188
|
+
const removeDuplicates = (arr) => Array.from(new Set(arr.flat()));
|
|
189
|
+
const temp = internalTemplatesList.map(this._getExternalLibs);
|
|
190
|
+
const result = await Promise.all(temp);
|
|
191
|
+
|
|
192
|
+
externalLibs = removeDuplicates(result);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
callback(error);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let externalLibPaths = [];
|
|
198
|
+
if (externalLibs.length) {
|
|
199
|
+
externalLibPaths = externalLibs.map((lib) =>
|
|
200
|
+
recursiveReadDir(path.join(this.themePath, 'node_modules', lib, 'templates'), [
|
|
201
|
+
'!*.html',
|
|
202
|
+
]),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
// eslint-disable-next-line node/no-unsupported-features/es-builtins
|
|
206
|
+
const res = await Promise.allSettled([
|
|
207
|
+
recursiveReadDir(this.templatesPath, ['!*.html']),
|
|
208
|
+
...externalLibPaths,
|
|
209
|
+
]);
|
|
210
|
+
const [{ value: internalTemplates }, ...externalTemplatesList] = res;
|
|
211
|
+
const internalPartials = internalTemplates.map((file) => {
|
|
212
|
+
return upath.toUnix(
|
|
213
|
+
file.replace(this.templatesPath + path.sep, '').replace(/\.html$/, ''),
|
|
197
214
|
);
|
|
198
215
|
});
|
|
216
|
+
const externalPartials = externalTemplatesList.reduce(
|
|
217
|
+
(partials, { value: externalTemplates }) => {
|
|
218
|
+
const extractedPartials = externalTemplates.map((file) => {
|
|
219
|
+
return upath.toUnix(
|
|
220
|
+
'external' + file.split('node_modules')[1].replace(/\.html$/, ''),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
partials.push(...extractedPartials);
|
|
224
|
+
|
|
225
|
+
return partials;
|
|
226
|
+
},
|
|
227
|
+
[],
|
|
228
|
+
);
|
|
229
|
+
const allPartials = [...externalPartials, ...internalPartials];
|
|
230
|
+
|
|
231
|
+
let results;
|
|
232
|
+
try {
|
|
233
|
+
results = await async.map(
|
|
234
|
+
allPartials,
|
|
235
|
+
templateAssembler.assembleAndBundle.bind(null, this.templatesPath),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const ret = {};
|
|
239
|
+
|
|
240
|
+
allPartials.forEach((file, index) => {
|
|
241
|
+
ret[file] = results[index];
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await Promise.all([
|
|
245
|
+
this._checkObjects.bind(this, results),
|
|
246
|
+
this._detectCycles.bind(this, results),
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
console.log(`${'ok'.green} -- Template Parsing Finished`);
|
|
250
|
+
|
|
251
|
+
return ret;
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return callback(err);
|
|
254
|
+
}
|
|
199
255
|
}
|
|
200
256
|
|
|
201
257
|
async assembleSchema() {
|
|
@@ -317,56 +373,59 @@ class Bundle {
|
|
|
317
373
|
typeof this.options.dest === 'string' ? this.options.dest : this.themePath;
|
|
318
374
|
const bundleZipPath = path.join(outputFolder, outputName);
|
|
319
375
|
|
|
320
|
-
|
|
321
|
-
if (err) {
|
|
322
|
-
return callback(err);
|
|
323
|
-
}
|
|
376
|
+
this.tasks.templates = this.assembleTemplatesTask.bind(this, callback);
|
|
324
377
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (manifestGenerationError) {
|
|
332
|
-
return callback(manifestGenerationError);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// eslint-disable-next-line no-param-reassign
|
|
336
|
-
taskResults.manifest = manifest;
|
|
337
|
-
// zip theme files
|
|
338
|
-
this._bundleThemeFiles(archive, this.themePath);
|
|
339
|
-
|
|
340
|
-
// zip all generated files
|
|
341
|
-
const failedTemplates = this._bundleParsedFiles(archive, taskResults);
|
|
342
|
-
|
|
343
|
-
fileStream.on('close', () => {
|
|
344
|
-
const stats = fs.statSync(bundleZipPath);
|
|
345
|
-
const { size } = stats;
|
|
346
|
-
|
|
347
|
-
if (failedTemplates.length) {
|
|
348
|
-
return console.error(
|
|
349
|
-
`Error: Your bundle failed as templates generated from the files below are greater than or equal to 1 megabyte in size:\n${failedTemplates.join(
|
|
350
|
-
'\n',
|
|
351
|
-
)}`,
|
|
352
|
-
);
|
|
353
|
-
}
|
|
378
|
+
async
|
|
379
|
+
.parallel(this.tasks)
|
|
380
|
+
.then((taskResults) => {
|
|
381
|
+
const archive = Archiver('zip');
|
|
382
|
+
const fileStream = fs.createWriteStream(bundleZipPath);
|
|
383
|
+
archive.pipe(fileStream);
|
|
354
384
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
);
|
|
385
|
+
// Create manifest will use taskResults to generate a manifest file
|
|
386
|
+
return this.generateManifest(taskResults, (manifestGenerationError, manifest) => {
|
|
387
|
+
if (manifestGenerationError) {
|
|
388
|
+
return callback(manifestGenerationError);
|
|
359
389
|
}
|
|
360
390
|
|
|
361
|
-
|
|
391
|
+
// eslint-disable-next-line no-param-reassign
|
|
392
|
+
taskResults.manifest = manifest;
|
|
393
|
+
// zip theme files
|
|
394
|
+
this._bundleThemeFiles(archive, this.themePath);
|
|
362
395
|
|
|
363
|
-
|
|
364
|
-
|
|
396
|
+
// zip all generated files
|
|
397
|
+
const failedTemplates = this._bundleParsedFiles(archive, taskResults);
|
|
398
|
+
|
|
399
|
+
fileStream.on('close', () => {
|
|
400
|
+
const stats = fs.statSync(bundleZipPath);
|
|
401
|
+
const { size } = stats;
|
|
402
|
+
|
|
403
|
+
if (failedTemplates.length) {
|
|
404
|
+
return console.error(
|
|
405
|
+
`Error: Your bundle failed as templates generated from the files below are greater than or equal to 1 megabyte in size:\n${failedTemplates.join(
|
|
406
|
+
'\n',
|
|
407
|
+
)}`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
365
410
|
|
|
366
|
-
|
|
367
|
-
|
|
411
|
+
if (size > MAX_SIZE_BUNDLE) {
|
|
412
|
+
return console.error(
|
|
413
|
+
`Error: Your bundle of size ${size} bytes is above the max size of ${MAX_SIZE_BUNDLE} bytes`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log(`${'ok'.green} -- Zipping Files Finished`);
|
|
418
|
+
|
|
419
|
+
return callback(null, bundleZipPath);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// This triggers 'close' event in the file stream. No need to callback()
|
|
423
|
+
return archive.finalize();
|
|
424
|
+
});
|
|
425
|
+
})
|
|
426
|
+
.catch((err) => {
|
|
427
|
+
console.log(err);
|
|
368
428
|
});
|
|
369
|
-
});
|
|
370
429
|
}
|
|
371
430
|
|
|
372
431
|
/**
|
package/lib/stencil-init.js
CHANGED
|
@@ -78,8 +78,8 @@ class StencilInit {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* @param {{port: (number), normalStoreUrl: (string), accessToken: (string)
|
|
82
|
-
* @param {{port: (number), normalStoreUrl: (string), accessToken: (string)
|
|
81
|
+
* @param {{port: (number), normalStoreUrl: (string), accessToken: (string)}} defaultAnswers
|
|
82
|
+
* @param {{port: (number), normalStoreUrl: (string), accessToken: (string)}} cliOptions
|
|
83
83
|
* @returns {{object[]}}
|
|
84
84
|
*/
|
|
85
85
|
getQuestions(defaultAnswers, cliOptions) {
|
|
@@ -105,15 +105,6 @@ class StencilInit {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
if (!cliOptions.apiHost) {
|
|
109
|
-
prompts.push({
|
|
110
|
-
type: 'input',
|
|
111
|
-
name: 'apiHost',
|
|
112
|
-
message: 'What API Host would you like to use?',
|
|
113
|
-
default: defaultAnswers.apiHost,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
108
|
if (!cliOptions.port) {
|
|
118
109
|
prompts.push({
|
|
119
110
|
type: 'input',
|
package/lib/stencil-push.js
CHANGED
|
@@ -13,6 +13,7 @@ function stencilPush(options = {}, callback) {
|
|
|
13
13
|
utils.notifyUserOfThemeLimitReachedIfNecessary,
|
|
14
14
|
utils.promptUserToDeleteThemesIfNecessary,
|
|
15
15
|
utils.deleteThemesIfNecessary,
|
|
16
|
+
utils.checkIfDeletionIsComplete(),
|
|
16
17
|
utils.uploadBundleAgainIfNecessary,
|
|
17
18
|
utils.notifyUserOfThemeUploadCompletion,
|
|
18
19
|
utils.pollForJobCompletion((data) => ({ themeId: data.theme_id })),
|
|
@@ -87,6 +87,7 @@ utils.uploadBundle = async (options) => {
|
|
|
87
87
|
config: { accessToken },
|
|
88
88
|
bundleZipPath,
|
|
89
89
|
storeHash,
|
|
90
|
+
uploadBundleAgain,
|
|
90
91
|
} = options;
|
|
91
92
|
|
|
92
93
|
const apiHost = options.apiHost || options.config.apiHost;
|
|
@@ -97,6 +98,7 @@ utils.uploadBundle = async (options) => {
|
|
|
97
98
|
apiHost,
|
|
98
99
|
bundleZipPath,
|
|
99
100
|
storeHash,
|
|
101
|
+
uploadBundleAgain,
|
|
100
102
|
});
|
|
101
103
|
return {
|
|
102
104
|
...options,
|
|
@@ -188,12 +190,54 @@ utils.deleteThemesIfNecessary = async (options) => {
|
|
|
188
190
|
return options;
|
|
189
191
|
};
|
|
190
192
|
|
|
193
|
+
utils.checkIfDeletionIsComplete = () => {
|
|
194
|
+
return async.retryable(
|
|
195
|
+
{
|
|
196
|
+
interval: 1000,
|
|
197
|
+
errorFilter: (err) => {
|
|
198
|
+
if (err.message === 'ThemeStillExists') {
|
|
199
|
+
console.log(`${'warning'.yellow} -- Theme still exists;Retrying ...`);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
},
|
|
204
|
+
times: 5,
|
|
205
|
+
},
|
|
206
|
+
utils.checkIfThemeIsDeleted(),
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
utils.checkIfThemeIsDeleted = () => async (options) => {
|
|
211
|
+
const {
|
|
212
|
+
themeLimitReached,
|
|
213
|
+
config: { accessToken },
|
|
214
|
+
storeHash,
|
|
215
|
+
themeIdsToDelete,
|
|
216
|
+
} = options;
|
|
217
|
+
|
|
218
|
+
if (!themeLimitReached) {
|
|
219
|
+
return options;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const apiHost = options.apiHost || options.config.apiHost;
|
|
223
|
+
|
|
224
|
+
const result = await themeApiClient.getThemes({ accessToken, apiHost, storeHash });
|
|
225
|
+
|
|
226
|
+
const themeStillExists = result.some((theme) => themeIdsToDelete.includes(theme.uuid));
|
|
227
|
+
|
|
228
|
+
if (themeStillExists) {
|
|
229
|
+
throw new Error('ThemeStillExists');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return options;
|
|
233
|
+
};
|
|
234
|
+
|
|
191
235
|
utils.uploadBundleAgainIfNecessary = async (options) => {
|
|
192
236
|
if (!options.themeLimitReached) {
|
|
193
237
|
return options;
|
|
194
238
|
}
|
|
195
239
|
|
|
196
|
-
return utils.uploadBundle(options);
|
|
240
|
+
return utils.uploadBundle({ ...options, uploadThemeAgain: true });
|
|
197
241
|
};
|
|
198
242
|
|
|
199
243
|
utils.notifyUserOfThemeUploadCompletion = async (options) => {
|
|
@@ -4,8 +4,59 @@ const fs = require('graceful-fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const upath = require('upath');
|
|
6
6
|
|
|
7
|
-
const partialRegex = /\{\{>\s*([_|\-|a-zA-Z0-9/]+)[^{]*?}}/g;
|
|
7
|
+
const partialRegex = /\{\{>\s*([_|\-|a-zA-Z0-9@'"/]+)[^{]*?}}/g;
|
|
8
8
|
const dynamicComponentRegex = /\{\{\s*?dynamicComponent\s*(?:'|")([_|\-|a-zA-Z0-9/]+)(?:'|").*?}}/g;
|
|
9
|
+
const includeRegex = /{{2}>\s*([_|\-|a-zA-Z0-9/]+)[^{]*?}{2}/g;
|
|
10
|
+
const pathStringRegex = /^["'].+["']$/;
|
|
11
|
+
const packageMarker = 'external/';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* indicates if a template source is a node modules dependency
|
|
15
|
+
*
|
|
16
|
+
* @param {string} templateName
|
|
17
|
+
* @returns {Boolean}
|
|
18
|
+
*/
|
|
19
|
+
const isExternalTemplate = (templateName) => {
|
|
20
|
+
return templateName.startsWith(packageMarker);
|
|
21
|
+
};
|
|
22
|
+
const defineBaseRoute = (route) => route.split('/').slice(0, 3).join('/');
|
|
23
|
+
const applyExternalPath = (templateName, templates) => {
|
|
24
|
+
return templates.map((t) => `${defineBaseRoute(templateName)}/${t}`);
|
|
25
|
+
};
|
|
26
|
+
const replacePartialNames = (content, partialRoute) => {
|
|
27
|
+
return content.replace(
|
|
28
|
+
includeRegex,
|
|
29
|
+
(__, partialName) => `{{> ${defineBaseRoute(partialRoute)}/${partialName} }}`,
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
const trimPartial = (partial) => {
|
|
33
|
+
if (pathStringRegex.test(partial)) {
|
|
34
|
+
return partial.slice(1, -1);
|
|
35
|
+
}
|
|
36
|
+
return partial;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Takes a templates folder and template name. It returns simple path for custom template if it's available
|
|
41
|
+
* or use default folder instead
|
|
42
|
+
*
|
|
43
|
+
* @param {string} templatesFolder
|
|
44
|
+
* @param {string} templateName
|
|
45
|
+
*/
|
|
46
|
+
function getCustomPath(templatesFolder, templateName) {
|
|
47
|
+
let customTemplatesDir;
|
|
48
|
+
let customTemplateName;
|
|
49
|
+
|
|
50
|
+
if (templateName.startsWith(packageMarker)) {
|
|
51
|
+
customTemplatesDir = templatesFolder.replace('templates', 'node_modules');
|
|
52
|
+
customTemplateName = templateName.slice(packageMarker.length);
|
|
53
|
+
} else {
|
|
54
|
+
customTemplatesDir = templatesFolder;
|
|
55
|
+
customTemplateName = templateName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return path.join(customTemplatesDir, `${customTemplateName}.html`);
|
|
59
|
+
}
|
|
9
60
|
|
|
10
61
|
/**
|
|
11
62
|
* Takes a list of templates and grabs their content. It returns simple key/val pair
|
|
@@ -20,14 +71,17 @@ function getContent(templatesFolder, templatePaths, callback) {
|
|
|
20
71
|
templatePaths,
|
|
21
72
|
{},
|
|
22
73
|
(acc, templatePath, reduceCallback) => {
|
|
23
|
-
const file =
|
|
74
|
+
const file = getCustomPath(templatesFolder, templatePath);
|
|
75
|
+
|
|
24
76
|
fs.readFile(file, { encoding: 'utf-8' }, (err, content) => {
|
|
25
77
|
if (err) {
|
|
26
78
|
reduceCallback(err);
|
|
27
79
|
return;
|
|
28
80
|
}
|
|
29
81
|
|
|
30
|
-
acc[templatePath] =
|
|
82
|
+
acc[templatePath] = templatePath.startsWith(packageMarker)
|
|
83
|
+
? replacePartialNames(content, templatePath)
|
|
84
|
+
: content;
|
|
31
85
|
|
|
32
86
|
reduceCallback(null, acc);
|
|
33
87
|
});
|
|
@@ -80,8 +134,9 @@ function getTemplatePaths(templatesFolder, templates, options, callback) {
|
|
|
80
134
|
return callback(null, templatePaths);
|
|
81
135
|
});
|
|
82
136
|
|
|
83
|
-
function resolvePartials(
|
|
84
|
-
const
|
|
137
|
+
function resolvePartials(t, cb2) {
|
|
138
|
+
const templateName = trimPartial(t);
|
|
139
|
+
const templatePath = getCustomPath(templatesFolder, templateName);
|
|
85
140
|
|
|
86
141
|
fs.readFile(templatePath, { encoding: 'utf-8' }, (err, content) => {
|
|
87
142
|
const componentPaths = [];
|
|
@@ -147,6 +202,10 @@ function getTemplatePaths(templatesFolder, templates, options, callback) {
|
|
|
147
202
|
return;
|
|
148
203
|
}
|
|
149
204
|
|
|
205
|
+
if (isExternalTemplate(templateName)) {
|
|
206
|
+
matches = applyExternalPath(templateName, matches);
|
|
207
|
+
}
|
|
208
|
+
|
|
150
209
|
async.each(matches, resolvePartials, cb2);
|
|
151
210
|
},
|
|
152
211
|
);
|
|
@@ -202,6 +261,7 @@ function getTemplateContentSync(templatesFolder, templateFile) {
|
|
|
202
261
|
}
|
|
203
262
|
|
|
204
263
|
module.exports = {
|
|
264
|
+
packageMarker,
|
|
205
265
|
partialRegex,
|
|
206
266
|
dynamicComponentRegex,
|
|
207
267
|
assemble,
|
package/lib/theme-api-client.js
CHANGED
|
@@ -278,9 +278,10 @@ async function getVariationsByThemeId({ accessToken, apiHost, storeHash, themeId
|
|
|
278
278
|
* @param {string} options.accessToken
|
|
279
279
|
* @param {string} options.apiHost
|
|
280
280
|
* @param {string} options.storeHash
|
|
281
|
+
* @param {boolean} options.uploadBundleAgain
|
|
281
282
|
* @returns {Promise<object>}
|
|
282
283
|
*/
|
|
283
|
-
async function postTheme({ bundleZipPath, accessToken, apiHost, storeHash }) {
|
|
284
|
+
async function postTheme({ bundleZipPath, accessToken, apiHost, storeHash, uploadBundleAgain }) {
|
|
284
285
|
const formData = new FormData();
|
|
285
286
|
formData.append('file', fs.createReadStream(bundleZipPath), {
|
|
286
287
|
filename: path.basename(bundleZipPath),
|
|
@@ -303,7 +304,12 @@ async function postTheme({ bundleZipPath, accessToken, apiHost, storeHash }) {
|
|
|
303
304
|
// This status code is overloaded, so we need to check the message to determine
|
|
304
305
|
// if we want to trigger the theme deletion flow.
|
|
305
306
|
const uploadLimitPattern = /You have reached your upload limit/;
|
|
306
|
-
if (
|
|
307
|
+
if (
|
|
308
|
+
payload &&
|
|
309
|
+
payload.title &&
|
|
310
|
+
uploadLimitPattern.exec(payload.title) &&
|
|
311
|
+
!uploadBundleAgain
|
|
312
|
+
) {
|
|
307
313
|
return { themeLimitReached: true };
|
|
308
314
|
}
|
|
309
315
|
|