@swell/cli 2.1.0 → 2.2.1
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/api/delete.d.ts +15 -0
- package/dist/commands/api/delete.js +26 -0
- package/dist/commands/api/get.d.ts +15 -0
- package/dist/commands/api/get.js +31 -0
- package/dist/commands/api/index.d.ts +7 -0
- package/dist/commands/api/index.js +16 -0
- package/dist/commands/api/post.d.ts +16 -0
- package/dist/commands/api/post.js +33 -0
- package/dist/commands/api/put.d.ts +16 -0
- package/dist/commands/api/put.js +33 -0
- package/dist/commands/app/dev.d.ts +9 -9
- package/dist/commands/app/dev.js +163 -139
- package/dist/commands/app/frontend/dev.d.ts +1 -1
- package/dist/commands/app/frontend/dev.js +84 -78
- package/dist/commands/app/install.js +3 -3
- package/dist/commands/app/pull.d.ts +1 -1
- package/dist/commands/app/pull.js +6 -6
- package/dist/commands/app/push.d.ts +3 -3
- package/dist/commands/app/push.js +8 -6
- package/dist/commands/app/release.d.ts +1 -1
- package/dist/commands/app/release.js +1 -1
- package/dist/commands/app/version.d.ts +1 -1
- package/dist/commands/app/version.js +5 -3
- package/dist/commands/create/app.d.ts +10 -2
- package/dist/commands/create/app.js +226 -57
- package/dist/commands/create/content.d.ts +4 -0
- package/dist/commands/create/content.js +83 -15
- package/dist/commands/create/function.js +27 -5
- package/dist/commands/create/index.js +5 -1
- package/dist/commands/create/model.d.ts +4 -0
- package/dist/commands/create/model.js +120 -25
- package/dist/commands/create/tests.d.ts +15 -0
- package/dist/commands/create/tests.js +79 -0
- package/dist/commands/inspect/content.d.ts +25 -0
- package/dist/commands/inspect/content.js +159 -0
- package/dist/commands/inspect/index.d.ts +7 -0
- package/dist/commands/inspect/index.js +16 -0
- package/dist/commands/inspect/models.d.ts +27 -0
- package/dist/commands/inspect/models.js +197 -0
- package/dist/commands/schema.d.ts +27 -0
- package/dist/commands/schema.js +231 -0
- package/dist/commands/theme/dev.d.ts +1 -1
- package/dist/commands/theme/dev.js +2 -2
- package/dist/commands/theme/init.d.ts +1 -1
- package/dist/commands/theme/init.js +1 -1
- package/dist/commands/theme/pull.d.ts +2 -2
- package/dist/commands/theme/pull.js +2 -2
- package/dist/commands/theme/push.d.ts +1 -1
- package/dist/commands/theme/push.js +1 -1
- package/dist/create-app-command.d.ts +12 -2
- package/dist/create-app-command.js +103 -80
- package/dist/create-collection-command.js +4 -1
- package/dist/create-config-command.js +4 -0
- package/dist/env/local.d.ts +1 -0
- package/dist/env/local.js +1 -0
- package/dist/env/production.d.ts +1 -0
- package/dist/env/production.js +1 -0
- package/dist/env/review.d.ts +1 -0
- package/dist/env/review.js +1 -0
- package/dist/env/staging.d.ts +1 -0
- package/dist/env/staging.js +1 -0
- package/dist/lib/api.d.ts +16 -5
- package/dist/lib/api.js +67 -25
- package/dist/lib/app-config.js +1 -0
- package/dist/lib/apps/index.d.ts +2 -0
- package/dist/lib/apps/index.js +20 -14
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +1 -0
- package/dist/lib/create/content.d.ts +5 -1
- package/dist/lib/create/content.js +8 -1
- package/dist/lib/create/function.d.ts +2 -1
- package/dist/lib/create/function.js +7 -3
- package/dist/lib/create/model.d.ts +1 -0
- package/dist/lib/create/schemas.d.ts +6 -0
- package/dist/lib/create/schemas.js +6 -0
- package/dist/lib/create/tests/templates/env-dts.d.ts +1 -0
- package/dist/lib/create/tests/templates/env-dts.js +14 -0
- package/dist/lib/create/tests/templates/index.d.ts +8 -0
- package/dist/lib/create/tests/templates/index.js +8 -0
- package/dist/lib/create/tests/templates/integration-test.d.ts +1 -0
- package/dist/lib/create/tests/templates/integration-test.js +19 -0
- package/dist/lib/create/tests/templates/mock-request.d.ts +2 -0
- package/dist/lib/create/tests/templates/mock-request.js +112 -0
- package/dist/lib/create/tests/templates/setup-globals.d.ts +1 -0
- package/dist/lib/create/tests/templates/setup-globals.js +23 -0
- package/dist/lib/create/tests/templates/swell-client.d.ts +1 -0
- package/dist/lib/create/tests/templates/swell-client.js +126 -0
- package/dist/lib/create/tests/templates/tsconfig.d.ts +1 -0
- package/dist/lib/create/tests/templates/tsconfig.js +15 -0
- package/dist/lib/create/tests/templates/unit-test.d.ts +1 -0
- package/dist/lib/create/tests/templates/unit-test.js +42 -0
- package/dist/lib/create/tests/templates/vitest-config.d.ts +2 -0
- package/dist/lib/create/tests/templates/vitest-config.js +121 -0
- package/dist/lib/create/tests/types.d.ts +21 -0
- package/dist/lib/create/tests/types.js +1 -0
- package/dist/lib/create/tests.d.ts +4 -0
- package/dist/lib/create/tests.js +113 -0
- package/dist/lib/sessions.js +4 -3
- package/dist/lib/style.js +0 -1
- package/dist/lib/theme-sync.d.ts +6 -6
- package/dist/lib/theme-sync.js +49 -39
- package/dist/push-app-command.d.ts +7 -7
- package/dist/push-app-command.js +38 -36
- package/dist/remote-app-command.d.ts +1 -1
- package/dist/remote-app-command.js +4 -2
- package/dist/swell-api-command.d.ts +13 -0
- package/dist/swell-api-command.js +75 -0
- package/dist/swell-command.d.ts +1 -1
- package/dist/swell-command.js +9 -9
- package/oclif.manifest.json +639 -46
- package/package.json +8 -1
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
|
|
20
|
+
if (!sessionId) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"Missing SWELL_SESSION_ID binding. Run \\\`swell login\\\` or set it explicitly for tests.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let endpointUrl = String(url).startsWith("/") ? url.substring(1) : String(url);
|
|
27
|
+
if (!endpointUrl.startsWith("data/")) {
|
|
28
|
+
endpointUrl = \`data/\${endpointUrl}\`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let fullUrl = \`\${baseUrl}/\${endpointUrl}\`;
|
|
32
|
+
const headers: HeadersInit = {
|
|
33
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
34
|
+
"User-Agent": "swell-app-tests/1.0",
|
|
35
|
+
"X-Session": sessionId,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const options: RequestInit = {
|
|
39
|
+
method,
|
|
40
|
+
headers,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (data) {
|
|
44
|
+
if (method === "GET") {
|
|
45
|
+
const params = new URLSearchParams();
|
|
46
|
+
|
|
47
|
+
function append(prefix: string, value: any) {
|
|
48
|
+
if (value === null || value === undefined) return;
|
|
49
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
50
|
+
for (const [k, v] of Object.entries(value)) {
|
|
51
|
+
append(\`\${prefix}[\${k}]\`, v);
|
|
52
|
+
}
|
|
53
|
+
} else if (Array.isArray(value)) {
|
|
54
|
+
for (const v of value) {
|
|
55
|
+
append(\`\${prefix}[]\`, v);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
params.append(prefix, String(value));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(data)) {
|
|
63
|
+
append(key, value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const query = params.toString();
|
|
67
|
+
if (query) {
|
|
68
|
+
fullUrl += \`?\${query}\`;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
options.body = JSON.stringify(data);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const response = await fetch(fullUrl, options);
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
|
|
78
|
+
let result: any;
|
|
79
|
+
try {
|
|
80
|
+
result = text ? JSON.parse(text) : null;
|
|
81
|
+
} catch {
|
|
82
|
+
result = text;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new (globalThis as any).SwellError(result || text || "Request failed", {
|
|
87
|
+
status: response.status,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (result?.errors) {
|
|
92
|
+
throw new (globalThis as any).SwellError(result.errors, {
|
|
93
|
+
status: 400,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createSwellClient(): BasicSwellClient {
|
|
101
|
+
return {
|
|
102
|
+
get(url: string, query?: any) {
|
|
103
|
+
return makeRequest("GET", url, query);
|
|
104
|
+
},
|
|
105
|
+
post(url: string, data?: any) {
|
|
106
|
+
return makeRequest("POST", url, data);
|
|
107
|
+
},
|
|
108
|
+
put(url: string, data?: any) {
|
|
109
|
+
return makeRequest("PUT", url, data);
|
|
110
|
+
},
|
|
111
|
+
delete(url: string, data?: any) {
|
|
112
|
+
return makeRequest("DELETE", url, data);
|
|
113
|
+
},
|
|
114
|
+
settings(id?: string) {
|
|
115
|
+
const appId = id || env.SWELL_APP_ID;
|
|
116
|
+
if (!appId) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"Missing app id. Pass an id to settings() or set SWELL_APP_ID binding.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return makeRequest("GET", \`/settings/\${appId}\`);
|
|
122
|
+
},
|
|
123
|
+
} as BasicSwellClient;
|
|
124
|
+
}
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function tsconfigTemplate(): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function tsconfigTemplate() {
|
|
2
|
+
const config = {
|
|
3
|
+
extends: '../tsconfig.json',
|
|
4
|
+
compilerOptions: {
|
|
5
|
+
moduleResolution: 'bundler',
|
|
6
|
+
types: [
|
|
7
|
+
'@cloudflare/vitest-pool-workers',
|
|
8
|
+
'@swell/app-types',
|
|
9
|
+
'vitest/globals',
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
include: ['./**/*.ts', '../functions/**/*.ts'],
|
|
13
|
+
};
|
|
14
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unitTestTemplate(): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function unitTestTemplate() {
|
|
2
|
+
return `\
|
|
3
|
+
import { describe, it, expect, vi } from "vitest";
|
|
4
|
+
import { createMockRequest } from "../helpers/mock-request";
|
|
5
|
+
|
|
6
|
+
// Import your functions to test:
|
|
7
|
+
// import handler from "../../functions/my-handler";
|
|
8
|
+
|
|
9
|
+
describe("Unit test examples", () => {
|
|
10
|
+
it("creates a mock request with data and session", () => {
|
|
11
|
+
const req = createMockRequest({
|
|
12
|
+
data: { product_id: "prod_123" },
|
|
13
|
+
session: { account_id: "acc_456" },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(req.data.product_id).toBe("prod_123");
|
|
17
|
+
expect(req.session.account_id).toBe("acc_456");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("mocks swell.get", async () => {
|
|
21
|
+
const mockGet = vi.fn().mockResolvedValue({ id: "123", name: "Test" });
|
|
22
|
+
const req = createMockRequest({ swell: { get: mockGet } });
|
|
23
|
+
|
|
24
|
+
const result = await req.swell.get("/products/{id}", { id: "123" });
|
|
25
|
+
|
|
26
|
+
expect(result.name).toBe("Test");
|
|
27
|
+
expect(mockGet).toHaveBeenCalledWith("/products/{id}", { id: "123" });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("mocks swell.settings", async () => {
|
|
31
|
+
const mockSettings = vi.fn().mockResolvedValue({
|
|
32
|
+
feature: { enabled: true },
|
|
33
|
+
});
|
|
34
|
+
const req = createMockRequest({ swell: { settings: mockSettings } });
|
|
35
|
+
|
|
36
|
+
const settings = await req.swell.settings();
|
|
37
|
+
|
|
38
|
+
expect(settings.feature.enabled).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export function vitestConfigTemplate(options) {
|
|
2
|
+
return `\
|
|
3
|
+
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
|
|
4
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
type SdkAuth = {
|
|
9
|
+
storeId: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
apiBaseUrl: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const isCI = Boolean(process.env.CI || process.env.CONTINUOUS_INTEGRATION);
|
|
15
|
+
|
|
16
|
+
function loadSwellAuth(): SdkAuth {
|
|
17
|
+
const envStoreId = process.env.SWELL_STORE_ID;
|
|
18
|
+
const envSessionId = process.env.SWELL_SESSION_ID;
|
|
19
|
+
const envApiBaseUrl = process.env.SWELL_API_BASE_URL;
|
|
20
|
+
|
|
21
|
+
if (envStoreId && envSessionId) {
|
|
22
|
+
return {
|
|
23
|
+
storeId: envStoreId,
|
|
24
|
+
sessionId: envSessionId,
|
|
25
|
+
apiBaseUrl: envApiBaseUrl || \`https://\${envStoreId}.swell.store/admin/api\`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (isCI) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"CI environment detected but SWELL_STORE_ID and SWELL_SESSION_ID are not set. " +
|
|
32
|
+
"Add these as CI secrets/variables to run integration tests.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const home = homedir();
|
|
37
|
+
const configPath = path.resolve(home, ".swell", "config.json");
|
|
38
|
+
if (!existsSync(configPath)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
\`Swell CLI config not found at \${configPath}. Run \\\`swell login\\\` or set SWELL_STORE_ID and SWELL_SESSION_ID.\`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rawConfig = readFileSync(configPath, "utf-8");
|
|
45
|
+
|
|
46
|
+
interface CliConfig {
|
|
47
|
+
defaultStore?: string;
|
|
48
|
+
stores?: { storeId: string; sessionId?: string }[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let configJson: CliConfig;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
configJson = JSON.parse(rawConfig) as CliConfig;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
\`Unable to parse Swell CLI config at \${configPath}: \${String(error)}\`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const defaultStore = configJson.defaultStore;
|
|
62
|
+
const stores = configJson.stores || [];
|
|
63
|
+
|
|
64
|
+
const store = defaultStore
|
|
65
|
+
? stores.find((item) => item.storeId === defaultStore)
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
if (!defaultStore || !store?.sessionId) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"No active Swell CLI session found. Run \\\`swell login\\\` or set SWELL_STORE_ID and SWELL_SESSION_ID.",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const envPath = path.join(home, ".swell", "env.json");
|
|
75
|
+
let apiBaseUrl = \`https://\${defaultStore}.swell.store/admin/api\`;
|
|
76
|
+
|
|
77
|
+
if (existsSync(envPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const rawEnv = readFileSync(envPath, "utf-8");
|
|
80
|
+
const envJson = JSON.parse(rawEnv) as { ADMIN_API_BASE_URL?: string };
|
|
81
|
+
if (envJson.ADMIN_API_BASE_URL) {
|
|
82
|
+
apiBaseUrl = envJson.ADMIN_API_BASE_URL.replace("\${STORE_ID}", defaultStore);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore env parsing errors and fall back to default
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
storeId: defaultStore,
|
|
91
|
+
sessionId: store.sessionId!,
|
|
92
|
+
apiBaseUrl,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sdkAuth = loadSwellAuth();
|
|
97
|
+
|
|
98
|
+
export default defineWorkersConfig({
|
|
99
|
+
test: {
|
|
100
|
+
globals: true,
|
|
101
|
+
include: ["test/**/*.test.ts"],
|
|
102
|
+
exclude: ["node_modules", "docs"],
|
|
103
|
+
setupFiles: ["./test/setup-globals.ts"],
|
|
104
|
+
poolOptions: {
|
|
105
|
+
workers: {
|
|
106
|
+
singleWorker: true,
|
|
107
|
+
isolatedStorage: true,
|
|
108
|
+
miniflare: {
|
|
109
|
+
bindings: {
|
|
110
|
+
SWELL_STORE_ID: sdkAuth.storeId,
|
|
111
|
+
SWELL_SESSION_ID: sdkAuth.sessionId,
|
|
112
|
+
SWELL_API_BASE_URL: sdkAuth.apiBaseUrl,
|
|
113
|
+
SWELL_APP_ID: "${options.appId}",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface TestTemplateOptions {
|
|
2
|
+
/** App id from swell.json, used for SWELL_APP_ID and settings() */
|
|
3
|
+
appId: string;
|
|
4
|
+
}
|
|
5
|
+
interface WriteTestsOptions extends TestTemplateOptions {
|
|
6
|
+
/** Absolute path to the app root */
|
|
7
|
+
appPath: string;
|
|
8
|
+
/** Overwrite existing files if present */
|
|
9
|
+
overwrite: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface ScaffoldResult {
|
|
12
|
+
/** Files that were created or overwritten */
|
|
13
|
+
createdFiles: string[];
|
|
14
|
+
/** Files that were skipped because they already exist */
|
|
15
|
+
skippedFiles: string[];
|
|
16
|
+
/** Whether package.json was updated */
|
|
17
|
+
packageJsonUpdated: boolean;
|
|
18
|
+
/** Warning messages to display to user */
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
|
21
|
+
export type { ScaffoldResult, TestTemplateOptions, WriteTestsOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { filePathExists, writeFile } from '../apps/index.js';
|
|
4
|
+
import { envDtsTemplate, integrationTestTemplate, mockRequestTemplate, setupGlobalsTemplate, swellClientTemplate, tsconfigTemplate, unitTestTemplate, vitestConfigTemplate, } from './tests/templates/index.js';
|
|
5
|
+
const TEST_DEV_DEPENDENCIES = {
|
|
6
|
+
'@cloudflare/vitest-pool-workers': 'latest',
|
|
7
|
+
vitest: 'latest',
|
|
8
|
+
'@swell/app-types': 'latest',
|
|
9
|
+
};
|
|
10
|
+
async function writeTextFile(filePath, contents, overwrite) {
|
|
11
|
+
if (!overwrite && filePathExists(filePath)) {
|
|
12
|
+
return { created: false, skipped: true };
|
|
13
|
+
}
|
|
14
|
+
await writeFile(filePath, contents);
|
|
15
|
+
return { created: true, skipped: false };
|
|
16
|
+
}
|
|
17
|
+
function updatePackageJson(appPath) {
|
|
18
|
+
const pkgPath = path.join(appPath, 'package.json');
|
|
19
|
+
if (!filePathExists(pkgPath)) {
|
|
20
|
+
return {
|
|
21
|
+
updated: false,
|
|
22
|
+
warning: 'package.json not found. Run `npm init` first.',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
let pkgRaw;
|
|
26
|
+
try {
|
|
27
|
+
pkgRaw = fs.readFileSync(pkgPath, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
updated: false,
|
|
32
|
+
warning: `Could not read package.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
let pkg;
|
|
36
|
+
try {
|
|
37
|
+
pkg = JSON.parse(pkgRaw);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
updated: false,
|
|
42
|
+
warning: `Could not parse package.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const devDeps = (pkg.devDependencies || {});
|
|
46
|
+
const deps = (pkg.dependencies || {});
|
|
47
|
+
devDeps['@cloudflare/vitest-pool-workers'] =
|
|
48
|
+
devDeps['@cloudflare/vitest-pool-workers'] ||
|
|
49
|
+
TEST_DEV_DEPENDENCIES['@cloudflare/vitest-pool-workers'];
|
|
50
|
+
devDeps.vitest = devDeps.vitest || TEST_DEV_DEPENDENCIES.vitest;
|
|
51
|
+
if (!devDeps['@swell/app-types'] && !deps['@swell/app-types']) {
|
|
52
|
+
devDeps['@swell/app-types'] = TEST_DEV_DEPENDENCIES['@swell/app-types'];
|
|
53
|
+
}
|
|
54
|
+
pkg.devDependencies = devDeps;
|
|
55
|
+
const scripts = (pkg.scripts || {});
|
|
56
|
+
scripts.test = scripts.test || 'vitest run';
|
|
57
|
+
pkg.scripts = scripts;
|
|
58
|
+
try {
|
|
59
|
+
const next = `${JSON.stringify(pkg, null, 2)}\n`;
|
|
60
|
+
fs.writeFileSync(pkgPath, next, 'utf8');
|
|
61
|
+
return { updated: true };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
updated: false,
|
|
66
|
+
warning: `Could not write package.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function validatePrerequisites(appPath) {
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const tsconfigPath = path.join(appPath, 'tsconfig.json');
|
|
73
|
+
if (!filePathExists(tsconfigPath)) {
|
|
74
|
+
warnings.push('tsconfig.json not found. test/tsconfig.json extends it; create one or tests may not compile.');
|
|
75
|
+
}
|
|
76
|
+
return warnings;
|
|
77
|
+
}
|
|
78
|
+
async function createTestsScaffold(options) {
|
|
79
|
+
const { appPath, appId, overwrite } = options;
|
|
80
|
+
const result = {
|
|
81
|
+
createdFiles: [],
|
|
82
|
+
skippedFiles: [],
|
|
83
|
+
packageJsonUpdated: false,
|
|
84
|
+
warnings: [],
|
|
85
|
+
};
|
|
86
|
+
const prereqWarnings = validatePrerequisites(appPath);
|
|
87
|
+
result.warnings.push(...prereqWarnings);
|
|
88
|
+
const trackFile = async (relativePath, contents) => {
|
|
89
|
+
const fullPath = path.join(appPath, relativePath);
|
|
90
|
+
const writeResult = await writeTextFile(fullPath, contents, overwrite);
|
|
91
|
+
if (writeResult.created) {
|
|
92
|
+
result.createdFiles.push(relativePath);
|
|
93
|
+
}
|
|
94
|
+
else if (writeResult.skipped) {
|
|
95
|
+
result.skippedFiles.push(relativePath);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
await trackFile('vitest.config.ts', vitestConfigTemplate({ appId }));
|
|
99
|
+
await trackFile('test/tsconfig.json', tsconfigTemplate());
|
|
100
|
+
await trackFile('test/env.d.ts', envDtsTemplate());
|
|
101
|
+
await trackFile('test/setup-globals.ts', setupGlobalsTemplate());
|
|
102
|
+
await trackFile('test/helpers/swell-client.ts', swellClientTemplate());
|
|
103
|
+
await trackFile('test/helpers/mock-request.ts', mockRequestTemplate({ appId }));
|
|
104
|
+
await trackFile('test/unit/example.test.ts', unitTestTemplate());
|
|
105
|
+
await trackFile('test/integration/example.test.ts', integrationTestTemplate());
|
|
106
|
+
const pkgResult = updatePackageJson(appPath);
|
|
107
|
+
result.packageJsonUpdated = pkgResult.updated;
|
|
108
|
+
if (pkgResult.warning) {
|
|
109
|
+
result.warnings.push(pkgResult.warning);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
export { createTestsScaffold };
|
package/dist/lib/sessions.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ux } from '@oclif/core';
|
|
|
2
2
|
import * as http from 'node:http';
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import { getLoginHost } from './constants.js';
|
|
5
|
+
import { getProxyUrl } from './proxy.js';
|
|
5
6
|
import style from './style.js';
|
|
6
7
|
/**
|
|
7
8
|
* Get the session id for the passed in store. If the session is not found,
|
|
@@ -90,10 +91,10 @@ function serverForLogin({ onPost, onStart, storeId, }) {
|
|
|
90
91
|
res.end('Not Found');
|
|
91
92
|
}
|
|
92
93
|
});
|
|
93
|
-
server.listen(0,
|
|
94
|
+
server.listen(0, async () => {
|
|
94
95
|
const serverAddr = server.address();
|
|
95
|
-
|
|
96
|
-
const localServerUrl = Buffer.from(
|
|
96
|
+
const proxyUrl = await getProxyUrl(serverAddr.port);
|
|
97
|
+
const localServerUrl = Buffer.from(proxyUrl).toString('base64');
|
|
97
98
|
const loginUrl = `${getLoginHost(storeId)}/admin?cli=${localServerUrl}`;
|
|
98
99
|
onStart(loginUrl);
|
|
99
100
|
});
|
package/dist/lib/style.js
CHANGED
package/dist/lib/theme-sync.d.ts
CHANGED
|
@@ -3,17 +3,17 @@ interface ThemeSyncOptions {
|
|
|
3
3
|
targetUrl: string;
|
|
4
4
|
}
|
|
5
5
|
export declare class ThemeSync {
|
|
6
|
-
private readonly
|
|
6
|
+
private readonly options;
|
|
7
7
|
private readonly proxy;
|
|
8
|
+
private readonly server;
|
|
8
9
|
private readonly wss;
|
|
9
|
-
private readonly options;
|
|
10
|
-
private selectPort;
|
|
11
10
|
constructor(options: ThemeSyncOptions);
|
|
12
|
-
start(): Promise<number>;
|
|
13
11
|
reload(): void;
|
|
14
|
-
|
|
12
|
+
start(): Promise<number>;
|
|
15
13
|
private decodeRequestBody;
|
|
16
|
-
private shouldInjectScript;
|
|
17
14
|
private injectScript;
|
|
15
|
+
private parseRequestBody;
|
|
16
|
+
private selectPort;
|
|
17
|
+
private shouldInjectScript;
|
|
18
18
|
}
|
|
19
19
|
export {};
|
package/dist/lib/theme-sync.js
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
1
|
import getPort, { portNumbers } from 'get-port';
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
3
|
+
// @ts-ignore - http-proxy has incomplete TypeScript definitions
|
|
4
|
+
import httpProxy from 'http-proxy';
|
|
5
|
+
import http from 'node:http';
|
|
3
6
|
import util from 'node:util';
|
|
4
7
|
import zlib from 'node:zlib';
|
|
5
|
-
// @ts-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
9
|
+
// @ts-ignore - ws library uses named exports differently
|
|
10
|
+
// eslint-disable-next-line import/no-named-as-default
|
|
8
11
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
9
12
|
const gunzipAsync = util.promisify(zlib.gunzip);
|
|
10
13
|
const inflateAsync = util.promisify(zlib.inflate);
|
|
11
14
|
const brotliDecompressAsync = util.promisify(zlib.brotliDecompress);
|
|
12
15
|
const FALLBACK_PORT = 3000;
|
|
13
16
|
export class ThemeSync {
|
|
14
|
-
|
|
17
|
+
options;
|
|
15
18
|
proxy;
|
|
19
|
+
server;
|
|
16
20
|
wss;
|
|
17
|
-
options;
|
|
18
|
-
async selectPort() {
|
|
19
|
-
return (await getPort({ port: portNumbers(3000, 3100) })) || FALLBACK_PORT;
|
|
20
|
-
}
|
|
21
21
|
constructor(options) {
|
|
22
22
|
this.options = options;
|
|
23
23
|
this.server = http.createServer();
|
|
24
24
|
this.proxy = httpProxy.createProxyServer({});
|
|
25
25
|
this.wss = new WebSocketServer({ server: this.server });
|
|
26
26
|
}
|
|
27
|
+
reload() {
|
|
28
|
+
// Get all connected WebSocket clients
|
|
29
|
+
const clients = [...this.wss.clients].filter((client) => client.readyState === WebSocket.OPEN);
|
|
30
|
+
// Notify clients to reload
|
|
31
|
+
for (const client of clients) {
|
|
32
|
+
client.send('reload');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
27
35
|
async start() {
|
|
28
36
|
// Select port if not specified
|
|
29
37
|
if (!this.options.port) {
|
|
@@ -85,38 +93,52 @@ export class ThemeSync {
|
|
|
85
93
|
});
|
|
86
94
|
});
|
|
87
95
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
decodeRequestBody(body, encoding) {
|
|
97
|
+
switch (encoding) {
|
|
98
|
+
case 'gzip': {
|
|
99
|
+
return gunzipAsync(body);
|
|
100
|
+
}
|
|
101
|
+
case 'deflate': {
|
|
102
|
+
return inflateAsync(body);
|
|
103
|
+
}
|
|
104
|
+
case 'br': {
|
|
105
|
+
return brotliDecompressAsync(body);
|
|
106
|
+
}
|
|
107
|
+
default: {
|
|
108
|
+
return Promise.reject(new Error(`Unsupported content encoding: ${encoding}`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Inject WebSocket client script into proxied HTML
|
|
113
|
+
injectScript(html, host) {
|
|
114
|
+
const script = `
|
|
115
|
+
<script>
|
|
116
|
+
const ws = new WebSocket('ws://${host}/ws');
|
|
117
|
+
ws.onmessage = () => {
|
|
118
|
+
window.location.reload();
|
|
119
|
+
};
|
|
120
|
+
</script>
|
|
121
|
+
`;
|
|
122
|
+
return html.replace('</body>', `${script}</body>`);
|
|
95
123
|
}
|
|
96
124
|
async parseRequestBody(proxyRes, body) {
|
|
97
125
|
// May contain multiple encodings
|
|
98
126
|
const contentEncoding = String(proxyRes.headers['content-encoding'] ?? '')
|
|
99
127
|
.split(',')
|
|
100
128
|
.reverse();
|
|
129
|
+
// Sequential decoding of content-encoding layers is required
|
|
130
|
+
/* eslint-disable no-await-in-loop */
|
|
101
131
|
for (const value of contentEncoding) {
|
|
102
132
|
const encoding = value.trim().toLowerCase();
|
|
103
133
|
if (encoding) {
|
|
104
134
|
body = await this.decodeRequestBody(body, encoding);
|
|
105
135
|
}
|
|
106
136
|
}
|
|
137
|
+
/* eslint-enable no-await-in-loop */
|
|
107
138
|
return body.toString('utf8');
|
|
108
139
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
case 'gzip':
|
|
112
|
-
return gunzipAsync(body);
|
|
113
|
-
case 'deflate':
|
|
114
|
-
return inflateAsync(body);
|
|
115
|
-
case 'br':
|
|
116
|
-
return brotliDecompressAsync(body);
|
|
117
|
-
default:
|
|
118
|
-
return Promise.reject(new Error(`Unsupported content encoding: ${encoding}`));
|
|
119
|
-
}
|
|
140
|
+
async selectPort() {
|
|
141
|
+
return (await getPort({ port: portNumbers(3000, 3100) })) || FALLBACK_PORT;
|
|
120
142
|
}
|
|
121
143
|
shouldInjectScript(proxyRes, html) {
|
|
122
144
|
// Check if this is an HTML response
|
|
@@ -129,16 +151,4 @@ export class ThemeSync {
|
|
|
129
151
|
}
|
|
130
152
|
return true;
|
|
131
153
|
}
|
|
132
|
-
// Inject WebSocket client script into proxied HTML
|
|
133
|
-
injectScript(html, host) {
|
|
134
|
-
const script = `
|
|
135
|
-
<script>
|
|
136
|
-
const ws = new WebSocket('ws://${host}/ws');
|
|
137
|
-
ws.onmessage = () => {
|
|
138
|
-
window.location.reload();
|
|
139
|
-
};
|
|
140
|
-
</script>
|
|
141
|
-
`;
|
|
142
|
-
return html.replace('</body>', `${script}</body>`);
|
|
143
|
-
}
|
|
144
154
|
}
|