@bigcommerce/stencil-cli 4.1.1 → 5.2.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 CHANGED
@@ -1,5 +1,29 @@
1
1
  ### Draft
2
2
 
3
+ ### 5.2.0 (2022-08-30)
4
+
5
+ - feat: STRF-9877 Publish stencil cli docker image ([972](https://github.com/bigcommerce/stencil-cli/pull/972))
6
+ - fix: debug command ([970](https://github.com/bigcommerce/stencil-cli/pull/970))
7
+
8
+ ### 5.1.0 (2022-07-14)
9
+
10
+ - fix: STRF-9883 Interm fix to grab 1st variation ([951](https://github.com/bigcommerce/stencil-cli/pull/951))
11
+ - feat(storefront): bctheme-1063 modify stencil to start locally with components ui library ([895](https://github.com/bigcommerce/stencil-cli/pull/895))
12
+ - feat(storefront): bctheme-1064 Modify Stencil bundle for using Components UI Library ([904](https://github.com/bigcommerce/stencil-cli/pull/904))
13
+ - feat: paypal-1514 add sort type to schema ([960](https://github.com/bigcommerce/stencil-cli/pull/960))
14
+ - feat(storefront): strf-9923 CLI should not ask users for which API hostname to use ([955](https://github.com/bigcommerce/stencil-cli/pull/955))
15
+ - fix(storefront): bctheme-1138 fix error on import components with @ symbol in the path ([953](https://github.com/bigcommerce/stencil-cli/pull/953))
16
+ - fix(storefront): bctheme-1145 Enable using of external lib nested into organization for templates ([954](https://github.com/bigcommerce/stencil-cli/pull/954))
17
+ - feat(config): add ability to specify the secret file name ([957](https://github.com/bigcommerce/stencil-cli/pull/957))
18
+
19
+ ### 5.0.0 (2022-06-07)
20
+
21
+ - fix: STRF-9835 Bump paper ([947](https://github.com/bigcommerce/stencil-cli/pull/947))
22
+ - feat: strf-9791 Drop Node 12 Support ([945](https://github.com/bigcommerce/stencil-cli/pull/945))
23
+ - feat: STRF-9807 Dockerize stencil cli ([938](https://github.com/bigcommerce/stencil-cli/pull/938))
24
+ - merc-8592 Fix stencil CLI Push resulting in intermittent failures ([935](https://github.com/bigcommerce/stencil-cli/pull/935))
25
+ - feat: strf-9923 CLI should not ask users for which API hostname to use ([955](https://github.com/bigcommerce/stencil-cli/pull/955))
26
+
3
27
  ### 4.1.1 (2022-05-24)
4
28
 
5
29
  - fix: strf-9781 Bump dev packages for linting ([932](https://github.com/bigcommerce/stencil-cli/pull/932))
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 12.x and 14.x (Recommended) are supported.
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`.
@@ -30,6 +30,7 @@ Commands:
30
30
  push Bundles up the theme into a zip file and uploads it to your store.
31
31
  pull Pulls the configuration from the active theme on your live store and updates your local configuration.
32
32
  download Downloads the theme files from the active theme on your live store.
33
+ debug Prints environment and theme settings for debug purposes.
33
34
  help [cmd] display help for [cmd]
34
35
 
35
36
  Options:
@@ -64,6 +65,8 @@ Run `stencil pull` to sync changes to your theme configuration from your live st
64
65
  been used to change certain theme settings, this will update those settings in config.json in your theme files so you
65
66
  don't overwrite them on your next upload.
66
67
 
68
+ Run `stencil debug` to get information about runtime environment and the configuration
69
+
67
70
  ## Features
68
71
 
69
72
  ### BrowserSync
@@ -95,6 +98,20 @@ If you need any help or experience any bugs, please create a GitHub issue in thi
95
98
  If you would like to improve this project check out the [Contributing Guide](./CONTRIBUTING.md). Also, you can find
96
99
  the implementation details there.
97
100
 
101
+ ## Running in docker
102
+
103
+ There is possibility to run stencil-cli in docker.
104
+ Here are steps to have this functionality.
105
+ First of all, build a docker image, let's name it `bc/stencil-cli`
106
+ Run `docker build . -t bc/stencil-cli` in stencil-cli repo directory.
107
+
108
+ Then, after the image is built, you want to run some commands against your theme.
109
+
110
+ For example
111
+ `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.
112
+
113
+ `docker run -p 3005:3005 -v /bigcommerce/cornerstone:/usr/src/app -it bc/stencil-cli stencil start` and so on
114
+
98
115
  ## License
99
116
 
100
117
  Copyright (c) 2015-present, BigCommerce Inc.
package/bin/stencil.js CHANGED
@@ -15,4 +15,5 @@ program
15
15
  .command('push', 'Bundles up the theme into a zip file and uploads it to your store.')
16
16
  .command('pull', 'Pulls currently active theme config files and overwrites local copy')
17
17
  .command('download', 'Downloads all the theme files')
18
+ .command('debug', 'Prints environment and theme settings for debug purposes')
18
19
  .parse(process.argv);
@@ -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._fs.existsSync(this.secretsPath)
54
- ? await this._fsUtils.parseJsonFile(this.secretsPath)
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
@@ -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.getRawConfig();
413
+ const rawConfig = await this.themeConfig.getConfig();
414
414
  const cssFiles = await this.getCssFiles();
415
415
 
416
416
  for (const file of cssFiles) {
@@ -33,7 +33,8 @@
33
33
  "imageDimension",
34
34
  "text",
35
35
  "optimizedCheckout-image",
36
- "input"
36
+ "input",
37
+ "sort"
37
38
  ]
38
39
  },
39
40
  "content": {
@@ -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
- assembleTemplatesTask(callback) {
155
- console.log('Template Parsing Started...');
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
- recursiveReadDir(this.templatesPath, ['!*.html'], (readdirError, files) => {
158
- if (readdirError) {
159
- return callback(readdirError);
160
- }
169
+ if (!externalTemplatesImports) return [];
161
170
 
162
- const partials = files.map((file) => {
163
- return upath.toUnix(
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
- return async.map(
169
- partials,
170
- templateAssembler.assembleAndBundle.bind(null, this.templatesPath),
171
- (err, results) => {
172
- const ret = {};
174
+ templateAssembler.partialRegex.lastIndex = 0;
173
175
 
174
- if (err) {
175
- return callback(err);
176
- }
176
+ return importPath
177
+ .split('/templates/')[0]
178
+ .slice(templateAssembler.packageMarker.length + 1);
179
+ });
180
+ }
177
181
 
178
- partials.forEach((file, index) => {
179
- ret[file] = results[index];
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
- return async.parallel(
183
- [
184
- this._checkObjects.bind(this, results),
185
- this._detectCycles.bind(this, results),
186
- ],
187
- (checkingError) => {
188
- if (checkingError) {
189
- callback(checkingError);
190
- }
191
-
192
- console.log(`${'ok'.green} -- Template Parsing Finished`);
193
- callback(null, ret);
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
- async.parallel(this.tasks, (err, taskResults) => {
321
- if (err) {
322
- return callback(err);
323
- }
376
+ this.tasks.templates = this.assembleTemplatesTask.bind(this, callback);
324
377
 
325
- const archive = Archiver('zip');
326
- const fileStream = fs.createWriteStream(bundleZipPath);
327
- archive.pipe(fileStream);
328
-
329
- // Create manifest will use taskResults to generate a manifest file
330
- return this.generateManifest(taskResults, (manifestGenerationError, manifest) => {
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
- if (size > MAX_SIZE_BUNDLE) {
356
- return console.error(
357
- `Error: Your bundle of size ${size} bytes is above the max size of ${MAX_SIZE_BUNDLE} bytes`,
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
- console.log(`${'ok'.green} -- Zipping Files Finished`);
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
- return callback(null, bundleZipPath);
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
- // This triggers 'close' event in the file stream. No need to callback()
367
- return archive.finalize();
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
  /**
@@ -78,8 +78,8 @@ class StencilInit {
78
78
  }
79
79
 
80
80
  /**
81
- * @param {{port: (number), normalStoreUrl: (string), accessToken: (string), apiHost: (string)}} defaultAnswers
82
- * @param {{port: (number), normalStoreUrl: (string), accessToken: (string), apiHost: (string)}} cliOptions
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',
@@ -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 = path.join(templatesFolder, `${templatePath}.html`);
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] = content;
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(templateName, cb2) {
84
- const templatePath = path.join(templatesFolder, `${templateName}.html`);
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,
@@ -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 (payload && payload.title && uploadLimitPattern.exec(payload.title)) {
307
+ if (
308
+ payload &&
309
+ payload.title &&
310
+ uploadLimitPattern.exec(payload.title) &&
311
+ !uploadBundleAgain
312
+ ) {
307
313
  return { themeLimitReached: true };
308
314
  }
309
315