@shopify/oxygen-cli 1.0.2
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/README.md +91 -0
- package/dist/commands/oxygen/deploy.d.ts +26 -0
- package/dist/commands/oxygen/deploy.js +121 -0
- package/dist/deploy/build-cancel.d.ts +6 -0
- package/dist/deploy/build-cancel.js +36 -0
- package/dist/deploy/build-cancel.test.d.ts +2 -0
- package/dist/deploy/build-cancel.test.js +73 -0
- package/dist/deploy/build-initiate.d.ts +6 -0
- package/dist/deploy/build-initiate.js +38 -0
- package/dist/deploy/build-initiate.test.d.ts +2 -0
- package/dist/deploy/build-initiate.test.js +81 -0
- package/dist/deploy/build-project.d.ts +5 -0
- package/dist/deploy/build-project.js +32 -0
- package/dist/deploy/build-project.test.d.ts +2 -0
- package/dist/deploy/build-project.test.js +53 -0
- package/dist/deploy/deployment-cancel.d.ts +6 -0
- package/dist/deploy/deployment-cancel.js +36 -0
- package/dist/deploy/deployment-cancel.test.d.ts +2 -0
- package/dist/deploy/deployment-cancel.test.js +78 -0
- package/dist/deploy/deployment-complete.d.ts +6 -0
- package/dist/deploy/deployment-complete.js +33 -0
- package/dist/deploy/deployment-complete.test.d.ts +2 -0
- package/dist/deploy/deployment-complete.test.js +77 -0
- package/dist/deploy/deployment-initiate.d.ts +17 -0
- package/dist/deploy/deployment-initiate.js +40 -0
- package/dist/deploy/deployment-initiate.test.d.ts +2 -0
- package/dist/deploy/deployment-initiate.test.js +136 -0
- package/dist/deploy/get-upload-files.d.ts +5 -0
- package/dist/deploy/get-upload-files.js +65 -0
- package/dist/deploy/get-upload-files.test.d.ts +2 -0
- package/dist/deploy/get-upload-files.test.js +56 -0
- package/dist/deploy/graphql/build-cancel.d.ts +14 -0
- package/dist/deploy/graphql/build-cancel.js +14 -0
- package/dist/deploy/graphql/build-initiate.d.ts +15 -0
- package/dist/deploy/graphql/build-initiate.js +15 -0
- package/dist/deploy/graphql/deployment-cancel.d.ts +14 -0
- package/dist/deploy/graphql/deployment-cancel.js +14 -0
- package/dist/deploy/graphql/deployment-complete.d.ts +17 -0
- package/dist/deploy/graphql/deployment-complete.js +16 -0
- package/dist/deploy/graphql/deployment-initiate.d.ts +28 -0
- package/dist/deploy/graphql/deployment-initiate.js +25 -0
- package/dist/deploy/index.d.ts +5 -0
- package/dist/deploy/index.js +74 -0
- package/dist/deploy/metadata.d.ts +11 -0
- package/dist/deploy/metadata.js +65 -0
- package/dist/deploy/metadata.test.d.ts +2 -0
- package/dist/deploy/metadata.test.js +131 -0
- package/dist/deploy/types.d.ts +52 -0
- package/dist/deploy/types.js +7 -0
- package/dist/deploy/upload-files.d.ts +6 -0
- package/dist/deploy/upload-files.js +156 -0
- package/dist/deploy/upload-files.test.d.ts +2 -0
- package/dist/deploy/upload-files.test.js +194 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/oxygen-cli.d.ts +1 -0
- package/dist/oxygen-cli.js +5 -0
- package/dist/utils/test-helper.d.ts +14 -0
- package/dist/utils/test-helper.js +27 -0
- package/dist/utils/utils.d.ts +20 -0
- package/dist/utils/utils.js +126 -0
- package/dist/utils/utils.test.d.ts +2 -0
- package/dist/utils/utils.test.js +154 -0
- package/oclif.manifest.json +108 -0
- package/package.json +68 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
import { Readable } from 'stream';
|
2
|
+
import { fetch } from '@shopify/cli-kit/node/http';
|
3
|
+
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
4
|
+
import { Response } from 'node-fetch';
|
5
|
+
import { createTestConfig } from '../utils/test-helper.js';
|
6
|
+
import { deployDefaults } from '../utils/utils.js';
|
7
|
+
import { uploadFiles } from './upload-files.js';
|
8
|
+
|
9
|
+
class NamedReadable extends Readable {
|
10
|
+
name = "dummy";
|
11
|
+
_read() {
|
12
|
+
}
|
13
|
+
}
|
14
|
+
vi.mock("@shopify/cli-kit/node/output");
|
15
|
+
vi.mock("@shopify/cli-kit/node/http", async () => {
|
16
|
+
const actual = await vi.importActual("@shopify/cli-kit/node/http");
|
17
|
+
return {
|
18
|
+
...actual,
|
19
|
+
fetch: vi.fn()
|
20
|
+
};
|
21
|
+
});
|
22
|
+
vi.mock("@shopify/cli-kit/node/fs", () => {
|
23
|
+
return {
|
24
|
+
createFileReadStream: vi.fn(() => {
|
25
|
+
const readable = new NamedReadable();
|
26
|
+
readable.push("dummy");
|
27
|
+
readable.emit("end");
|
28
|
+
return readable;
|
29
|
+
})
|
30
|
+
};
|
31
|
+
});
|
32
|
+
vi.mock("fs", () => {
|
33
|
+
return {
|
34
|
+
createReadStream: vi.fn(() => {
|
35
|
+
const readable = new NamedReadable();
|
36
|
+
readable.push("dummy");
|
37
|
+
readable.emit("end");
|
38
|
+
return readable;
|
39
|
+
})
|
40
|
+
};
|
41
|
+
});
|
42
|
+
const testConfig = createTestConfig("/tmp/deploymentRoot");
|
43
|
+
describe("UploadFiles", () => {
|
44
|
+
beforeEach(() => {
|
45
|
+
vi.mocked(fetch).mockReset();
|
46
|
+
});
|
47
|
+
it("Performs a form upload", async () => {
|
48
|
+
const response = new Response();
|
49
|
+
vi.mocked(fetch).mockResolvedValueOnce(response);
|
50
|
+
const testWorkerUpload = [
|
51
|
+
{
|
52
|
+
filePath: "index.js",
|
53
|
+
fileSize: 1,
|
54
|
+
uploadUrl: "https://storage.googleapis.com/the-bucket/",
|
55
|
+
fileType: "WORKER",
|
56
|
+
parameters: [{ name: "someName", value: "someValue" }]
|
57
|
+
}
|
58
|
+
];
|
59
|
+
await uploadFiles(testConfig, testWorkerUpload);
|
60
|
+
expect(vi.mocked(fetch)).toHaveBeenCalledTimes(1);
|
61
|
+
expect(vi.mocked(fetch)).toHaveBeenCalledWith(
|
62
|
+
"https://storage.googleapis.com/the-bucket/",
|
63
|
+
expect.objectContaining({
|
64
|
+
method: "POST",
|
65
|
+
body: expect.objectContaining({
|
66
|
+
_streams: expect.arrayContaining([
|
67
|
+
expect.stringContaining('name="someName"'),
|
68
|
+
expect.stringMatching("someValue")
|
69
|
+
]),
|
70
|
+
_valuesToMeasure: expect.arrayContaining([
|
71
|
+
expect.objectContaining({ name: "dummy" })
|
72
|
+
])
|
73
|
+
})
|
74
|
+
})
|
75
|
+
);
|
76
|
+
});
|
77
|
+
it("Retries a failed form upload until the max upload attempts then throws", async () => {
|
78
|
+
vi.mocked(fetch).mockRejectedValue(new Error("some error"));
|
79
|
+
const testWorkerUpload = [
|
80
|
+
{
|
81
|
+
filePath: "index.js",
|
82
|
+
fileSize: 1,
|
83
|
+
uploadUrl: "https://storage.googleapis.com/the-bucket/",
|
84
|
+
fileType: "WORKER",
|
85
|
+
parameters: [{ name: "someName", value: "someValue" }]
|
86
|
+
}
|
87
|
+
];
|
88
|
+
await expect(uploadFiles(testConfig, testWorkerUpload)).rejects.toThrow(
|
89
|
+
"Failed to upload file index.js"
|
90
|
+
);
|
91
|
+
expect(vi.mocked(fetch)).toHaveBeenCalledTimes(
|
92
|
+
Number(deployDefaults.maxUploadAttempts) + 1
|
93
|
+
);
|
94
|
+
});
|
95
|
+
it("Performs a resumable upload", async () => {
|
96
|
+
const headers = {
|
97
|
+
"x-guploader-uploadid": "some-id",
|
98
|
+
location: "https://upload-it-here.com/"
|
99
|
+
};
|
100
|
+
const response = new Response(null, { headers });
|
101
|
+
vi.mocked(fetch).mockResolvedValue(response);
|
102
|
+
const testWorkerUpload = [
|
103
|
+
{
|
104
|
+
filePath: "index.js",
|
105
|
+
fileSize: 1,
|
106
|
+
uploadUrl: "https://storage.googleapis.com/the-bucket/",
|
107
|
+
fileType: "WORKER",
|
108
|
+
parameters: null
|
109
|
+
}
|
110
|
+
];
|
111
|
+
await uploadFiles(testConfig, testWorkerUpload);
|
112
|
+
expect(vi.mocked(fetch)).toHaveBeenCalledTimes(2);
|
113
|
+
const secondCall = vi.mocked(fetch).mock.calls[1];
|
114
|
+
expect(secondCall[0]).toBe("https://upload-it-here.com/");
|
115
|
+
const validateRequestBody = (req) => {
|
116
|
+
return req.method === "PUT" && req.body instanceof NamedReadable;
|
117
|
+
};
|
118
|
+
expect(validateRequestBody(secondCall[1])).toBe(true);
|
119
|
+
});
|
120
|
+
it("Resumes a resumable upload", async () => {
|
121
|
+
console.error = () => {
|
122
|
+
};
|
123
|
+
let fetchCounter = 0;
|
124
|
+
vi.mocked(fetch).mockImplementation(async () => {
|
125
|
+
fetchCounter++;
|
126
|
+
if (fetchCounter === 1) {
|
127
|
+
const headers = {
|
128
|
+
"x-guploader-uploadid": "some-id",
|
129
|
+
location: "https://upload-it-here.com/"
|
130
|
+
};
|
131
|
+
const response = new Response(null, { headers });
|
132
|
+
return Promise.resolve(response);
|
133
|
+
} else if (fetchCounter === 4) {
|
134
|
+
return Promise.resolve(new Response(null, { status: 200 }));
|
135
|
+
} else {
|
136
|
+
return Promise.reject(new Error("some error"));
|
137
|
+
}
|
138
|
+
});
|
139
|
+
const testWorkerUpload = [
|
140
|
+
{
|
141
|
+
filePath: "index.js",
|
142
|
+
fileSize: 1,
|
143
|
+
uploadUrl: "https://storage.googleapis.com/the-bucket/",
|
144
|
+
fileType: "WORKER",
|
145
|
+
parameters: null
|
146
|
+
}
|
147
|
+
];
|
148
|
+
await uploadFiles(testConfig, testWorkerUpload);
|
149
|
+
expect(vi.mocked(fetch)).toHaveBeenCalledTimes(4);
|
150
|
+
const statusCall = vi.mocked(fetch).mock.calls[2];
|
151
|
+
expect(statusCall[0]).toBe("https://upload-it-here.com/");
|
152
|
+
expect(statusCall[1]).toMatchObject({
|
153
|
+
method: "PUT",
|
154
|
+
headers: {
|
155
|
+
"Content-Range": "bytes */1",
|
156
|
+
"Content-Length": "0"
|
157
|
+
}
|
158
|
+
});
|
159
|
+
});
|
160
|
+
it("Throws when a resumable upload fails after exhausting attempt count", async () => {
|
161
|
+
console.error = () => {
|
162
|
+
};
|
163
|
+
let fetchCounter = 0;
|
164
|
+
vi.mocked(fetch).mockImplementation(async () => {
|
165
|
+
fetchCounter++;
|
166
|
+
if (fetchCounter === 1) {
|
167
|
+
const headers = {
|
168
|
+
"x-guploader-uploadid": "some-id",
|
169
|
+
location: "https://upload-it-here.com/"
|
170
|
+
};
|
171
|
+
const response = new Response(null, { headers });
|
172
|
+
return Promise.resolve(response);
|
173
|
+
} else {
|
174
|
+
return Promise.reject(new Error("some error"));
|
175
|
+
}
|
176
|
+
});
|
177
|
+
const testWorkerUpload = [
|
178
|
+
{
|
179
|
+
filePath: "index.js",
|
180
|
+
fileSize: 1,
|
181
|
+
uploadUrl: "https://storage.googleapis.com/the-bucket/",
|
182
|
+
fileType: "WORKER",
|
183
|
+
parameters: null
|
184
|
+
}
|
185
|
+
];
|
186
|
+
await expect(uploadFiles(testConfig, testWorkerUpload)).rejects.toThrow(
|
187
|
+
`Failed to upload file index.js after ${deployDefaults.maxResumabeUploadAttempts} attempts`
|
188
|
+
);
|
189
|
+
expect(vi.mocked(fetch)).toHaveBeenCalledTimes(
|
190
|
+
// for each attempt, we make two calls to fetch, plus the initial attempt makes two calls
|
191
|
+
Number(deployDefaults.maxResumabeUploadAttempts) * 2 + 2
|
192
|
+
);
|
193
|
+
});
|
194
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
#!/usr/bin/env node
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { DeployConfig } from '../deploy/types.js';
|
2
|
+
|
3
|
+
declare const testToken: {
|
4
|
+
accessToken: string;
|
5
|
+
allowedResource: string;
|
6
|
+
appId: string;
|
7
|
+
client: string;
|
8
|
+
expiresAt: string;
|
9
|
+
namespace: string;
|
10
|
+
namespaceId: string;
|
11
|
+
};
|
12
|
+
declare function createTestConfig(rootFolder: string): DeployConfig;
|
13
|
+
|
14
|
+
export { createTestConfig, testToken };
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import { deployDefaults } from './utils.js';
|
2
|
+
|
3
|
+
const testToken = {
|
4
|
+
accessToken: "some_token",
|
5
|
+
allowedResource: "gid://oxygen-hub/Namespace/1",
|
6
|
+
appId: "gid://oxygen-hub/App/1",
|
7
|
+
client: "gid://oxygen-hub/Client/1",
|
8
|
+
expiresAt: "2023-04-08T09:38:50.368Z",
|
9
|
+
namespace: "fresh-namespace",
|
10
|
+
namespaceId: "gid://oxygen-hub/Namespace/1"
|
11
|
+
};
|
12
|
+
function createTestConfig(rootFolder) {
|
13
|
+
return {
|
14
|
+
assetsDir: "/assets/",
|
15
|
+
buildCommand: String(deployDefaults.buildCommandDefault),
|
16
|
+
deploymentToken: testToken,
|
17
|
+
environmentTag: "environment",
|
18
|
+
deploymentUrl: "https://localhost:3000",
|
19
|
+
metadata: {},
|
20
|
+
rootPath: rootFolder,
|
21
|
+
skipBuild: false,
|
22
|
+
workerDir: "/worker/",
|
23
|
+
workerOnly: false
|
24
|
+
};
|
25
|
+
}
|
26
|
+
|
27
|
+
export { createTestConfig, testToken };
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { DeployConfig, ClientError, DeploymentToken } from '../deploy/types.js';
|
2
|
+
|
3
|
+
declare const deployDefaults: {
|
4
|
+
[key: string]: string | number;
|
5
|
+
};
|
6
|
+
declare function errorHandler(error: any): void;
|
7
|
+
declare function getBuildCommandFromLockFile(config: DeployConfig): string;
|
8
|
+
declare enum Header {
|
9
|
+
OxygenNamespaceHandle = "X-Oxygen-Namespace-Handle"
|
10
|
+
}
|
11
|
+
declare function isClientError(error: unknown): error is ClientError;
|
12
|
+
declare const maxLabelLength = 90;
|
13
|
+
declare function parseToken(inputToken: string): DeploymentToken;
|
14
|
+
interface VerifyConfigParams {
|
15
|
+
config: DeployConfig;
|
16
|
+
performedBuild?: boolean;
|
17
|
+
}
|
18
|
+
declare function verifyConfig({ config, performedBuild, }: VerifyConfigParams): Promise<void>;
|
19
|
+
|
20
|
+
export { Header, deployDefaults, errorHandler, getBuildCommandFromLockFile, isClientError, maxLabelLength, parseToken, verifyConfig };
|
@@ -0,0 +1,126 @@
|
|
1
|
+
import { fileExistsSync, fileExists } from '@shopify/cli-kit/node/fs';
|
2
|
+
import { outputWarn, outputInfo } from '@shopify/cli-kit/node/output';
|
3
|
+
import { joinPath } from '@shopify/cli-kit/node/path';
|
4
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
5
|
+
|
6
|
+
const deployDefaults = {
|
7
|
+
assetsDirDefault: "dist/client/",
|
8
|
+
buildCommandDefault: "yarn build",
|
9
|
+
maxUploadAttempts: 3,
|
10
|
+
maxResumabeUploadAttempts: 9,
|
11
|
+
workerDirDefault: "dist/worker/"
|
12
|
+
};
|
13
|
+
function errorHandler(error) {
|
14
|
+
if (isClientError(error)) {
|
15
|
+
if (error.statusCode === 401) {
|
16
|
+
throw new AbortError(
|
17
|
+
"You are not authorized to perform this action. Please check your deployment token."
|
18
|
+
);
|
19
|
+
}
|
20
|
+
if (error.statusCode === 429) {
|
21
|
+
throw new AbortError(
|
22
|
+
"You've made too many requests. Please try again later"
|
23
|
+
);
|
24
|
+
}
|
25
|
+
}
|
26
|
+
if (error instanceof AbortError && error.message.includes("503")) {
|
27
|
+
throw new AbortError(
|
28
|
+
"The server is currently unavailable. Please try again later."
|
29
|
+
);
|
30
|
+
}
|
31
|
+
}
|
32
|
+
function getBuildCommandFromLockFile(config) {
|
33
|
+
const lockFileBuildCommands = /* @__PURE__ */ new Map([
|
34
|
+
["package-lock.json", "npm run build"],
|
35
|
+
["pnpm-lock.yaml", "pnpm run build"],
|
36
|
+
["yarn.lock", "yarn build"]
|
37
|
+
]);
|
38
|
+
const foundLockFiles = [];
|
39
|
+
for (const [lockFileName, buildCommand] of lockFileBuildCommands) {
|
40
|
+
if (fileExistsSync(joinPath(config.rootPath, lockFileName))) {
|
41
|
+
foundLockFiles.push({ lockFileName, buildCommand });
|
42
|
+
}
|
43
|
+
}
|
44
|
+
if (foundLockFiles.length > 1) {
|
45
|
+
const lockFilesList = foundLockFiles.map(({ lockFileName }) => lockFileName).join(", ");
|
46
|
+
outputWarn(`Warning: Multiple lock files found: (${lockFilesList}).`);
|
47
|
+
}
|
48
|
+
if (foundLockFiles.length > 0) {
|
49
|
+
const { lockFileName, buildCommand } = foundLockFiles[0];
|
50
|
+
const infoMsg = foundLockFiles.length > 1 ? "" : `Found: ${lockFileName}. `;
|
51
|
+
outputInfo(
|
52
|
+
`${infoMsg}Assuming "${buildCommand}" as build command. Use the buildCommand flag to override.`
|
53
|
+
);
|
54
|
+
return buildCommand;
|
55
|
+
}
|
56
|
+
return String(deployDefaults.buildCommandDefault);
|
57
|
+
}
|
58
|
+
var Header = /* @__PURE__ */ ((Header2) => {
|
59
|
+
Header2["OxygenNamespaceHandle"] = "X-Oxygen-Namespace-Handle";
|
60
|
+
return Header2;
|
61
|
+
})(Header || {});
|
62
|
+
function isClientError(error) {
|
63
|
+
return typeof error === "object" && error !== null && "statusCode" in error;
|
64
|
+
}
|
65
|
+
const maxLabelLength = 90;
|
66
|
+
function parseToken(inputToken) {
|
67
|
+
try {
|
68
|
+
const decodedToken = Buffer.from(inputToken, "base64").toString("utf-8");
|
69
|
+
const rawToken = JSON.parse(decodedToken);
|
70
|
+
return convertKeysToCamelCase(rawToken);
|
71
|
+
} catch (error) {
|
72
|
+
throw new Error(
|
73
|
+
`Error processing deployment token. Please check your token and try again.`
|
74
|
+
);
|
75
|
+
}
|
76
|
+
}
|
77
|
+
async function verifyConfig({
|
78
|
+
config,
|
79
|
+
performedBuild = false
|
80
|
+
}) {
|
81
|
+
const { rootPath, workerDir, assetsDir, skipBuild, workerOnly } = config;
|
82
|
+
const checkPaths = {
|
83
|
+
root: rootPath
|
84
|
+
};
|
85
|
+
if (skipBuild || performedBuild) {
|
86
|
+
checkPaths.worker = joinPath(rootPath, workerDir);
|
87
|
+
if (!workerOnly) {
|
88
|
+
checkPaths.assets = joinPath(rootPath, assetsDir);
|
89
|
+
}
|
90
|
+
}
|
91
|
+
for (const pathType of Object.keys(checkPaths)) {
|
92
|
+
await checkPath(checkPaths[pathType], pathType);
|
93
|
+
}
|
94
|
+
const addressRegex = /^(http|https):\/\/(?:[\w-]+\.)*[\w-]+|(?:http|https):\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/;
|
95
|
+
if (!addressRegex.test(config.deploymentUrl)) {
|
96
|
+
throw new Error(`Invalid deployment service URL: ${config.deploymentUrl}`);
|
97
|
+
}
|
98
|
+
}
|
99
|
+
async function checkPath(path, pathType) {
|
100
|
+
if (!await fileExists(path)) {
|
101
|
+
if (pathType === "assets") {
|
102
|
+
outputWarn(
|
103
|
+
`Use the "workerOnly" flag to perform a worker-only deployment.`
|
104
|
+
);
|
105
|
+
}
|
106
|
+
throw new Error(`Path not found: ${path}`);
|
107
|
+
}
|
108
|
+
}
|
109
|
+
function convertKeysToCamelCase(obj) {
|
110
|
+
if (typeof obj === "object") {
|
111
|
+
return Object.keys(obj).reduce((result, key) => {
|
112
|
+
const camelCaseKey = key.replace(
|
113
|
+
/([-_][a-z])/gi,
|
114
|
+
($1) => $1.toUpperCase().replace("-", "").replace("_", "")
|
115
|
+
);
|
116
|
+
if (obj[key] === void 0) {
|
117
|
+
throw new Error(`Invalid token: ${key} is undefined`);
|
118
|
+
}
|
119
|
+
result[camelCaseKey] = convertKeysToCamelCase(obj[key]);
|
120
|
+
return result;
|
121
|
+
}, {});
|
122
|
+
}
|
123
|
+
return obj;
|
124
|
+
}
|
125
|
+
|
126
|
+
export { Header, deployDefaults, errorHandler, getBuildCommandFromLockFile, isClientError, maxLabelLength, parseToken, verifyConfig };
|
@@ -0,0 +1,154 @@
|
|
1
|
+
import { mkdir, rmdir, touchFile } from '@shopify/cli-kit/node/fs';
|
2
|
+
import { joinPath } from '@shopify/cli-kit/node/path';
|
3
|
+
import { outputInfo, outputWarn } from '@shopify/cli-kit/node/output';
|
4
|
+
import { vi, beforeAll, afterAll, describe, test, expect } from 'vitest';
|
5
|
+
import { createTestConfig, testToken } from './test-helper.js';
|
6
|
+
import * as utils from './utils.js';
|
7
|
+
|
8
|
+
const randomString = Math.random().toString(36).substring(7);
|
9
|
+
const rootFolder = `/tmp/${randomString}/`;
|
10
|
+
const testConfig = createTestConfig(rootFolder);
|
11
|
+
vi.mock("@shopify/cli-kit/node/output");
|
12
|
+
beforeAll(async () => {
|
13
|
+
await mkdir(rootFolder);
|
14
|
+
await mkdir(`${rootFolder}/worker`);
|
15
|
+
await mkdir(`${rootFolder}/assets`);
|
16
|
+
});
|
17
|
+
afterAll(async () => {
|
18
|
+
await rmdir(`${rootFolder}/worker`, { force: true });
|
19
|
+
await rmdir(`${rootFolder}/assets`, { force: true });
|
20
|
+
await rmdir(rootFolder, { force: true });
|
21
|
+
});
|
22
|
+
describe("getBuildCommandFromLockfile", () => {
|
23
|
+
test("getBuildCommandFromLockfile returns the default build command without a lockfile", () => {
|
24
|
+
const buildCommand = utils.getBuildCommandFromLockFile(testConfig);
|
25
|
+
expect(buildCommand).toBe(utils.deployDefaults.buildCommandDefault);
|
26
|
+
});
|
27
|
+
test("getBuildCommandFromLockfile returns the corresponding build command for a lockfile", async () => {
|
28
|
+
await touchFile(`${rootFolder}/pnpm-lock.yaml`);
|
29
|
+
const buildCommand = utils.getBuildCommandFromLockFile(testConfig);
|
30
|
+
expect(buildCommand).toBe("pnpm run build");
|
31
|
+
expect(outputInfo).toHaveBeenCalledWith(
|
32
|
+
'Found: pnpm-lock.yaml. Assuming "pnpm run build" as build command. Use the buildCommand flag to override.'
|
33
|
+
);
|
34
|
+
});
|
35
|
+
test("getBuildCommandFromLockfile the first build command and warns when multiple lockfiles are found", async () => {
|
36
|
+
await touchFile(`${rootFolder}/yarn.lock`);
|
37
|
+
const buildCommand = utils.getBuildCommandFromLockFile(testConfig);
|
38
|
+
expect(buildCommand).toBe("pnpm run build");
|
39
|
+
expect(outputInfo).toHaveBeenCalledWith(
|
40
|
+
'Assuming "pnpm run build" as build command. Use the buildCommand flag to override.'
|
41
|
+
);
|
42
|
+
expect(outputWarn).toHaveBeenCalledWith(
|
43
|
+
"Warning: Multiple lock files found: (pnpm-lock.yaml, yarn.lock)."
|
44
|
+
);
|
45
|
+
});
|
46
|
+
});
|
47
|
+
describe("parseToken", () => {
|
48
|
+
test("parseToken returns a token object when given a valid token", () => {
|
49
|
+
const base64EncodedToken = Buffer.from(JSON.stringify(testToken)).toString(
|
50
|
+
"base64"
|
51
|
+
);
|
52
|
+
const parsedToken = utils.parseToken(base64EncodedToken);
|
53
|
+
expect(parsedToken).toEqual(testToken);
|
54
|
+
});
|
55
|
+
test("parseToken throws when given an invalid token", () => {
|
56
|
+
try {
|
57
|
+
utils.parseToken("invalidToken");
|
58
|
+
} catch (err) {
|
59
|
+
expect(err).toEqual(
|
60
|
+
new Error(
|
61
|
+
`Error processing deployment token. Please check your token and try again.`
|
62
|
+
)
|
63
|
+
);
|
64
|
+
}
|
65
|
+
});
|
66
|
+
});
|
67
|
+
describe("verifyConfig", () => {
|
68
|
+
test("verifyConfig resolves with a valid configuration", async () => {
|
69
|
+
await expect(utils.verifyConfig({ config: testConfig })).resolves.toBe(
|
70
|
+
void 0
|
71
|
+
);
|
72
|
+
});
|
73
|
+
test("verifyConfig throws when the rootPath cannot be resolved", async () => {
|
74
|
+
const invalidConfig = {
|
75
|
+
...testConfig,
|
76
|
+
rootPath: "/doesNotExist"
|
77
|
+
};
|
78
|
+
await expect(utils.verifyConfig({ config: invalidConfig })).rejects.toThrow(
|
79
|
+
new Error("Path not found: /doesNotExist")
|
80
|
+
);
|
81
|
+
});
|
82
|
+
test("verifyConfig throws when the assetsDir cannot be resolved", async () => {
|
83
|
+
const invalidConfig = {
|
84
|
+
...testConfig,
|
85
|
+
assetsDir: "not_there",
|
86
|
+
skipBuild: true
|
87
|
+
};
|
88
|
+
await expect(utils.verifyConfig({ config: invalidConfig })).rejects.toThrow(
|
89
|
+
new Error(
|
90
|
+
`Path not found: ${joinPath(
|
91
|
+
invalidConfig.rootPath,
|
92
|
+
invalidConfig.assetsDir
|
93
|
+
)}`
|
94
|
+
)
|
95
|
+
);
|
96
|
+
expect(outputWarn).toHaveBeenCalledWith(
|
97
|
+
`Use the "workerOnly" flag to perform a worker-only deployment.`
|
98
|
+
);
|
99
|
+
});
|
100
|
+
test("verifyConfig throws when the workerDir cannot be resolved after a build", async () => {
|
101
|
+
const invalidConfig = {
|
102
|
+
...testConfig,
|
103
|
+
skipBuild: false,
|
104
|
+
workerDir: "not_there",
|
105
|
+
workerOnly: true
|
106
|
+
};
|
107
|
+
await expect(
|
108
|
+
utils.verifyConfig({ config: invalidConfig, performedBuild: true })
|
109
|
+
).rejects.toThrow(
|
110
|
+
new Error(
|
111
|
+
`Path not found: ${joinPath(
|
112
|
+
invalidConfig.rootPath,
|
113
|
+
invalidConfig.workerDir
|
114
|
+
)}`
|
115
|
+
)
|
116
|
+
);
|
117
|
+
});
|
118
|
+
test("verifyConfig throws when given an invalid deploymentUrl", async () => {
|
119
|
+
const invalidConfig = {
|
120
|
+
...testConfig,
|
121
|
+
deploymentUrl: "invalidAddress"
|
122
|
+
};
|
123
|
+
await expect(utils.verifyConfig({ config: invalidConfig })).rejects.toThrow(
|
124
|
+
new Error("Invalid deployment service URL: invalidAddress")
|
125
|
+
);
|
126
|
+
});
|
127
|
+
test("parseToken returns a token object when given a valid token", () => {
|
128
|
+
const unparsedToken = {
|
129
|
+
access_token: "some_token",
|
130
|
+
allowed_resource: "gid://oxygen-hub/Namespace/1",
|
131
|
+
app_id: "gid://oxygen-hub/App/1",
|
132
|
+
client: "gid://oxygen-hub/Client/1",
|
133
|
+
expires_at: "2023-04-08T09:38:50.368Z",
|
134
|
+
namespace: "fresh-namespace",
|
135
|
+
namespace_id: "gid://oxygen-hub/Namespace/1"
|
136
|
+
};
|
137
|
+
const base64EncodedToken = Buffer.from(
|
138
|
+
JSON.stringify(unparsedToken)
|
139
|
+
).toString("base64");
|
140
|
+
const parsedToken = utils.parseToken(base64EncodedToken);
|
141
|
+
expect(parsedToken).toEqual(testToken);
|
142
|
+
});
|
143
|
+
test("parseToken throws when given an invalid token", () => {
|
144
|
+
try {
|
145
|
+
utils.parseToken("invalidToken");
|
146
|
+
} catch (err) {
|
147
|
+
expect(err).toEqual(
|
148
|
+
new Error(
|
149
|
+
`Error processing deployment token. Please check your token and try again.`
|
150
|
+
)
|
151
|
+
);
|
152
|
+
}
|
153
|
+
});
|
154
|
+
});
|
@@ -0,0 +1,108 @@
|
|
1
|
+
{
|
2
|
+
"version": "1.0.2",
|
3
|
+
"commands": {
|
4
|
+
"oxygen:deploy": {
|
5
|
+
"id": "oxygen:deploy",
|
6
|
+
"description": "Creates a deployment to Oxygen",
|
7
|
+
"strict": true,
|
8
|
+
"pluginName": "@shopify/oxygen-cli",
|
9
|
+
"pluginAlias": "@shopify/oxygen-cli",
|
10
|
+
"pluginType": "core",
|
11
|
+
"hidden": false,
|
12
|
+
"aliases": [],
|
13
|
+
"flags": {
|
14
|
+
"token": {
|
15
|
+
"name": "token",
|
16
|
+
"type": "option",
|
17
|
+
"char": "t",
|
18
|
+
"description": "Oxygen deployment token",
|
19
|
+
"required": true,
|
20
|
+
"multiple": false
|
21
|
+
},
|
22
|
+
"rootPath": {
|
23
|
+
"name": "rootPath",
|
24
|
+
"type": "option",
|
25
|
+
"char": "r",
|
26
|
+
"description": "Root path",
|
27
|
+
"required": false,
|
28
|
+
"multiple": false,
|
29
|
+
"default": "./"
|
30
|
+
},
|
31
|
+
"environmentTag": {
|
32
|
+
"name": "environmentTag",
|
33
|
+
"type": "option",
|
34
|
+
"char": "e",
|
35
|
+
"description": "Tag of the environment to deploy to",
|
36
|
+
"required": false,
|
37
|
+
"multiple": false
|
38
|
+
},
|
39
|
+
"workerFolder": {
|
40
|
+
"name": "workerFolder",
|
41
|
+
"type": "option",
|
42
|
+
"char": "w",
|
43
|
+
"description": "Worker folder",
|
44
|
+
"required": false,
|
45
|
+
"multiple": false,
|
46
|
+
"default": "dist/worker/"
|
47
|
+
},
|
48
|
+
"assetsFolder": {
|
49
|
+
"name": "assetsFolder",
|
50
|
+
"type": "option",
|
51
|
+
"char": "a",
|
52
|
+
"description": "Assets folder",
|
53
|
+
"required": false,
|
54
|
+
"multiple": false,
|
55
|
+
"default": "dist/client/"
|
56
|
+
},
|
57
|
+
"workerOnly": {
|
58
|
+
"name": "workerOnly",
|
59
|
+
"type": "boolean",
|
60
|
+
"char": "o",
|
61
|
+
"description": "Worker only deployment",
|
62
|
+
"required": false,
|
63
|
+
"allowNo": false
|
64
|
+
},
|
65
|
+
"skipBuild": {
|
66
|
+
"name": "skipBuild",
|
67
|
+
"type": "boolean",
|
68
|
+
"char": "s",
|
69
|
+
"description": "Skip running build command",
|
70
|
+
"required": false,
|
71
|
+
"allowNo": false
|
72
|
+
},
|
73
|
+
"buildCommand": {
|
74
|
+
"name": "buildCommand",
|
75
|
+
"type": "option",
|
76
|
+
"char": "b",
|
77
|
+
"description": "Build command",
|
78
|
+
"required": false,
|
79
|
+
"multiple": false,
|
80
|
+
"default": "yarn build"
|
81
|
+
},
|
82
|
+
"metadataUrl": {
|
83
|
+
"name": "metadataUrl",
|
84
|
+
"type": "option",
|
85
|
+
"description": "URL that links to the deployment",
|
86
|
+
"required": false,
|
87
|
+
"multiple": false
|
88
|
+
},
|
89
|
+
"metadataUser": {
|
90
|
+
"name": "metadataUser",
|
91
|
+
"type": "option",
|
92
|
+
"description": "User that initiated the deployment",
|
93
|
+
"required": false,
|
94
|
+
"multiple": false
|
95
|
+
},
|
96
|
+
"metadataVersion": {
|
97
|
+
"name": "metadataVersion",
|
98
|
+
"type": "option",
|
99
|
+
"description": "A version identifier for the deployment",
|
100
|
+
"required": false,
|
101
|
+
"multiple": false
|
102
|
+
}
|
103
|
+
},
|
104
|
+
"args": {},
|
105
|
+
"hasCustomBuildCommand": false
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|