@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.
- package/dist/commands/app/dev.d.ts +1 -0
- package/dist/commands/app/dev.js +26 -2
- package/dist/commands/create/component.d.ts +22 -0
- package/dist/commands/create/component.js +129 -0
- package/dist/commands/create/index.js +5 -1
- package/dist/commands/create/tests.js +11 -2
- package/dist/commands/inspect/extensions.js +3 -3
- package/dist/lib/apps/app-config.d.ts +15 -0
- package/dist/lib/apps/app-config.js +99 -30
- package/dist/lib/apps/component-extension.d.ts +23 -0
- package/dist/lib/apps/component-extension.js +60 -0
- package/dist/lib/apps/components.d.ts +32 -0
- package/dist/lib/apps/components.js +81 -0
- package/dist/lib/apps/frontend-scaffold.js +1 -1
- package/dist/lib/apps/index.d.ts +1 -0
- package/dist/lib/apps/index.js +5 -3
- package/dist/lib/bundle.d.ts +21 -1
- package/dist/lib/bundle.js +88 -5
- package/dist/lib/create/component.d.ts +22 -0
- package/dist/lib/create/component.js +194 -0
- package/dist/lib/create/content.d.ts +1 -1
- package/dist/lib/create/setting.d.ts +1 -1
- package/dist/lib/create/tests/templates/index.d.ts +0 -3
- package/dist/lib/create/tests/templates/index.js +0 -3
- package/dist/lib/create/tests/templates/mock-request.js +4 -10
- package/dist/lib/create/tests/templates/tsconfig.js +1 -1
- package/dist/lib/create/tests/templates/vitest-config.d.ts +1 -2
- package/dist/lib/create/tests/templates/vitest-config.js +17 -113
- package/dist/lib/create/tests/types.d.ts +2 -0
- package/dist/lib/create/tests.js +64 -12
- package/dist/lib/function-source-analysis.d.ts +11 -0
- package/dist/lib/function-source-analysis.js +33 -0
- package/dist/lib/inspect/extensions.d.ts +8 -1
- package/dist/lib/inspect/extensions.js +12 -2
- package/dist/lib/swell-function-wrapper.js +31 -0
- package/dist/push-app-command.js +11 -0
- package/oclif.manifest.json +82 -3
- package/package.json +3 -1
- package/dist/lib/create/tests/templates/env-dts.d.ts +0 -1
- package/dist/lib/create/tests/templates/env-dts.js +0 -15
- package/dist/lib/create/tests/templates/integration-test.d.ts +0 -1
- package/dist/lib/create/tests/templates/integration-test.js +0 -19
- package/dist/lib/create/tests/templates/swell-client.d.ts +0 -1
- 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(
|
|
2
|
+
export function vitestConfigTemplate() {
|
|
3
3
|
return `\
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 */
|
package/dist/lib/create/tests.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
7
|
-
'@cloudflare/vitest-
|
|
8
|
-
vitest: '
|
|
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':
|
|
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[
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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({
|
package/dist/push-app-command.js
CHANGED
|
@@ -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
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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;
|