@swell/cli 2.7.1 → 2.9.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.
@@ -8,6 +8,7 @@ export default class AppDev extends PushAppCommand {
8
8
  'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
9
  port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
10
  'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
12
  'storefront-id': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
13
  'storefront-select': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
13
14
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
@@ -17,6 +18,7 @@ export default class AppDev extends PushAppCommand {
17
18
  };
18
19
  static summary: string;
19
20
  functionErrors: Map<string, string>;
21
+ functionFilter: string | null;
20
22
  functionPorts: Map<string, number>;
21
23
  functionProcesses: Map<string, ChildProcess>;
22
24
  tmpDir: string;
@@ -18,6 +18,7 @@ export default class AppDev extends PushAppCommand {
18
18
  'swell app dev --storefront-id <id>',
19
19
  'swell app dev --port 3000',
20
20
  'swell app dev --port 3000 --frontend-port 4000',
21
+ 'swell app dev --function my-function',
21
22
  ];
22
23
  static flags = {
23
24
  'no-push': Flags.boolean({
@@ -30,6 +31,9 @@ export default class AppDev extends PushAppCommand {
30
31
  'frontend-port': Flags.integer({
31
32
  description: 'specify the port for the frontend dev server when running with frontend',
32
33
  }),
34
+ function: Flags.string({
35
+ description: 'run only the named function',
36
+ }),
33
37
  'storefront-id': Flags.string({
34
38
  description: 'for storefront apps, identify a storefront to preview and push theme files to',
35
39
  }),
@@ -48,6 +52,8 @@ export default class AppDev extends PushAppCommand {
48
52
  };
49
53
  static summary = `Run an app in dev mode from your local machine.`;
50
54
  functionErrors = new Map();
55
+ // When set, only this function name is bundled, started, and watched
56
+ functionFilter = null;
51
57
  // All available functions
52
58
  functionPorts = new Map();
53
59
  // Wrangler child processes to kill on cleanup
@@ -62,6 +68,7 @@ export default class AppDev extends PushAppCommand {
62
68
  const { flags } = await this.parse(AppDev);
63
69
  const { port, 'frontend-port': frontendPort } = flags;
64
70
  const noPush = flags['no-push'];
71
+ this.functionFilter = flags.function ?? null;
65
72
  if (!(await this.ensureAppExists(undefined, false))) {
66
73
  return;
67
74
  }
@@ -213,12 +220,18 @@ ENVIRONMENT = "development"
213
220
  if (!config.isRootFunction()) {
214
221
  continue; // Skip if not a root function config
215
222
  }
223
+ if (this.functionFilter && config.name !== this.functionFilter) {
224
+ continue;
225
+ }
216
226
  functions.push(config);
217
227
  }
218
228
  }
219
229
  catch {
220
230
  // functions directory doesn't exist
221
231
  }
232
+ if (this.functionFilter && functions.length === 0) {
233
+ this.error(`Function '${this.functionFilter}' not found in ${style.path('functions/')}.`);
234
+ }
222
235
  return functions;
223
236
  }
224
237
  async logAllFunctions(functions) {
@@ -293,6 +306,9 @@ ENVIRONMENT = "development"
293
306
  if (appConfig?.type !== ConfigType.FUNCTION) {
294
307
  return;
295
308
  }
309
+ if (this.functionFilter && appConfig.name !== this.functionFilter) {
310
+ return;
311
+ }
296
312
  try {
297
313
  const fullPath = path.join(this.appPath, appConfig.filePath);
298
314
  // Re-bundle the function
@@ -10,6 +10,7 @@ export default class AppFrontendDev extends PushAppCommand {
10
10
  'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
11
  port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
12
  'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
14
  };
14
15
  static orientation: {
15
16
  env: string;
@@ -175,7 +175,7 @@ export default class Logs extends SwellCommand {
175
175
  type: Flags.string({
176
176
  description: 'filter logs by type',
177
177
  multiple: true,
178
- options: ['api', 'function', 'webhook'],
178
+ options: ['api', 'function', 'webhook', 'transaction'],
179
179
  }),
180
180
  };
181
181
  static summary = 'Output or stream store logs to the terminal.';
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { ConfigType } from './index.js';
3
4
  export declare class IgnoringFileError extends Error {
4
5
  constructor(message: string);
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { AppConfig } from './app-config.js';
3
4
  export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js';
4
5
  export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, } from './paths.js';
@@ -354,8 +354,11 @@ export function appAssetImage(appPath, fileName) {
354
354
  }
355
355
  }
356
356
  }
357
+ function getNameFromFilePath(filePath) {
358
+ return path.parse(filePath).name.replaceAll('_', '-');
359
+ }
357
360
  export function appConfigFromFile(filePath, configType, appPath) {
358
- const { name } = path.parse(filePath);
361
+ const name = getNameFromFilePath(filePath);
359
362
  // Notification basenames must not contain dots: the inspect identifier
360
363
  // grammar (app.<slug>.<model>.<name>) splits on '.', so a dotted name
361
364
  // would silently misparse on lookup.
@@ -376,7 +379,7 @@ export function appConfigFromFile(filePath, configType, appPath) {
376
379
  return config;
377
380
  }
378
381
  export function findAppConfig(app, filePath, configType) {
379
- const { name } = path.parse(filePath);
382
+ const name = getNameFromFilePath(filePath);
380
383
  const config = app?.configs?.find((c) => c && c.type === configType && c.name === name);
381
384
  return config;
382
385
  }
@@ -110,7 +110,7 @@ declare class SwellRequest {
110
110
  * @returns {object} existing app data merged with values
111
111
  * @throws {Error} if app id is missing or values is not a plain object
112
112
  */
113
- appValues(idOrValues: object | string, values?: object | undefined): object;
113
+ appValues(idOrValues: object | string, values: object | undefined): object;
114
114
  }
115
115
  /**
116
116
  * Class representing the Swell backend API.
@@ -129,7 +129,36 @@ declare class SwellAPI {
129
129
  post(url: any, data: any): Promise<any>;
130
130
  delete(url: any, data: any): Promise<any>;
131
131
  settings(id?: any): Promise<any>;
132
+ /**
133
+ * Atomic multi-op write. Throws SwellError with a stable `error.code`
134
+ * (transaction_conflict | transaction_timeout | transaction_throttled
135
+ * | transaction_op_failed | transaction_error). Retry is off by
136
+ * default — opt in with `{ retry: true }` or pass overrides.
137
+ *
138
+ * @param {Array<{method: string, url: string, data?: any}>} ops
139
+ * @param {{ retry?: true | { limit?: number, base?: number, max?: number, jitter?: boolean } }} [opts]
140
+ * @returns {Promise<any[]>}
141
+ */
142
+ transaction(ops: Array<{
143
+ method: string;
144
+ url: string;
145
+ data?: any;
146
+ }>, opts?: {
147
+ retry?: true | {
148
+ limit?: number | undefined;
149
+ base?: number | undefined;
150
+ max?: number | undefined;
151
+ jitter?: boolean | undefined;
152
+ } | undefined;
153
+ } | undefined): Promise<any[]>;
132
154
  }
155
+ declare const DEFAULT_RETRY: Readonly<{
156
+ limit: 3;
157
+ base: 100;
158
+ max: 5000;
159
+ jitter: true;
160
+ }>;
161
+ declare const RETRYABLE_CODES: Set<string>;
133
162
  /**
134
163
  * Class representing a Swell error.
135
164
  */
@@ -137,6 +166,9 @@ declare class SwellError extends Error {
137
166
  constructor(message: any, options?: {});
138
167
  status: any;
139
168
  body: any;
169
+ code: any;
170
+ retry: any;
171
+ get isRetryable(): boolean;
140
172
  }
141
173
  /**
142
174
  * Class representing a Swell response.
@@ -23,10 +23,14 @@ async function request(originalRequest, _env, context) {
23
23
  try {
24
24
  response = await executeModuleHandler(req, context);
25
25
  }
26
- catch (err) {
26
+ catch (error) {
27
27
  // Log the error for Swell
28
- console.error(err);
29
- response = new SwellResponse({ error: err.message }, { status: err.status || 500 });
28
+ console.error(error);
29
+ const errorBody = {
30
+ ...(error.body ?? { error: error.message }),
31
+ retry: error.retry === false ? false : undefined,
32
+ };
33
+ response = new SwellResponse(errorBody, { status: error.status || 500 });
30
34
  }
31
35
  return SwellResponse._respond(req, response, context);
32
36
  }
@@ -71,11 +75,11 @@ async function executeModuleHandler(req, context) {
71
75
  if (moduleExports[method]) {
72
76
  return moduleExports[method](req, context);
73
77
  }
74
- else if (defaults) {
78
+ if (defaults) {
75
79
  if (typeof defaults === 'function') {
76
80
  return defaults(req, context);
77
81
  }
78
- else if (defaults[method]) {
82
+ if (defaults[method]) {
79
83
  return defaults[method](req, context);
80
84
  }
81
85
  }
@@ -120,9 +124,9 @@ class SwellRequest {
120
124
  this._logs = [];
121
125
  }
122
126
  assignRequestProps(req) {
123
- ['ur', 'method', 'headers', 'referrer', 'credentials'].forEach((prop) => {
127
+ for (const prop of ['ur', 'method', 'headers', 'referrer', 'credentials']) {
124
128
  this[prop] = req[prop];
125
- });
129
+ }
126
130
  }
127
131
  async initialize() {
128
132
  this.rawBody = await this.originalRequest.text();
@@ -131,15 +135,15 @@ class SwellRequest {
131
135
  this.data = JSON.parse(this.rawBody);
132
136
  this.body = { ...this.data };
133
137
  }
134
- catch (err) {
138
+ catch {
135
139
  this.data = {};
136
140
  }
137
141
  this.url = new URL(this.originalRequest.url);
138
142
  // Convert the query parameters to an object
139
- this.url.searchParams.forEach((value, key) => {
143
+ for (const [key, value] of this.url.searchParams.entries()) {
140
144
  this.query[key] = value;
141
145
  this.data[key] = value;
142
- });
146
+ }
143
147
  // Bind the console methods to the request
144
148
  console.log = this.log.bind(this, 'info');
145
149
  console.info = this.log.bind(this, 'info');
@@ -151,7 +155,7 @@ class SwellRequest {
151
155
  try {
152
156
  return JSON.parse(input);
153
157
  }
154
- catch (err) {
158
+ catch {
155
159
  return {};
156
160
  }
157
161
  }
@@ -160,7 +164,7 @@ class SwellRequest {
160
164
  this._logs.push({
161
165
  date: Date.now(),
162
166
  line: line.map((l) => (l instanceof Error ? l.stack : JSON.stringify(l))),
163
- ...(level !== 'info' ? { level } : {}),
167
+ ...(level === 'info' ? {} : { level }),
164
168
  });
165
169
  }
166
170
  getIngestableLogs(response) {
@@ -186,7 +190,7 @@ class SwellRequest {
186
190
  formatRequestData() {
187
191
  try {
188
192
  const stringData = JSON.stringify(this.body ?? null);
189
- return stringData.substring(0, 1024000);
193
+ return stringData.slice(0, 1024000);
190
194
  }
191
195
  catch {
192
196
  return '';
@@ -211,7 +215,7 @@ class SwellRequest {
211
215
  * @returns {object} existing app data merged with values
212
216
  * @throws {Error} if app id is missing or values is not a plain object
213
217
  */
214
- appValues(idOrValues, values = undefined) {
218
+ appValues(idOrValues, values) {
215
219
  const appId = typeof idOrValues === 'string' ? idOrValues : this.appId;
216
220
  const appValues = typeof idOrValues === 'string' ? values : idOrValues;
217
221
  if (!appId) {
@@ -283,7 +287,7 @@ class SwellAPI {
283
287
  throw new Error(`Error serializing data: ${data}`);
284
288
  }
285
289
  }
286
- const endpointUrl = String(url).startsWith('/') ? url.substring(1) : url;
290
+ const endpointUrl = String(url).startsWith('/') ? url.slice(1) : url;
287
291
  const response = await fetch(`${this.baseUrl}/${endpointUrl}${query}`, requestOptions);
288
292
  const responseText = await response.text();
289
293
  let result;
@@ -320,7 +324,54 @@ class SwellAPI {
320
324
  async settings(id = this.request.appId) {
321
325
  return this.makeRequest('GET', `/settings/${id}`);
322
326
  }
327
+ /**
328
+ * Atomic multi-op write. Throws SwellError with a stable `error.code`
329
+ * (transaction_conflict | transaction_timeout | transaction_throttled
330
+ * | transaction_op_failed | transaction_error). Retry is off by
331
+ * default — opt in with `{ retry: true }` or pass overrides.
332
+ *
333
+ * @param {Array<{method: string, url: string, data?: any}>} ops
334
+ * @param {{ retry?: true | { limit?: number, base?: number, max?: number, jitter?: boolean } }} [opts]
335
+ * @returns {Promise<any[]>}
336
+ */
337
+ async transaction(ops, opts = {}) {
338
+ if (!opts.retry) {
339
+ return this.makeRequest('POST', '/:transaction', ops);
340
+ }
341
+ const cfg = {
342
+ ...DEFAULT_RETRY,
343
+ ...(opts.retry === true ? {} : opts.retry),
344
+ };
345
+ let attempt = 0;
346
+ // eslint-disable-next-line no-constant-condition
347
+ while (true) {
348
+ try {
349
+ return await this.makeRequest('POST', '/:transaction', ops);
350
+ }
351
+ catch (error) {
352
+ if (!(error instanceof SwellError) ||
353
+ !error.isRetryable ||
354
+ attempt >= cfg.limit) {
355
+ throw error;
356
+ }
357
+ const delay = Math.min(cfg.base * 2 ** attempt, cfg.max);
358
+ const wait = cfg.jitter ? delay * (0.5 + Math.random() * 0.5) : delay;
359
+ await new Promise((resolve) => setTimeout(resolve, wait));
360
+ attempt++;
361
+ }
362
+ }
363
+ }
323
364
  }
365
+ const DEFAULT_RETRY = Object.freeze({
366
+ limit: 3,
367
+ base: 100,
368
+ max: 5000,
369
+ jitter: true,
370
+ });
371
+ const RETRYABLE_CODES = new Set([
372
+ 'transaction_conflict',
373
+ 'transaction_throttled',
374
+ ]);
324
375
  /**
325
376
  * Class representing a Swell error.
326
377
  */
@@ -331,6 +382,9 @@ class SwellError extends Error {
331
382
  if (typeof message === 'string') {
332
383
  formattedMessage = message;
333
384
  }
385
+ else if (typeof body?.error?.message === 'string') {
386
+ formattedMessage = body.error.message;
387
+ }
334
388
  else {
335
389
  formattedMessage = JSON.stringify(message, null, 2);
336
390
  }
@@ -341,6 +395,11 @@ class SwellError extends Error {
341
395
  this.name = 'SwellError';
342
396
  this.status = options.status || 500;
343
397
  this.body = body;
398
+ this.code = options.code || body?.error?.code;
399
+ this.retry = options.retry;
400
+ }
401
+ get isRetryable() {
402
+ return RETRYABLE_CODES.has(this.code);
344
403
  }
345
404
  }
346
405
  /**
@@ -363,7 +422,7 @@ class SwellResponse extends Response {
363
422
  ...options,
364
423
  headers: {
365
424
  ...resultHeaders,
366
- ...(options.headers || {}),
425
+ ...options.headers,
367
426
  },
368
427
  });
369
428
  // Saved for future access
@@ -396,9 +455,9 @@ class SwellResponse extends Response {
396
455
  }
397
456
  static async _consumeNativeResponse(response) {
398
457
  const headers = {};
399
- response.headers.forEach((value, key) => {
458
+ for (const [key, value] of response.headers.entries()) {
400
459
  headers[key] = value;
401
- });
460
+ }
402
461
  try {
403
462
  const text = await response.text();
404
463
  let data;
@@ -413,8 +472,8 @@ class SwellResponse extends Response {
413
472
  headers,
414
473
  });
415
474
  }
416
- catch (err) {
417
- return new SwellResponse({ error: `Unable to read response body: ${err.message}` }, { status: 500 });
475
+ catch (error) {
476
+ return new SwellResponse({ error: `Unable to read response body: ${error.message}` }, { status: 500 });
418
477
  }
419
478
  }
420
479
  static _respondWithLogs(response, req) {
@@ -217,7 +217,8 @@
217
217
  "options": [
218
218
  "api",
219
219
  "function",
220
- "webhook"
220
+ "webhook",
221
+ "transaction"
221
222
  ],
222
223
  "type": "option"
223
224
  }
@@ -720,7 +721,8 @@
720
721
  "swell app dev",
721
722
  "swell app dev --storefront-id <id>",
722
723
  "swell app dev --port 3000",
723
- "swell app dev --port 3000 --frontend-port 4000"
724
+ "swell app dev --port 3000 --frontend-port 4000",
725
+ "swell app dev --function my-function"
724
726
  ],
725
727
  "flags": {
726
728
  "app-path": {
@@ -751,6 +753,13 @@
751
753
  "multiple": false,
752
754
  "type": "option"
753
755
  },
756
+ "function": {
757
+ "description": "run only the named function",
758
+ "name": "function",
759
+ "hasDynamicHelp": false,
760
+ "multiple": false,
761
+ "type": "option"
762
+ },
754
763
  "storefront-id": {
755
764
  "description": "for storefront apps, identify a storefront to preview and push theme files to",
756
765
  "name": "storefront-id",
@@ -3307,6 +3316,13 @@
3307
3316
  "multiple": false,
3308
3317
  "type": "option"
3309
3318
  },
3319
+ "function": {
3320
+ "description": "run only the named function",
3321
+ "name": "function",
3322
+ "hasDynamicHelp": false,
3323
+ "multiple": false,
3324
+ "type": "option"
3325
+ },
3310
3326
  "storefront-id": {
3311
3327
  "description": "identify a storefront to preview with and push theme files to",
3312
3328
  "name": "storefront-id",
@@ -3362,5 +3378,5 @@
3362
3378
  ]
3363
3379
  }
3364
3380
  },
3365
- "version": "2.7.1"
3381
+ "version": "2.9.2"
3366
3382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.7.1",
3
+ "version": "2.9.2",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [
@@ -70,7 +70,7 @@
70
70
  "@types/inquirer": "9.0.6",
71
71
  "@types/localtunnel": "2.0.4",
72
72
  "@types/mocha": "10.0.3",
73
- "@types/node": "20.8.8",
73
+ "@types/node": "22.10.0",
74
74
  "@types/qs": "6.9.15",
75
75
  "@types/ws": "8.18.1",
76
76
  "@typescript-eslint/eslint-plugin": "6.9.0",
@@ -116,12 +116,12 @@
116
116
  "lint": "eslint . --ext .ts --config .eslintrc",
117
117
  "postpack": "shx rm -f oclif.manifest.json",
118
118
  "posttest": "npm run lint",
119
- "prepack": "npm run build && oclif manifest && oclif readme",
119
+ "prepack": "npm run build && oclif manifest && NODE_ENV=production oclif readme",
120
120
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
121
121
  "publish-alpha": "npm version prerelease --preid=alpha && npm publish --tag alpha"
122
122
  },
123
123
  "engines": {
124
- "node": ">= 18.16.1"
124
+ "node": ">= 22.0.0"
125
125
  },
126
126
  "types": "dist/index.d.ts"
127
127
  }