@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.
Files changed (65) hide show
  1. package/README.md +91 -0
  2. package/dist/commands/oxygen/deploy.d.ts +26 -0
  3. package/dist/commands/oxygen/deploy.js +121 -0
  4. package/dist/deploy/build-cancel.d.ts +6 -0
  5. package/dist/deploy/build-cancel.js +36 -0
  6. package/dist/deploy/build-cancel.test.d.ts +2 -0
  7. package/dist/deploy/build-cancel.test.js +73 -0
  8. package/dist/deploy/build-initiate.d.ts +6 -0
  9. package/dist/deploy/build-initiate.js +38 -0
  10. package/dist/deploy/build-initiate.test.d.ts +2 -0
  11. package/dist/deploy/build-initiate.test.js +81 -0
  12. package/dist/deploy/build-project.d.ts +5 -0
  13. package/dist/deploy/build-project.js +32 -0
  14. package/dist/deploy/build-project.test.d.ts +2 -0
  15. package/dist/deploy/build-project.test.js +53 -0
  16. package/dist/deploy/deployment-cancel.d.ts +6 -0
  17. package/dist/deploy/deployment-cancel.js +36 -0
  18. package/dist/deploy/deployment-cancel.test.d.ts +2 -0
  19. package/dist/deploy/deployment-cancel.test.js +78 -0
  20. package/dist/deploy/deployment-complete.d.ts +6 -0
  21. package/dist/deploy/deployment-complete.js +33 -0
  22. package/dist/deploy/deployment-complete.test.d.ts +2 -0
  23. package/dist/deploy/deployment-complete.test.js +77 -0
  24. package/dist/deploy/deployment-initiate.d.ts +17 -0
  25. package/dist/deploy/deployment-initiate.js +40 -0
  26. package/dist/deploy/deployment-initiate.test.d.ts +2 -0
  27. package/dist/deploy/deployment-initiate.test.js +136 -0
  28. package/dist/deploy/get-upload-files.d.ts +5 -0
  29. package/dist/deploy/get-upload-files.js +65 -0
  30. package/dist/deploy/get-upload-files.test.d.ts +2 -0
  31. package/dist/deploy/get-upload-files.test.js +56 -0
  32. package/dist/deploy/graphql/build-cancel.d.ts +14 -0
  33. package/dist/deploy/graphql/build-cancel.js +14 -0
  34. package/dist/deploy/graphql/build-initiate.d.ts +15 -0
  35. package/dist/deploy/graphql/build-initiate.js +15 -0
  36. package/dist/deploy/graphql/deployment-cancel.d.ts +14 -0
  37. package/dist/deploy/graphql/deployment-cancel.js +14 -0
  38. package/dist/deploy/graphql/deployment-complete.d.ts +17 -0
  39. package/dist/deploy/graphql/deployment-complete.js +16 -0
  40. package/dist/deploy/graphql/deployment-initiate.d.ts +28 -0
  41. package/dist/deploy/graphql/deployment-initiate.js +25 -0
  42. package/dist/deploy/index.d.ts +5 -0
  43. package/dist/deploy/index.js +74 -0
  44. package/dist/deploy/metadata.d.ts +11 -0
  45. package/dist/deploy/metadata.js +65 -0
  46. package/dist/deploy/metadata.test.d.ts +2 -0
  47. package/dist/deploy/metadata.test.js +131 -0
  48. package/dist/deploy/types.d.ts +52 -0
  49. package/dist/deploy/types.js +7 -0
  50. package/dist/deploy/upload-files.d.ts +6 -0
  51. package/dist/deploy/upload-files.js +156 -0
  52. package/dist/deploy/upload-files.test.d.ts +2 -0
  53. package/dist/deploy/upload-files.test.js +194 -0
  54. package/dist/index.d.ts +3 -0
  55. package/dist/index.js +11 -0
  56. package/dist/oxygen-cli.d.ts +1 -0
  57. package/dist/oxygen-cli.js +5 -0
  58. package/dist/utils/test-helper.d.ts +14 -0
  59. package/dist/utils/test-helper.js +27 -0
  60. package/dist/utils/utils.d.ts +20 -0
  61. package/dist/utils/utils.js +126 -0
  62. package/dist/utils/utils.test.d.ts +2 -0
  63. package/dist/utils/utils.test.js +154 -0
  64. package/oclif.manifest.json +108 -0
  65. 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
+ });
@@ -0,0 +1,3 @@
1
+ declare function runOxygenCLI(): Promise<void>;
2
+
3
+ export { runOxygenCLI as default };
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { runCLI } from '@shopify/cli-kit/node/cli';
2
+
3
+ async function runOxygenCLI() {
4
+ await runCLI({
5
+ moduleURL: import.meta.url,
6
+ development: false
7
+ });
8
+ }
9
+ var src_default = runOxygenCLI;
10
+
11
+ export { src_default as default };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import runCLI from './index.js';
3
+
4
+ process.removeAllListeners("warning");
5
+ runCLI();
@@ -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,2 @@
1
+
2
+ export { }
@@ -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
+ }