@swell/cli 2.0.1-alpha.13 → 2.0.1-alpha.17

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