@swell/cli 2.9.6 → 2.9.8

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.
@@ -14,6 +14,7 @@ export declare abstract class AppCommand extends SwellCommand {
14
14
  static baseFlags: {
15
15
  'app-path': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
16
16
  };
17
+ static requiresSwellConfig: boolean;
17
18
  appPath: string;
18
19
  swellConfig: LocalApp;
19
20
  init(): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import * as path from 'node:path';
3
- import { default as swellConfig } from './lib/app-config.js';
3
+ import { default as swellConfig, swellConfigFileExists, } from './lib/app-config.js';
4
+ import style from './lib/style.js';
4
5
  import { SwellCommand } from './swell-command.js';
5
6
  /**
6
7
  * A base class for Swell CLI commands that do not require an app to be saved
@@ -20,6 +21,8 @@ export class AppCommand extends SwellCommand {
20
21
  description: 'Path to your app directory',
21
22
  }),
22
23
  };
24
+ // set to false for commands that may run before an app directory exists (pull)
25
+ static requiresSwellConfig = true;
23
26
  // the local path to the app directory
24
27
  appPath = '';
25
28
  // local app configuration, what developers define
@@ -35,8 +38,17 @@ export class AppCommand extends SwellCommand {
35
38
  // ensure the base flags are available
36
39
  klass.flags = { ...klass.flags, ...AppCommand.baseFlags };
37
40
  const { flags } = await this.parse(klass);
41
+ const targetDir = flags['app-path']
42
+ ? path.resolve(flags['app-path'])
43
+ : process.cwd();
44
+ // swell.json is only ever looked up in this exact directory
45
+ // Searching parent directories (the previous behavior) can silently pick up an unrelated app's config
46
+ if (klass.requiresSwellConfig &&
47
+ !(await swellConfigFileExists(path.join(targetDir, 'swell.json')))) {
48
+ this.error(`No swell.json found in ${style.path(targetDir)}. Run this command from your app's root directory, or pass --app-path.`);
49
+ }
38
50
  // if the user passed an app path, use it: reload the app config
39
- this.swellConfig = await swellConfig(flags['app-path']);
51
+ this.swellConfig = await swellConfig(targetDir);
40
52
  this.appPath = path.resolve(this.swellConfig.swDirPath);
41
53
  }
42
54
  }
@@ -11,6 +11,7 @@ export default class AppDev extends PushAppCommand {
11
11
  function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
12
  'storefront-id': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
13
  'storefront-select': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
14
+ 'json-events': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
14
15
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
15
16
  };
16
17
  static orientation: {
@@ -24,10 +25,12 @@ export default class AppDev extends PushAppCommand {
24
25
  tmpDir: string;
25
26
  frontendPort: number | null;
26
27
  isCleaningUp: boolean;
28
+ jsonEvents: boolean;
27
29
  run(): Promise<void>;
28
30
  private cleanupOnExit;
29
31
  runAppFrontendDevIfApplicable(frontendPort?: number): Promise<number | undefined>;
30
32
  private createFunctionRouter;
33
+ private emitLifecycleEvent;
31
34
  private createTmpDirectory;
32
35
  private generateWranglerConfig;
33
36
  private getAppFunctions;
@@ -42,6 +42,10 @@ export default class AppDev extends PushAppCommand {
42
42
  default: false,
43
43
  description: 'for storefront apps, prompt to select a storefront to preview',
44
44
  }),
45
+ 'json-events': Flags.boolean({
46
+ description: 'emit machine-readable dev server lifecycle events',
47
+ hidden: true,
48
+ }),
45
49
  // Declared here so oclif accepts -y; passed through to app:frontend:dev via this.argv
46
50
  yes: Flags.boolean({
47
51
  char: 'y',
@@ -65,10 +69,12 @@ export default class AppDev extends PushAppCommand {
65
69
  frontendPort = null;
66
70
  // Guard against multiple cleanup calls
67
71
  isCleaningUp = false;
72
+ jsonEvents = false;
68
73
  async run() {
69
74
  const { flags } = await this.parse(AppDev);
70
75
  const { port, 'frontend-port': frontendPort } = flags;
71
76
  const noPush = flags['no-push'];
77
+ this.jsonEvents = flags['json-events'];
72
78
  this.functionFilter = flags.function ?? null;
73
79
  if (!(await this.ensureAppExists(undefined, false))) {
74
80
  return;
@@ -88,6 +94,10 @@ export default class AppDev extends PushAppCommand {
88
94
  process.on('SIGTERM', this.cleanupOnExit.bind(this));
89
95
  const serverPort = await this.startProxyServer(port);
90
96
  await this.startAppFunctionServer(spinner, serverPort);
97
+ this.emitLifecycleEvent('preview:proxy-ready', {
98
+ proxyPort: serverPort,
99
+ storefrontId: this.storefront?.id ?? null,
100
+ });
91
101
  await this.runAppFrontendDevIfApplicable(frontendPort);
92
102
  }
93
103
  async cleanupOnExit() {
@@ -103,6 +113,9 @@ export default class AppDev extends PushAppCommand {
103
113
  catch {
104
114
  // Ignore errors during cleanup
105
115
  }
116
+ this.emitLifecycleEvent('preview:stopped', {
117
+ storefrontId: this.storefront?.id ?? null,
118
+ });
106
119
  this.log();
107
120
  // eslint-disable-next-line no-process-exit, unicorn/no-process-exit
108
121
  process.exit();
@@ -199,7 +212,18 @@ export default class AppDev extends PushAppCommand {
199
212
  : ''}`);
200
213
  }
201
214
  });
202
- server.listen(serverPort);
215
+ await new Promise((resolve, reject) => {
216
+ server.once('error', reject);
217
+ server.listen(serverPort, () => {
218
+ server.off('error', reject);
219
+ resolve();
220
+ });
221
+ });
222
+ }
223
+ emitLifecycleEvent(type, data) {
224
+ if (!this.jsonEvents)
225
+ return;
226
+ this.log(JSON.stringify({ type, ...data }));
203
227
  }
204
228
  async createTmpDirectory() {
205
229
  const tmpBase = path.join(os.tmpdir(), 'swell-cli');
@@ -11,6 +11,7 @@ export default class AppFrontendDev extends PushAppCommand {
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
13
  function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
14
+ 'json-events': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
14
15
  };
15
16
  static orientation: {
16
17
  env: string;
@@ -4,6 +4,7 @@ export default class AppPull extends PushAppCommand {
4
4
  appId: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
5
5
  targetPath: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
6
6
  };
7
+ static requiresSwellConfig: boolean;
7
8
  static description: string;
8
9
  static examples: string[];
9
10
  static flags: {
@@ -11,6 +11,7 @@ export default class AppPull extends PushAppCommand {
11
11
  description: 'path to download app files into',
12
12
  }),
13
13
  };
14
+ static requiresSwellConfig = false;
14
15
  static description = `Pull all app files, a specific file, or a specific configuration
15
16
  type from an app in your store's test environment to your local machine.
16
17
 
@@ -14,6 +14,8 @@ export default class CreateApp extends CreateAppCommand {
14
14
  static helpMeta: HelpMeta;
15
15
  static flags: any;
16
16
  appType: string;
17
+ private jsonOutput;
18
+ log(message?: string, ...args: any[]): void;
17
19
  createSwellConfig({ allowOverwrite, inputId, inputStorefrontApp, inputType, inputName, inputVersion, inputDescription, inputFrontend, inputIntegrationType, inputIntegrationId, inputYes, nestedPath, }: {
18
20
  allowOverwrite?: boolean;
19
21
  inputDescription?: string;
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import ora from 'ora';
5
5
  import { CreateAppCommand } from '../../create-app-command.js';
6
6
  import { newConfig, swellConfigFileExists } from '../../lib/app-config.js';
7
+ import { buildCreateAppResult } from '../../lib/apps/create-app-result.js';
7
8
  import { toAppId, toAppName } from '../../lib/create/index.js';
8
9
  import style from '../../lib/style.js';
9
10
  /**
@@ -90,8 +91,16 @@ export default class CreateApp extends CreateAppCommand {
90
91
  char: 'd',
91
92
  description: 'Description',
92
93
  }),
94
+ json: Flags.boolean({
95
+ description: 'Output the command result as JSON',
96
+ }),
93
97
  };
94
98
  appType = '';
99
+ jsonOutput = false;
100
+ log(message, ...args) {
101
+ if (!this.jsonOutput)
102
+ super.log(message, ...args);
103
+ }
95
104
  async createSwellConfig({ allowOverwrite = true, inputId = '', inputStorefrontApp, inputType, inputName, inputVersion, inputDescription, inputFrontend, inputIntegrationType, inputIntegrationId, inputYes, nestedPath = true, }) {
96
105
  const confirmYes = inputYes;
97
106
  let appId = toAppId(inputId || '');
@@ -352,7 +361,8 @@ export default class CreateApp extends CreateAppCommand {
352
361
  async run() {
353
362
  const { args, flags } = await this.parse(CreateApp);
354
363
  const { id } = args;
355
- const { pkg, yes, name, type, version, description, frontend } = flags;
364
+ const { pkg, yes, name, type, version, description, frontend, json } = flags;
365
+ this.jsonOutput = Boolean(json);
356
366
  const confirmYes = Boolean(yes);
357
367
  const createParams = await this.createSwellConfig({
358
368
  inputId: id,
@@ -369,7 +379,7 @@ export default class CreateApp extends CreateAppCommand {
369
379
  if (!createParams) {
370
380
  return;
371
381
  }
372
- const { appId, config, installedStorefrontApp } = createParams;
382
+ const { appId, config, installedStorefrontApp, swellConfigJson } = createParams;
373
383
  !confirmYes && this.log();
374
384
  const spinner = ora();
375
385
  spinner.start('Creating app...');
@@ -378,21 +388,37 @@ export default class CreateApp extends CreateAppCommand {
378
388
  }
379
389
  await this.createAppConfigFolders(config);
380
390
  spinner.succeed(`${style.appConfigValue(config.get('name'))} app created.\n`);
381
- let createdFrontend;
391
+ let frontendResult;
392
+ let resources;
382
393
  let createdTheme;
383
394
  if (installedStorefrontApp) {
384
395
  createdTheme = await this.createThemeApp(config, flags, installedStorefrontApp);
385
396
  }
386
397
  else if (config.get('type') === 'storefront') {
387
- createdFrontend = await this.createStorefrontApp(config, flags);
398
+ const storefrontResult = await this.createStorefrontApp(config, flags, {
399
+ provisionResources: true,
400
+ });
401
+ frontendResult = storefrontResult;
402
+ resources = storefrontResult.resources;
388
403
  }
389
404
  else {
390
- createdFrontend = await this.createFrontendApp(config, flags);
405
+ frontendResult = await this.createFrontendApp(config, flags);
406
+ }
407
+ if (json) {
408
+ super.log(JSON.stringify(buildCreateAppResult({
409
+ privateId: swellConfigJson.id,
410
+ resources,
411
+ name: swellConfigJson.name,
412
+ type: swellConfigJson.type,
413
+ frontend: frontendResult?.framework ?? null,
414
+ path: path.dirname(config.path),
415
+ })));
416
+ return;
391
417
  }
392
418
  this.log('\nNext steps:');
393
419
  this.log(`Run ${style.command('swell app push')} to push configurations to your test store.`);
394
420
  this.log(`Run ${style.command('swell app install')} to install the app in another store or environment.`);
395
- if (createdFrontend) {
421
+ if (frontendResult?.created) {
396
422
  this.log(`Run ${style.command('swell app frontend dev')} to start a local dev server for your frontend app.`);
397
423
  }
398
424
  else if (createdTheme) {
@@ -32,7 +32,9 @@ export default class CreateFrontend extends CreateAppCommand {
32
32
  return;
33
33
  }
34
34
  await (appType === 'storefront'
35
- ? this.createStorefrontApp(newSwellConfig, flags, true)
35
+ ? this.createStorefrontApp(newSwellConfig, flags, {
36
+ directCreate: true,
37
+ })
36
38
  : this.createFrontendApp(newSwellConfig, flags, true));
37
39
  }
38
40
  }
@@ -1,5 +1,18 @@
1
+ import { type StorefrontResourceResult } from './lib/apps/storefront-resources.js';
1
2
  import { PackageManager } from './lib/package-manager.js';
2
3
  import { SwellCommand } from './swell-command.js';
4
+ export interface FrontendCreationResult {
5
+ created: boolean;
6
+ framework: string | null;
7
+ provisionsStorefront: boolean;
8
+ }
9
+ export interface StorefrontCreationResult extends FrontendCreationResult {
10
+ resources?: StorefrontResourceResult;
11
+ }
12
+ export interface StorefrontCreationOptions {
13
+ directCreate?: boolean;
14
+ provisionResources?: boolean;
15
+ }
3
16
  export declare abstract class CreateAppCommand extends SwellCommand {
4
17
  protected commandExample: string;
5
18
  static baseFlags: {
@@ -25,8 +38,8 @@ export declare abstract class CreateAppCommand extends SwellCommand {
25
38
  */
26
39
  private addViteAllowedHosts;
27
40
  createAppConfigFolders(swellConfig: any): Promise<void>;
28
- createFrontendApp(swellConfig: any, flags: any, directCreate?: boolean, kind?: string): Promise<boolean>;
29
- createStorefrontApp(swellConfig: any, flags: any, directCreate?: boolean): Promise<boolean>;
41
+ createFrontendApp(swellConfig: any, flags: any, directCreate?: boolean, kind?: string): Promise<FrontendCreationResult>;
42
+ createStorefrontApp(swellConfig: any, flags: any, options?: StorefrontCreationOptions): Promise<StorefrontCreationResult>;
30
43
  createThemeApp(config: any, flags: any, installedStorefrontApp: any): Promise<boolean>;
31
44
  doesPackageManagerExist(packageManager: string): Promise<boolean>;
32
45
  execWithStdio(cwd: string, command: string, onOutput?: (string: string) => any | false): Promise<void>;
@@ -40,6 +53,7 @@ export declare abstract class CreateAppCommand extends SwellCommand {
40
53
  displayOrder?: number | undefined;
41
54
  mainPackage: string;
42
55
  name: string;
56
+ provisionsStorefront?: boolean | undefined;
43
57
  slug: string;
44
58
  };
45
59
  setupPackage(name: string, config: any, pkg: PackageManager): Promise<void>;
@@ -9,6 +9,7 @@ import { promisify } from 'node:util';
9
9
  import ora from 'ora';
10
10
  import Api from './lib/api.js';
11
11
  import { FrontendProjectTypes, getFrontendProjectValidValues, getAllConfigPaths, getFrontendProjectSlugs, writeFile, writeJsonFile, } from './lib/apps/index.js';
12
+ import { ensureStorefrontResources, } from './lib/apps/storefront-resources.js';
12
13
  import { getPackageManagerCommands, transformCreateCommand, } from './lib/package-manager.js';
13
14
  import style from './lib/style.js';
14
15
  import { SwellCommand } from './swell-command.js';
@@ -208,12 +209,11 @@ export class CreateAppCommand extends SwellCommand {
208
209
  await fs.writeFile(configFilePath, content, 'utf8');
209
210
  }
210
211
  async createAppConfigFolders(swellConfig) {
212
+ const configPath = path.dirname(swellConfig.path);
211
213
  for (const type of getAllConfigPaths(swellConfig.get('type'))) {
212
214
  // Create a config folder
213
215
  // eslint-disable-next-line no-await-in-loop
214
- await execAsync(`mkdir -p ${type}`, {
215
- cwd: path.dirname(swellConfig.path),
216
- });
216
+ await fs.mkdir(path.join(configPath, type), { recursive: true });
217
217
  }
218
218
  }
219
219
  async createFrontendApp(swellConfig, flags, directCreate, kind) {
@@ -287,7 +287,11 @@ export class CreateAppCommand extends SwellCommand {
287
287
  const files = await fs.readdir(frontendPath);
288
288
  if (files.length > 0) {
289
289
  spinner.fail('frontend/ folder is not empty. Please remove existing files before scaffolding.');
290
- return false;
290
+ return {
291
+ created: false,
292
+ framework: projectType.slug,
293
+ provisionsStorefront: Boolean(projectType.provisionsStorefront),
294
+ };
291
295
  }
292
296
  }
293
297
  catch {
@@ -295,14 +299,31 @@ export class CreateAppCommand extends SwellCommand {
295
299
  }
296
300
  spinner.start(`Creating ${projectType?.name} frontend app (this may take a while)...`);
297
301
  try {
298
- await execAsync(`mkdir -p frontend`, {
299
- cwd: configPath,
300
- });
301
- // Transform install command for the selected package manager
302
- const installCommand = transformCreateCommand(projectType.installCommand, pkg);
303
- await execAsync(installCommand, {
304
- cwd: configPath,
302
+ await fs.mkdir(path.join(configPath, 'frontend'), {
303
+ recursive: true,
305
304
  });
305
+ // SWELL_LOCAL_TEMPLATE_PATH overrides the scaffold for local dev — copies
306
+ // the directory at the path into frontend/ instead of running the install
307
+ // command. Used to iterate on the canonical template repo without pushing.
308
+ const localTemplatePath = process.env.SWELL_LOCAL_TEMPLATE_PATH;
309
+ if (localTemplatePath) {
310
+ const source = path.resolve(localTemplatePath);
311
+ const dest = path.join(configPath, 'frontend');
312
+ await fs.cp(source, dest, {
313
+ recursive: true,
314
+ filter: (src) => !src.includes('/node_modules') &&
315
+ !src.endsWith('/.git') &&
316
+ !src.includes('/dist') &&
317
+ !src.includes('/.wrangler'),
318
+ });
319
+ }
320
+ else {
321
+ // Transform install command for the selected package manager
322
+ const installCommand = transformCreateCommand(projectType.installCommand, pkg);
323
+ await execAsync(installCommand, {
324
+ cwd: configPath,
325
+ });
326
+ }
306
327
  // Use this command to debug output, i.e.e when command becomes non-responsive
307
328
  /* await this.execWithStdio(
308
329
  configPath,
@@ -317,7 +338,11 @@ export class CreateAppCommand extends SwellCommand {
317
338
  .join('\n') || (error.message ?? '').trim();
318
339
  if (detail)
319
340
  this.log(detail);
320
- return false;
341
+ return {
342
+ created: false,
343
+ framework: projectType.slug,
344
+ provisionsStorefront: Boolean(projectType.provisionsStorefront),
345
+ };
321
346
  }
322
347
  // Ensure frontend package.json has correct name for workspace
323
348
  try {
@@ -346,12 +371,34 @@ export class CreateAppCommand extends SwellCommand {
346
371
  if (directCreate) {
347
372
  this.log();
348
373
  }
349
- return true;
374
+ return {
375
+ created: true,
376
+ framework: projectType.slug,
377
+ provisionsStorefront: Boolean(projectType.provisionsStorefront),
378
+ };
350
379
  }
351
- return false;
380
+ return {
381
+ created: false,
382
+ framework: null,
383
+ provisionsStorefront: false,
384
+ };
352
385
  }
353
- async createStorefrontApp(swellConfig, flags, directCreate) {
354
- return this.createFrontendApp(swellConfig, flags, directCreate, 'storefront');
386
+ async createStorefrontApp(swellConfig, flags, options = {}) {
387
+ const frontend = await this.createFrontendApp(swellConfig, flags, options.directCreate, 'storefront');
388
+ if (!options.provisionResources ||
389
+ !frontend.created ||
390
+ !frontend.provisionsStorefront) {
391
+ return frontend;
392
+ }
393
+ const resources = await ensureStorefrontResources(this.api, {
394
+ description: swellConfig.get('description'),
395
+ name: swellConfig.get('name'),
396
+ privateId: swellConfig.get('id'),
397
+ type: 'storefront',
398
+ version: swellConfig.get('version'),
399
+ });
400
+ this.log(`Storefront ${style.appConfigValue(resources.storefront.id)} created for app ${style.appConfigValue(resources.app.id)}.`);
401
+ return { ...frontend, resources };
355
402
  }
356
403
  async createThemeApp(config, flags, installedStorefrontApp) {
357
404
  const spinner = ora();
@@ -369,16 +416,14 @@ export class CreateAppCommand extends SwellCommand {
369
416
  this.log();
370
417
  spinner.start(`Creating theme template...`);
371
418
  try {
372
- await execAsync(`mkdir -p theme`, {
373
- cwd: configPath,
374
- });
419
+ await fs.mkdir(path.join(configPath, 'theme'), { recursive: true });
375
420
  for (const themeConfig of defaultThemeConfigs.results) {
376
421
  const filePath = themeConfig.file_path.replace(/^frontend\/theme-template\//, '');
377
422
  const fileContent = themeConfig.file_data;
378
423
  const isJson = filePath.endsWith('.json');
379
424
  // eslint-disable-next-line no-await-in-loop
380
- await execAsync(`mkdir -p ${path.dirname(filePath)}`, {
381
- cwd: configPath,
425
+ await fs.mkdir(path.join(configPath, path.dirname(filePath)), {
426
+ recursive: true,
382
427
  });
383
428
  // eslint-disable-next-line no-await-in-loop
384
429
  await (isJson
@@ -0,0 +1,31 @@
1
+ import type { StorefrontResourceResult } from './storefront-resources.js';
2
+ export interface CreateAppResultInput {
3
+ frontend: string | null;
4
+ name: string;
5
+ path: string;
6
+ privateId: string;
7
+ resources?: StorefrontResourceResult;
8
+ type: string;
9
+ }
10
+ export interface CreateAppResult {
11
+ app: {
12
+ name: string;
13
+ privateId: string;
14
+ resourceId: string | null;
15
+ type: string;
16
+ };
17
+ frontend: string | null;
18
+ path: string;
19
+ storefront: {
20
+ resourceId: string;
21
+ } | null;
22
+ }
23
+ /**
24
+ * Build the stable JSON contract for every `swell create app` variant.
25
+ * Remote resource IDs are nullable because only provisioning frontends create
26
+ * them; the shape itself never changes by app type or frontend.
27
+ *
28
+ * @param input completed local and optional remote creation result
29
+ * @returns serializable command result
30
+ */
31
+ export declare function buildCreateAppResult(input: CreateAppResultInput): CreateAppResult;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Build the stable JSON contract for every `swell create app` variant.
3
+ * Remote resource IDs are nullable because only provisioning frontends create
4
+ * them; the shape itself never changes by app type or frontend.
5
+ *
6
+ * @param input completed local and optional remote creation result
7
+ * @returns serializable command result
8
+ */
9
+ export function buildCreateAppResult(input) {
10
+ return {
11
+ app: {
12
+ privateId: input.privateId,
13
+ resourceId: input.resources?.app.id ?? null,
14
+ name: input.name,
15
+ type: input.type,
16
+ },
17
+ storefront: input.resources
18
+ ? { resourceId: input.resources.storefront.id }
19
+ : null,
20
+ frontend: input.frontend,
21
+ path: input.path,
22
+ };
23
+ }
@@ -2,7 +2,7 @@
2
2
  /// <reference types="node" resolution-mode="require"/>
3
3
  import { AppConfig } from './app-config.js';
4
4
  export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js';
5
- export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, } from './paths.js';
5
+ export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, toPosixPath, } from './paths.js';
6
6
  export declare const PUSH_CONCURRENCY = 3;
7
7
  /**
8
8
  * An app as defined by the Swell API. This is different than an app
@@ -128,6 +128,7 @@ export interface FrontendProjectType {
128
128
  installCommand?: string;
129
129
  mainPackage: string;
130
130
  name: string;
131
+ provisionsStorefront?: boolean;
131
132
  slug: string;
132
133
  }
133
134
  export declare const CUSTOM_FRAMEWORK_SLUG = "custom";
@@ -5,7 +5,7 @@ import * as path from 'node:path';
5
5
  import { detectPackageManager, transformCommand } from '../package-manager.js';
6
6
  import { AppConfig } from './app-config.js';
7
7
  export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js';
8
- export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, } from './paths.js';
8
+ export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, toPosixPath, } from './paths.js';
9
9
  export const PUSH_CONCURRENCY = 3;
10
10
  export var SwellJsonFields;
11
11
  (function (SwellJsonFields) {
@@ -130,6 +130,7 @@ export const FrontendProjectTypes = [
130
130
  installCommand: 'npm create cloudflare@latest -- frontend --template=swellstores/storefront-react-ai-template#main --deploy=false --git=false --no-agents',
131
131
  mainPackage: '@swell/storefront-app-sdk-react',
132
132
  name: 'Swell React Storefront',
133
+ provisionsStorefront: true,
133
134
  slug: 'react-storefront',
134
135
  },
135
136
  {
@@ -1,5 +1,6 @@
1
1
  import { type GlobbyFilterFunction } from 'globby';
2
2
  import { ConfigType } from './index.js';
3
+ export declare function toPosixPath(inputPath: string): string;
3
4
  export declare function globAllFilesByPath(appPath: string, dirPath?: string): Promise<string[]>;
4
5
  export declare function getGlobIgnorePathsChecker(appPath: string): Promise<GlobbyFilterFunction>;
5
6
  export declare function allConfigDirsPaths(appPath: string): Generator<{
@@ -27,11 +27,15 @@ function globOptions(appPath, options) {
27
27
  ],
28
28
  };
29
29
  }
30
+ // use forward slashes to support Windows
31
+ export function toPosixPath(inputPath) {
32
+ return inputPath.split(path.sep).join('/');
33
+ }
30
34
  function globFilesSync(pattern, appPath, dirPath = '.', options = {}) {
31
- return globbySync(`${dirPath}${path.sep}${pattern}`, globOptions(appPath, options));
35
+ return globbySync(`${toPosixPath(dirPath)}/${pattern}`, globOptions(appPath, options));
32
36
  }
33
37
  function globFiles(pattern, appPath, dirPath = '.', options = {}) {
34
- return globby(`${dirPath}${path.sep}${pattern}`, globOptions(appPath, options));
38
+ return globby(`${toPosixPath(dirPath)}/${pattern}`, globOptions(appPath, options));
35
39
  }
36
40
  export function globAllFilesByPath(appPath, dirPath) {
37
41
  return globFiles('**', appPath, dirPath);
@@ -0,0 +1,28 @@
1
+ import Api from '../api.js';
2
+ export interface StorefrontResourceInput {
3
+ description?: string;
4
+ name: string;
5
+ privateId: string;
6
+ type: 'storefront';
7
+ version: string;
8
+ }
9
+ export interface StorefrontResourceResult {
10
+ app: {
11
+ id: string;
12
+ privateId: string;
13
+ };
14
+ storefront: {
15
+ id: string;
16
+ };
17
+ }
18
+ /**
19
+ * Ensure the remote development app and its storefront exist.
20
+ *
21
+ * `swell create app --frontend react-storefront` calls this after the local
22
+ * scaffold succeeds. Retrying is safe: existing resources are reused.
23
+ *
24
+ * @param api authenticated Swell API client
25
+ * @param input storefront app identity from the generated swell.json
26
+ * @returns the stable remote app and storefront IDs
27
+ */
28
+ export declare function ensureStorefrontResources(api: Api, input: StorefrontResourceInput): Promise<StorefrontResourceResult>;
@@ -0,0 +1,60 @@
1
+ import config from '../config.js';
2
+ /**
3
+ * Ensure the remote development app and its storefront exist.
4
+ *
5
+ * `swell create app --frontend react-storefront` calls this after the local
6
+ * scaffold succeeds. Retrying is safe: existing resources are reused.
7
+ *
8
+ * @param api authenticated Swell API client
9
+ * @param input storefront app identity from the generated swell.json
10
+ * @returns the stable remote app and storefront IDs
11
+ */
12
+ export async function ensureStorefrontResources(api, input) {
13
+ const storeId = config.getDefaultStore();
14
+ await api.setStoreEnv(storeId, 'test');
15
+ let app;
16
+ try {
17
+ const existing = await api.get({
18
+ adminPath: `/apps/${input.privateId}`,
19
+ });
20
+ if (existing?.client_id === storeId) {
21
+ app = existing;
22
+ }
23
+ }
24
+ catch {
25
+ // App not found — create it below.
26
+ }
27
+ if (!app) {
28
+ app = await api.post({ adminPath: '/apps' }, {
29
+ body: {
30
+ description: input.description,
31
+ name: input.name,
32
+ private_id: input.privateId,
33
+ type: input.type,
34
+ version: input.version,
35
+ permissions: [],
36
+ },
37
+ });
38
+ }
39
+ const storefronts = await api.get({
40
+ adminPath: `/apps/${app.id}/storefronts`,
41
+ });
42
+ let storefront = storefronts?.results?.[0];
43
+ if (!storefront) {
44
+ storefront = await api.post({ adminPath: '/storefronts' }, {
45
+ body: {
46
+ app_id: app.id,
47
+ name: input.name,
48
+ },
49
+ });
50
+ }
51
+ return {
52
+ app: {
53
+ id: app.id,
54
+ privateId: input.privateId,
55
+ },
56
+ storefront: {
57
+ id: storefront.id,
58
+ },
59
+ };
60
+ }
@@ -6,7 +6,7 @@ import * as path from 'node:path';
6
6
  import Stream from 'node:stream';
7
7
  import ora from 'ora';
8
8
  import { default as swellConfig } from './lib/app-config.js';
9
- import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExistsAsync, findAppConfig, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
9
+ import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExistsAsync, findAppConfig, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashString, isPathDirectory, toPosixPath, } from './lib/apps/index.js';
10
10
  import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
11
11
  import { slugFromApp } from './lib/apps/slug.js';
12
12
  import { default as localConfig } from './lib/config.js';
@@ -32,14 +32,12 @@ export class PushAppCommand extends RemoteAppCommand {
32
32
  if (!filename) {
33
33
  return;
34
34
  }
35
- if (this.watchingIgnoreFilter && this.watchingIgnoreFilter(filename)) {
35
+ const configFile = toPosixPath(filename);
36
+ if (this.watchingIgnoreFilter && this.watchingIgnoreFilter(configFile)) {
36
37
  return;
37
38
  }
38
- const configFile = filename;
39
- const pathParsed = path.parse(configFile);
40
- const fileDirs = pathParsed.dir.split(path.sep);
41
39
  // only the first directory is relevant for us
42
- const configDir = fileDirs[0];
40
+ const configDir = configFile.split('/')[0];
43
41
  // we only want to watch files in the root of the app directory
44
42
  // and ignore build files etc
45
43
  // TODO: renaming a folder doesn't seem to work
@@ -433,7 +431,7 @@ export class PushAppCommand extends RemoteAppCommand {
433
431
  async getFrontendDeploymentHash() {
434
432
  const localConfigs = await this.getLocalAppConfigs();
435
433
  let frontendHashes = localConfigs
436
- .filter((config) => config.filePath?.startsWith(`frontend${path.sep}`))
434
+ .filter((config) => config.filePath?.startsWith('frontend/'))
437
435
  .map((config) => config.hash)
438
436
  .sort()
439
437
  .join('|');
@@ -675,16 +673,18 @@ export class PushAppCommand extends RemoteAppCommand {
675
673
  this.log(`View your storefront at ${style.link(this.storefrontFrontendUrl(currentStore, storefront, branchId))}.`);
676
674
  }
677
675
  async pushFile(relativePath) {
678
- const basePath = relativePath.split(path.sep)[0];
676
+ const relativePathPosix = toPosixPath(relativePath);
677
+ const basePath = relativePathPosix.split('/')[0];
679
678
  const configType = getConfigTypeFromPath(basePath) || ConfigType.FILE;
680
- const config = await appConfigFromFile(relativePath, configType, this.appPath);
679
+ const config = await appConfigFromFile(relativePathPosix, configType, this.appPath);
681
680
  await this.pushRemoteFile(config);
682
681
  }
683
682
  async pushFilePath(relativePath, force) {
684
683
  const topDir = relativePath.split(path.sep)[0];
685
684
  const configType = getConfigTypeFromPath(topDir) || ConfigType.FILE;
686
685
  const configTypeKey = getConfigTypeKeyFromValue(configType) || ConfigType.FILE;
687
- const exAppConfigs = (this.app?.configs || []).filter((config) => config.filePath?.startsWith(`${relativePath}/`));
686
+ const relativePathPosix = toPosixPath(relativePath);
687
+ const exAppConfigs = (this.app?.configs || []).filter((config) => config.filePath?.startsWith(`${relativePathPosix}/`));
688
688
  const promises = [];
689
689
  for (const { configFile } of allConfigFilesInDir(this.appPath, relativePath, configTypeKey)) {
690
690
  promises.push(appConfigFromFile(configFile, configType, this.appPath));
@@ -708,6 +708,7 @@
708
708
  "pluginType": "core",
709
709
  "strict": true,
710
710
  "enableJsonFlag": false,
711
+ "requiresSwellConfig": true,
711
712
  "delayOrientation": false,
712
713
  "orientation": {
713
714
  "env": "test"
@@ -779,6 +780,13 @@
779
780
  "allowNo": false,
780
781
  "type": "boolean"
781
782
  },
783
+ "json-events": {
784
+ "description": "emit machine-readable dev server lifecycle events",
785
+ "hidden": true,
786
+ "name": "json-events",
787
+ "allowNo": false,
788
+ "type": "boolean"
789
+ },
782
790
  "yes": {
783
791
  "char": "y",
784
792
  "description": "skip prompts (non-interactive mode)",
@@ -844,6 +852,7 @@
844
852
  "strict": true,
845
853
  "summary": "Show information about your Swell app.",
846
854
  "enableJsonFlag": false,
855
+ "requiresSwellConfig": true,
847
856
  "delayOrientation": false,
848
857
  "isESM": true,
849
858
  "relativePath": [
@@ -976,6 +985,12 @@
976
985
  "hasDynamicHelp": false,
977
986
  "multiple": false,
978
987
  "type": "option"
988
+ },
989
+ "json": {
990
+ "description": "Output the command result as JSON",
991
+ "name": "json",
992
+ "allowNo": false,
993
+ "type": "boolean"
979
994
  }
980
995
  },
981
996
  "hasDynamicHelp": false,
@@ -1088,6 +1103,7 @@
1088
1103
  "strict": true,
1089
1104
  "summary": "Install an existing app in another store environment.",
1090
1105
  "enableJsonFlag": false,
1106
+ "requiresSwellConfig": true,
1091
1107
  "delayOrientation": false,
1092
1108
  "orientation": {
1093
1109
  "env": "test"
@@ -1144,6 +1160,7 @@
1144
1160
  "pluginType": "core",
1145
1161
  "strict": true,
1146
1162
  "summary": "Pull app files from Swell to your local machine.",
1163
+ "requiresSwellConfig": false,
1147
1164
  "orientation": {
1148
1165
  "env": "test"
1149
1166
  },
@@ -1536,6 +1553,12 @@
1536
1553
  "hasDynamicHelp": false,
1537
1554
  "multiple": false,
1538
1555
  "type": "option"
1556
+ },
1557
+ "json": {
1558
+ "description": "Output the command result as JSON",
1559
+ "name": "json",
1560
+ "allowNo": false,
1561
+ "type": "boolean"
1539
1562
  }
1540
1563
  },
1541
1564
  "hasDynamicHelp": false,
@@ -2279,6 +2302,7 @@
2279
2302
  "strict": true,
2280
2303
  "summary": "Create tests scaffolding for your Swell app.",
2281
2304
  "enableJsonFlag": false,
2305
+ "requiresSwellConfig": true,
2282
2306
  "helpMeta": {
2283
2307
  "usageDirect": "[-y]"
2284
2308
  },
@@ -3215,6 +3239,12 @@
3215
3239
  "hasDynamicHelp": false,
3216
3240
  "multiple": false,
3217
3241
  "type": "option"
3242
+ },
3243
+ "json": {
3244
+ "description": "Output the command result as JSON",
3245
+ "name": "json",
3246
+ "allowNo": false,
3247
+ "type": "boolean"
3218
3248
  }
3219
3249
  },
3220
3250
  "hasDynamicHelp": false,
@@ -3301,6 +3331,7 @@
3301
3331
  "pluginName": "@swell/cli",
3302
3332
  "pluginType": "core",
3303
3333
  "summary": "Pull theme files from Swell to your local machine.",
3334
+ "requiresSwellConfig": false,
3304
3335
  "orientation": {
3305
3336
  "env": "test"
3306
3337
  },
@@ -3514,6 +3545,13 @@
3514
3545
  "allowNo": false,
3515
3546
  "type": "boolean"
3516
3547
  },
3548
+ "json-events": {
3549
+ "description": "emit machine-readable dev server lifecycle events",
3550
+ "hidden": true,
3551
+ "name": "json-events",
3552
+ "allowNo": false,
3553
+ "type": "boolean"
3554
+ },
3517
3555
  "yes": {
3518
3556
  "char": "y",
3519
3557
  "description": "skip prompts (non-interactive mode)",
@@ -3556,5 +3594,5 @@
3556
3594
  ]
3557
3595
  }
3558
3596
  },
3559
- "version": "2.9.6"
3597
+ "version": "2.9.8"
3560
3598
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.9.6",
3
+ "version": "2.9.8",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [