@shopify/oxygen-cli 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|