@swell/cli 2.9.17 → 2.9.19

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.
package/README.md CHANGED
@@ -1,3 +1,93 @@
1
1
  # @swell/cli 2 alpha
2
2
 
3
3
  Find the alpha docs at https://www.notion.so/swellcorp/CLI-reference-c3553ebb3de24d94becbedf95a5a25d8?pvs=4
4
+
5
+ ## Frontend scaffolding
6
+
7
+ Run `swell create app` for interactive setup. The frontend picker groups templates
8
+ by hosting mode. App creation without a frontend remains supported.
9
+
10
+ | `--frontend` | Starter | Hosting |
11
+ | --- | --- | --- |
12
+ | `swell-react` | React + Vite, with server endpoints | Swell managed |
13
+ | `swell-vinext` | Vinext, with server rendering and server components | Swell managed |
14
+ | `swell-spa` | Client-only React storefront with commerce hooks and UI components | Swell managed; storefront apps only |
15
+ | `nextjs`, `astro`, `nuxt`, `react`, `hono`, `angular` | Framework starter | Self-hosted; requires your own Cloudflare account |
16
+ | `none` | No frontend | Default for `create app -y` |
17
+
18
+ ```sh
19
+ swell create app my-app -t admin --frontend swell-react -y
20
+ swell create app my-app -t admin --frontend swell-vinext -y
21
+ swell create app my-store -t storefront --frontend swell-spa -p bun -y
22
+ swell create app my-app -t admin --frontend astro -y
23
+ ```
24
+
25
+ Managed starters come from
26
+ [`swell-app-templates`](https://github.com/swellstores/swell-app-templates).
27
+ Use Node.js 22.22.2 or newer and select a package manager with
28
+ `--pkg npm|yarn|pnpm|bun`. pnpm must allow the `esbuild` and `workerd` install
29
+ scripts. Hosting is recorded in `swell.json`; no developer Cloudflare account is
30
+ needed for managed hosting.
31
+
32
+ From the generated app directory, run `swell app frontend dev` for local
33
+ development and `swell app push` to deploy through Swell. Managed hosting requires
34
+ a compatible Swell platform deployment.
35
+ The SPA starter includes commerce components but starts with an empty route tree;
36
+ add storefront pages before expecting a visible storefront. SPA creation also
37
+ creates the development app and storefront in your configured test store.
38
+
39
+ To add a frontend to an existing app:
40
+
41
+ ```sh
42
+ swell create frontend
43
+ swell create frontend --frontend swell-react -y
44
+ ```
45
+
46
+ `create frontend -y` requires an explicit template. Without `--pkg` it uses the
47
+ package manager of the workspace lockfile, as `app frontend dev` and `app push`
48
+ do. It preserves existing frontend files and does not provision remote
49
+ storefront resources. `swell app init` only creates `swell.json`; use
50
+ `create frontend` afterward to scaffold a frontend.
51
+
52
+ Existing generic framework values retain self-hosted behavior. The former
53
+ internal `react-storefront` value is accepted as a hidden alias for managed
54
+ `swell-spa`; existing app deployments keep their configured hosting. The managed
55
+ SPA has no supported self-hosted scaffold at this time.
56
+
57
+ ## Managed deployment failures
58
+
59
+ If managed deployment fails, Swell retains the uploaded package and keeps the
60
+ working frontend active. Correct any reported errors and repeat `swell app push`
61
+ or `swell app frontend deploy`. These commands build and upload the frontend
62
+ again; no separate retry command is needed.
63
+
64
+ If another operation is updating the app, Swell returns 409 without waiting.
65
+ Wait briefly and repeat the install, upgrade or deployment. An activation-write
66
+ failure also returns 409; provider outages remain server errors. A merchant who
67
+ cannot install an app because its frontend is invalid should contact the app
68
+ developer.
69
+
70
+ Managed frontend runtime logs are not available in this release. `swell logs`
71
+ does not include managed frontend Worker console output or runtime exceptions.
72
+ Use local frontend development logs to investigate application behavior and the
73
+ deployment error returned by the CLI for build/upload failures. For deployment
74
+ support, provide the app ID, selected version, store/environment, operation time
75
+ and error code/message; do not include credentials or session cookies.
76
+
77
+ ## Frontend routes in development and deployment
78
+
79
+ Swell owns `/api` and its existing platform endpoints on Swell-routed domains.
80
+ Use native paths such as `/app-api/quote` for frontend handlers. Calls to
81
+ `fetch('/api/products')` still reach Swell. The React and Vinext starters use
82
+ `/app-api`; the SPA starter remains client-only.
83
+
84
+ Development and deployment warn about conventional Next/Vinext App Router and
85
+ Pages Router API handlers under `/api`, including `src/` layouts. This check
86
+ does not analyze arbitrary Worker routing, rewrites, custom route extensions,
87
+ or every platform endpoint. It does not move source files or change a base path.
88
+
89
+ Existing handlers can use `/<app_private_id>/api/...` as a compatibility mount
90
+ for the app selected by the domain. The prefix is removed before forwarding,
91
+ with the query retained. A different app's prefix cannot select that app.
92
+ Redirects, callback URLs and cookie paths may need explicit configuration for
93
+ this mount. Storefront password protection still applies.
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import getPort, { portNumbers } from 'get-port';
3
+ import httpProxy from 'http-proxy';
3
4
  import { spawn } from 'node:child_process';
4
5
  import * as fs from 'node:fs/promises';
5
6
  import * as http from 'node:http';
@@ -148,29 +149,17 @@ export default class AppDev extends PushAppCommand {
148
149
  }
149
150
  }
150
151
  async createFunctionRouter(serverPort) {
152
+ const proxy = httpProxy.createProxyServer();
151
153
  const server = http.createServer((req, res) => {
152
154
  const url = new URL(req.url, `http://localhost:${serverPort}`);
153
155
  const functionName = url.pathname.slice(1); // Remove leading slash
156
+ const fail = (status, message) => {
157
+ if (!res.headersSent)
158
+ res.writeHead(status);
159
+ res.end(message);
160
+ };
154
161
  if (this.functionPorts.has(functionName)) {
155
162
  const targetPort = this.functionPorts.get(functionName);
156
- // Proxy the request to the function server
157
- const proxyReq = http.request({
158
- hostname: 'localhost',
159
- port: targetPort,
160
- path: req.url,
161
- method: req.method,
162
- headers: {
163
- ...req.headers,
164
- 'Swell-Local-Dev': 'true',
165
- },
166
- }, (proxyRes) => {
167
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
168
- proxyRes.pipe(res);
169
- });
170
- proxyReq.on('error', (error) => {
171
- res.writeHead(500);
172
- res.end(`Error proxying to function ${functionName}: ${error.message}`);
173
- });
174
163
  // Log when response is finished, method, status, and time to execute by proxy
175
164
  const startTime = Date.now();
176
165
  res.on('finish', async () => {
@@ -181,25 +170,15 @@ export default class AppDev extends PushAppCommand {
181
170
  });
182
171
  this.log(`\n${style.appConfigValue(`→ ${functionName}`)} ${this.timestampStyled()} [${res.statusCode}] ${req.method} ${req.url} (${duration}ms)\n`);
183
172
  });
184
- req.pipe(proxyReq);
173
+ // Proxy the request to the function server
174
+ proxy.web(req, res, {
175
+ target: `http://localhost:${targetPort}`,
176
+ headers: { 'Swell-Local-Dev': 'true' },
177
+ }, (error) => fail(500, `Error proxying to function ${functionName}: ${error.message}`));
185
178
  }
186
179
  else if (this.frontendPort) {
187
180
  // Proxy non-function requests to frontend dev server
188
- const proxyReq = http.request({
189
- hostname: 'localhost',
190
- port: this.frontendPort,
191
- path: req.url,
192
- method: req.method,
193
- headers: req.headers,
194
- }, (proxyRes) => {
195
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
196
- proxyRes.pipe(res);
197
- });
198
- proxyReq.on('error', (error) => {
199
- res.writeHead(502);
200
- res.end(`Frontend dev server error: ${error.message}`);
201
- });
202
- req.pipe(proxyReq);
181
+ proxy.web(req, res, { target: `http://localhost:${this.frontendPort}` }, (error) => fail(502, `Frontend dev server error: ${error.message}`));
203
182
  }
204
183
  else {
205
184
  res.writeHead(404);
@@ -212,6 +191,13 @@ export default class AppDev extends PushAppCommand {
212
191
  : ''}`);
213
192
  }
214
193
  });
194
+ server.on('upgrade', (req, socket, head) => {
195
+ if (!this.frontendPort) {
196
+ socket.destroy();
197
+ return;
198
+ }
199
+ proxy.ws(req, socket, head, { target: `http://localhost:${this.frontendPort}` }, () => socket.destroy());
200
+ });
215
201
  await new Promise((resolve, reject) => {
216
202
  server.once('error', reject);
217
203
  server.listen(serverPort, () => {
@@ -219,6 +205,7 @@ export default class AppDev extends PushAppCommand {
219
205
  resolve();
220
206
  });
221
207
  });
208
+ return server;
222
209
  }
223
210
  emitLifecycleEvent(type, data) {
224
211
  if (!this.jsonEvents)
@@ -2,9 +2,9 @@ import { Flags } from '@oclif/core';
2
2
  import { PushAppCommand } from '../../../push-app-command.js';
3
3
  export default class AppFrontendDeploy extends PushAppCommand {
4
4
  static description = `Creates a new build using the relevant command depending on your frontend framework.
5
- Once the build succeeds, this command will invoke Cloudflare Wrangler to upload
6
- build assets using your own Cloudflare account. Once complete, Swell will update
7
- the app's deployment URL and push the updated frontend files.`;
5
+ Managed frontends are uploaded to Swell and deployed using Swell's Cloudflare account.
6
+ Self-hosted frontends deploy through Wrangler using your own Cloudflare account.
7
+ If deployment fails, correct any reported errors and run this command again.`;
8
8
  static examples = [
9
9
  'swell app frontend deploy',
10
10
  'swell app frontend deploy --force',
@@ -1,4 +1,5 @@
1
1
  import { Flags } from '@oclif/core';
2
+ import { warnFrontendRouteCollisions } from '../../../lib/apps/frontend-routes.js';
2
3
  import { CUSTOM_FRAMEWORK_SLUG, getManualDevHint, getProjectCommands, } from '../../../lib/apps/index.js';
3
4
  import { PushAppCommand } from '../../../push-app-command.js';
4
5
  import AppDev from '../dev.js';
@@ -76,6 +77,7 @@ export default class AppFrontendDev extends PushAppCommand {
76
77
  if (!projectType) {
77
78
  return;
78
79
  }
80
+ await warnFrontendRouteCollisions(this.appPath, projectType.slug, (message) => this.warn(message));
79
81
  // Manual mode for unrecognized frameworks
80
82
  if (projectType.slug === CUSTOM_FRAMEWORK_SLUG) {
81
83
  const hintCommand = await getManualDevHint(this.appPath, serverPort);
@@ -189,9 +191,10 @@ export default class AppFrontendDev extends PushAppCommand {
189
191
  break;
190
192
  }
191
193
  case 'react':
192
- case 'react-storefront': {
194
+ case 'react-storefront':
195
+ case 'vinext': {
193
196
  // Vite emits the Local URL as its ready signal — no separate
194
- // detector needed.
197
+ // detector needed. Vinext dev delegates URL printing to Vite.
195
198
  if (/➜\s+local:\s+http:\/\//i.test(string) ||
196
199
  /➜\s+network:/i.test(string)) {
197
200
  if (!serverReadyDetected) {
@@ -1,4 +1,4 @@
1
- import { Args } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import path from 'node:path';
3
3
  import CreateApp, { appHelpMetaBase } from '../create/app.js';
4
4
  export default class InitApp extends CreateApp {
@@ -16,22 +16,32 @@ export default class InitApp extends CreateApp {
16
16
  '$ swell app init my-app',
17
17
  '$ swell app init -t admin -y',
18
18
  '$ swell app init my-app -t admin -y',
19
- '$ swell app init my-app -t admin --frontend astro -y',
20
19
  '$ swell app init my_theme -t theme --storefront-app proxima -y',
21
20
  ];
22
21
  static helpMeta = {
23
22
  ...appHelpMetaBase,
23
+ typeSection: {
24
+ ...appHelpMetaBase.typeSection,
25
+ types: appHelpMetaBase.typeSection.types.map((type) => ({
26
+ ...type,
27
+ optional: [],
28
+ })),
29
+ },
24
30
  usageDirect: '[id] -t <type> [...] -y',
25
31
  };
26
32
  // Inherit all flags from CreateApp (includes -p, -t, -n, -v, -d)
27
33
  // baseFlags from CreateAppCommand (-y, --frontend, etc.) are merged automatically by oclif
28
34
  static flags = {
29
35
  ...CreateApp.flags,
36
+ frontend: Flags.string({ hidden: true }),
30
37
  };
31
38
  async run() {
32
39
  const { args, flags } = await this.parse(InitApp);
33
40
  const { id: argId } = args;
34
41
  const { yes: confirmYes, type, name, version, description, frontend, } = flags;
42
+ if (frontend !== undefined) {
43
+ this.error('app init only creates swell.json. Run swell create frontend --frontend <template> afterward.');
44
+ }
35
45
  // Use provided id argument, or fall back to current directory name
36
46
  const id = argId || process.cwd().split(path.sep).pop() || 'Swell App';
37
47
  await this.createSwellConfig({
@@ -1,3 +1,4 @@
1
+ import type { FrontendTemplate } from '../../lib/apps/frontend-templates.js';
1
2
  import { CreateAppCommand } from '../../create-app-command.js';
2
3
  import { HelpMeta } from '../../help/types.js';
3
4
  /**
@@ -16,7 +17,7 @@ export default class CreateApp extends CreateAppCommand {
16
17
  appType: string;
17
18
  private jsonOutput;
18
19
  log(message?: string, ...args: any[]): void;
19
- createSwellConfig({ allowOverwrite, inputId, inputStorefrontApp, inputType, inputName, inputVersion, inputDescription, inputFrontend, inputIntegrationType, inputIntegrationId, inputYes, nestedPath, }: {
20
+ createSwellConfig({ allowOverwrite, inputId, inputStorefrontApp, inputType, inputName, inputVersion, inputDescription, inputFrontend, inputIntegrationType, inputIntegrationId, inputYes, nestedPath, scaffoldFrontend, }: {
20
21
  allowOverwrite?: boolean;
21
22
  inputDescription?: string;
22
23
  inputFrontend?: string;
@@ -29,11 +30,15 @@ export default class CreateApp extends CreateAppCommand {
29
30
  inputVersion?: string;
30
31
  inputYes?: boolean;
31
32
  nestedPath?: boolean;
33
+ scaffoldFrontend?: boolean;
32
34
  }): Promise<{
33
35
  appId: string;
34
36
  config: import("conf").default<Record<string, unknown>>;
35
37
  installedStorefrontApp: any;
36
38
  swellConfigJson: {
39
+ frontend?: {
40
+ hosting: "managed" | "self-hosted";
41
+ } | undefined;
37
42
  extensions?: any[] | undefined;
38
43
  theme: {
39
44
  storefront: {
@@ -46,6 +51,9 @@ export default class CreateApp extends CreateAppCommand {
46
51
  type: string;
47
52
  version: string;
48
53
  } | {
54
+ frontend?: {
55
+ hosting: "managed" | "self-hosted";
56
+ } | undefined;
49
57
  extensions?: any[] | undefined;
50
58
  permissions: never[];
51
59
  description: string;
@@ -54,6 +62,7 @@ export default class CreateApp extends CreateAppCommand {
54
62
  type: string;
55
63
  version: string;
56
64
  };
65
+ template: FrontendTemplate | undefined;
57
66
  } | undefined>;
58
67
  run(): Promise<void>;
59
68
  }
@@ -5,6 +5,7 @@ import ora from 'ora';
5
5
  import { CreateAppCommand } from '../../create-app-command.js';
6
6
  import { newConfig, swellConfigFileExists } from '../../lib/app-config.js';
7
7
  import { buildCreateAppResult } from '../../lib/apps/create-app-result.js';
8
+ import { setupPackage } from '../../lib/apps/frontend-scaffold.js';
8
9
  import { toAppId, toAppName } from '../../lib/create/index.js';
9
10
  import style from '../../lib/style.js';
10
11
  /**
@@ -33,6 +34,7 @@ export const appHelpMetaBase = {
33
34
  {
34
35
  name: 'integration',
35
36
  description: 'Third-party service',
37
+ optional: ['--frontend'],
36
38
  requires: [
37
39
  '--integration-type',
38
40
  '--integration-id (for payment | shipping | tax)',
@@ -58,16 +60,25 @@ export default class CreateApp extends CreateAppCommand {
58
60
  static examples = [
59
61
  '$ swell create app',
60
62
  '$ swell create app my_app -t admin -y',
61
- '$ swell create app my_app -t admin --frontend astro -y',
63
+ '$ swell create app my_app -t admin --frontend swell-react -y',
64
+ '$ swell create app my_shop -t storefront --frontend swell-spa -y',
62
65
  '$ swell create app my_theme -t theme --storefront-app proxima -y',
63
66
  '$ swell create app my_payment -t integration --integration-type payment --integration-id card -y',
64
- '$ swell create app my_shipping -t integration --integration-type shipping --integration-id fedex -y',
65
67
  ];
66
68
  static helpMeta = {
67
69
  ...appHelpMetaBase,
70
+ frontendTemplates: 'optional',
68
71
  usageDirect: '<id> -t <type> [...] -y',
69
72
  };
70
73
  static flags = {
74
+ 'storefront-app': Flags.string({ description: 'Target storefront app ID' }),
75
+ 'integration-type': Flags.string({
76
+ description: 'Integration: generic | payment | shipping | tax',
77
+ options: ['generic', 'payment', 'shipping', 'tax'],
78
+ }),
79
+ 'integration-id': Flags.string({
80
+ description: 'Service ID (e.g., card, fedex)',
81
+ }),
71
82
  pkg: Flags.string({
72
83
  char: 'p',
73
84
  default: 'npm',
@@ -101,7 +112,7 @@ export default class CreateApp extends CreateAppCommand {
101
112
  if (!this.jsonOutput)
102
113
  super.log(message, ...args);
103
114
  }
104
- async createSwellConfig({ allowOverwrite = true, inputId = '', inputStorefrontApp, inputType, inputName, inputVersion, inputDescription, inputFrontend, inputIntegrationType, inputIntegrationId, inputYes, nestedPath = true, }) {
115
+ async createSwellConfig({ allowOverwrite = true, inputId = '', inputStorefrontApp, inputType, inputName, inputVersion, inputDescription, inputFrontend, inputIntegrationType, inputIntegrationId, inputYes, nestedPath = true, scaffoldFrontend = false, }) {
105
116
  const confirmYes = inputYes;
106
117
  let appId = toAppId(inputId || '');
107
118
  let storefrontInstalledApps;
@@ -292,16 +303,15 @@ export default class CreateApp extends CreateAppCommand {
292
303
  });
293
304
  }
294
305
  }
295
- // check inputFrontend value
296
- if (!installedStorefrontApp &&
297
- confirmYes &&
298
- inputFrontend &&
299
- inputFrontend !== 'none') {
300
- const projectType = this.getProjectType(inputFrontend);
301
- if (!projectType) {
302
- // error should be thrown from getProjectType
303
- return;
304
- }
306
+ let template;
307
+ if (scaffoldFrontend && type !== 'theme') {
308
+ template = await this.selectFrontend(type, {
309
+ frontend: inputFrontend,
310
+ yes: confirmYes,
311
+ });
312
+ }
313
+ else if (inputFrontend && inputFrontend !== 'none') {
314
+ this.error('A theme cannot have a frontend.');
305
315
  }
306
316
  const swellConfigJson = {
307
317
  description,
@@ -321,6 +331,7 @@ export default class CreateApp extends CreateAppCommand {
321
331
  permissions: [],
322
332
  }),
323
333
  ...(extensions && { extensions }),
334
+ ...(template && { frontend: { hosting: template.hosting } }),
324
335
  };
325
336
  const appPath = nestedPath
326
337
  ? path.join(process.cwd(), inputId || appId)
@@ -345,6 +356,12 @@ export default class CreateApp extends CreateAppCommand {
345
356
  }
346
357
  this.log(`\nCreating ${this.appType === 'theme' ? 'theme' : 'app'} ${style.appConfigValue(name)} in ${appPath}/swell.json`);
347
358
  this.log(`\n${JSON.stringify(swellConfigJson, null, 2)}\n`);
359
+ if (template) {
360
+ this.log(`Frontend: ${template.name} (${template.hosting})`);
361
+ if (template.provisionsStorefront) {
362
+ this.log('A development app and storefront will also be created in your test store.');
363
+ }
364
+ }
348
365
  const confirmCreateApp = confirmYes
349
366
  ? true
350
367
  : await confirm({
@@ -356,7 +373,13 @@ export default class CreateApp extends CreateAppCommand {
356
373
  const config = newConfig(nestedPath ? inputId || appId : '');
357
374
  config.clear();
358
375
  config.set(swellConfigJson);
359
- return { appId, config, installedStorefrontApp, swellConfigJson };
376
+ return {
377
+ appId,
378
+ config,
379
+ installedStorefrontApp,
380
+ swellConfigJson,
381
+ template,
382
+ };
360
383
  }
361
384
  async run() {
362
385
  const { args, flags } = await this.parse(CreateApp);
@@ -375,16 +398,17 @@ export default class CreateApp extends CreateAppCommand {
375
398
  inputFrontend: frontend,
376
399
  inputIntegrationType: flags['integration-type'],
377
400
  inputIntegrationId: flags['integration-id'],
401
+ scaffoldFrontend: true,
378
402
  });
379
403
  if (!createParams) {
380
404
  return;
381
405
  }
382
- const { appId, config, installedStorefrontApp, swellConfigJson } = createParams;
406
+ const { appId, config, installedStorefrontApp, swellConfigJson, template } = createParams;
383
407
  !confirmYes && this.log();
384
408
  const spinner = ora();
385
409
  spinner.start('Creating app...');
386
410
  if (pkg !== 'none') {
387
- await this.tryPackageSetup(appId, config, pkg);
411
+ await setupPackage(appId, config, pkg);
388
412
  }
389
413
  await this.createAppConfigFolders(config);
390
414
  spinner.succeed(`${style.appConfigValue(config.get('name'))} app created.\n`);
@@ -394,15 +418,11 @@ export default class CreateApp extends CreateAppCommand {
394
418
  if (installedStorefrontApp) {
395
419
  createdTheme = await this.createThemeApp(config, flags, installedStorefrontApp);
396
420
  }
397
- else if (config.get('type') === 'storefront') {
398
- const storefrontResult = await this.createStorefrontApp(config, flags, {
399
- provisionResources: true,
421
+ else if (template) {
422
+ frontendResult = await this.createFrontend(config, template, pkg, {
423
+ provisionResources: config.get('type') === 'storefront',
400
424
  });
401
- frontendResult = storefrontResult;
402
- resources = storefrontResult.resources;
403
- }
404
- else {
405
- frontendResult = await this.createFrontendApp(config, flags);
425
+ resources = frontendResult.resources;
406
426
  }
407
427
  if (json) {
408
428
  super.log(JSON.stringify(buildCreateAppResult({
@@ -410,7 +430,10 @@ export default class CreateApp extends CreateAppCommand {
410
430
  resources,
411
431
  name: swellConfigJson.name,
412
432
  type: swellConfigJson.type,
413
- frontend: frontendResult?.framework ?? null,
433
+ // Echo the requested value, including the hidden alias.
434
+ frontend: frontendResult
435
+ ? frontend || frontendResult.template
436
+ : null,
414
437
  path: path.dirname(config.path),
415
438
  })));
416
439
  return;
@@ -1,3 +1,4 @@
1
+ import type { HelpMeta } from '../../help/types.js';
1
2
  import { CreateAppCommand } from '../../create-app-command.js';
2
3
  export default class CreateFrontend extends CreateAppCommand {
3
4
  static description: string;
@@ -5,8 +6,9 @@ export default class CreateFrontend extends CreateAppCommand {
5
6
  command: string;
6
7
  description: string;
7
8
  }[];
9
+ static helpMeta: HelpMeta;
8
10
  static flags: {
9
- pkg: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ pkg: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
12
  };
11
13
  run(): Promise<void>;
12
14
  }
@@ -1,7 +1,8 @@
1
1
  import { Flags } from '@oclif/core';
2
- import ora from 'ora';
2
+ import path from 'node:path';
3
3
  import { CreateAppCommand } from '../../create-app-command.js';
4
4
  import swellConfig from '../../lib/app-config.js';
5
+ import { detectPackageManager } from '../../lib/package-manager.js';
5
6
  export default class CreateFrontend extends CreateAppCommand {
6
7
  static description = 'Initialize a frontend app framework and folder structure.';
7
8
  static examples = [
@@ -10,15 +11,22 @@ export default class CreateFrontend extends CreateAppCommand {
10
11
  description: 'Create frontend app following command prompts.',
11
12
  },
12
13
  {
13
- command: 'swell create frontend -y',
14
- description: 'Create frontend app and accept all default values.',
14
+ command: 'swell create frontend --frontend swell-react -y',
15
+ description: 'Add a React frontend hosted by Swell.',
16
+ },
17
+ {
18
+ command: 'swell create frontend --frontend astro -y',
19
+ description: 'Add a self-hosted Astro frontend.',
15
20
  },
16
21
  ];
22
+ static helpMeta = {
23
+ frontendTemplates: 'required',
24
+ usageDirect: '--frontend <template> [...] -y',
25
+ };
17
26
  static flags = {
18
27
  pkg: Flags.string({
19
28
  char: 'p',
20
- default: 'npm',
21
- description: 'Package manager: npm | yarn | pnpm | bun | none',
29
+ description: 'Package manager: npm | yarn | pnpm | bun | none (default: the workspace lockfile, else npm)',
22
30
  options: ['npm', 'yarn', 'pnpm', 'bun', 'none'],
23
31
  }),
24
32
  };
@@ -26,15 +34,15 @@ export default class CreateFrontend extends CreateAppCommand {
26
34
  const { flags } = await this.parse(CreateFrontend);
27
35
  const newSwellConfig = await swellConfig();
28
36
  const appType = newSwellConfig?.get('type');
29
- const spinner = ora();
30
37
  if (appType === 'theme') {
31
- spinner.fail("You can't create app frontend in a theme.");
32
- return;
38
+ this.error("You can't create app frontend in a theme.");
39
+ }
40
+ const template = await this.selectFrontend(appType, flags, true);
41
+ if (template) {
42
+ // Match the manager that app dev and push will detect for this workspace.
43
+ const pkg = flags.pkg ??
44
+ (await detectPackageManager(path.dirname(newSwellConfig.path)));
45
+ await this.scaffoldFrontend(newSwellConfig, template, pkg, true);
33
46
  }
34
- await (appType === 'storefront'
35
- ? this.createStorefrontApp(newSwellConfig, flags, {
36
- directCreate: true,
37
- })
38
- : this.createFrontendApp(newSwellConfig, flags, true));
39
47
  }
40
48
  }
@@ -1,71 +1,30 @@
1
+ import { type FrontendTemplate } from './lib/apps/frontend-templates.js';
1
2
  import { type StorefrontResourceResult } from './lib/apps/storefront-resources.js';
2
- import { PackageManager } from './lib/package-manager.js';
3
3
  import { SwellCommand } from './swell-command.js';
4
4
  export interface FrontendCreationResult {
5
5
  created: boolean;
6
- framework: string | null;
7
- provisionsStorefront: boolean;
8
- }
9
- export interface StorefrontCreationResult extends FrontendCreationResult {
10
6
  resources?: StorefrontResourceResult;
7
+ /** Resolved template slug; the JSON result echoes the requested value. */
8
+ template: string;
11
9
  }
12
- export interface StorefrontCreationOptions {
13
- directCreate?: boolean;
10
+ export interface FrontendCreationOptions {
14
11
  provisionResources?: boolean;
15
12
  }
16
13
  export declare abstract class CreateAppCommand extends SwellCommand {
17
14
  protected commandExample: string;
18
15
  static baseFlags: {
19
16
  frontend: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
20
- 'storefront-app': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
21
- 'integration-type': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
22
- 'integration-id': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
23
17
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
24
18
  };
25
19
  private devApi;
26
20
  constructor(argv: string[], config: any);
27
- addAllowedHostsToAngular(configPath: string): Promise<void>;
28
- addAllowedHostsToAstro(configPath: string): Promise<void>;
29
- addAllowedHostsToNuxt(configPath: string): Promise<void>;
30
- /**
31
- * For frameworks whose dev server is Vite directly. Astro/Nuxt nest the
32
- * vite block inside their own defineConfig — see addViteAllowedHosts.
33
- */
34
- addAllowedHostsToVite(configPath: string): Promise<void>;
35
- /**
36
- * Add vite allowedHosts configuration to a framework config file.
37
- * Supports config files that use a defineX({}) pattern (Astro, Nuxt, etc.)
38
- */
39
- private addViteAllowedHosts;
40
21
  createAppConfigFolders(swellConfig: any): Promise<void>;
41
- createFrontendApp(swellConfig: any, flags: any, directCreate?: boolean, kind?: string): Promise<FrontendCreationResult>;
42
- createStorefrontApp(swellConfig: any, flags: any, options?: StorefrontCreationOptions): Promise<StorefrontCreationResult>;
22
+ selectFrontend(appType: string, flags: any, direct?: boolean): Promise<FrontendTemplate | undefined>;
23
+ /** Scaffold the local frontend workspace without touching remote resources. */
24
+ scaffoldFrontend(swellConfig: any, template: FrontendTemplate, packageManager?: string, directCreate?: boolean): Promise<FrontendCreationResult>;
25
+ /** Scaffold the frontend and, when asked, its remote storefront resources. */
26
+ createFrontend(swellConfig: any, template: FrontendTemplate, packageManager: string | undefined, options?: FrontendCreationOptions): Promise<FrontendCreationResult>;
43
27
  createThemeApp(config: any, flags: any, installedStorefrontApp: any): Promise<boolean>;
44
- doesPackageManagerExist(packageManager: string): Promise<boolean>;
45
- execWithStdio(cwd: string, command: string, onOutput?: (string: string) => any | false): Promise<void>;
46
- findPackageManager(pkg?: string): Promise<string | undefined>;
47
28
  getInstalledStorefrontApps(): Promise<any[] | boolean>;
48
- getProjectType(frameworkType: string): {
49
- installCommand: string;
50
- appTypes?: string[] | undefined;
51
- buildCommand?: string | undefined;
52
- devCommand: string;
53
- displayOrder?: number | undefined;
54
- mainPackage: string;
55
- name: string;
56
- provisionsStorefront?: boolean | undefined;
57
- slug: string;
58
- };
59
- setupPackage(name: string, config: any, pkg: PackageManager): Promise<void>;
60
- tryPackageSetup(name: string, config: any, pkg: PackageManager): Promise<void>;
61
- /**
62
- * Fix wrangler.jsonc `main` field when it points to a build artifact that
63
- * doesn't exist at dev time. The @cloudflare/vite-plugin validates this path
64
- * at startup and crashes during `astro dev` if it can't resolve it.
65
- *
66
- * Only replaces the exact legacy value that C3 currently scaffolds.
67
- * Once C3 updates its template, this becomes a no-op.
68
- */
69
- private fixAstroWranglerEntrypoint;
70
- private addFrontendAllowedHosts;
29
+ resolveFrontendTemplate(value: string): FrontendTemplate;
71
30
  }