@swell/cli 2.9.22 → 2.9.23

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.
@@ -31,7 +31,7 @@ export default class ApiPost extends SwellApiCommand {
31
31
  'swell api post /products --body \'{"name": "Product", "price": 100}\'',
32
32
  'swell api post /products --body ./product.json',
33
33
  'swell api post /functions/my-app/create-order --body \'{"items":["sku-1"]}\'',
34
- `swell api post /functions/my-app/webhook --body @./payload.json -H 'Stripe-Signature: t=123,v1=abc'`,
34
+ `swell api post /functions/my-app/webhook --body ./payload.json -H 'X-Webhook-Event: order.created'`,
35
35
  `swell api post /products --body '{"name": "Product"}' --api frontend`,
36
36
  ];
37
37
  get method() {
@@ -6,6 +6,7 @@ export default class AppDev extends PushAppCommand {
6
6
  static examples: string[];
7
7
  static flags: {
8
8
  'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
+ 'store-user': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
10
  port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
11
  'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
12
  function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
@@ -9,10 +9,30 @@ import * as path from 'node:path';
9
9
  import ora from 'ora';
10
10
  import { ConfigType, allConfigFilesInDir, appConfigFromFile, } from '../../lib/apps/index.js';
11
11
  import { bundleFunction } from '../../lib/bundle.js';
12
+ import { getLocalFrontendHost } from '../../lib/constants.js';
12
13
  import { analyzeFunctionSource, hasKindDiagnostic, } from '../../lib/function-source-analysis.js';
13
14
  import { detectPackageManager, getExecSpawnArgs, } from '../../lib/package-manager.js';
14
15
  import style from '../../lib/style.js';
15
16
  import { PushAppCommand } from '../../push-app-command.js';
17
+ /**
18
+ * Headers that show the frontend dev server the public host and scheme, as
19
+ * managed hosting does. The tunnel replaces both with its own, so without them
20
+ * same-origin checks in the dev server reject the browser's requests.
21
+ *
22
+ * @param req - Request received from the tunnel
23
+ * @returns Header overrides, empty when the request did not come through Swell
24
+ */
25
+ function publicRequestHeaders(req) {
26
+ const host = String(req.headers['x-forwarded-host'] ?? '')
27
+ .split(',')[0]
28
+ .trim();
29
+ if (!host) {
30
+ return {};
31
+ }
32
+ // The tunnel always reports https; the preview URL has the browser's scheme.
33
+ const { protocol } = new URL(getLocalFrontendHost('store', 'local'));
34
+ return { host, 'x-forwarded-proto': protocol.slice(0, -1) };
35
+ }
16
36
  export default class AppDev extends PushAppCommand {
17
37
  static delayOrientation = true;
18
38
  static examples = [
@@ -26,6 +46,10 @@ export default class AppDev extends PushAppCommand {
26
46
  'no-push': Flags.boolean({
27
47
  description: 'skip pushing app files initially',
28
48
  }),
49
+ 'store-user': Flags.boolean({
50
+ default: false,
51
+ description: 'for dashboard apps, preview as your store user instead of a visitor',
52
+ }),
29
53
  port: Flags.integer({
30
54
  char: 'p',
31
55
  description: 'override the default port to run the app locally',
@@ -76,6 +100,7 @@ export default class AppDev extends PushAppCommand {
76
100
  const { port, 'frontend-port': frontendPort } = flags;
77
101
  const noPush = flags['no-push'];
78
102
  this.jsonEvents = flags['json-events'];
103
+ this.setStoreUser(flags['store-user']);
79
104
  this.functionFilter = flags.function ?? null;
80
105
  if (!(await this.ensureAppExists(undefined, false))) {
81
106
  return;
@@ -98,6 +123,7 @@ export default class AppDev extends PushAppCommand {
98
123
  this.emitLifecycleEvent('preview:proxy-ready', {
99
124
  proxyPort: serverPort,
100
125
  storefrontId: this.storefront?.id ?? null,
126
+ storeUser: this.storeUser,
101
127
  });
102
128
  await this.runAppFrontendDevIfApplicable(frontendPort);
103
129
  }
@@ -178,7 +204,10 @@ export default class AppDev extends PushAppCommand {
178
204
  }
179
205
  else if (this.frontendPort) {
180
206
  // Proxy non-function requests to frontend dev server
181
- proxy.web(req, res, { target: `http://localhost:${this.frontendPort}` }, (error) => fail(502, `Frontend dev server error: ${error.message}`));
207
+ proxy.web(req, res, {
208
+ target: `http://localhost:${this.frontendPort}`,
209
+ headers: publicRequestHeaders(req),
210
+ }, (error) => fail(502, `Frontend dev server error: ${error.message}`));
182
211
  }
183
212
  else {
184
213
  res.writeHead(404);
@@ -196,7 +225,10 @@ export default class AppDev extends PushAppCommand {
196
225
  socket.destroy();
197
226
  return;
198
227
  }
199
- proxy.ws(req, socket, head, { target: `http://localhost:${this.frontendPort}` }, () => socket.destroy());
228
+ proxy.ws(req, socket, head, {
229
+ target: `http://localhost:${this.frontendPort}`,
230
+ headers: publicRequestHeaders(req),
231
+ }, () => socket.destroy());
200
232
  });
201
233
  await new Promise((resolve, reject) => {
202
234
  server.once('error', reject);
@@ -298,6 +330,9 @@ ENVIRONMENT = "development"
298
330
  }
299
331
  if (analysis.kind === 'workflow') {
300
332
  this.log(` Workflow: deploys through ${style.command('swell app push')}`);
333
+ if (analysis.config?.action) {
334
+ this.log(` Trigger: Action (runs the pushed workflow)`);
335
+ }
301
336
  return;
302
337
  }
303
338
  const { config } = await bundleFunction(fullPath);
@@ -312,6 +347,9 @@ ENVIRONMENT = "development"
312
347
  this.log(` Trigger: Cron`);
313
348
  this.log(` Schedule: ${style.dim(config.cron.schedule)}`);
314
349
  }
350
+ else if (config?.action) {
351
+ this.log(` Trigger: Action`);
352
+ }
315
353
  else if (config?.route) {
316
354
  this.log(` Trigger: Route`);
317
355
  if (config.route.methods?.length) {
@@ -8,6 +8,7 @@ export default class AppFrontendDev extends PushAppCommand {
8
8
  'app-dev': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
9
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
10
  'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ 'store-user': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
12
  port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
13
  'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
14
  function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
@@ -1,4 +1,5 @@
1
1
  import { Flags } from '@oclif/core';
2
+ import { stripVTControlCharacters } from 'node:util';
2
3
  import { warnFrontendRouteCollisions } from '../../../lib/apps/frontend-routes.js';
3
4
  import { CUSTOM_FRAMEWORK_SLUG, getManualDevHint, getProjectCommands, } from '../../../lib/apps/index.js';
4
5
  import { PushAppCommand } from '../../../push-app-command.js';
@@ -40,6 +41,7 @@ export default class AppFrontendDev extends PushAppCommand {
40
41
  const { flags } = await this.parse(AppFrontendDev);
41
42
  const { port, 'proxy-port': proxyPort, 'app-dev': isAppDev } = flags;
42
43
  const noPush = flags['no-push'];
44
+ this.setStoreUser(flags['store-user']);
43
45
  if (!(await this.ensureAppExists(undefined, false))) {
44
46
  return;
45
47
  }
@@ -97,10 +99,7 @@ export default class AppFrontendDev extends PushAppCommand {
97
99
  const { devCommand } = await getProjectCommands(this.appPath, projectType);
98
100
  let serverReadyDetected = false;
99
101
  await this.execFrontend(devCommand.replace('${PORT}', String(serverPort)), (string) => {
100
- // Shared: Hide Vite noise output common across frameworks
101
- if (/press h \+ enter to show help/i.test(string)) {
102
- return false;
103
- }
102
+ const output = stripVTControlCharacters(string);
104
103
  // Framework-specific output handling and ready detection
105
104
  switch (projectType.slug) {
106
105
  case 'nextjs': {
@@ -193,10 +192,11 @@ export default class AppFrontendDev extends PushAppCommand {
193
192
  case 'react':
194
193
  case 'react-storefront':
195
194
  case 'vinext': {
196
- // Vite emits the Local URL as its ready signal — no separate
197
- // detector needed. Vinext dev delegates URL printing to Vite.
198
- if (/➜\s+local:\s+http:\/\//i.test(string) ||
199
- /➜\s+network:/i.test(string)) {
195
+ // The ready banner can arrive without a URL. Detect readiness
196
+ // before filtering help text, which may share the same chunk.
197
+ if (/\bvite\s+v\S+\s+ready in\b/i.test(output) ||
198
+ /➜\s+local:\s+http:\/\//i.test(output) ||
199
+ /➜\s+network:/i.test(output)) {
200
200
  if (!serverReadyDetected) {
201
201
  serverReadyDetected = true;
202
202
  this.log(`Started ${projectType.name} dev server on port ${serverPort}.\n`);
@@ -228,6 +228,9 @@ export default class AppFrontendDev extends PushAppCommand {
228
228
  }
229
229
  // No default
230
230
  }
231
+ if (/press h \+ enter to show help/i.test(output)) {
232
+ return false;
233
+ }
231
234
  });
232
235
  }
233
236
  }
@@ -7,6 +7,7 @@ interface FunctionRecord {
7
7
  cron?: {
8
8
  schedule?: string;
9
9
  };
10
+ action?: Record<string, unknown>;
10
11
  route?: {
11
12
  headers?: string[];
12
13
  methods?: string[];
@@ -7,6 +7,10 @@ function deriveTrigger(r) {
7
7
  const n = r.model.events.length;
8
8
  return `model (${n} event${n === 1 ? '' : 's'})`;
9
9
  }
10
+ // Checked before route: an installed function can also carry `route: {}`.
11
+ if (r.action) {
12
+ return 'action';
13
+ }
10
14
  if (r.route) {
11
15
  const { methods } = r.route;
12
16
  if (!methods?.length)
@@ -262,7 +262,7 @@ export default class Schema extends SwellCommand {
262
262
  // Validate trigger exclusivity
263
263
  const triggers = getFunctionTriggers(config);
264
264
  if (triggers.length === 0) {
265
- throw new Error('Invalid function config:\n• Config must specify one of: route, model, cron');
265
+ throw new Error('Invalid function config:\n• Config must specify one of: route, model, cron, action');
266
266
  }
267
267
  if (triggers.length > 1) {
268
268
  throw new Error(`Invalid function config:\n• Multiple triggers specified: ${triggers.join(', ')}. Use exactly one.`);
@@ -301,10 +301,7 @@ export async function captureFrontendPackage(frontend, framework) {
301
301
  Buffer.from(await part.arrayBuffer())));
302
302
  }
303
303
  const bindings = (metadata.bindings ?? []).map((binding) => {
304
- if ((binding.type === 'assets' && binding.name === 'ASSETS') ||
305
- (framework === 'vinext' &&
306
- binding.type === 'version_metadata' &&
307
- binding.name === 'CF_VERSION_METADATA')) {
304
+ if (binding.type === 'assets' && binding.name === 'ASSETS') {
308
305
  return binding.name;
309
306
  }
310
307
  throw new Error(`Unsupported managed frontend binding: ${binding.name}`);
@@ -4,7 +4,7 @@ export declare const FRONTEND_HOSTING_LABELS: {
4
4
  'self-hosted': string;
5
5
  };
6
6
  export declare const C3_VERSION = "2.72.7";
7
- export declare const FRONTEND_TEMPLATE_REF = "v0.1";
7
+ export declare const FRONTEND_TEMPLATE_REF = "v0.2";
8
8
  export interface FrontendTemplate {
9
9
  /** `--frontend` value and picker value. */
10
10
  slug: string;
@@ -9,7 +9,7 @@ export const C3_VERSION = '2.72.7';
9
9
  // template hotfixes need no CLI release; a breaking template change gets a new
10
10
  // minor tag and a CLI release that points at it. degit resolves only branch
11
11
  // and tag tips, never arbitrary commits.
12
- export const FRONTEND_TEMPLATE_REF = 'v0.1';
12
+ export const FRONTEND_TEMPLATE_REF = 'v0.2';
13
13
  // npx pins the generator even on Yarn Classic. The caller passes the selected
14
14
  // manager's user agent for C3's dependency edits.
15
15
  const managedInstallCommand = (directory) => `npx --yes create-cloudflare@${C3_VERSION} frontend --template=swellstores/swell-app-templates/${directory}#${FRONTEND_TEMPLATE_REF} --deploy=false --git=false --no-agents --no-auto-update --lang=ts`;
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
2
2
  import { createRequire } from 'node:module';
3
3
  const require = createRequire(import.meta.url);
4
4
  const ts = require('typescript');
5
- const ORDINARY_TRIGGER_KEYS = ['route', 'model', 'cron'];
5
+ const ORDINARY_TRIGGER_KEYS = ['route', 'model', 'cron', 'action'];
6
6
  const WORKFLOW_FORBIDDEN_KEYS = [
7
7
  'route',
8
8
  'model',
@@ -111,8 +111,9 @@ class SwellRequest {
111
111
  this.swell = new SwellAPI(this, context);
112
112
  // URL of the original request
113
113
  this.url;
114
- // Raw request body text, untouched by parsing.
115
- // Use on route triggers for HMAC/webhook signature verification.
114
+ // Request body text. On a route called through the storefront gateway with
115
+ // a JSON body, this is the exact text the caller sent, so verify HMAC/webhook
116
+ // signatures against it. Other bodies arrive re-serialized as JSON.
116
117
  this.rawBody = '';
117
118
  // Parsed JSON body as object, or raw text string when body isn't JSON.
118
119
  this.body = {};
@@ -27,6 +27,9 @@ export interface WorkflowFailure {
27
27
  message?: string;
28
28
  }
29
29
  export interface WorkflowRunRecord {
30
+ action?: {
31
+ id?: string;
32
+ };
30
33
  app_id?: string;
31
34
  cf_instance_id?: string;
32
35
  date_completed?: string;
@@ -113,6 +113,9 @@ export function workflowRunMeta(run) {
113
113
  if (run.status) {
114
114
  parts.push(run.status);
115
115
  }
116
+ if (run.trigger === 'action') {
117
+ parts.push(run.action?.id ? `action (${run.action.id})` : 'action');
118
+ }
116
119
  const date = workflowRunDate(run);
117
120
  if (date) {
118
121
  parts.push(formatWorkflowRelativeTime(date));
@@ -13,6 +13,7 @@ interface WatchingChange {
13
13
  export declare abstract class PushAppCommand extends RemoteAppCommand {
14
14
  frontendPath: string;
15
15
  hasLoggedLocalPreviewUrl: boolean;
16
+ storeUser: boolean;
16
17
  logWatchChanges: boolean;
17
18
  onWatchChange?: (appConfig?: AppConfig, action?: string, result?: any) => void;
18
19
  watchingChangeQueue: Map<string, WatchingChange>;
@@ -90,6 +91,7 @@ export declare abstract class PushAppCommand extends RemoteAppCommand {
90
91
  runWatchChangeQueue(): Promise<void>;
91
92
  saveCurrentStorefront(): void;
92
93
  logLocalPreviewUrl(): void;
94
+ setStoreUser(storeUser: boolean): void;
93
95
  setWatchingFiles(): Promise<void>;
94
96
  startProxyServer(port?: number): Promise<number>;
95
97
  updateFrontendDeployment(currentStore: string, projectType: FrontendProjectType, deploymentUrl: string, deploymentHash: string): Promise<void>;
@@ -46,6 +46,7 @@ function readWranglerMain(frontendPath) {
46
46
  export class PushAppCommand extends RemoteAppCommand {
47
47
  frontendPath = '';
48
48
  hasLoggedLocalPreviewUrl = false;
49
+ storeUser = false;
49
50
  logWatchChanges = true;
50
51
  onWatchChange;
51
52
  watchingChangeQueue = new Map();
@@ -870,8 +871,17 @@ export class PushAppCommand extends RemoteAppCommand {
870
871
  ? `/${this.app.private_id}`
871
872
  : '';
872
873
  this.log(`View it at ${style.link(`${this.localFrontendUrl(currentStore, localId)}${frontendAppRoute}`)}\n`);
874
+ if (this.app.type !== 'storefront') {
875
+ this.log(`${this.storeUser ? 'Store-user' : 'Visitor'} preview. Anyone with the link sees the same view.\n`);
876
+ }
873
877
  this.hasLoggedLocalPreviewUrl = true;
874
878
  }
879
+ setStoreUser(storeUser) {
880
+ this.storeUser = storeUser;
881
+ if (storeUser && this.app.type === 'storefront') {
882
+ this.error('--store-user is not supported for storefront previews.');
883
+ }
884
+ }
875
885
  async setWatchingFiles() {
876
886
  this.watchingFiles = new Set(await globAllFilesByPath(this.appPath));
877
887
  }
@@ -912,13 +922,25 @@ export class PushAppCommand extends RemoteAppCommand {
912
922
  async updateLocalProxy(proxyUrl, storefrontId) {
913
923
  const storefront = this.app.type === 'storefront' &&
914
924
  (await this.getAppStorefront({ storefront_id: storefrontId }));
915
- await this.handleRequestErrors(async () => this.api.put({ adminPath: `/client/apps/${this.app.id}/local-proxy` }, {
925
+ const storeUser = Boolean(proxyUrl && this.storeUser);
926
+ const putLocalProxy = (proxy_url, store_user) => this.api.put({ adminPath: `/client/apps/${this.app.id}/local-proxy` }, {
916
927
  body: {
917
- proxy_url: proxyUrl,
928
+ proxy_url,
918
929
  storefront_id: storefront?.id || null,
919
930
  storefront_slug: storefront?.slug || null,
931
+ store_user,
920
932
  },
921
- }));
933
+ });
934
+ const response = await this.handleRequestErrors(async () => putLocalProxy(proxyUrl, storeUser));
935
+ if (storeUser && response?.local_proxy?.store_user !== true) {
936
+ try {
937
+ await putLocalProxy(null, false);
938
+ }
939
+ catch {
940
+ // Cleanup is best-effort and silent; the unsupported-mode error follows.
941
+ }
942
+ this.error('This Swell server does not support --store-user. Restart without the flag.');
943
+ }
922
944
  }
923
945
  async watchForChanges({ logChanges, onChange, syncAll, } = {}) {
924
946
  this.watchingIgnoreFilter = await getGlobIgnorePathsChecker(this.appPath);
@@ -546,7 +546,7 @@
546
546
  "swell api post /products --body '{\"name\": \"Product\", \"price\": 100}'",
547
547
  "swell api post /products --body ./product.json",
548
548
  "swell api post /functions/my-app/create-order --body '{\"items\":[\"sku-1\"]}'",
549
- "swell api post /functions/my-app/webhook --body @./payload.json -H 'Stripe-Signature: t=123,v1=abc'",
549
+ "swell api post /functions/my-app/webhook --body ./payload.json -H 'X-Webhook-Event: order.created'",
550
550
  "swell api post /products --body '{\"name\": \"Product\"}' --api frontend"
551
551
  ],
552
552
  "flags": {
@@ -745,6 +745,12 @@
745
745
  "allowNo": false,
746
746
  "type": "boolean"
747
747
  },
748
+ "store-user": {
749
+ "description": "for dashboard apps, preview as your store user instead of a visitor",
750
+ "name": "store-user",
751
+ "allowNo": false,
752
+ "type": "boolean"
753
+ },
748
754
  "port": {
749
755
  "char": "p",
750
756
  "description": "override the default port to run the app locally",
@@ -3510,6 +3516,12 @@
3510
3516
  "allowNo": false,
3511
3517
  "type": "boolean"
3512
3518
  },
3519
+ "store-user": {
3520
+ "description": "for dashboard apps, preview as your store user instead of a visitor",
3521
+ "name": "store-user",
3522
+ "allowNo": false,
3523
+ "type": "boolean"
3524
+ },
3513
3525
  "port": {
3514
3526
  "char": "p",
3515
3527
  "description": "override the default port to run the app locally",
@@ -3594,5 +3606,5 @@
3594
3606
  ]
3595
3607
  }
3596
3608
  },
3597
- "version": "2.9.22"
3609
+ "version": "2.9.23"
3598
3610
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.9.22",
3
+ "version": "2.9.23",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [