@swell/cli 2.9.26 → 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 (44) hide show
  1. package/dist/commands/app/dev.d.ts +1 -0
  2. package/dist/commands/app/dev.js +26 -2
  3. package/dist/commands/create/component.d.ts +22 -0
  4. package/dist/commands/create/component.js +129 -0
  5. package/dist/commands/create/index.js +5 -1
  6. package/dist/commands/create/tests.js +11 -2
  7. package/dist/commands/inspect/extensions.js +3 -3
  8. package/dist/lib/apps/app-config.d.ts +15 -0
  9. package/dist/lib/apps/app-config.js +99 -30
  10. package/dist/lib/apps/component-extension.d.ts +23 -0
  11. package/dist/lib/apps/component-extension.js +60 -0
  12. package/dist/lib/apps/components.d.ts +32 -0
  13. package/dist/lib/apps/components.js +81 -0
  14. package/dist/lib/apps/frontend-scaffold.js +1 -1
  15. package/dist/lib/apps/index.d.ts +1 -0
  16. package/dist/lib/apps/index.js +5 -3
  17. package/dist/lib/bundle.d.ts +21 -1
  18. package/dist/lib/bundle.js +88 -5
  19. package/dist/lib/create/component.d.ts +22 -0
  20. package/dist/lib/create/component.js +194 -0
  21. package/dist/lib/create/content.d.ts +1 -1
  22. package/dist/lib/create/setting.d.ts +1 -1
  23. package/dist/lib/create/tests/templates/index.d.ts +0 -3
  24. package/dist/lib/create/tests/templates/index.js +0 -3
  25. package/dist/lib/create/tests/templates/mock-request.js +4 -10
  26. package/dist/lib/create/tests/templates/tsconfig.js +1 -1
  27. package/dist/lib/create/tests/templates/vitest-config.d.ts +1 -2
  28. package/dist/lib/create/tests/templates/vitest-config.js +17 -113
  29. package/dist/lib/create/tests/types.d.ts +2 -0
  30. package/dist/lib/create/tests.js +64 -12
  31. package/dist/lib/function-source-analysis.d.ts +11 -0
  32. package/dist/lib/function-source-analysis.js +33 -0
  33. package/dist/lib/inspect/extensions.d.ts +8 -1
  34. package/dist/lib/inspect/extensions.js +12 -2
  35. package/dist/lib/swell-function-wrapper.js +31 -0
  36. package/dist/push-app-command.js +11 -0
  37. package/oclif.manifest.json +82 -3
  38. package/package.json +3 -1
  39. package/dist/lib/create/tests/templates/env-dts.d.ts +0 -1
  40. package/dist/lib/create/tests/templates/env-dts.js +0 -15
  41. package/dist/lib/create/tests/templates/integration-test.d.ts +0 -1
  42. package/dist/lib/create/tests/templates/integration-test.js +0 -19
  43. package/dist/lib/create/tests/templates/swell-client.d.ts +0 -1
  44. package/dist/lib/create/tests/templates/swell-client.js +0 -128
@@ -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) {
@@ -14,3 +14,14 @@ export declare function analyzeFunctionSourceText(sourceText: string, filePath?:
14
14
  export declare function getFunctionTriggers(config: Record<string, unknown>): string[];
15
15
  export declare function hasKindDiagnostic(analysis: FunctionSourceAnalysis): boolean;
16
16
  export declare function workflowStaticConfigError(): Error;
17
+ export interface StaticConfig {
18
+ config?: Record<string, unknown>;
19
+ errors: string[];
20
+ }
21
+ /**
22
+ * Reads `export const config = {...}` as static JSON-like data without running the module.
23
+ * @param {string} sourceText source code of the file
24
+ * @param {string} filePath file name, used to pick the parser (TS, TSX, JS or JSX)
25
+ * @returns {StaticConfig} the config when present and static, otherwise the errors
26
+ */
27
+ export declare function readStaticConfig(sourceText: string, filePath: string): StaticConfig;
@@ -76,6 +76,39 @@ export function hasKindDiagnostic(analysis) {
76
76
  export function workflowStaticConfigError() {
77
77
  return new Error('Workflow configs must export a static object literal so the CLI can choose the workflow bundling path before compiling.');
78
78
  }
79
+ /**
80
+ * Reads `export const config = {...}` as static JSON-like data without running the module.
81
+ * @param {string} sourceText source code of the file
82
+ * @param {string} filePath file name, used to pick the parser (TS, TSX, JS or JSX)
83
+ * @returns {StaticConfig} the config when present and static, otherwise the errors
84
+ */
85
+ export function readStaticConfig(sourceText, filePath) {
86
+ const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, scriptKind(filePath));
87
+ const configExport = findConfigExport(sourceFile);
88
+ if (!configExport) {
89
+ return { errors: [] };
90
+ }
91
+ if (!configExport.initializer) {
92
+ return { errors: ['config must be initialized with an object literal.'] };
93
+ }
94
+ const result = evaluateStatic(configExport.initializer);
95
+ if (result.errors.length > 0) {
96
+ return { errors: result.errors };
97
+ }
98
+ if (!isRecord(result.value)) {
99
+ return { errors: ['config must be an object literal.'] };
100
+ }
101
+ return { config: result.value, errors: [] };
102
+ }
103
+ function scriptKind(filePath) {
104
+ if (filePath.endsWith('.tsx'))
105
+ return ts.ScriptKind.TSX;
106
+ if (filePath.endsWith('.jsx'))
107
+ return ts.ScriptKind.JSX;
108
+ if (filePath.endsWith('.ts'))
109
+ return ts.ScriptKind.TS;
110
+ return ts.ScriptKind.JS;
111
+ }
79
112
  function findConfigExport(sourceFile) {
80
113
  const localVariables = new Map();
81
114
  let exportedConfigLocalName;
@@ -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
  }
@@ -107,6 +107,10 @@ class SwellRequest {
107
107
  this.id = req.headers.get('Swell-Request-ID') || this.logParams?.req_id;
108
108
  // Check if the request is from a local development environment
109
109
  this.isLocalDev = req.headers.get('Swell-Local-Dev') === 'true';
110
+ // Context of a call from an app component (Swell-Context). Only the platform
111
+ // sets this header; verify it with verifySwellContext from @swell/apps-sdk
112
+ // when a function needs cryptographic proof
113
+ this.swellContext = this.parseSwellContext(req.headers.get('Swell-Context'));
110
114
  // Swell client
111
115
  this.swell = new SwellAPI(this, context);
112
116
  // URL of the original request
@@ -160,6 +164,33 @@ class SwellRequest {
160
164
  return {};
161
165
  }
162
166
  }
167
+ parseSwellContext(token) {
168
+ const payload = token?.split('.')[1];
169
+ if (!payload) {
170
+ return null;
171
+ }
172
+ let claims;
173
+ try {
174
+ const binary = atob(payload.replaceAll('-', '+').replaceAll('_', '/'));
175
+ claims = JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0))));
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ if (!claims || typeof claims !== 'object') {
181
+ return null;
182
+ }
183
+ return {
184
+ appId: claims.app_id,
185
+ installationId: claims.installation_id,
186
+ storeId: claims.store_id,
187
+ storeUser: claims.admin?.user_id
188
+ ? { storeId: claims.store_id, userId: claims.admin.user_id }
189
+ : null,
190
+ storefrontId: claims.storefront_id ?? undefined,
191
+ surface: claims.surface ?? undefined,
192
+ };
193
+ }
163
194
  log(level, ...line) {
164
195
  originalConsole[level]?.(...line);
165
196
  this._logs.push({
@@ -6,6 +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 { componentsToRebuild } from './lib/apps/components.js';
9
10
  import { buildFrontendPackage, getManagedFrontendFramework, } from './lib/apps/frontend-package.js';
10
11
  import { warnFrontendRouteCollisions } from './lib/apps/frontend-routes.js';
11
12
  import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExistsAsync, findAppConfig, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashFile, hashString, isPathDirectory, toPosixPath, } from './lib/apps/index.js';
@@ -714,6 +715,16 @@ export class PushAppCommand extends RemoteAppCommand {
714
715
  appConfig: config,
715
716
  configFile,
716
717
  });
718
+ // Root components bundle their shared code, so push them again too
719
+ const componentFiles = await componentsToRebuild(this.appPath, configFile);
720
+ const componentConfigs = await Promise.all(componentFiles.map((file) => appConfigFromFile(file, ConfigType.COMPONENT, this.appPath)));
721
+ for (const appConfig of componentConfigs) {
722
+ this.watchingChangeQueue.set(appConfig.filePath, {
723
+ action: 'push',
724
+ appConfig,
725
+ configFile: appConfig.filePath,
726
+ });
727
+ }
717
728
  break;
718
729
  }
719
730
  }
@@ -1652,6 +1652,85 @@
1652
1652
  "app.js"
1653
1653
  ]
1654
1654
  },
1655
+ "create:component": {
1656
+ "aliases": [],
1657
+ "args": {
1658
+ "name": {
1659
+ "default": "",
1660
+ "description": "Component name (e.g., ColorPicker)",
1661
+ "name": "name"
1662
+ }
1663
+ },
1664
+ "description": "Create an app component in the components folder.",
1665
+ "examples": [
1666
+ "$ swell create component",
1667
+ "$ swell create component ColorPicker -d \"Brand color picker\" -y",
1668
+ "$ swell create component MyComponent --extension <extension-id> --target <target-id> -y"
1669
+ ],
1670
+ "flags": {
1671
+ "app-path": {
1672
+ "description": "Path to your app directory",
1673
+ "name": "app-path",
1674
+ "hasDynamicHelp": false,
1675
+ "multiple": false,
1676
+ "type": "option"
1677
+ },
1678
+ "description": {
1679
+ "char": "d",
1680
+ "description": "Description",
1681
+ "name": "description",
1682
+ "default": "",
1683
+ "hasDynamicHelp": false,
1684
+ "multiple": false,
1685
+ "type": "option"
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
+ },
1694
+ "overwrite": {
1695
+ "description": "Overwrite existing file",
1696
+ "name": "overwrite",
1697
+ "allowNo": false,
1698
+ "type": "boolean"
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
+ },
1707
+ "yes": {
1708
+ "char": "y",
1709
+ "description": "Skip prompts, require all arguments",
1710
+ "name": "yes",
1711
+ "allowNo": false,
1712
+ "type": "boolean"
1713
+ }
1714
+ },
1715
+ "hasDynamicHelp": false,
1716
+ "hiddenAliases": [],
1717
+ "id": "create:component",
1718
+ "pluginAlias": "@swell/cli",
1719
+ "pluginName": "@swell/cli",
1720
+ "pluginType": "core",
1721
+ "strict": true,
1722
+ "summary": "Create an app component in the components folder.",
1723
+ "helpMeta": {
1724
+ "usageDirect": "<name> [-d <description>] [--extension <extension-id>] [--target <target-id>...] -y"
1725
+ },
1726
+ "isESM": true,
1727
+ "relativePath": [
1728
+ "dist",
1729
+ "commands",
1730
+ "create",
1731
+ "component.js"
1732
+ ]
1733
+ },
1655
1734
  "create:content": {
1656
1735
  "aliases": [],
1657
1736
  "args": {
@@ -1978,7 +2057,7 @@
1978
2057
  "create": {
1979
2058
  "aliases": [],
1980
2059
  "args": {},
1981
- "description": "Scaffold apps, models, functions, and notifications.",
2060
+ "description": "Scaffold apps, models, functions, components, and notifications.",
1982
2061
  "examples": [
1983
2062
  "<%= config.bin %> <%= command.id %>"
1984
2063
  ],
@@ -2282,7 +2361,7 @@
2282
2361
  "create:tests": {
2283
2362
  "aliases": [],
2284
2363
  "args": {},
2285
- "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.",
2286
2365
  "examples": [
2287
2366
  "$ swell create tests",
2288
2367
  "$ swell create tests --overwrite"
@@ -3606,5 +3685,5 @@
3606
3685
  ]
3607
3686
  }
3608
3687
  },
3609
- "version": "2.9.26"
3688
+ "version": "2.9.28"
3610
3689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.9.26",
3
+ "version": "2.9.28",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [
@@ -80,9 +80,11 @@
80
80
  "eslint-config-prettier": "9.0.0",
81
81
  "eslint-plugin-prettier": "5.0.1",
82
82
  "eslint-plugin-unicorn": "48.0.1",
83
+ "happy-dom": "20.14.5",
83
84
  "mocha": "11.7.6",
84
85
  "nock": "13.3.6",
85
86
  "oclif": "4.0.3",
87
+ "preact": "10.28.0",
86
88
  "shx": "0.3.4",
87
89
  "testdouble": "3.20.0",
88
90
  "ts-node": "10.9.1"
@@ -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;