@swell/cli 2.0.1-alpha.9 → 2.0.2

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.
Files changed (102) hide show
  1. package/dist/app-command.d.ts +1 -1
  2. package/dist/app-command.js +9 -9
  3. package/dist/commands/app/dev.d.ts +16 -0
  4. package/dist/commands/app/dev.js +28 -0
  5. package/dist/commands/app/frontend/deploy.d.ts +15 -0
  6. package/dist/commands/app/frontend/deploy.js +41 -0
  7. package/dist/commands/app/frontend/dev.d.ts +19 -0
  8. package/dist/commands/app/frontend/dev.js +108 -0
  9. package/dist/commands/app/info.js +14 -9
  10. package/dist/commands/app/init.d.ts +12 -0
  11. package/dist/commands/app/init.js +34 -0
  12. package/dist/commands/app/install.d.ts +4 -4
  13. package/dist/commands/app/install.js +36 -42
  14. package/dist/commands/app/pull.d.ts +15 -0
  15. package/dist/commands/app/pull.js +50 -0
  16. package/dist/commands/app/push.d.ts +11 -14
  17. package/dist/commands/app/push.js +64 -138
  18. package/dist/commands/app/release.d.ts +13 -0
  19. package/dist/commands/app/release.js +148 -0
  20. package/dist/commands/app/version.d.ts +5 -4
  21. package/dist/commands/app/version.js +63 -42
  22. package/dist/commands/app/watch.d.ts +2 -4
  23. package/dist/commands/app/watch.js +3 -83
  24. package/dist/commands/create/app.d.ts +35 -11
  25. package/dist/commands/create/app.js +143 -137
  26. package/dist/commands/create/content.d.ts +2 -2
  27. package/dist/commands/create/content.js +10 -10
  28. package/dist/commands/create/frontend.d.ts +12 -0
  29. package/dist/commands/create/frontend.js +38 -0
  30. package/dist/commands/create/function.d.ts +3 -3
  31. package/dist/commands/create/function.js +22 -22
  32. package/dist/commands/create/model.d.ts +2 -2
  33. package/dist/commands/create/model.js +11 -11
  34. package/dist/commands/create/notification.d.ts +4 -4
  35. package/dist/commands/create/notification.js +38 -40
  36. package/dist/commands/env.d.ts +12 -0
  37. package/dist/commands/env.js +31 -0
  38. package/dist/commands/login.d.ts +1 -1
  39. package/dist/commands/login.js +18 -24
  40. package/dist/commands/logs.js +0 -1
  41. package/dist/commands/theme/dev.d.ts +21 -0
  42. package/dist/commands/theme/dev.js +164 -0
  43. package/dist/commands/theme/init.d.ts +9 -0
  44. package/dist/commands/theme/init.js +15 -0
  45. package/dist/commands/theme/pull.d.ts +20 -0
  46. package/dist/commands/theme/pull.js +65 -0
  47. package/dist/commands/theme/push.d.ts +19 -0
  48. package/dist/commands/theme/push.js +89 -0
  49. package/dist/create-app-command.d.ts +20 -0
  50. package/dist/create-app-command.js +253 -0
  51. package/dist/create-collection-command.d.ts +9 -9
  52. package/dist/create-collection-command.js +23 -23
  53. package/dist/{create-command.d.ts → create-config-command.d.ts} +1 -1
  54. package/dist/{create-command.js → create-config-command.js} +7 -7
  55. package/dist/env/local.d.ts +8 -0
  56. package/dist/env/local.js +7 -0
  57. package/dist/env/production.d.ts +8 -0
  58. package/dist/env/production.js +7 -0
  59. package/dist/env/review.d.ts +8 -0
  60. package/dist/env/review.js +7 -0
  61. package/dist/env/staging.d.ts +8 -0
  62. package/dist/env/staging.js +7 -0
  63. package/dist/lib/api.d.ts +4 -2
  64. package/dist/lib/api.js +50 -21
  65. package/dist/lib/app-config.d.ts +13 -5
  66. package/dist/lib/app-config.js +62 -24
  67. package/dist/lib/apps/app-config.d.ts +17 -3
  68. package/dist/lib/apps/app-config.js +194 -66
  69. package/dist/lib/apps/index.d.ts +93 -12
  70. package/dist/lib/apps/index.js +155 -50
  71. package/dist/lib/apps/paths.d.ts +14 -7
  72. package/dist/lib/apps/paths.js +73 -28
  73. package/dist/lib/bundle.js +15 -3
  74. package/dist/lib/config.d.ts +9 -2
  75. package/dist/lib/config.js +49 -1
  76. package/dist/lib/constants.d.ts +5 -0
  77. package/dist/lib/constants.js +24 -6
  78. package/dist/lib/create/content.d.ts +2 -2
  79. package/dist/lib/create/content.js +1 -1
  80. package/dist/lib/create/function.d.ts +2 -2
  81. package/dist/lib/create/function.js +2 -2
  82. package/dist/lib/create/index.d.ts +1 -1
  83. package/dist/lib/create/index.js +11 -7
  84. package/dist/lib/create/model.d.ts +2 -2
  85. package/dist/lib/create/notification.d.ts +2 -2
  86. package/dist/lib/info.js +2 -2
  87. package/dist/lib/logs/index.js +1 -1
  88. package/dist/lib/sessions.js +2 -2
  89. package/dist/lib/stores.d.ts +9 -4
  90. package/dist/lib/stores.js +13 -8
  91. package/dist/lib/style.d.ts +2 -0
  92. package/dist/lib/style.js +2 -0
  93. package/dist/lib/swell-function-wrapper.d.ts +40 -1
  94. package/dist/lib/swell-function-wrapper.js +107 -22
  95. package/dist/push-app-command.d.ts +73 -0
  96. package/dist/push-app-command.js +743 -0
  97. package/dist/remote-app-command.d.ts +37 -11
  98. package/dist/remote-app-command.js +432 -169
  99. package/oclif.manifest.json +872 -123
  100. package/package.json +14 -1
  101. package/dist/commands/app/_pull.d.ts +0 -8
  102. package/dist/commands/app/_pull.js +0 -45
@@ -0,0 +1,743 @@
1
+ import { confirm, input, select } from '@inquirer/prompts';
2
+ import { $ } from 'execa';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import Stream from 'node:stream';
6
+ import ora from 'ora';
7
+ import { default as swellConfig } from './lib/app-config.js';
8
+ import { ConfigType, FrontendProjectTypes, allConfigFilesInDir, appConfigFromFile, filePathExists, findAppConfig, getAppSlugId, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
9
+ import { default as localConfig } from './lib/config.js';
10
+ import { toAppId } from './lib/create/index.js';
11
+ import style from './lib/style.js';
12
+ import { RemoteAppCommand } from './remote-app-command.js';
13
+ const PUSH_CONCURRENCY = 3;
14
+ export class PushAppCommand extends RemoteAppCommand {
15
+ frontendPath = '';
16
+ logWatchChanges = true;
17
+ onWatchChange;
18
+ watchingChangeQueue = [];
19
+ watchingChangeQueueRunning = false;
20
+ watchingFiles = [];
21
+ watchListener = async (eventType, filename) => {
22
+ const configFile = filename;
23
+ const pathParsed = path.parse(configFile);
24
+ const fileDirs = pathParsed.dir.split(path.sep);
25
+ // only the first directory is relevant for us
26
+ const configDir = fileDirs[0];
27
+ // we only want to watch files in the root of the app directory
28
+ // and ignore build files etc
29
+ this.setWatchingFiles();
30
+ const isWatching = this.watchingFiles.includes(configFile);
31
+ // TODO: renaming a folder doesn't seem to work
32
+ // because isWatching = false for the new files
33
+ if (!configDir) {
34
+ return;
35
+ }
36
+ const configType = getConfigTypeFromPath(configDir);
37
+ if (!configType) {
38
+ return;
39
+ }
40
+ let watchFileEvent;
41
+ // per node docs:
42
+ //
43
+ // On most platforms, 'rename' is emitted whenever a filename appears
44
+ // or disappears in the directory.
45
+ //
46
+ // we want however to identify delete file events so we can send the
47
+ // correct API request
48
+ if (eventType === 'rename') {
49
+ watchFileEvent = 'new';
50
+ if (!filePathExists(path.join(this.appPath, configFile))) {
51
+ watchFileEvent = 'deleted';
52
+ }
53
+ else if (!isWatching) {
54
+ return;
55
+ }
56
+ }
57
+ else if (isWatching) {
58
+ watchFileEvent = 'change';
59
+ }
60
+ else {
61
+ return;
62
+ }
63
+ try {
64
+ await this.handleWatchFileChange(configFile, configType, watchFileEvent);
65
+ }
66
+ catch (error) {
67
+ // we don't want to break the watcher if an error is thrown while
68
+ // handling a file change
69
+ // for tests though, we want to break the watcher
70
+ if (process.env.NODE_ENV === 'test') {
71
+ // throwing does not break the test so we need to hard exit and log
72
+ // the error
73
+ console.error(style.error(error));
74
+ // eslint-disable-next-line no-process-exit, unicorn/no-process-exit
75
+ process.exit(1);
76
+ }
77
+ }
78
+ };
79
+ async buildFrontend(projectType) {
80
+ this.log(`Building ${projectType.name} frontend...\n`);
81
+ // TODO: check package.json for a "build" command and run that if it exists
82
+ await this.exec(projectType.buildCommand);
83
+ }
84
+ async chooseAppToPull(query) {
85
+ const typeLabelPlural = this.appType === 'theme' ? 'themes' : 'apps';
86
+ const typeLabelPrefixed = this.appType === 'theme' ? 'a theme' : 'an app';
87
+ const typeQuery = this.appType === 'theme' ? { type: 'theme' } : { type: 'storefront' };
88
+ const apps = await this.api.get({
89
+ adminPath: `/apps`,
90
+ }, { query: { limit: null, ...typeQuery, ...query } });
91
+ if (!apps.count) {
92
+ const currentStore = localConfig.getDefaultStore();
93
+ this.error(`There are no ${typeLabelPlural} associated with your store ${style.storeId(currentStore)}`);
94
+ }
95
+ const appSlugId = (await select({
96
+ choices: apps.results.map((app) => ({
97
+ name: `${style.appConfigValue(app.name)} (${getAppSlugId(app)})`,
98
+ value: app.private_id,
99
+ })),
100
+ message: `Choose ${typeLabelPrefixed} to pull`,
101
+ }));
102
+ const appId = apps.results.find((app) => app.private_id === appSlugId).id;
103
+ const app = await this.getApp(appId);
104
+ this.log();
105
+ return app;
106
+ }
107
+ async createAppStorefront(hasOtherStorefronts = true) {
108
+ let appId;
109
+ let themeAppId;
110
+ if (this.app.type === 'theme') {
111
+ themeAppId = this.app.id;
112
+ const storefrontApps = await this.handleRequestErrors(async () => this.api.get({
113
+ adminPath: `/apps?type=storefront&kind=shop`,
114
+ }));
115
+ const compatibleStorefrontApps = storefrontApps.results.filter((app) => app.installed &&
116
+ app.storefront?.theme?.provider === 'app' &&
117
+ this.app.theme?.storefront?.app === toAppId(app.private_id));
118
+ if (compatibleStorefrontApps.length === 0) {
119
+ this.error('No compatible storefront apps found for this theme. Install a new storefront app first.');
120
+ }
121
+ const targetApp = compatibleStorefrontApps.length === 1
122
+ ? compatibleStorefrontApps[0]
123
+ : await select({
124
+ choices: compatibleStorefrontApps.map((app) => ({
125
+ name: `${style.appConfigValue(app.name)} v${app.version}`,
126
+ value: app,
127
+ })),
128
+ message: 'Choose a storefront app',
129
+ });
130
+ appId = targetApp.id;
131
+ this.log(`\n${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
132
+ }
133
+ else if (this.app.type === 'storefront') {
134
+ appId = this.app.id;
135
+ const themeApps = await this.handleRequestErrors(async () => this.api.get({
136
+ adminPath: `/apps?type=theme`,
137
+ }));
138
+ const compatibleThemeApps = themeApps.results.filter((app) => app.installed &&
139
+ Object.keys(app.compatibilities || {}).includes(this.app.private_id));
140
+ if (compatibleThemeApps.length === 0) {
141
+ this.error('No compatible theme apps found for this storefront. Install a new theme app first.');
142
+ }
143
+ let targetApp;
144
+ if (compatibleThemeApps.length === 1) {
145
+ targetApp = compatibleThemeApps[0];
146
+ }
147
+ else {
148
+ targetApp = await select({
149
+ choices: compatibleThemeApps.map((app) => ({
150
+ name: `${style.appConfigValue(app.name)} v${app.version}`,
151
+ value: app,
152
+ })),
153
+ message: 'Choose a theme app',
154
+ });
155
+ this.log();
156
+ }
157
+ themeAppId = targetApp.id;
158
+ this.log(`${style.success('✔')} Using ${style.appConfigValue(targetApp.name)} v${targetApp.version}\n`);
159
+ }
160
+ else {
161
+ this.error('App type not supported for storefront creation');
162
+ }
163
+ let name;
164
+ if (hasOtherStorefronts) {
165
+ name = await input({
166
+ default: this.app.name,
167
+ message: 'Name your storefront',
168
+ });
169
+ if (name === this.app.name) {
170
+ name = undefined; // undefined allows the api to auto increment name
171
+ }
172
+ }
173
+ return this.handleRequestErrors(async () => this.api.post({
174
+ adminPath: `/storefronts`,
175
+ }, {
176
+ body: {
177
+ app_id: appId,
178
+ name,
179
+ theme_app_id: themeAppId,
180
+ },
181
+ onAsyncGetPath: (response) => ({
182
+ adminPath: `/storefronts/${response.id}`,
183
+ }),
184
+ }));
185
+ }
186
+ async deployAppFrontend(force, log) {
187
+ const currentStore = localConfig.getDefaultStore();
188
+ const projectType = this.getFrontendProjectType(false);
189
+ if (!projectType) {
190
+ return;
191
+ }
192
+ const deploymentHash = this.getFrontendDeploymentHash();
193
+ // Build and deploy only if there are files to push
194
+ if (deploymentHash &&
195
+ (force || deploymentHash !== this.app.frontend?.deployment_hash)) {
196
+ await this.buildFrontend(projectType);
197
+ const deploymentUrl = await this.deployFrontend(projectType);
198
+ await this.updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash);
199
+ }
200
+ if (log !== false) {
201
+ if (this.app.type === 'storefront' && this.storefront) {
202
+ this.logStorefrontFrontendUrl(this.storefront);
203
+ }
204
+ else if (this.app.type !== 'storefront') {
205
+ // Get updated app with `installed` property
206
+ this.app = await this.getApp(this.app.id);
207
+ this.logAppFrontendUrl();
208
+ }
209
+ }
210
+ }
211
+ async deployFrontend(projectType) {
212
+ // TODO: check package.json for a "deploy" command and run that if it exists
213
+ // Note it must return a URL somehow
214
+ try {
215
+ return this.wranglerDeployFrontend(projectType);
216
+ }
217
+ catch (error) {
218
+ this.error(`Error deploying frontend: ${error.message}`);
219
+ }
220
+ }
221
+ async ensureAppExists(file, shouldCreate = true) {
222
+ let currentStore = localConfig.getDefaultStore();
223
+ this.api.setStoreEnv(currentStore, 'test');
224
+ const loggedIn = await this.ensureLoggedIn();
225
+ if (loggedIn) {
226
+ currentStore = localConfig.getDefaultStore();
227
+ this.api.setStoreEnv(currentStore, 'test');
228
+ }
229
+ const app = await this.getAppWithConfig('').catch((_error) =>
230
+ // Ignore so we can continue to create it
231
+ null);
232
+ const isTheme = app?.type === 'theme';
233
+ if (app && !isTheme && this.appType === 'theme') {
234
+ this.error(`App ${style.appConfigValue(app?.name)} is not a theme.`);
235
+ }
236
+ const confirmExistingApp = await this.confirmRemoveInstalledApp(app, currentStore);
237
+ if (confirmExistingApp === false) {
238
+ return false;
239
+ }
240
+ // Do not create or update if app already exists and not forcing it
241
+ if (!shouldCreate && app) {
242
+ this.app = app;
243
+ return true;
244
+ }
245
+ // If app exists but owned by another client, create a new instance
246
+ if (app?.client_id && app.client_id !== currentStore) {
247
+ // Create a new app
248
+ this.app = await this.getCreateUpdateApp();
249
+ if (!this.app) {
250
+ return false;
251
+ }
252
+ }
253
+ else if (!file) {
254
+ // Update existing app if not targeting a file
255
+ this.app = await this.getCreateUpdateApp(app);
256
+ }
257
+ return true;
258
+ }
259
+ async ensureLoggedIn() {
260
+ try {
261
+ const user = await this.api.get({ adminPath: 'user' });
262
+ if (!user) {
263
+ throw new Error('Failed to log in');
264
+ }
265
+ }
266
+ catch {
267
+ this.log('Logging in...\n');
268
+ await this.config.runCommand('login');
269
+ this.log();
270
+ return true;
271
+ }
272
+ }
273
+ async exec(command, onOutput) {
274
+ const $$ = $({
275
+ cwd: this.frontendPath,
276
+ shell: true,
277
+ stderr: onOutput ? 'pipe' : 'inherit',
278
+ stdin: 'inherit',
279
+ stdout: onOutput ? 'pipe' : 'inherit',
280
+ });
281
+ if (onOutput) {
282
+ const outStream = new Stream.Writable();
283
+ outStream._write = (chunk, _encoding, next) => {
284
+ const string = chunk.toString();
285
+ const out = onOutput(string);
286
+ if (out !== false) {
287
+ console.log(string);
288
+ }
289
+ next();
290
+ };
291
+ await $$ `${command}`.pipeStdout?.(outStream).pipeStderr?.(outStream);
292
+ }
293
+ else {
294
+ await $$ `${command}`;
295
+ }
296
+ }
297
+ async getAllAppStorefronts(params) {
298
+ const query = Object.entries(params || {})
299
+ .map(([key, value]) => `${key}=${value}`)
300
+ .join('&');
301
+ return this.handleRequestErrors(async () => this.api.get({
302
+ adminPath: `/apps/${this.app.id}/storefronts?${query}`,
303
+ }));
304
+ }
305
+ async getAllStorefronts(params) {
306
+ const query = Object.entries(params || {})
307
+ .map(([key, value]) => `${key}=${value}`)
308
+ .join('&');
309
+ return this.handleRequestErrors(async () => this.api.get({
310
+ adminPath: `/storefronts?${query}`,
311
+ }));
312
+ }
313
+ async getAppStorefront(params) {
314
+ const query = Object.entries(params || {})
315
+ .map(([key, value]) => `${key}=${value}`)
316
+ .join('&');
317
+ try {
318
+ return this.handleRequestErrors(async () => this.api.get({
319
+ adminPath: `/apps/${this.app.id}/storefront?${query}`,
320
+ }));
321
+ }
322
+ catch {
323
+ // noop
324
+ }
325
+ }
326
+ async getAppToPull(args, shouldChoose = true) {
327
+ const { appId, targetPath } = args;
328
+ const currentStore = localConfig.getDefaultStore();
329
+ this.api.setStoreEnv(currentStore, 'test');
330
+ this.resolveAppPath(appId, targetPath);
331
+ if (isPathDirectory(this.appPath)) {
332
+ this.swellConfig = swellConfig(this.appPath);
333
+ const localAppId = this.swellConfig.get('id');
334
+ const localAppType = this.swellConfig.get('type');
335
+ if (localAppId) {
336
+ if (appId && appId !== localAppId) {
337
+ this.error(`${localAppType === 'theme' ? 'A theme' : 'An app'} already exists in this path with a different ID: ${style.appConfigValue(localAppId)}`);
338
+ }
339
+ return this.getExistingApp(localAppId);
340
+ }
341
+ }
342
+ const app = appId
343
+ ? await this.getExistingApp(appId)
344
+ : shouldChoose
345
+ ? await this.chooseAppToPull()
346
+ : undefined;
347
+ if (shouldChoose) {
348
+ this.swellConfig = swellConfig(this.appPath);
349
+ }
350
+ return app;
351
+ }
352
+ async getExistingApp(appId) {
353
+ try {
354
+ return this.getApp(appId);
355
+ }
356
+ catch (error) {
357
+ if (error.status === 404) {
358
+ const labelPrefix = this.appType === 'theme' ? 'Theme' : 'App';
359
+ this.error(`${labelPrefix} '${appId}' not found.`);
360
+ }
361
+ else {
362
+ throw error;
363
+ }
364
+ }
365
+ }
366
+ getFrontendDeploymentHash() {
367
+ const localConfigs = this.getLocalAppConfigs();
368
+ let frontendHashes = localConfigs
369
+ .filter((config) => config.filePath?.startsWith(`frontend${path.sep}`))
370
+ .map((config) => config.hash)
371
+ .sort()
372
+ .join('|');
373
+ if (frontendHashes.length > 0) {
374
+ const packageHash = localConfigs.find((config) => config.filePath === 'package.json')?.hash;
375
+ frontendHashes += `|${packageHash}`;
376
+ }
377
+ return frontendHashes?.length > 0 ? hashString(frontendHashes) : undefined;
378
+ }
379
+ getFrontendProjectType(required = true) {
380
+ this.frontendPath = path.join(this.appPath, 'frontend');
381
+ if (!required) {
382
+ const frontendExists = filePathExists(this.frontendPath);
383
+ if (!frontendExists) {
384
+ return null;
385
+ }
386
+ }
387
+ const projectType = getFrontendProjectType(this.appPath);
388
+ if (!projectType) {
389
+ this.error(`No valid frontend app found in ${this.appPath}/${this.frontendPath}. Supported frameworks include: ${FrontendProjectTypes.map((type) => type.slug).join(', ')}.`);
390
+ }
391
+ return projectType;
392
+ }
393
+ async getStorefrontToPull(args = {}, flags = {}) {
394
+ const { appId, targetPath } = args;
395
+ const targetApp = await this.getAppToPull(args, false);
396
+ const targetStorefront = flags['storefront-id'];
397
+ this.resolveAppPath(appId, targetPath);
398
+ // Only use default if path exists
399
+ let currentStorefront;
400
+ if (isPathDirectory(this.appPath)) {
401
+ currentStorefront = localConfig.getDefaultStorefront(this.appPath);
402
+ }
403
+ let storefrontId = targetStorefront || targetApp ? currentStorefront : undefined;
404
+ if (!storefrontId || flags['storefront-select']) {
405
+ const storefronts = await this.getAllStorefronts(targetApp?.id
406
+ ? this.appType === 'theme'
407
+ ? { theme_app_id: targetApp?.id }
408
+ : { app_id: targetApp?.id }
409
+ : { type: this.appType });
410
+ if (storefronts.count > 0) {
411
+ storefrontId = await select({
412
+ choices: storefronts.results.map((storefront) => ({
413
+ name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
414
+ value: storefront.id,
415
+ })),
416
+ message: 'Choose a storefront',
417
+ });
418
+ this.storefront = storefronts.results.find((storefront) => storefront.id === storefrontId);
419
+ const appId = this.appType === 'theme'
420
+ ? this.storefront.theme_app_id
421
+ : this.storefront.app_id;
422
+ this.resolveAppPath(this.storefront.name, targetPath);
423
+ this.app = await this.getApp(appId);
424
+ }
425
+ else {
426
+ // Theme apps should always have a storefront, so this shouldn't be necessary
427
+ // However it's a good fallback in case the storefront was deleted
428
+ // Choosing a theme app will proceed to pull app files
429
+ this.app = await this.chooseAppToPull();
430
+ this.resolveAppPath(this.app.name, targetPath);
431
+ }
432
+ }
433
+ else {
434
+ this.app = targetApp;
435
+ }
436
+ if (storefrontId && !this.storefront) {
437
+ this.storefront = await this.getAppStorefront({
438
+ storefront_id: storefrontId,
439
+ type: 'theme',
440
+ });
441
+ if (!this.storefront) {
442
+ this.error(`Storefront ${style.appConfigValue(storefrontId)} not found.`);
443
+ }
444
+ const appId = this.appType === 'theme'
445
+ ? this.storefront.theme_app_id
446
+ : this.storefront.app_id;
447
+ this.app = await this.getApp(appId);
448
+ }
449
+ this.swellConfig = swellConfig(this.appPath);
450
+ return this.storefront;
451
+ }
452
+ async getStorefrontToPush(flags = {}) {
453
+ const targetStorefront = flags['storefront-id'];
454
+ const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
455
+ let storefrontId = targetStorefront || currentStorefront;
456
+ if (!storefrontId || flags['storefront-select']) {
457
+ const storefronts = await this.getAllAppStorefronts({
458
+ type: this.appType,
459
+ });
460
+ if (storefronts.count > 0) {
461
+ storefrontId = await select({
462
+ choices: [
463
+ ...storefronts.results.map((storefront) => ({
464
+ name: `${style.appConfigValue(storefront.name)} (${storefront.id}) ${storefront.primary ? '[primary]' : ''}`,
465
+ value: storefront.id,
466
+ })),
467
+ {
468
+ name: 'Create new storefront',
469
+ value: null,
470
+ },
471
+ ],
472
+ message: 'Choose a storefront',
473
+ });
474
+ }
475
+ if (!storefrontId) {
476
+ this.storefront = await this.createAppStorefront(storefronts.count > 0);
477
+ }
478
+ }
479
+ if (storefrontId) {
480
+ this.storefront = await this.getAppStorefront({
481
+ storefront_id: storefrontId,
482
+ type: this.appType,
483
+ });
484
+ if (!this.storefront) {
485
+ if (targetStorefront) {
486
+ this.error(`Storefront ${style.appConfigValue(storefrontId)} not found.`);
487
+ }
488
+ this.log(`Storefront ${style.appConfigValue(storefrontId)} not found.\n`);
489
+ localConfig.setDefaultStorefront(this.appPath, '');
490
+ return this.getStorefrontToPush();
491
+ }
492
+ }
493
+ this.app = await this.getApp(this.app.id);
494
+ return this.storefront;
495
+ }
496
+ async handleWatchFileChange(configFile, configType, event) {
497
+ let config;
498
+ if (event === 'deleted') {
499
+ config = findAppConfig(this.app, configFile, configType);
500
+ if (!config) {
501
+ return;
502
+ }
503
+ this.watchingChangeQueue.push({
504
+ action: 'remove',
505
+ appConfig: config,
506
+ configFile,
507
+ });
508
+ }
509
+ else {
510
+ config = appConfigFromFile(configFile, configType, this.appPath);
511
+ this.watchingChangeQueue.push({
512
+ action: 'push',
513
+ appConfig: config,
514
+ configFile,
515
+ });
516
+ }
517
+ await this.runWatchChangeQueue();
518
+ }
519
+ logAppFrontendUrl() {
520
+ const currentStore = localConfig.getDefaultStore();
521
+ // Shouldn't happen, but just in case
522
+ if (!this.app.installed?.id) {
523
+ return;
524
+ }
525
+ this.log(`View your app at ${style.link(this.appFrontendUrl(currentStore, this.app.installed.id))}.`);
526
+ }
527
+ logDashboardUrl(flags) {
528
+ const currentStore = localConfig.getDefaultStore();
529
+ this.log(`View the ${style.appConfigValue(this.app.name)} app in your dashboard at ${style.link(this.dashboardUrl(flags, currentStore))}.`);
530
+ }
531
+ logStorefrontConnected() {
532
+ if (!this.storefront) {
533
+ this.error('Storefront not connected');
534
+ }
535
+ const { name, theme_app } = this.storefront;
536
+ const themeLog = theme_app && theme_app.name !== name
537
+ ? ` [theme] ${style.appName(theme_app.name)} v${theme_app.version}`
538
+ : '';
539
+ this.log(`[storefront] ${style.appName(name)}${themeLog}\n`);
540
+ }
541
+ logStorefrontFrontendUrl(storefront, branchId) {
542
+ const currentStore = localConfig.getDefaultStore();
543
+ this.log(`View your storefront at ${style.link(this.storefrontFrontendUrl(currentStore, storefront, branchId))}.`);
544
+ }
545
+ async pushFile(relativePath) {
546
+ const basePath = relativePath.split(path.sep)[0];
547
+ const configType = getConfigTypeFromPath(basePath) || ConfigType.FILE;
548
+ const config = appConfigFromFile(relativePath, configType, this.appPath);
549
+ await this.pushRemoteFile(config);
550
+ }
551
+ async pushFilePath(relativePath, force) {
552
+ const topDir = relativePath.split(path.sep)[0];
553
+ const configType = (getConfigTypeFromPath(topDir) || 'file');
554
+ const configTypeKey = getConfigTypeKeyFromValue(configType) || 'file';
555
+ const exAppConfigs = (this.app?.configs || []).filter((config) => config.filePath?.startsWith(`${relativePath}/`));
556
+ const allFiles = [];
557
+ const pushFiles = [];
558
+ for (const { configFile } of allConfigFilesInDir(this.appPath, relativePath, configTypeKey)) {
559
+ const config = appConfigFromFile(configFile, configType, this.appPath);
560
+ allFiles.push(config);
561
+ const exConfig = exAppConfigs.find((exConfig) => config.filePath === exConfig.filePath);
562
+ if (!force && exConfig && exConfig.hash === config.hash) {
563
+ continue;
564
+ }
565
+ pushFiles.push(config);
566
+ }
567
+ const filesToRemove = exAppConfigs.filter((config) => !allFiles.some((appConfig) => appConfig.filePath === config.filePath));
568
+ const hasChanges = pushFiles.length > 0 || filesToRemove.length > 0;
569
+ if (pushFiles.length > 0) {
570
+ const pushConcurrency = configTypeKey === 'ASSET' ? 1 : PUSH_CONCURRENCY;
571
+ while (pushFiles.length > 0) {
572
+ // eslint-disable-next-line no-await-in-loop
573
+ await Promise.all(pushFiles
574
+ .splice(0, pushConcurrency)
575
+ .map(async (appConfig) => {
576
+ await this.pushRemoteFile(appConfig);
577
+ }));
578
+ }
579
+ }
580
+ // Remove any deleted files
581
+ if (filesToRemove.length > 0) {
582
+ while (filesToRemove.length > 0) {
583
+ // eslint-disable-next-line no-await-in-loop
584
+ await Promise.all(filesToRemove.splice(0, PUSH_CONCURRENCY).map(async (appConfig) => {
585
+ await this.removeRemoteFile(appConfig);
586
+ }));
587
+ }
588
+ }
589
+ if (!hasChanges && !force) {
590
+ this.log(`${style.success('✔')} All ${style.path(relativePath)} files are up to date.`);
591
+ }
592
+ else {
593
+ this.log();
594
+ this.log(`${style.success('✔')} Pushed ${style.path(relativePath)} to ${style.appConfigValue(this.app.name)}.`);
595
+ }
596
+ }
597
+ resolveAppPath(appId, targetPath) {
598
+ const resolvedAppId = appId || '';
599
+ let resolvedPath = targetPath || resolvedAppId;
600
+ // If app_id matches directory, use it as target path
601
+ if (!targetPath &&
602
+ isPathDirectory(path.resolve(process.cwd(), resolvedAppId))) {
603
+ resolvedPath = resolvedAppId;
604
+ }
605
+ // Reset app config and path if appId is provided
606
+ if (resolvedPath && this.appPath === '.') {
607
+ this.appPath = path.resolve(process.cwd(), resolvedPath);
608
+ }
609
+ return this.appPath;
610
+ }
611
+ async runWatchChangeQueue() {
612
+ if (this.watchingChangeQueueRunning) {
613
+ return;
614
+ }
615
+ while (this.watchingChangeQueue.length > 0) {
616
+ this.watchingChangeQueueRunning = true;
617
+ // eslint-disable-next-line no-await-in-loop
618
+ await Promise.all(this.watchingChangeQueue
619
+ .splice(0, PUSH_CONCURRENCY)
620
+ .map(async ({ action, appConfig }) => {
621
+ const result = await (action === 'remove'
622
+ ? this.removeRemoteFile(appConfig, this.logWatchChanges)
623
+ : this.pushRemoteFile(appConfig, this.logWatchChanges));
624
+ this.onWatchChange?.(appConfig, result);
625
+ }));
626
+ }
627
+ this.watchingChangeQueueRunning = false;
628
+ // refetch app to get updated configs
629
+ this.app = await this.getAppWithConfig(this.app.id);
630
+ }
631
+ saveCurrentStorefront() {
632
+ const currentStorefront = localConfig.getDefaultStorefront(this.appPath);
633
+ if (this.storefront && this.storefront.id !== currentStorefront) {
634
+ localConfig.setDefaultStorefront(this.appPath, this.storefront.id);
635
+ }
636
+ }
637
+ setWatchingFiles() {
638
+ this.watchingFiles = globAllFilesByPath(this.appPath);
639
+ }
640
+ async updateFrontendDeployment(currentStore, projectType, deploymentUrl, deploymentHash) {
641
+ const spinner = ora();
642
+ spinner.start(`Updating frontend deployment...`);
643
+ await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${this.app.id}` }, {
644
+ body: {
645
+ $client_id: currentStore,
646
+ $environment_id: 'test',
647
+ frontend: {
648
+ deployment_hash: deploymentHash,
649
+ framework: projectType.slug,
650
+ service: 'cloudflare',
651
+ url: deploymentUrl,
652
+ },
653
+ },
654
+ }), () => spinner.fail('Error updating app deployment'));
655
+ spinner.succeed('Updated frontend deployment.\n');
656
+ }
657
+ async watchForChanges({ logChanges, onChange, syncAll, } = {}) {
658
+ this.logWatchChanges = logChanges ?? true;
659
+ this.onWatchChange = onChange;
660
+ this.setWatchingFiles();
661
+ if (syncAll) {
662
+ await this.pushAppConfigs();
663
+ fs.watch(this.appPath, { recursive: true }, async (_eventType, _filename) => {
664
+ await this.pushAppConfigs();
665
+ this.setWatchingFiles();
666
+ });
667
+ return;
668
+ }
669
+ fs.watch(this.appPath, { recursive: true }, this.watchListener);
670
+ }
671
+ async wranglerDeployFrontend(projectType) {
672
+ let deploymentUrl;
673
+ let interactiveError = false;
674
+ this.log(`\nDeploying to Cloudflare...\n`);
675
+ try {
676
+ await this.exec(`npx wrangler pages deploy ${this.appPath}/frontend/${projectType.deployPath}`, (string) => {
677
+ // Dependent on wrangler output
678
+ // Parse the deployment URL from the wrangler output.
679
+ const match = string.match(/Take a peek over at (http\S+)/);
680
+ if (match && match.length > 0) {
681
+ deploymentUrl = match[1];
682
+ return false;
683
+ }
684
+ if (interactiveError ||
685
+ string.includes('non-interactive mode') ||
686
+ string.includes('non-interactive environment')) {
687
+ interactiveError = true;
688
+ return false;
689
+ }
690
+ });
691
+ }
692
+ catch (error) {
693
+ // noop
694
+ if (interactiveError) {
695
+ this.log(style.funcWarn(`Your Cloudflare environment must be initialized by logging in with \`wrangler login\`, and exporting the \`CLOUDFLARE_ACCOUNT_ID\` environment variable. Refer to https://developers.cloudflare.com/workers/wrangler/configuration/ for details, and re-run this command to connect the deployment with your app.`));
696
+ // eslint-disable-next-line no-process-exit, unicorn/no-process-exit
697
+ process.exit(1);
698
+ }
699
+ else {
700
+ throw error;
701
+ }
702
+ }
703
+ if (!deploymentUrl) {
704
+ this.error('Unable to retrieve deployment URL.');
705
+ }
706
+ return deploymentUrl;
707
+ }
708
+ async confirmRemoveInstalledApp(app, currentStore) {
709
+ if (!app?.private_id) {
710
+ return true;
711
+ }
712
+ const existingInstalledApp = await this.api.get({
713
+ adminPath: `/client/apps/${app.private_id}`,
714
+ }, { query: { versioned: true } });
715
+ if (existingInstalledApp) {
716
+ const continuePush = await confirm({
717
+ default: false,
718
+ message: `${style.storeId(currentStore)} has an installed version of ${app.name ? style.appConfigValue(app.name) : 'this app'} v${existingInstalledApp.version}. Do you want to replace it with a development instance?`,
719
+ });
720
+ if (!continuePush) {
721
+ return false;
722
+ }
723
+ // uninstall the existing app
724
+ const spinner = ora();
725
+ spinner.start('Removing installed app...');
726
+ try {
727
+ await this.api.put({
728
+ adminPath: `/client/apps/${app.id}`,
729
+ }, {
730
+ body: { uninstalled: true },
731
+ });
732
+ spinner.succeed('Installed app removed.');
733
+ this.log();
734
+ }
735
+ catch (error) {
736
+ spinner.fail(`Error removing installed app from ${style.storeId(currentStore)}.`);
737
+ this.logError(error.message);
738
+ return false;
739
+ }
740
+ }
741
+ return existingInstalledApp;
742
+ }
743
+ }