@swell/cli 2.9.31 → 2.9.33

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 (57) hide show
  1. package/dist/commands/app/action.d.ts +25 -0
  2. package/dist/commands/app/action.js +233 -0
  3. package/dist/commands/app/dev.js +2 -1
  4. package/dist/commands/app/frontend/deploy.d.ts +1 -0
  5. package/dist/commands/app/frontend/deploy.js +4 -0
  6. package/dist/commands/app/frontend/dev.js +4 -2
  7. package/dist/commands/app/push.d.ts +3 -0
  8. package/dist/commands/app/push.js +100 -4
  9. package/dist/commands/app/version.js +3 -0
  10. package/dist/commands/create/function.js +14 -4
  11. package/dist/commands/create/model.js +9 -24
  12. package/dist/commands/create/webhook.js +7 -13
  13. package/dist/commands/inspect/components.d.ts +31 -0
  14. package/dist/commands/inspect/components.js +110 -0
  15. package/dist/commands/inspect/extensions.js +18 -9
  16. package/dist/commands/inspect/frontend.d.ts +37 -0
  17. package/dist/commands/inspect/frontend.js +75 -0
  18. package/dist/commands/inspect/functions.d.ts +11 -1
  19. package/dist/commands/inspect/functions.js +89 -6
  20. package/dist/commands/inspect/index.js +2 -0
  21. package/dist/commands/schema.js +5 -0
  22. package/dist/help/custom-help.js +5 -1
  23. package/dist/inspect-resource-command.d.ts +1 -1
  24. package/dist/inspect-resource-command.js +8 -2
  25. package/dist/lib/api-errors.d.ts +1 -0
  26. package/dist/lib/api-errors.js +28 -0
  27. package/dist/lib/apps/actions.d.ts +45 -0
  28. package/dist/lib/apps/actions.js +114 -0
  29. package/dist/lib/apps/components.d.ts +7 -0
  30. package/dist/lib/apps/components.js +10 -0
  31. package/dist/lib/apps/extension-manifest.d.ts +1 -0
  32. package/dist/lib/apps/extension-manifest.js +43 -0
  33. package/dist/lib/apps/index.d.ts +1 -0
  34. package/dist/lib/apps/index.js +30 -0
  35. package/dist/lib/apps/listing.d.ts +1 -0
  36. package/dist/lib/apps/listing.js +41 -0
  37. package/dist/lib/apps/paths.d.ts +8 -0
  38. package/dist/lib/apps/paths.js +34 -4
  39. package/dist/lib/bundle.js +26 -41
  40. package/dist/lib/create/function.d.ts +1 -1
  41. package/dist/lib/create/function.js +9 -5
  42. package/dist/lib/create/index.js +2 -1
  43. package/dist/lib/create/model.d.ts +7 -3
  44. package/dist/lib/create/model.js +67 -9
  45. package/dist/lib/function-declarations.d.ts +8 -0
  46. package/dist/lib/function-declarations.js +27 -0
  47. package/dist/lib/inspect/extensions.d.ts +4 -3
  48. package/dist/lib/inspect/extensions.js +36 -5
  49. package/dist/lib/swell-function-wrapper.js +13 -5
  50. package/dist/lib/swell-workflow-request.d.ts +34 -0
  51. package/dist/lib/swell-workflow-request.js +58 -0
  52. package/dist/push-app-command.d.ts +2 -1
  53. package/dist/push-app-command.js +43 -22
  54. package/dist/remote-app-command.d.ts +2 -2
  55. package/dist/remote-app-command.js +79 -51
  56. package/oclif.manifest.json +263 -10
  57. package/package.json +2 -1
@@ -24,7 +24,7 @@ export interface ManifestEntry {
24
24
  }
25
25
  /** CLI-resolved type. Diverges from raw manifest `type` for payment (card vs alt). */
26
26
  export type ExtensionType = 'card' | 'alt' | 'shipping' | 'tax';
27
- export type ExtensionStatus = 'not deployed' | 'not activated' | 'app id mismatch' | 'id mismatch' | 'not selected' | 'gateway missing' | 'not enabled' | 'no handler' | 'handler mismatch' | 'activated';
27
+ export type ExtensionStatus = 'not deployed' | 'settings unavailable' | 'not activated' | 'app id mismatch' | 'id mismatch' | 'not selected' | 'gateway missing' | 'not enabled' | 'no handler' | 'handler mismatch' | 'activated';
28
28
  export type ActionOwner = 'dev' | 'merchant' | null;
29
29
  export interface FunctionRecord {
30
30
  id?: string;
@@ -163,8 +163,6 @@ export declare function formatExtensionKey(slug: string, extId: string): string;
163
163
  * resolves to `card` even with no explicit `method`.
164
164
  */
165
165
  export declare function resolveExtensionType(entry: Pick<ManifestEntry, 'type' | 'method' | 'id'>): ExtensionType;
166
- /** Apply the platform's `if(method, method, id)` formula. */
167
- export declare function paymentMethodIdFor(entry: Pick<ManifestEntry, 'method' | 'id'>): string;
168
166
  /** Shipping records use `app_<appId>_<extId>` as both the row id and the carrier id. */
169
167
  export declare function shippingBindId(appId: string, extId: string): string;
170
168
  /** Card-gateway records use the same id form as shipping rows. */
@@ -229,12 +227,15 @@ export interface BuildDetailInput {
229
227
  payments?: PaymentSettings | null;
230
228
  shipments?: ShippingSettings | null;
231
229
  taxes?: TaxSettingsRecord | null;
230
+ /** Why each settings record could not be read, if it could not. */
231
+ unavailableSettings?: UnavailableSettings;
232
232
  /** Functions in the same app whose top-level `extension` matches this extId. */
233
233
  functions: FunctionRecord[];
234
234
  /** Components in the same app whose `values.extension` matches this extId. */
235
235
  components: ComponentRecord[];
236
236
  localDiff?: LocalDiff | null;
237
237
  }
238
+ export type UnavailableSettings = Partial<Record<'payments' | 'shipments' | 'taxes', string>>;
238
239
  export interface BuildOrphanInput {
239
240
  appId: string;
240
241
  appSlug: string;
@@ -42,10 +42,6 @@ export function resolveExtensionType(entry) {
42
42
  const method = entry.method ?? entry.id;
43
43
  return method === 'card' ? 'card' : 'alt';
44
44
  }
45
- /** Apply the platform's `if(method, method, id)` formula. */
46
- export function paymentMethodIdFor(entry) {
47
- return entry.method ?? entry.id;
48
- }
49
45
  /** Shipping records use `app_<appId>_<extId>` as both the row id and the carrier id. */
50
46
  export function shippingBindId(appId, extId) {
51
47
  return `app_${appId}_${extId}`;
@@ -200,7 +196,9 @@ function findShippingCarrier(shipments, carrierId) {
200
196
  * gate on `enabled`) is intentional.
201
197
  */
202
198
  function deriveAltDispatch(ctx) {
203
- const methodId = paymentMethodIdFor(ctx.manifest);
199
+ // The dashboard keys an extension's method by the extension's id, whatever
200
+ // its `method`, and checkout dispatches on that id
201
+ const methodId = ctx.extId;
204
202
  const expectedGatewayId = gatewayBindId(ctx.appId, ctx.extId);
205
203
  const method = findPaymentMethod(ctx.payments, methodId);
206
204
  const gateway = findPaymentGateway(ctx.payments, expectedGatewayId);
@@ -254,8 +252,11 @@ function deriveCardDispatch(ctx) {
254
252
  const expectedGatewayId = gatewayBindId(ctx.appId, ctx.extId);
255
253
  const cardMethod = findPaymentMethod(ctx.payments, 'card');
256
254
  const gateway = findPaymentGateway(ctx.payments, expectedGatewayId);
255
+ // Charges and refunds go to the app and extension the card method names
257
256
  const methodChecks = {
258
257
  gateway: cardMethod?.gateway === expectedGatewayId,
258
+ extension_app_id: cardMethod?.extension_app_id === ctx.appId,
259
+ extension_config_id: cardMethod?.extension_config_id === ctx.extId,
259
260
  };
260
261
  const gatewayChecks = {
261
262
  extension_app_id: gateway?.extension_app_id === ctx.appId,
@@ -285,6 +286,12 @@ function deriveCardDispatch(ctx) {
285
286
  if (cardMethod?.gateway !== expectedGatewayId) {
286
287
  return { status: 'not activated', native_bindings };
287
288
  }
289
+ if (cardMethod.extension_app_id !== ctx.appId) {
290
+ return { status: 'app id mismatch', native_bindings };
291
+ }
292
+ if (cardMethod.extension_config_id !== ctx.extId) {
293
+ return { status: 'id mismatch', native_bindings };
294
+ }
288
295
  return { status: 'activated', native_bindings };
289
296
  }
290
297
  /**
@@ -401,6 +408,26 @@ export function buildExtensionDetail(input) {
401
408
  throw new Error('buildExtensionDetail: missing manifest for a deployed extension');
402
409
  }
403
410
  const type = resolveExtensionType(input.manifest);
411
+ // Without the settings the activation cannot be told
412
+ const settingsRecord = type === 'shipping' ? 'shipments' : type === 'tax' ? 'taxes' : 'payments';
413
+ const unavailable = input.unavailableSettings?.[settingsRecord];
414
+ if (unavailable) {
415
+ return {
416
+ status: 'settings unavailable',
417
+ action_owner: 'dev',
418
+ action: `Could not read /settings/${settingsRecord}: ${unavailable}`,
419
+ type,
420
+ manifest: input.manifest,
421
+ native_bindings: [],
422
+ bound: {
423
+ functions: input.functions.map((f) => liftBoundFunction(f)),
424
+ components: input.components.map((c) => liftBoundComponent(c)),
425
+ },
426
+ required_events: requiredEventsFor(type),
427
+ missing_required_events: missingRequiredEvents(type, input.functions),
428
+ local_diff: input.localDiff ?? null,
429
+ };
430
+ }
404
431
  const dispatch = deriveDispatch(type, {
405
432
  appId: input.appId,
406
433
  extId: input.extId,
@@ -487,6 +514,10 @@ function describeAction(status, ctx) {
487
514
  case 'not deployed': {
488
515
  return { action: 'swell app push', action_owner: 'dev' };
489
516
  }
517
+ // buildExtensionDetail names the settings it could not read
518
+ case 'settings unavailable': {
519
+ return { action: null, action_owner: 'dev' };
520
+ }
490
521
  case 'not activated': {
491
522
  return {
492
523
  action: `Open Settings → ${section} → ${label} → Save`,
@@ -30,7 +30,13 @@ async function request(originalRequest, _env, context) {
30
30
  ...(error.body ?? { error: error.message }),
31
31
  retry: error.retry === false ? false : undefined,
32
32
  };
33
- response = new SwellResponse(errorBody, { status: error.status || 500 });
33
+ const retryAfter = Number(error.retryAfter);
34
+ response = new SwellResponse(errorBody, {
35
+ status: error.status || 500,
36
+ ...(retryAfter > 0
37
+ ? { headers: { 'Retry-After': String(Math.ceil(retryAfter)) } }
38
+ : {}),
39
+ });
34
40
  }
35
41
  return SwellResponse._respond(req, response, context);
36
42
  }
@@ -118,9 +124,9 @@ class SwellRequest {
118
124
  this.swell = new SwellAPI(this, context);
119
125
  // URL of the original request
120
126
  this.url;
121
- // Request body text. On a route called through the storefront gateway with
122
- // a JSON body, this is the exact text the caller sent, so verify HMAC/webhook
123
- // signatures against it. Other bodies arrive re-serialized as JSON.
127
+ // Request body text. On a route called through the storefront gateway, this
128
+ // is the exact body the caller sent, so verify HMAC/webhook signatures
129
+ // against it. Form-encoded bodies arrive re-serialized as JSON.
124
130
  this.rawBody = '';
125
131
  // Parsed JSON body as object, or raw text string when body isn't JSON.
126
132
  this.body = {};
@@ -447,6 +453,7 @@ class SwellError extends Error {
447
453
  this.body = body;
448
454
  this.code = options.code || body?.error?.code;
449
455
  this.retry = options.retry;
456
+ this.retryAfter = options.retryAfter;
450
457
  }
451
458
  get isRetryable() {
452
459
  return RETRYABLE_CODES.has(this.code);
@@ -501,7 +508,8 @@ class SwellResponse extends Response {
501
508
  }
502
509
  static async _respond(req, response, context) {
503
510
  let finalResponse = response;
504
- const isHook = Boolean(req.data?.$event?.hook);
511
+ // Only the platform marks a hook's call: a route's data comes from its caller
512
+ const isHook = Boolean(req.headers.get('Swell-Event-Hook'));
505
513
  if (finalResponse instanceof Response &&
506
514
  !(finalResponse instanceof SwellResponse)) {
507
515
  // Non-hook responses pass through unchanged so the handler's body,
@@ -0,0 +1,34 @@
1
+ export function runtimeEnvironmentId(runtime: any): any;
2
+ export function buildWorkflowRequest(invocation: any, proxy: any): {
3
+ id: any;
4
+ appId: any;
5
+ environmentId: any;
6
+ store: {
7
+ id: any;
8
+ };
9
+ app: {
10
+ admin_url: any;
11
+ frontend_url: any;
12
+ };
13
+ data: any;
14
+ workflow: {
15
+ workflow_id: any;
16
+ workflow_name: any;
17
+ workflow_instance_id: any;
18
+ trigger: any;
19
+ request_id: any;
20
+ };
21
+ isLocalDev: boolean;
22
+ swell: {
23
+ get: (path: any, data: any) => any;
24
+ post: (path: any, data: any) => any;
25
+ put: (path: any, data: any) => any;
26
+ delete: (path: any, data: any) => any;
27
+ settings: () => any;
28
+ };
29
+ appValues(idOrValues: any, values: any): {
30
+ $app: {
31
+ [x: number]: any;
32
+ };
33
+ };
34
+ };
@@ -0,0 +1,58 @@
1
+ // The request a workflow's run(req, step) gets, built from the invocation the
2
+ // platform starts the run with. Every workflow bundle includes this module.
3
+ // 'test' in the test environment, null in live
4
+ export function runtimeEnvironmentId(runtime) {
5
+ return runtime.environment_id || null;
6
+ }
7
+ export function buildWorkflowRequest(invocation, proxy) {
8
+ const { data, runtime } = invocation;
9
+ const appId = runtime.app_slug || runtime.app_id;
10
+ return {
11
+ id: runtime.request_id,
12
+ appId,
13
+ environmentId: runtimeEnvironmentId(runtime),
14
+ store: {
15
+ id: runtime.store_id,
16
+ },
17
+ app: {
18
+ admin_url: runtime.app_admin_url,
19
+ frontend_url: runtime.app_frontend_url,
20
+ },
21
+ data,
22
+ workflow: {
23
+ workflow_id: runtime.workflow_id,
24
+ workflow_name: runtime.workflow_name,
25
+ workflow_instance_id: runtime.workflow_instance_id,
26
+ trigger: runtime.trigger,
27
+ request_id: runtime.request_id,
28
+ },
29
+ isLocalDev: false,
30
+ swell: {
31
+ get: (path, data) => proxy.api('get', path, data),
32
+ post: (path, data) => proxy.api('post', path, data),
33
+ put: (path, data) => proxy.api('put', path, data),
34
+ delete: (path, data) => proxy.api('delete', path, data),
35
+ settings: () => proxy.settings(),
36
+ },
37
+ appValues(idOrValues, values) {
38
+ const targetAppId = typeof idOrValues === 'string' ? idOrValues : appId;
39
+ const appValues = typeof idOrValues === 'string' ? values : idOrValues;
40
+ if (!targetAppId) {
41
+ throw new Error('appValues: missing app id (req.appId is empty)');
42
+ }
43
+ if (!isOrdinaryObject(appValues)) {
44
+ throw new Error('appValues: values must be a plain object (arrays, class instances, null, and primitives are not allowed)');
45
+ }
46
+ return {
47
+ $app: {
48
+ [targetAppId]: appValues,
49
+ },
50
+ };
51
+ },
52
+ };
53
+ }
54
+ function isOrdinaryObject(val) {
55
+ return (typeof val === 'object' &&
56
+ val !== null &&
57
+ Object.getPrototypeOf(val) === Object.prototype);
58
+ }
@@ -21,13 +21,14 @@ export declare abstract class PushAppCommand extends RemoteAppCommand {
21
21
  watchingIgnoreFilter: GlobbyFilterFunction | null;
22
22
  watchingTimer: NodeJS.Timeout | null;
23
23
  watchListener: (eventType: fs.WatchEventType, filename: null | string) => Promise<void>;
24
+ protected pushesSwellJson(file?: string): boolean;
24
25
  protected showFrontendMigrationError(): never;
25
26
  buildFrontend(projectType?: FrontendProjectType): Promise<void>;
26
27
  chooseAppToPull(query?: any): Promise<App>;
27
28
  createAppStorefront(hasOtherStorefronts?: boolean, nonInteractive?: boolean): Promise<any>;
28
29
  deployAppFrontend(force?: boolean, log?: boolean): Promise<void>;
29
30
  deployFrontend(projectType: FrontendProjectType): Promise<string>;
30
- ensureAppExists(file?: string, shouldCreate?: boolean, force?: boolean): Promise<boolean>;
31
+ ensureAppExists(file?: string, shouldCreate?: boolean): Promise<boolean>;
31
32
  ensureLoggedIn(): Promise<void>;
32
33
  exec(command: string, cwd?: string, onOutput?: (string: string) => any | false): Promise<void>;
33
34
  execFrontend(command: string, onOutput?: (string: string) => any | false): Promise<void>;
@@ -7,10 +7,12 @@ import Stream from 'node:stream';
7
7
  import ora from 'ora';
8
8
  import { default as swellConfig } from './lib/app-config.js';
9
9
  import { componentsToRebuild } from './lib/apps/components.js';
10
+ import { extensionManifestWarnings } from './lib/apps/extension-manifest.js';
10
11
  import { buildFrontendPackage, getManagedFrontendFramework, } from './lib/apps/frontend-package.js';
11
12
  import { warnFrontendRouteCollisions } from './lib/apps/frontend-routes.js';
12
13
  import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExistsAsync, findAppConfig, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashFile, hashString, isPathDirectory, toPosixPath, } from './lib/apps/index.js';
13
- import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
14
+ import { warnListingSources } from './lib/apps/listing.js';
15
+ import { getGlobIgnorePathsChecker, globFrontendEnvFiles, isCredentialFile, } from './lib/apps/paths.js';
14
16
  import { slugFromApp } from './lib/apps/slug.js';
15
17
  import { getLocalInputs } from './lib/bundle.js';
16
18
  import { default as localConfig } from './lib/config.js';
@@ -119,6 +121,10 @@ export class PushAppCommand extends RemoteAppCommand {
119
121
  }
120
122
  }
121
123
  };
124
+ // The platform applies the listing in swell.json when the file is pushed
125
+ pushesSwellJson(file) {
126
+ return !file;
127
+ }
122
128
  showFrontendMigrationError() {
123
129
  return this.error(style.funcWarn(`⚠️ Your frontend directory exists but appears to use an older structure.\n` +
124
130
  `Swell CLI v2.1.0+ requires a Workers-based frontend with package.json.\n\n` +
@@ -330,7 +336,7 @@ export class PushAppCommand extends RemoteAppCommand {
330
336
  this.error(`Error deploying frontend: ${error.message}`);
331
337
  }
332
338
  }
333
- async ensureAppExists(file, shouldCreate = true, force = false) {
339
+ async ensureAppExists(file, shouldCreate = true) {
334
340
  const klass = this.ctor;
335
341
  const currentStore = localConfig.getDefaultStore();
336
342
  await this.ensureLoggedIn();
@@ -356,15 +362,21 @@ export class PushAppCommand extends RemoteAppCommand {
356
362
  this.app = app;
357
363
  return true;
358
364
  }
365
+ if (this.pushesSwellJson(file)) {
366
+ warnListingSources(this.appPath, this.swellConfig.store, (message) => this.warn(message));
367
+ for (const message of extensionManifestWarnings(this.swellConfig.get('extensions'))) {
368
+ this.warn(message);
369
+ }
370
+ }
359
371
  // If app exists but owned by another client, create a new instance
360
372
  if (app?.client_id && app.client_id !== currentStore) {
361
373
  // Create a new app
362
- this.app = await this.getCreateUpdateApp(undefined, force);
374
+ this.app = await this.getCreateUpdateApp();
363
375
  if (!this.app) {
364
376
  return false;
365
377
  }
366
378
  }
367
- else if (file) {
379
+ else if (file && !this.pushesSwellJson(file)) {
368
380
  // Targeting a file: don't update the app from swell.json, but keep the
369
381
  // remote one to diff config hashes and the frontend deployment hash
370
382
  if (app) {
@@ -372,8 +384,9 @@ export class PushAppCommand extends RemoteAppCommand {
372
384
  }
373
385
  }
374
386
  else {
375
- // Update existing app if not targeting a file
376
- this.app = await this.getCreateUpdateApp(app, force);
387
+ // The app takes its details from swell.json before any upload, so that
388
+ // details the app refuses fail the push first
389
+ this.app = await this.getCreateUpdateApp(app);
377
390
  }
378
391
  return true;
379
392
  }
@@ -493,8 +506,12 @@ export class PushAppCommand extends RemoteAppCommand {
493
506
  if (frontendHashes.length > 0) {
494
507
  const packageHash = localConfigs.find((config) => config.filePath === 'package.json')?.hash;
495
508
  frontendHashes += `|${packageHash}`;
496
- // Shared code the worker imports from outside frontend/, e.g. functions/lib
497
- for (const input of await this.getFrontendSharedInputs()) {
509
+ // Shared code the worker imports from outside frontend/, e.g.
510
+ // functions/lib, and the .env files its build reads
511
+ for (const input of [
512
+ ...(await this.getFrontendSharedInputs()),
513
+ ...(await globFrontendEnvFiles(this.appPath)),
514
+ ]) {
498
515
  frontendHashes += `|${hashFile(path.join(this.appPath, input), undefined, input)}`;
499
516
  }
500
517
  }
@@ -787,6 +804,11 @@ export class PushAppCommand extends RemoteAppCommand {
787
804
  }
788
805
  async pushFile(relativePath) {
789
806
  const relativePathPosix = toPosixPath(relativePath);
807
+ if (isCredentialFile(relativePathPosix)) {
808
+ ora().fail(`${relativePathPosix} is never pushed: .env and .dev.vars files can hold credentials`);
809
+ this.failedFiles.push(relativePathPosix);
810
+ return;
811
+ }
790
812
  const basePath = relativePathPosix.split('/')[0];
791
813
  const configType = getConfigTypeFromPath(basePath) || ConfigType.FILE;
792
814
  const config = await appConfigFromFile(relativePathPosix, configType, this.appPath);
@@ -817,7 +839,7 @@ export class PushAppCommand extends RemoteAppCommand {
817
839
  const pushConcurrency = configTypeKey === 'ASSET' ? 1 : PUSH_CONCURRENCY;
818
840
  while (pushFiles.length > 0) {
819
841
  // eslint-disable-next-line no-await-in-loop
820
- await Promise.all(pushFiles
842
+ await this.runTogether(pushFiles
821
843
  .splice(0, pushConcurrency)
822
844
  .map((appConfig) => this.pushRemoteFile(appConfig)));
823
845
  }
@@ -826,7 +848,7 @@ export class PushAppCommand extends RemoteAppCommand {
826
848
  if (filesToRemove.length > 0) {
827
849
  while (filesToRemove.length > 0) {
828
850
  // eslint-disable-next-line no-await-in-loop
829
- await Promise.all(filesToRemove
851
+ await this.runTogether(filesToRemove
830
852
  .splice(0, PUSH_CONCURRENCY)
831
853
  .map((appConfig) => this.removeRemoteFile(appConfig)));
832
854
  }
@@ -862,18 +884,17 @@ export class PushAppCommand extends RemoteAppCommand {
862
884
  // Process queue in batches with concurrency control
863
885
  /* eslint-disable no-await-in-loop */
864
886
  while (queue.length > 0) {
865
- try {
866
- await Promise.all(queue
867
- .splice(0, PUSH_CONCURRENCY)
868
- .map(async ({ action, appConfig }) => {
869
- const result = await (action === 'remove'
870
- ? this.removeRemoteFile(appConfig, this.logWatchChanges)
871
- : this.pushRemoteFile(appConfig, this.logWatchChanges));
872
- this.onWatchChange?.(appConfig, action, result);
873
- }));
874
- }
875
- catch {
876
- // TODO: Ignore only some errors
887
+ const results = await Promise.allSettled(queue.splice(0, PUSH_CONCURRENCY).map(async ({ action, appConfig }) => {
888
+ const result = await (action === 'remove'
889
+ ? this.removeRemoteFile(appConfig, this.logWatchChanges)
890
+ : this.pushRemoteFile(appConfig, this.logWatchChanges));
891
+ this.onWatchChange?.(appConfig, action, result);
892
+ }));
893
+ // A failed change ends no watch: its reasons show, and watching goes on
894
+ for (const result of results) {
895
+ if (result.status === 'rejected') {
896
+ this.logError(result.reason?.message ?? String(result.reason));
897
+ }
877
898
  }
878
899
  }
879
900
  /* eslint-enable no-await-in-loop */
@@ -26,7 +26,7 @@ export declare abstract class RemoteAppCommand extends AppCommand {
26
26
  protected getApp(id?: string): Promise<App>;
27
27
  protected getAppWithConfig(id?: string): Promise<App>;
28
28
  getConfigData(configId: string): Promise<any>;
29
- protected getCreateUpdateApp(updateApp?: App | null, force?: boolean): Promise<App>;
29
+ protected getCreateUpdateApp(updateApp?: App | null): Promise<App>;
30
30
  protected getInstalledApp(): Promise<any>;
31
31
  /**
32
32
  * Anything on the target store already using the services named by
@@ -88,9 +88,9 @@ export declare abstract class RemoteAppCommand extends AppCommand {
88
88
  removed: string[];
89
89
  }>;
90
90
  protected pushRemoteFile(config: AppConfig, log?: boolean): Promise<any>;
91
+ protected runTogether(tasks: Promise<unknown>[]): Promise<void>;
91
92
  protected removeLocalFile(config: AppConfig, log?: boolean): Promise<any>;
92
93
  protected removeRemoteFile(config: AppConfig, log?: boolean): Promise<any>;
93
- protected showErrors(errors: any, spinnerError?: () => void): void;
94
94
  storefrontFrontendUrl(storeId: string, storefront?: any, branchId?: string, preview?: boolean): string;
95
95
  protected swellRcPath(): string;
96
96
  protected timestampStyled(): string;