@swell/cli 2.9.27 → 2.9.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/commands/create/component.d.ts +3 -0
  2. package/dist/commands/create/component.js +48 -3
  3. package/dist/commands/create/tests.js +11 -2
  4. package/dist/commands/inspect/extensions.js +3 -3
  5. package/dist/lib/apps/app-config.d.ts +2 -0
  6. package/dist/lib/apps/app-config.js +36 -4
  7. package/dist/lib/apps/component-extension.d.ts +23 -0
  8. package/dist/lib/apps/component-extension.js +60 -0
  9. package/dist/lib/apps/frontend-scaffold.js +1 -1
  10. package/dist/lib/create/component.d.ts +6 -2
  11. package/dist/lib/create/component.js +13 -5
  12. package/dist/lib/create/tests/templates/index.d.ts +0 -3
  13. package/dist/lib/create/tests/templates/index.js +0 -3
  14. package/dist/lib/create/tests/templates/mock-request.js +4 -10
  15. package/dist/lib/create/tests/templates/tsconfig.js +1 -1
  16. package/dist/lib/create/tests/templates/vitest-config.d.ts +1 -2
  17. package/dist/lib/create/tests/templates/vitest-config.js +17 -113
  18. package/dist/lib/create/tests/types.d.ts +2 -0
  19. package/dist/lib/create/tests.js +64 -12
  20. package/dist/lib/inspect/extensions.d.ts +8 -1
  21. package/dist/lib/inspect/extensions.js +12 -2
  22. package/oclif.manifest.json +19 -4
  23. package/package.json +1 -1
  24. package/dist/lib/create/tests/templates/env-dts.d.ts +0 -1
  25. package/dist/lib/create/tests/templates/env-dts.js +0 -15
  26. package/dist/lib/create/tests/templates/integration-test.d.ts +0 -1
  27. package/dist/lib/create/tests/templates/integration-test.js +0 -19
  28. package/dist/lib/create/tests/templates/swell-client.d.ts +0 -1
  29. package/dist/lib/create/tests/templates/swell-client.js +0 -128
@@ -10,10 +10,13 @@ export default class CreateComponent extends CreateConfigCommand {
10
10
  static helpMeta: HelpMeta;
11
11
  static flags: {
12
12
  description: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ extension: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
14
  overwrite: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
15
+ target: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
14
16
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
15
17
  };
16
18
  static summary: string;
17
19
  createType: ConfigType;
20
+ private checkConfig;
18
21
  run(): Promise<void>;
19
22
  }
@@ -1,8 +1,13 @@
1
1
  import { input } from '@inquirer/prompts';
2
2
  import { Args, Flags } from '@oclif/core';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as path from 'node:path';
3
5
  import { CreateConfigCommand } from '../../create-config-command.js';
6
+ import { resolveComponentValues, } from '../../lib/apps/component-extension.js';
7
+ import { listRootComponentFiles } from '../../lib/apps/components.js';
4
8
  import { ConfigType } from '../../lib/apps/index.js';
5
9
  import { getComponentTemplate, setupComponentSupport, toComponentName, } from '../../lib/create/component.js';
10
+ import { readStaticConfig } from '../../lib/function-source-analysis.js';
6
11
  export default class CreateComponent extends CreateConfigCommand {
7
12
  static args = {
8
13
  name: Args.string({
@@ -14,9 +19,10 @@ export default class CreateComponent extends CreateConfigCommand {
14
19
  static examples = [
15
20
  '$ swell create component',
16
21
  '$ swell create component ColorPicker -d "Brand color picker" -y',
22
+ '$ swell create component MyComponent --extension <extension-id> --target <target-id> -y',
17
23
  ];
18
24
  static helpMeta = {
19
- usageDirect: '<name> [-d <description>] -y',
25
+ usageDirect: '<name> [-d <description>] [--extension <extension-id>] [--target <target-id>...] -y',
20
26
  };
21
27
  static flags = {
22
28
  description: Flags.string({
@@ -24,10 +30,17 @@ export default class CreateComponent extends CreateConfigCommand {
24
30
  default: '',
25
31
  description: 'Description',
26
32
  }),
33
+ extension: Flags.string({
34
+ description: 'Extension id from swell.json: makes the component the UI of that extension',
35
+ }),
27
36
  overwrite: Flags.boolean({
28
37
  default: false,
29
38
  description: 'Overwrite existing file',
30
39
  }),
40
+ target: Flags.string({
41
+ description: 'Target id where the component renders; repeat for more targets',
42
+ multiple: true,
43
+ }),
31
44
  yes: Flags.boolean({
32
45
  char: 'y',
33
46
  description: 'Skip prompts, require all arguments',
@@ -35,9 +48,35 @@ export default class CreateComponent extends CreateConfigCommand {
35
48
  };
36
49
  static summary = 'Create an app component in the components folder.';
37
50
  createType = ConfigType.COMPONENT;
51
+ // The same rules as push, so a scaffold that would fail to push is refused here
52
+ async checkConfig(componentName, extension, targets) {
53
+ const configured = this.swellConfig.get('extensions');
54
+ const extensions = Array.isArray(configured)
55
+ ? configured
56
+ : [];
57
+ const files = await listRootComponentFiles(this.appPath);
58
+ const siblings = await Promise.all(files.map(async (file) => {
59
+ const source = await fs
60
+ .readFile(path.join(this.appPath, file), 'utf8')
61
+ .catch(() => '');
62
+ return {
63
+ config: readStaticConfig(source, file).config,
64
+ name: path.basename(file).replace(/\.[^.]+$/, ''),
65
+ };
66
+ }));
67
+ try {
68
+ resolveComponentValues(componentName, {
69
+ ...(extension && { extension: { id: extension } }),
70
+ ...(targets.length > 0 && { targets: targets.map((id) => ({ id })) }),
71
+ }, extensions, siblings);
72
+ }
73
+ catch (error) {
74
+ this.error(error.message, { exit: 1 });
75
+ }
76
+ }
38
77
  async run() {
39
78
  const { args, flags } = await this.parse(CreateComponent);
40
- const { description: descriptionFlag, overwrite, yes } = flags;
79
+ const { description: descriptionFlag, extension, overwrite, target: targets = [], yes, } = flags;
41
80
  let name = args.name;
42
81
  let description = descriptionFlag;
43
82
  if (yes) {
@@ -62,9 +101,15 @@ export default class CreateComponent extends CreateConfigCommand {
62
101
  if (!componentName) {
63
102
  this.error(`Invalid component name: ${name}`, { exit: 1 });
64
103
  }
104
+ if (extension || targets.length > 0) {
105
+ await this.checkConfig(componentName, extension, targets);
106
+ }
65
107
  const created = await this.createFile({
66
108
  extension: 'tsx',
67
- fileBody: getComponentTemplate(componentName, description),
109
+ fileBody: getComponentTemplate(componentName, description, {
110
+ extension,
111
+ targets,
112
+ }),
68
113
  fileName: componentName,
69
114
  }, overwrite, !yes);
70
115
  if (!created) {
@@ -4,7 +4,7 @@ import { toAppId } from '../../lib/create/index.js';
4
4
  import { createTestsScaffold, } from '../../lib/create/tests.js';
5
5
  import { detectPackageManager, getPackageManagerCommands, } from '../../lib/package-manager.js';
6
6
  export default class CreateTests extends AppCommand {
7
- static description = 'Initialize a vitest + Cloudflare Workers test setup that reuses swell-cli authentication.';
7
+ static description = 'Initialize a vitest + Cloudflare Workers test setup for app functions.';
8
8
  static examples = [
9
9
  '$ swell create tests',
10
10
  '$ swell create tests --overwrite',
@@ -43,7 +43,9 @@ export default class CreateTests extends AppCommand {
43
43
  this.logScaffoldResult(result, pm);
44
44
  }
45
45
  logScaffoldResult(result, pm) {
46
- const hasChanges = result.createdFiles.length > 0 || result.packageJsonUpdated;
46
+ const hasChanges = result.createdFiles.length > 0 ||
47
+ result.removedFiles.length > 0 ||
48
+ result.packageJsonUpdated;
47
49
  if (hasChanges) {
48
50
  this.log('');
49
51
  this.log('✓ Created test scaffolding');
@@ -62,6 +64,13 @@ export default class CreateTests extends AppCommand {
62
64
  this.log(` ${file}`);
63
65
  }
64
66
  }
67
+ if (result.removedFiles.length > 0) {
68
+ this.log('');
69
+ this.log('Removed (no longer part of the test setup):');
70
+ for (const file of result.removedFiles) {
71
+ this.log(` ${file}`);
72
+ }
73
+ }
65
74
  if (result.packageJsonUpdated) {
66
75
  this.log('');
67
76
  this.log('Updated package.json with test scripts and devDependencies.');
@@ -6,7 +6,7 @@ import { resolveInspectScope, ScopeError, } from '../../lib/apps/inspect-scope.j
6
6
  import { HEX24, isObjectId } from '../../lib/apps/object-id.js';
7
7
  import { resolveAppId } from '../../lib/apps/resolve.js';
8
8
  import { default as localConfig } from '../../lib/config.js';
9
- import { buildExtensionDetail, buildOrphanDetail, componentMatchesExtension, diffManifestEntries, formatExtensionKey, functionMatchesExtension, listGroupForApp, listGroupForOrphans, listMetaFor, nextStepLines, orphanListMeta, parseExtensionKey, } from '../../lib/inspect/extensions.js';
9
+ import { buildExtensionDetail, buildOrphanDetail, componentMatchesExtension, diffManifestEntries, formatExtensionKey, componentExtensionId, functionMatchesExtension, listGroupForApp, listGroupForOrphans, listMetaFor, nextStepLines, orphanListMeta, parseExtensionKey, } from '../../lib/inspect/extensions.js';
10
10
  import { renderKeyMetaTable } from '../../lib/inspect/table.js';
11
11
  import { SwellCommand } from '../../swell-command.js';
12
12
  export default class Extensions extends SwellCommand {
@@ -144,7 +144,7 @@ Identifier forms:
144
144
  }
145
145
  }
146
146
  for (const comp of appComponents) {
147
- const ext = comp.values?.extension;
147
+ const ext = componentExtensionId(comp.values);
148
148
  if (!ext)
149
149
  continue;
150
150
  if (declaredIds.has(ext))
@@ -331,7 +331,7 @@ Identifier forms:
331
331
  // but no manifest entry declares it. Surface as the orphan envelope only
332
332
  // when we actually find handlers; otherwise it's a "not found" lookup.
333
333
  const orphanFns = allFunctions.filter((f) => f.extension === extId);
334
- const orphanComps = allComponents.filter((c) => c.values?.extension === extId);
334
+ const orphanComps = allComponents.filter((c) => componentExtensionId(c.values) === extId);
335
335
  if (orphanFns.length === 0 && orphanComps.length === 0) {
336
336
  this.error(`No extension '${extId}' declared by app '${appSlug}', and no handlers reference it. ` +
337
337
  `List available: swell inspect extensions${scope.appSlug ? ` --app=${scope.appSlug}` : ''}`, { exit: 1 });
@@ -118,6 +118,8 @@ export declare class AppConfigComponent extends AppConfig {
118
118
  protected parsesJsonValues(): boolean;
119
119
  isRootComponent(): boolean;
120
120
  protected contentHash(): Promise<string | undefined>;
121
+ private manifestExtensions;
122
+ private siblingComponents;
121
123
  preparePostData(postData: any): Promise<any>;
122
124
  }
123
125
  export declare class AppConfigAsset extends AppConfigDefault {
@@ -5,7 +5,9 @@ import * as fs from 'node:fs/promises';
5
5
  import * as path from 'node:path';
6
6
  import { bundleFunction, bundleComponent, bundleComponentModule, bundleWorkflow, getLocalInputs, } from '../bundle.js';
7
7
  import { analyzeFunctionSource, hasKindDiagnostic, readStaticConfig, workflowStaticConfigError, } from '../function-source-analysis.js';
8
- import { AllConfigPaths, ConfigType, filePathExistsAsync, hashFile, hashString, } from './index.js';
8
+ import { isLegacyComponent, resolveComponentValues, } from './component-extension.js';
9
+ import { listRootComponentFiles } from './components.js';
10
+ import { AllConfigPaths, ConfigType, filePathExistsAsync, getNameFromFilePath, hashFile, hashString, } from './index.js';
9
11
  export class IgnoringFileError extends Error {
10
12
  constructor(message) {
11
13
  super(message);
@@ -380,6 +382,31 @@ export class AppConfigComponent extends AppConfig {
380
382
  async contentHash() {
381
383
  return this.isRootComponent() ? this.bundleHash() : this.fileHash;
382
384
  }
385
+ // Read only for components that name an extension, so other apps never need a readable swell.json
386
+ async manifestExtensions() {
387
+ try {
388
+ const manifest = JSON.parse(await fs.readFile(path.join(this.appRoot(), 'swell.json'), 'utf8'));
389
+ return Array.isArray(manifest?.extensions) ? manifest.extensions : [];
390
+ }
391
+ catch (error) {
392
+ throw new Error(`cannot read swell.json: ${error.message}`);
393
+ }
394
+ }
395
+ // Siblings whose config cannot be read are skipped: their own push reports them
396
+ async siblingComponents() {
397
+ const appRoot = this.appRoot();
398
+ const files = await listRootComponentFiles(appRoot);
399
+ return Promise.all(files
400
+ .filter((file) => file !== this.filePath)
401
+ .map(async (file) => {
402
+ const source = await fs
403
+ .readFile(path.join(appRoot, file), 'utf8')
404
+ // A sibling that vanished or was renamed reads as empty and is skipped
405
+ .catch(() => '');
406
+ const { config } = readStaticConfig(source, file);
407
+ return { name: getNameFromFilePath(file), config };
408
+ }));
409
+ }
383
410
  async preparePostData(postData) {
384
411
  if (!this.isRootComponent()) {
385
412
  return postData;
@@ -395,8 +422,13 @@ export class AppConfigComponent extends AppConfig {
395
422
  if (errors.length > 0) {
396
423
  throw new Error(`config must be static metadata: ${errors.join('; ')}`);
397
424
  }
398
- // Checkout payment components still load through swell-js, which evaluates the legacy IIFE
399
- if (config?.extension) {
425
+ const name = getNameFromFilePath(this.filePath);
426
+ // Without an object extension only the targets shape is checked, with no manifest read
427
+ const values = config?.extension !== undefined && !isLegacyComponent(config)
428
+ ? resolveComponentValues(name, config, await this.manifestExtensions(), await this.siblingComponents())
429
+ : resolveComponentValues(name, config, [], []);
430
+ // Legacy payment components (string extension) still load through swell.app, which evaluates an IIFE
431
+ if (isLegacyComponent(config)) {
400
432
  postData.build_file = {
401
433
  content_type: 'application/javascript',
402
434
  data: await bundleComponent(this.appPath),
@@ -415,7 +447,7 @@ export class AppConfigComponent extends AppConfig {
415
447
  postData.file = {
416
448
  data: fileData,
417
449
  };
418
- postData.values = config ?? {};
450
+ postData.values = values;
419
451
  }
420
452
  catch (error) {
421
453
  throw new FunctionProcessingError(`Unable to compile component ${this.name}`, error);
@@ -0,0 +1,23 @@
1
+ export interface ManifestExtension {
2
+ id: string;
3
+ type: string;
4
+ }
5
+ export interface SiblingComponent {
6
+ name: string;
7
+ config?: Record<string, unknown>;
8
+ }
9
+ /**
10
+ * Whether the config is the legacy swell.app contract (string extension)
11
+ * @param {object} config static component config
12
+ * @returns {boolean} true for a string extension
13
+ */
14
+ export declare function isLegacyComponent(config: Record<string, unknown> | undefined): boolean;
15
+ /**
16
+ * The values to push for a component's static config
17
+ * @param {string} name component name
18
+ * @param {object} config static component config
19
+ * @param {ManifestExtension[]} extensions extensions of swell.json
20
+ * @param {SiblingComponent[]} siblings the app's other root components
21
+ * @returns {object} values with the extension type added
22
+ */
23
+ export declare function resolveComponentValues(name: string, config: Record<string, unknown> | undefined, extensions: ManifestExtension[], siblings: SiblingComponent[]): Record<string, unknown>;
@@ -0,0 +1,60 @@
1
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
2
+ const TARGETS_SHAPE = 'targets must be a list of objects with an id, like [{ id: "<target id>" }]';
3
+ /**
4
+ * Whether the config is the legacy swell.app contract (string extension)
5
+ * @param {object} config static component config
6
+ * @returns {boolean} true for a string extension
7
+ */
8
+ export function isLegacyComponent(config) {
9
+ return typeof config?.extension === 'string';
10
+ }
11
+ // Target ids and what each target needs are the platform's to check at push
12
+ function checkTargets(config) {
13
+ if (config.targets === undefined) {
14
+ return;
15
+ }
16
+ if (!Array.isArray(config.targets) ||
17
+ !config.targets.every((item) => isRecord(item) && typeof item.id === 'string' && item.id !== '')) {
18
+ throw new Error(TARGETS_SHAPE);
19
+ }
20
+ }
21
+ /**
22
+ * The values to push for a component's static config
23
+ * @param {string} name component name
24
+ * @param {object} config static component config
25
+ * @param {ManifestExtension[]} extensions extensions of swell.json
26
+ * @param {SiblingComponent[]} siblings the app's other root components
27
+ * @returns {object} values with the extension type added
28
+ */
29
+ export function resolveComponentValues(name, config, extensions, siblings) {
30
+ const values = { ...config };
31
+ checkTargets(values);
32
+ const { extension } = values;
33
+ // A string extension is the legacy swell.app contract
34
+ if (extension === undefined || typeof extension === 'string') {
35
+ return values;
36
+ }
37
+ if (!isRecord(extension) ||
38
+ typeof extension.id !== 'string' ||
39
+ !extension.id) {
40
+ throw new Error('extension must be an object with an id, like { id: "card" }');
41
+ }
42
+ const matches = extensions.filter((item) => item.id === extension.id);
43
+ if (matches.length > 1) {
44
+ throw new Error(`extension "${extension.id}" is declared more than once in swell.json`);
45
+ }
46
+ const [declared] = matches;
47
+ if (!declared) {
48
+ throw new Error(`extension "${extension.id}" is not declared in swell.json`);
49
+ }
50
+ const other = siblings.find((sibling) => {
51
+ const siblingExtension = sibling.config?.extension;
52
+ return (sibling.name !== name &&
53
+ isRecord(siblingExtension) &&
54
+ siblingExtension.id === extension.id);
55
+ });
56
+ if (other) {
57
+ throw new Error(`extension "${extension.id}" already has the component ${other.name}`);
58
+ }
59
+ return { ...values, extension: { ...extension, type: declared.type } };
60
+ }
@@ -252,7 +252,7 @@ export async function setupPackage(name, config, pkg) {
252
252
  moduleResolution: 'bundler',
253
253
  types: ['@swell/app-types'],
254
254
  },
255
- exclude: ['node_modules', 'frontend', 'test', 'vitest.config.ts'],
255
+ exclude: ['node_modules', 'frontend', 'test', 'vitest.config.mts'],
256
256
  };
257
257
  await writeJsonFile(path.join(configPath, 'package.json'), packageJson);
258
258
  // create frontend can initialize a workspace around an existing manifest.
@@ -1,7 +1,11 @@
1
1
  export declare const PREACT_VERSION = "^10.28.0";
2
- export declare const APPS_SDK_VERSION = "^2.0.0-alpha.6";
2
+ export declare const APPS_SDK_VERSION = "^2.0.0-alpha.7";
3
3
  export declare const toComponentName: (value: string) => string;
4
- export declare function getComponentTemplate(name: string, description: string): string;
4
+ export interface ComponentTemplateOptions {
5
+ extension?: string;
6
+ targets?: string[];
7
+ }
8
+ export declare function getComponentTemplate(name: string, description: string, { extension, targets }?: ComponentTemplateOptions): string;
5
9
  export interface ComponentSetupResult {
6
10
  changed: string[];
7
11
  dependenciesAdded: boolean;
@@ -2,8 +2,8 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import * as semver from 'semver';
4
4
  export const PREACT_VERSION = '^10.28.0';
5
- export const APPS_SDK_VERSION = '^2.0.0-alpha.6';
6
- // The first release with the ./components entry
5
+ export const APPS_SDK_VERSION = '^2.0.0-alpha.7';
6
+ // The minimum SDK the scaffold needs
7
7
  const COMPONENTS_SDK = semver.minVersion(APPS_SDK_VERSION);
8
8
  // Example: 'color picker' => 'ColorPicker'
9
9
  export const toComponentName = (value) => {
@@ -16,11 +16,19 @@ export const toComponentName = (value) => {
16
16
  // A component name is also its function name, so it must start with a letter
17
17
  return /^[A-Za-z]/.test(name) ? name : '';
18
18
  };
19
- export function getComponentTemplate(name, description) {
19
+ export function getComponentTemplate(name, description, { extension, targets = [] } = {}) {
20
+ const config = [` description: ${JSON.stringify(description)},`];
21
+ if (extension) {
22
+ config.push(` extension: { id: ${JSON.stringify(extension)} },`);
23
+ }
24
+ if (targets.length > 0) {
25
+ const ids = targets.map((id) => `{ id: ${JSON.stringify(id)} }`);
26
+ config.push(` targets: [${ids.join(', ')}],`);
27
+ }
20
28
  return `import type { ComponentProps } from '@swell/apps-sdk/components';
21
29
 
22
30
  export const config = {
23
- description: ${JSON.stringify(description)},
31
+ ${config.join('\n')}
24
32
  };
25
33
 
26
34
  export default function ${name}({ value, setValue, readonly }: ComponentProps<string>) {
@@ -119,7 +127,7 @@ export async function setupComponentSupport(appPath) {
119
127
  const range = pkg[sdkSection]['@swell/apps-sdk'];
120
128
  const min = semver.validRange(range) ? semver.minVersion(range) : null;
121
129
  if (min && semver.lt(min, COMPONENTS_SDK)) {
122
- // `swell create tests` pins an earlier 2.x; 1.x is another SDK the app may use
130
+ // `swell create tests` pinned an earlier 2.x; 1.x is another SDK the app may use
123
131
  if (semver.major(min) === 2) {
124
132
  pkg[sdkSection]['@swell/apps-sdk'] = APPS_SDK_VERSION;
125
133
  result.dependenciesAdded = true;
@@ -1,8 +1,5 @@
1
- export { envDtsTemplate } from './env-dts.js';
2
- export { integrationTestTemplate } from './integration-test.js';
3
1
  export { mockRequestTemplate } from './mock-request.js';
4
2
  export { setupGlobalsTemplate } from './setup-globals.js';
5
- export { swellClientTemplate } from './swell-client.js';
6
3
  export { tsconfigTemplate } from './tsconfig.js';
7
4
  export { unitTestTemplate } from './unit-test.js';
8
5
  export { vitestConfigTemplate } from './vitest-config.js';
@@ -1,8 +1,5 @@
1
- export { envDtsTemplate } from './env-dts.js';
2
- export { integrationTestTemplate } from './integration-test.js';
3
1
  export { mockRequestTemplate } from './mock-request.js';
4
2
  export { setupGlobalsTemplate } from './setup-globals.js';
5
- export { swellClientTemplate } from './swell-client.js';
6
3
  export { tsconfigTemplate } from './tsconfig.js';
7
4
  export { unitTestTemplate } from './unit-test.js';
8
5
  export { vitestConfigTemplate } from './vitest-config.js';
@@ -1,8 +1,5 @@
1
1
  export function mockRequestTemplate(options) {
2
2
  return `\
3
- import { env } from "cloudflare:test";
4
- import { createSwellClient } from "./swell-client";
5
-
6
3
  export interface MockRequestOptions {
7
4
  data?: SwellData;
8
5
  query?: { [key: string]: string };
@@ -13,7 +10,6 @@ export interface MockRequestOptions {
13
10
  swell?: Partial<SwellAPI>;
14
11
  store?: Partial<SwellStore>;
15
12
  appId?: string;
16
- useRealSwell?: boolean;
17
13
  }
18
14
 
19
15
  function createDefaultSwellMock(): Partial<SwellAPI> {
@@ -42,7 +38,6 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
42
38
  swell,
43
39
  store,
44
40
  appId,
45
- useRealSwell = false,
46
41
  } = options;
47
42
 
48
43
  const requestHeaders = new Headers(headers);
@@ -58,7 +53,7 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
58
53
  body: method === "GET" ? undefined : JSON.stringify(data),
59
54
  });
60
55
 
61
- const storeId = store?.id || env.SWELL_STORE_ID || "test-store";
56
+ const storeId = store?.id || "test-store";
62
57
 
63
58
  const resolvedStore = {
64
59
  id: storeId,
@@ -72,10 +67,9 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
72
67
  },
73
68
  };
74
69
 
75
- const swellClient =
76
- swell || (useRealSwell ? createSwellClient() : createDefaultSwellMock());
70
+ const swellClient = swell || createDefaultSwellMock();
77
71
 
78
- const resolvedAppId = appId || env.SWELL_APP_ID || "${options.appId}";
72
+ const resolvedAppId = appId || "${options.appId}";
79
73
 
80
74
  const req: Partial<SwellRequest> & { appId: string; storeId: string } = {
81
75
  originalRequest,
@@ -91,7 +85,7 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
91
85
  publicKey: null,
92
86
  store: resolvedStore,
93
87
  session,
94
- apiHost: env.SWELL_API_BASE_URL || "",
88
+ apiHost: "",
95
89
  logParams: undefined,
96
90
  swell: swellClient as SwellAPI,
97
91
  rawBody: method === "GET" ? "" : JSON.stringify(data),
@@ -5,7 +5,7 @@ export function tsconfigTemplate() {
5
5
  moduleResolution: 'bundler',
6
6
  skipLibCheck: true,
7
7
  types: [
8
- '@cloudflare/vitest-pool-workers',
8
+ '@cloudflare/vitest-plugin/types',
9
9
  '@swell/app-types',
10
10
  'vitest/globals',
11
11
  ],
@@ -1,2 +1 @@
1
- import type { TestTemplateOptions } from '../types.js';
2
- export declare function vitestConfigTemplate(options: TestTemplateOptions): string;
1
+ export declare function vitestConfigTemplate(): string;
@@ -1,123 +1,27 @@
1
1
  import { FUNCTION_COMPATIBILITY_DATE } from '../../../constants.js';
2
- export function vitestConfigTemplate(options) {
2
+ export function vitestConfigTemplate() {
3
3
  return `\
4
- import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
5
- import { readFileSync, existsSync } from "node:fs";
6
- import { homedir } from "node:os";
7
- import path from "node:path";
8
-
9
- type SdkAuth = {
10
- storeId: string;
11
- sessionId: string;
12
- apiBaseUrl: string;
13
- };
14
-
15
- const isCI = Boolean(process.env.CI || process.env.CONTINUOUS_INTEGRATION);
16
-
17
- function loadSwellAuth(): SdkAuth {
18
- const envStoreId = process.env.SWELL_STORE_ID;
19
- const envSessionId = process.env.SWELL_SESSION_ID;
20
- const envApiBaseUrl = process.env.SWELL_API_BASE_URL;
21
-
22
- if (envStoreId && envSessionId) {
23
- return {
24
- storeId: envStoreId,
25
- sessionId: envSessionId,
26
- apiBaseUrl: envApiBaseUrl || \`https://\${envStoreId}.swell.store/admin/api\`,
27
- };
28
- }
29
-
30
- if (isCI) {
31
- throw new Error(
32
- "CI environment detected but SWELL_STORE_ID and SWELL_SESSION_ID are not set. " +
33
- "Add these as CI secrets/variables to run integration tests.",
34
- );
35
- }
36
-
37
- const home = homedir();
38
- const configPath = path.resolve(home, ".swell", "config.json");
39
- if (!existsSync(configPath)) {
40
- throw new Error(
41
- \`Swell CLI config not found at \${configPath}. Run \\\`swell login\\\` or set SWELL_STORE_ID and SWELL_SESSION_ID.\`
42
- );
43
- }
44
-
45
- const rawConfig = readFileSync(configPath, "utf-8");
46
-
47
- interface CliConfig {
48
- defaultStore?: string;
49
- stores?: { storeId: string; sessionId?: string }[];
50
- }
51
-
52
- let configJson: CliConfig;
53
-
54
- try {
55
- configJson = JSON.parse(rawConfig) as CliConfig;
56
- } catch (error) {
57
- throw new Error(
58
- \`Unable to parse Swell CLI config at \${configPath}: \${String(error)}\`
59
- );
60
- }
61
-
62
- const defaultStore = configJson.defaultStore;
63
- const stores = configJson.stores || [];
64
-
65
- const store = defaultStore
66
- ? stores.find((item) => item.storeId === defaultStore)
67
- : undefined;
68
-
69
- if (!defaultStore || !store?.sessionId) {
70
- throw new Error(
71
- "No active Swell CLI session found. Run \\\`swell login\\\` or set SWELL_STORE_ID and SWELL_SESSION_ID.",
72
- );
73
- }
74
-
75
- const envPath = path.join(home, ".swell", "env.json");
76
- let apiBaseUrl = \`https://\${defaultStore}.swell.store/admin/api\`;
77
-
78
- if (existsSync(envPath)) {
79
- try {
80
- const rawEnv = readFileSync(envPath, "utf-8");
81
- const envJson = JSON.parse(rawEnv) as { ADMIN_API_BASE_URL?: string };
82
- if (envJson.ADMIN_API_BASE_URL) {
83
- apiBaseUrl = envJson.ADMIN_API_BASE_URL.replace("\${STORE_ID}", defaultStore);
84
- }
85
- } catch {
86
- // Ignore env parsing errors and fall back to default
87
- }
88
- }
89
-
90
- return {
91
- storeId: defaultStore,
92
- sessionId: store.sessionId!,
93
- apiBaseUrl,
94
- };
95
- }
96
-
97
- const sdkAuth = loadSwellAuth();
98
-
99
- export default defineWorkersConfig({
4
+ import { cloudflareTest } from "@cloudflare/vitest-plugin";
5
+ import { defineConfig } from "vitest/config";
6
+
7
+ export default defineConfig({
8
+ plugins: [
9
+ cloudflareTest({
10
+ miniflare: {
11
+ compatibilityDate: "${FUNCTION_COMPATIBILITY_DATE}",
12
+ // Reports a rejection as unhandled only if nothing has handled it by the end of the
13
+ // microtask queue. Without it, a rejection handled one step later fails the run
14
+ compatibilityFlags: ["unhandled_rejection_after_microtask_checkpoint"],
15
+ },
16
+ }),
17
+ ],
100
18
  test: {
101
19
  globals: true,
20
+ // The Workers runtime cannot read Vitest's module cache from disk
21
+ fsModuleCache: false,
102
22
  include: ["test/**/*.test.ts"],
103
23
  exclude: ["node_modules", "docs"],
104
24
  setupFiles: ["./test/setup-globals.ts"],
105
- poolOptions: {
106
- workers: {
107
- singleWorker: true,
108
- isolatedStorage: true,
109
- miniflare: {
110
- compatibilityDate: "${FUNCTION_COMPATIBILITY_DATE}",
111
- bindings: {
112
- SWELL_STORE_ID: sdkAuth.storeId,
113
- SWELL_SESSION_ID: sdkAuth.sessionId,
114
- SWELL_API_BASE_URL: sdkAuth.apiBaseUrl,
115
- SWELL_APP_ID: "${options.appId}",
116
- SWELL_ENVIRONMENT: "test",
117
- },
118
- },
119
- },
120
- },
121
25
  },
122
26
  });
123
27
  `;
@@ -13,6 +13,8 @@ export interface ScaffoldResult {
13
13
  createdFiles: string[];
14
14
  /** Files that were skipped because they already exist */
15
15
  skippedFiles: string[];
16
+ /** Files of an earlier scaffold that were removed */
17
+ removedFiles: string[];
16
18
  /** Whether package.json was updated */
17
19
  packageJsonUpdated: boolean;
18
20
  /** Warning messages to display to user */
@@ -1,14 +1,28 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { filePathExistsAsync, writeFile } from '../apps/index.js';
4
- import { envDtsTemplate, integrationTestTemplate, mockRequestTemplate, setupGlobalsTemplate, swellClientTemplate, tsconfigTemplate, unitTestTemplate, vitestConfigTemplate, } from './tests/templates/index.js';
4
+ import { APPS_SDK_VERSION } from './component.js';
5
+ import { mockRequestTemplate, setupGlobalsTemplate, tsconfigTemplate, unitTestTemplate, vitestConfigTemplate, } from './tests/templates/index.js';
5
6
  const TEST_DEV_DEPENDENCIES = {
6
- // 0.13+ requires vitest 4 and drops the /config entry the vitest template imports
7
- '@cloudflare/vitest-pool-workers': '0.12.x',
8
- vitest: '3.2.x',
7
+ // npm 10 cannot resolve vitest 4.1 next to the plugin; vitest 5 needs Node 22.12 or later
8
+ '@cloudflare/vitest-plugin': '1.4.x',
9
+ vitest: '5.0.x',
9
10
  '@swell/app-types': 'latest',
10
- '@swell/apps-sdk': '2.0.0-alpha.0',
11
+ '@swell/apps-sdk': APPS_SDK_VERSION,
11
12
  };
13
+ // Earlier scaffolds used the deprecated pool package and a vitest.config.ts,
14
+ // which Vitest would load ahead of vitest.config.mts
15
+ const LEGACY_POOL_PACKAGE = '@cloudflare/vitest-pool-workers';
16
+ const LEGACY_VITEST_CONFIG = 'vitest.config.ts';
17
+ // The plugin is ESM-only and scaffolded apps are not "type": "module"
18
+ const VITEST_CONFIG = 'vitest.config.mts';
19
+ // Files of earlier scaffolds that have no place in the current one
20
+ const RETIRED_FILES = [
21
+ LEGACY_VITEST_CONFIG,
22
+ 'test/env.d.ts',
23
+ 'test/helpers/swell-client.ts',
24
+ 'test/integration/example.test.ts',
25
+ ];
12
26
  async function writeTextFile(filePath, contents, overwrite) {
13
27
  if (!overwrite && (await filePathExistsAsync(filePath))) {
14
28
  return { created: false, skipped: true };
@@ -46,9 +60,16 @@ async function updatePackageJson(appPath) {
46
60
  }
47
61
  const devDeps = (pkg.devDependencies || {});
48
62
  const deps = (pkg.dependencies || {});
49
- devDeps['@cloudflare/vitest-pool-workers'] =
50
- devDeps['@cloudflare/vitest-pool-workers'] ||
51
- TEST_DEV_DEPENDENCIES['@cloudflare/vitest-pool-workers'];
63
+ if (devDeps[LEGACY_POOL_PACKAGE] || deps[LEGACY_POOL_PACKAGE]) {
64
+ // The plugin does not support the vitest version the pool package required
65
+ for (const section of [deps, devDeps]) {
66
+ delete section[LEGACY_POOL_PACKAGE];
67
+ delete section.vitest;
68
+ }
69
+ }
70
+ devDeps['@cloudflare/vitest-plugin'] =
71
+ devDeps['@cloudflare/vitest-plugin'] ||
72
+ TEST_DEV_DEPENDENCIES['@cloudflare/vitest-plugin'];
52
73
  devDeps.vitest = devDeps.vitest || TEST_DEV_DEPENDENCIES.vitest;
53
74
  if (!devDeps['@swell/app-types'] && !deps['@swell/app-types']) {
54
75
  devDeps['@swell/app-types'] = TEST_DEV_DEPENDENCIES['@swell/app-types'];
@@ -72,6 +93,29 @@ async function updatePackageJson(appPath) {
72
93
  };
73
94
  }
74
95
  }
96
+ async function hasLegacySetup(appPath) {
97
+ if (await filePathExistsAsync(path.join(appPath, LEGACY_VITEST_CONFIG))) {
98
+ return true;
99
+ }
100
+ try {
101
+ const pkg = JSON.parse(await fs.readFile(path.join(appPath, 'package.json'), 'utf8'));
102
+ return Boolean(pkg.devDependencies?.[LEGACY_POOL_PACKAGE] ||
103
+ pkg.dependencies?.[LEGACY_POOL_PACKAGE]);
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ // The app tsconfig.json covers functions only; the config file runs in Node
110
+ async function excludeVitestConfig(appPath) {
111
+ const tsconfigPath = path.join(appPath, 'tsconfig.json');
112
+ if (!(await filePathExistsAsync(tsconfigPath)))
113
+ return;
114
+ const tsconfig = await fs.readFile(tsconfigPath, 'utf8');
115
+ const next = tsconfig.replace(`"${LEGACY_VITEST_CONFIG}"`, `"${VITEST_CONFIG}"`);
116
+ if (next !== tsconfig)
117
+ await fs.writeFile(tsconfigPath, next, 'utf8');
118
+ }
75
119
  async function validatePrerequisites(appPath) {
76
120
  const warnings = [];
77
121
  const tsconfigPath = path.join(appPath, 'tsconfig.json');
@@ -85,6 +129,7 @@ export async function createTestsScaffold(options) {
85
129
  const result = {
86
130
  createdFiles: [],
87
131
  skippedFiles: [],
132
+ removedFiles: [],
88
133
  packageJsonUpdated: false,
89
134
  warnings: [],
90
135
  };
@@ -100,14 +145,21 @@ export async function createTestsScaffold(options) {
100
145
  result.skippedFiles.push(relativePath);
101
146
  }
102
147
  };
103
- await trackFile('vitest.config.ts', vitestConfigTemplate({ appId }));
148
+ if (overwrite) {
149
+ const exists = await Promise.all(RETIRED_FILES.map((file) => filePathExistsAsync(path.join(appPath, file))));
150
+ result.removedFiles = RETIRED_FILES.filter((_, index) => exists[index]);
151
+ await Promise.all(result.removedFiles.map((file) => fs.rm(path.join(appPath, file))));
152
+ }
153
+ else if (await hasLegacySetup(appPath)) {
154
+ result.warnings.push(`This app has an earlier test setup (${LEGACY_VITEST_CONFIG} with ${LEGACY_POOL_PACKAGE}, which is deprecated). Nothing was changed. Run \`swell create tests --overwrite\` to move it to @cloudflare/vitest-plugin; this replaces the scaffolded test files.`);
155
+ return result;
156
+ }
157
+ await trackFile(VITEST_CONFIG, vitestConfigTemplate());
158
+ await excludeVitestConfig(appPath);
104
159
  await trackFile('test/tsconfig.json', tsconfigTemplate());
105
- await trackFile('test/env.d.ts', envDtsTemplate());
106
160
  await trackFile('test/setup-globals.ts', setupGlobalsTemplate());
107
- await trackFile('test/helpers/swell-client.ts', swellClientTemplate());
108
161
  await trackFile('test/helpers/mock-request.ts', mockRequestTemplate({ appId }));
109
162
  await trackFile('test/unit/example.test.ts', unitTestTemplate());
110
- await trackFile('test/integration/example.test.ts', integrationTestTemplate());
111
163
  const pkgResult = await updatePackageJson(appPath);
112
164
  result.packageJsonUpdated = pkgResult.updated;
113
165
  if (pkgResult.warning) {
@@ -42,7 +42,10 @@ export interface ComponentRecord {
42
42
  file_path?: string;
43
43
  values?: {
44
44
  [key: string]: unknown;
45
- extension?: string;
45
+ extension?: string | {
46
+ id: string;
47
+ type?: string;
48
+ };
46
49
  };
47
50
  [key: string]: unknown;
48
51
  }
@@ -202,6 +205,10 @@ export declare function stripHookPrefix(event: string): string;
202
205
  export declare function eventsCoveredByFunctions(functions: Pick<FunctionRecord, 'model'>[]): Set<string>;
203
206
  /** Required events not covered by any of the bound functions. */
204
207
  export declare function missingRequiredEvents(type: ExtensionType, functions: Pick<FunctionRecord, 'model'>[]): string[];
208
+ /**
209
+ * The extension id a component binds to: a string (legacy) or `{ id }` (object)
210
+ */
211
+ export declare function componentExtensionId(values: ComponentRecord['values']): string | undefined;
205
212
  /** Lift a component record into the synthesized `BoundComponent` shape. */
206
213
  export declare function liftBoundComponent(record: ComponentRecord): BoundComponent;
207
214
  /** Lift a function record into the synthesized `BoundFunction` shape. */
@@ -127,12 +127,22 @@ export function missingRequiredEvents(type, functions) {
127
127
  const covered = eventsCoveredByFunctions(functions);
128
128
  return requiredEventsFor(type).filter((ev) => !covered.has(ev));
129
129
  }
130
+ /**
131
+ * The extension id a component binds to: a string (legacy) or `{ id }` (object)
132
+ */
133
+ export function componentExtensionId(values) {
134
+ const extension = values?.extension;
135
+ if (typeof extension === 'string') {
136
+ return extension;
137
+ }
138
+ return typeof extension?.id === 'string' ? extension.id : undefined;
139
+ }
130
140
  /** Lift a component record into the synthesized `BoundComponent` shape. */
131
141
  export function liftBoundComponent(record) {
132
142
  return {
133
143
  id: record.id,
134
144
  name: record.name,
135
- extension: record.values?.extension,
145
+ extension: componentExtensionId(record.values),
136
146
  file_path: record.file_path,
137
147
  };
138
148
  }
@@ -686,5 +696,5 @@ export function functionMatchesExtension(fn, extId) {
686
696
  return fn.extension === extId;
687
697
  }
688
698
  export function componentMatchesExtension(comp, extId) {
689
- return comp.values?.extension === extId;
699
+ return componentExtensionId(comp.values) === extId;
690
700
  }
@@ -1664,7 +1664,8 @@
1664
1664
  "description": "Create an app component in the components folder.",
1665
1665
  "examples": [
1666
1666
  "$ swell create component",
1667
- "$ swell create component ColorPicker -d \"Brand color picker\" -y"
1667
+ "$ swell create component ColorPicker -d \"Brand color picker\" -y",
1668
+ "$ swell create component MyComponent --extension <extension-id> --target <target-id> -y"
1668
1669
  ],
1669
1670
  "flags": {
1670
1671
  "app-path": {
@@ -1683,12 +1684,26 @@
1683
1684
  "multiple": false,
1684
1685
  "type": "option"
1685
1686
  },
1687
+ "extension": {
1688
+ "description": "Extension id from swell.json: makes the component the UI of that extension",
1689
+ "name": "extension",
1690
+ "hasDynamicHelp": false,
1691
+ "multiple": false,
1692
+ "type": "option"
1693
+ },
1686
1694
  "overwrite": {
1687
1695
  "description": "Overwrite existing file",
1688
1696
  "name": "overwrite",
1689
1697
  "allowNo": false,
1690
1698
  "type": "boolean"
1691
1699
  },
1700
+ "target": {
1701
+ "description": "Target id where the component renders; repeat for more targets",
1702
+ "name": "target",
1703
+ "hasDynamicHelp": false,
1704
+ "multiple": true,
1705
+ "type": "option"
1706
+ },
1692
1707
  "yes": {
1693
1708
  "char": "y",
1694
1709
  "description": "Skip prompts, require all arguments",
@@ -1706,7 +1721,7 @@
1706
1721
  "strict": true,
1707
1722
  "summary": "Create an app component in the components folder.",
1708
1723
  "helpMeta": {
1709
- "usageDirect": "<name> [-d <description>] -y"
1724
+ "usageDirect": "<name> [-d <description>] [--extension <extension-id>] [--target <target-id>...] -y"
1710
1725
  },
1711
1726
  "isESM": true,
1712
1727
  "relativePath": [
@@ -2346,7 +2361,7 @@
2346
2361
  "create:tests": {
2347
2362
  "aliases": [],
2348
2363
  "args": {},
2349
- "description": "Initialize a vitest + Cloudflare Workers test setup that reuses swell-cli authentication.",
2364
+ "description": "Initialize a vitest + Cloudflare Workers test setup for app functions.",
2350
2365
  "examples": [
2351
2366
  "$ swell create tests",
2352
2367
  "$ swell create tests --overwrite"
@@ -3670,5 +3685,5 @@
3670
3685
  ]
3671
3686
  }
3672
3687
  },
3673
- "version": "2.9.27"
3688
+ "version": "2.9.28"
3674
3689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.9.27",
3
+ "version": "2.9.28",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- export declare function envDtsTemplate(): string;
@@ -1,15 +0,0 @@
1
- export function envDtsTemplate() {
2
- return `\
3
- declare module "cloudflare:test" {
4
- interface ProvidedEnv {
5
- SWELL_STORE_ID: string;
6
- SWELL_SESSION_ID: string;
7
- SWELL_API_BASE_URL: string;
8
- SWELL_APP_ID: string;
9
- SWELL_ENVIRONMENT?: string;
10
- }
11
- }
12
-
13
- export {};
14
- `;
15
- }
@@ -1 +0,0 @@
1
- export declare function integrationTestTemplate(): string;
@@ -1,19 +0,0 @@
1
- export function integrationTestTemplate() {
2
- return `\
3
- import { describe, it, expect } from "vitest";
4
- import { createSwellClient } from "../helpers/swell-client";
5
-
6
- describe("Store settings (read-only)", () => {
7
- it("loads general store settings using CLI auth", async () => {
8
- const swell = createSwellClient();
9
-
10
- const general = await swell.get("/settings/general");
11
-
12
- expect(general).toBeDefined();
13
- expect(general.id).toBe("general");
14
- expect(general.features).toBeDefined();
15
- expect(typeof general.features.carts).toBe("boolean");
16
- });
17
- });
18
- `;
19
- }
@@ -1 +0,0 @@
1
- export declare function swellClientTemplate(): string;
@@ -1,128 +0,0 @@
1
- export function swellClientTemplate() {
2
- return `\
3
- import { env } from "cloudflare:test";
4
-
5
- type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
6
-
7
- type BasicSwellClient = Pick<
8
- SwellAPI,
9
- "get" | "post" | "put" | "delete" | "settings"
10
- >;
11
-
12
- async function makeRequest(
13
- method: HttpMethod,
14
- url: string,
15
- data?: any
16
- ): Promise<any> {
17
- const baseUrl = env.SWELL_API_BASE_URL || "https://api.swell.store";
18
- const sessionId = env.SWELL_SESSION_ID;
19
- const environment = env.SWELL_ENVIRONMENT || "test";
20
-
21
- if (!sessionId) {
22
- throw new Error(
23
- "Missing SWELL_SESSION_ID binding. Run \\\`swell login\\\` or set it explicitly for tests.",
24
- );
25
- }
26
-
27
- let endpointUrl = String(url).startsWith("/") ? url.substring(1) : String(url);
28
- if (!endpointUrl.startsWith("data/")) {
29
- endpointUrl = \`data/\${endpointUrl}\`;
30
- }
31
-
32
- let fullUrl = \`\${baseUrl}/\${endpointUrl}\`;
33
- const headers: HeadersInit = {
34
- "Content-Type": "application/json;charset=UTF-8",
35
- "User-Agent": "swell-app-tests/1.0",
36
- "X-Session": sessionId,
37
- "Swell-Env": environment,
38
- };
39
-
40
- const options: RequestInit = {
41
- method,
42
- headers,
43
- };
44
-
45
- if (data) {
46
- if (method === "GET") {
47
- const params = new URLSearchParams();
48
-
49
- function append(prefix: string, value: any) {
50
- if (value === null || value === undefined) return;
51
- if (typeof value === "object" && !Array.isArray(value)) {
52
- for (const [k, v] of Object.entries(value)) {
53
- append(\`\${prefix}[\${k}]\`, v);
54
- }
55
- } else if (Array.isArray(value)) {
56
- for (const v of value) {
57
- append(\`\${prefix}[]\`, v);
58
- }
59
- } else {
60
- params.append(prefix, String(value));
61
- }
62
- }
63
-
64
- for (const [key, value] of Object.entries(data)) {
65
- append(key, value);
66
- }
67
-
68
- const query = params.toString();
69
- if (query) {
70
- fullUrl += \`?\${query}\`;
71
- }
72
- } else {
73
- options.body = JSON.stringify(data);
74
- }
75
- }
76
-
77
- const response = await fetch(fullUrl, options);
78
- const text = await response.text();
79
-
80
- let result: any;
81
- try {
82
- result = text ? JSON.parse(text) : null;
83
- } catch {
84
- result = text;
85
- }
86
-
87
- if (!response.ok) {
88
- throw new (globalThis as any).SwellError(result || text || "Request failed", {
89
- status: response.status,
90
- });
91
- }
92
-
93
- if (result?.errors) {
94
- throw new (globalThis as any).SwellError(result.errors, {
95
- status: 400,
96
- });
97
- }
98
-
99
- return result;
100
- }
101
-
102
- export function createSwellClient(): BasicSwellClient {
103
- return {
104
- get(url: string, query?: any) {
105
- return makeRequest("GET", url, query);
106
- },
107
- post(url: string, data?: any) {
108
- return makeRequest("POST", url, data);
109
- },
110
- put(url: string, data?: any) {
111
- return makeRequest("PUT", url, data);
112
- },
113
- delete(url: string, data?: any) {
114
- return makeRequest("DELETE", url, data);
115
- },
116
- settings(id?: string) {
117
- const appId = id || env.SWELL_APP_ID;
118
- if (!appId) {
119
- throw new Error(
120
- "Missing app id. Pass an id to settings() or set SWELL_APP_ID binding.",
121
- );
122
- }
123
- return makeRequest("GET", \`/settings/\${appId}\`);
124
- },
125
- } as BasicSwellClient;
126
- }
127
- `;
128
- }