@shopify/oxygen-cli 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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,28 @@
1
+ import { OxygenError } from '../types.js';
2
+
3
+ declare const DeploymentInitiateQuery = "\n mutation DeploymentInitiate($buildId: ID, $environment: EnvironmentSelectorInput, $labels: [String!], $files: [FileInput!]!) {\n deploymentInitiate(buildId: $buildId, environment: $environment, labels: $labels, files: $files) {\n deployment {\n id\n status\n }\n deploymentTargets {\n filePath\n fileSize\n uploadUrl\n fileType\n parameters {\n name\n value\n }\n }\n userErrors {\n message\n }\n }\n }\n";
4
+ interface DeploymentInitiateQueryData {
5
+ deploymentInitiate: DeploymentInitiateResponse;
6
+ }
7
+ interface DeploymentInitiateResponse {
8
+ deployment: Deployment;
9
+ deploymentTargets: DeploymentTargetResponse[];
10
+ userErrors: OxygenError[];
11
+ }
12
+ interface DeploymentTargetResponse {
13
+ filePath: string;
14
+ fileSize: number;
15
+ uploadUrl: string;
16
+ fileType: string;
17
+ parameters: DeploymentInitiateParameters[] | null;
18
+ }
19
+ interface Deployment {
20
+ id: string;
21
+ status: string;
22
+ }
23
+ interface DeploymentInitiateParameters {
24
+ name: string;
25
+ value: string;
26
+ }
27
+
28
+ export { DeploymentInitiateQuery, DeploymentInitiateQueryData, DeploymentInitiateResponse, DeploymentTargetResponse };
@@ -0,0 +1,25 @@
1
+ const DeploymentInitiateQuery = `
2
+ mutation DeploymentInitiate($buildId: ID, $environment: EnvironmentSelectorInput, $labels: [String!], $files: [FileInput!]!) {
3
+ deploymentInitiate(buildId: $buildId, environment: $environment, labels: $labels, files: $files) {
4
+ deployment {
5
+ id
6
+ status
7
+ }
8
+ deploymentTargets {
9
+ filePath
10
+ fileSize
11
+ uploadUrl
12
+ fileType
13
+ parameters {
14
+ name
15
+ value
16
+ }
17
+ }
18
+ userErrors {
19
+ message
20
+ }
21
+ }
22
+ }
23
+ `;
24
+
25
+ export { DeploymentInitiateQuery };
@@ -0,0 +1,5 @@
1
+ import { DeployConfig } from './types.js';
2
+
3
+ declare function createDeploy(config: DeployConfig): Promise<void>;
4
+
5
+ export { createDeploy };
@@ -0,0 +1,74 @@
1
+ import { outputSuccess, outputWarn, consoleError } from '@shopify/cli-kit/node/output';
2
+ import { verifyConfig } from '../utils/utils.js';
3
+ import { buildInitiate } from './build-initiate.js';
4
+ import { buildCancel } from './build-cancel.js';
5
+ import { getUploadFiles } from './get-upload-files.js';
6
+ import { deploymentInitiate } from './deployment-initiate.js';
7
+ import { deploymentComplete } from './deployment-complete.js';
8
+ import { deploymentCancel } from './deployment-cancel.js';
9
+ import { uploadFiles } from './upload-files.js';
10
+ import { buildProject } from './build-project.js';
11
+ import { getMetadata, createLabels, getEnvironmentInput } from './metadata.js';
12
+
13
+ async function createDeploy(config) {
14
+ const build = {};
15
+ let buildCompleted;
16
+ let deployment;
17
+ try {
18
+ const metadata = await getMetadata(config);
19
+ const labels = createLabels(metadata);
20
+ const environment = getEnvironmentInput(config, metadata);
21
+ if (!config.workerOnly && !config.skipBuild) {
22
+ const buildInitiateResponse = await buildInitiate(
23
+ config,
24
+ environment,
25
+ labels
26
+ );
27
+ build.id = buildInitiateResponse.build.id;
28
+ build.assetPath = buildInitiateResponse.build.assetPath;
29
+ }
30
+ if (!config.skipBuild) {
31
+ await buildProject(config, build.assetPath);
32
+ verifyConfig({ config, performedBuild: true });
33
+ }
34
+ buildCompleted = true;
35
+ const manifest = await getUploadFiles(config);
36
+ const deploymentInitiateInput = build.id ? { buildId: build.id, manifest } : { environment, manifest, labels };
37
+ deployment = await deploymentInitiate(config, deploymentInitiateInput);
38
+ await uploadFiles(config, deployment.deploymentTargets);
39
+ const deploymentCompleteOp = await deploymentComplete(
40
+ config,
41
+ deployment.deployment.id
42
+ );
43
+ outputSuccess(
44
+ `Deployment complete: ${deploymentCompleteOp.deployment.url}`
45
+ );
46
+ } catch (error) {
47
+ if (!(error instanceof Error)) {
48
+ console.error("Unknown error", error);
49
+ return;
50
+ }
51
+ if (build.id && !buildCompleted) {
52
+ outputWarn(`Build failed with: ${error.message}, cancelling build.`);
53
+ await buildCancel(config, build.id, error.message).catch((err) => {
54
+ if (err instanceof Error) {
55
+ outputWarn(`Failed to cancel build: ${err.message}`);
56
+ }
57
+ });
58
+ } else if (deployment?.deployment.id) {
59
+ outputWarn(
60
+ `Deployment failed with: ${error.message}, cancelling deployment.`
61
+ );
62
+ await deploymentCancel(config, deployment.deployment.id, "failed").catch(
63
+ (err) => {
64
+ if (err instanceof Error) {
65
+ outputWarn(`Failed to cancel deployment: ${err.message}`);
66
+ }
67
+ }
68
+ );
69
+ }
70
+ consoleError(error.message);
71
+ }
72
+ }
73
+
74
+ export { createDeploy };
@@ -0,0 +1,11 @@
1
+ import { CIMetadata } from '@shopify/cli-kit/node/context/local';
2
+ import { DeployConfig, EnvironmentInput } from './types.js';
3
+
4
+ type Metadata = CIMetadata & {
5
+ name: string;
6
+ };
7
+ declare function getMetadata(config: DeployConfig): Promise<Metadata>;
8
+ declare function getEnvironmentInput(config: DeployConfig, metadata: CIMetadata): EnvironmentInput | undefined;
9
+ declare function createLabels(metadata: Metadata): string[];
10
+
11
+ export { createLabels, getEnvironmentInput, getMetadata };
@@ -0,0 +1,65 @@
1
+ import { ciPlatform } from '@shopify/cli-kit/node/context/local';
2
+ import { getLatestGitCommit } from '@shopify/cli-kit/node/git';
3
+ import { outputWarn } from '@shopify/cli-kit/node/output';
4
+ import { maxLabelLength } from '../utils/utils.js';
5
+
6
+ async function getMetadata(config) {
7
+ const ciInfo = ciPlatform();
8
+ let metadata = {};
9
+ if (ciInfo.isCI && ciInfo.name !== "unknown") {
10
+ metadata = ciInfo.metadata;
11
+ } else {
12
+ try {
13
+ const gitCommit = await getLatestGitCommit(config.rootPath);
14
+ const branch = gitCommit.refs.split(" ");
15
+ metadata = {
16
+ actor: gitCommit.author_name,
17
+ branch: branch[branch.length - 1],
18
+ commitSha: gitCommit.hash,
19
+ commitMessage: gitCommit.message
20
+ };
21
+ } catch (error) {
22
+ outputWarn("No CI metadata loaded from environment");
23
+ }
24
+ }
25
+ return {
26
+ name: ciInfo.isCI ? ciInfo.name : "unknown",
27
+ ...metadata,
28
+ actor: config.metadata.user ?? metadata.actor,
29
+ commitSha: config.metadata.version ?? metadata.commitSha,
30
+ url: config.metadata.url ?? metadata.url
31
+ };
32
+ }
33
+ function getEnvironmentInput(config, metadata) {
34
+ const tag = config.environmentTag || metadata.branch;
35
+ return tag ? { tag } : void 0;
36
+ }
37
+ function createLabels(metadata) {
38
+ const labels = [];
39
+ const checkLabel = (labelKey, labelValue) => {
40
+ if (labelValue.length > maxLabelLength) {
41
+ throw new Error(
42
+ `Provided ${labelKey} metadata exceeds maximum length (max ${maxLabelLength} characters).`
43
+ );
44
+ }
45
+ const label = `${labelKey}=${JSON.stringify(labelValue)}`;
46
+ labels.push(label);
47
+ };
48
+ if (metadata.name !== "unknown" && metadata.run) {
49
+ labels.push(`${metadata.name}-runId=${JSON.stringify(metadata.run)}`);
50
+ }
51
+ const keys = ["url", "actor", "commitSha"];
52
+ const keyMapping = {
53
+ actor: "user",
54
+ commitSha: "version"
55
+ };
56
+ keys.forEach((key) => {
57
+ if (metadata[key]) {
58
+ const labelKey = keyMapping[key] || key;
59
+ checkLabel(labelKey, metadata[key]);
60
+ }
61
+ });
62
+ return labels;
63
+ }
64
+
65
+ export { createLabels, getEnvironmentInput, getMetadata };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,131 @@
1
+ import { getLatestGitCommit } from '@shopify/cli-kit/node/git';
2
+ import { ciPlatform } from '@shopify/cli-kit/node/context/local';
3
+ import { vi, describe, test, expect } from 'vitest';
4
+ import { maxLabelLength } from '../utils/utils.js';
5
+ import { createTestConfig } from '../utils/test-helper.js';
6
+ import { getMetadata, getEnvironmentInput, createLabels } from './metadata.js';
7
+
8
+ vi.mock("@shopify/cli-kit/node/context/local");
9
+ vi.mock("@shopify/cli-kit/node/git");
10
+ describe("getMetadata", () => {
11
+ test("should return custom metadata from environment", async () => {
12
+ vi.mocked(ciPlatform).mockReturnValueOnce({
13
+ isCI: true,
14
+ name: "circle",
15
+ metadata: {
16
+ actor: "circle_actor",
17
+ commitSha: "circle_sha",
18
+ url: "circle_url"
19
+ }
20
+ });
21
+ const testConfig = createTestConfig("/tmp/deploymentRoot/");
22
+ const metadataResult = await getMetadata(testConfig);
23
+ expect(metadataResult.actor).toBe("circle_actor");
24
+ expect(metadataResult.commitSha).toBe("circle_sha");
25
+ expect(metadataResult.name).toBe("circle");
26
+ expect(metadataResult.url).toBe("circle_url");
27
+ });
28
+ test("should return custom metadata when provided in config", async () => {
29
+ vi.mocked(ciPlatform).mockReturnValueOnce({
30
+ isCI: true,
31
+ name: "circle",
32
+ metadata: {
33
+ actor: "circle_actor",
34
+ commitSha: "circle_sha",
35
+ url: "circle_url"
36
+ }
37
+ });
38
+ const testConfig = createTestConfig("/tmp/deploymentRoot/");
39
+ testConfig.metadata = {
40
+ user: "custom_user",
41
+ version: "custom_version",
42
+ url: "custom_url"
43
+ };
44
+ const metadataResult = await getMetadata(testConfig);
45
+ expect(metadataResult.actor).toBe("custom_user");
46
+ expect(metadataResult.commitSha).toBe("custom_version");
47
+ expect(metadataResult.name).toBe("circle");
48
+ expect(metadataResult.url).toBe("custom_url");
49
+ });
50
+ test("should return commit data from latest commit through getLastestGitCommit", async () => {
51
+ vi.mocked(ciPlatform).mockReturnValueOnce({
52
+ isCI: false
53
+ });
54
+ vi.mocked(getLatestGitCommit).mockResolvedValueOnce({
55
+ author_name: "gh_author",
56
+ author_email: "test@github.com",
57
+ hash: "gh_hash",
58
+ message: "gh_message",
59
+ refs: "gh_refs",
60
+ date: "gh_date",
61
+ body: "gh_body"
62
+ });
63
+ const testConfig = createTestConfig("/tmp/deploymentRoot/");
64
+ const metadataResult = await getMetadata(testConfig);
65
+ expect(metadataResult.actor).toBe("gh_author");
66
+ expect(metadataResult.commitSha).toBe("gh_hash");
67
+ expect(metadataResult.name).toBe("unknown");
68
+ expect(metadataResult.url).toBe(void 0);
69
+ });
70
+ });
71
+ describe("getEnvironmentInput", () => {
72
+ test("should return environment tag from config", () => {
73
+ const testConfig = createTestConfig("/tmp/deploymentRoot/");
74
+ testConfig.environmentTag = "test_tag";
75
+ const metadata = {
76
+ branch: "test_branch"
77
+ };
78
+ const environmentInput = getEnvironmentInput(testConfig, metadata);
79
+ expect(environmentInput.tag).toBe("test_tag");
80
+ });
81
+ test("should return environment tag from metadata", () => {
82
+ const testConfig = createTestConfig("/tmp/deploymentRoot/");
83
+ delete testConfig.environmentTag;
84
+ const metadata = {
85
+ branch: "test_branch"
86
+ };
87
+ const environmentInput = getEnvironmentInput(testConfig, metadata);
88
+ expect(environmentInput.tag).toBe("test_branch");
89
+ });
90
+ test("should return undefined if no environment tag is provided", () => {
91
+ const testConfig = createTestConfig("/tmp/deploymentRoot/");
92
+ delete testConfig.environmentTag;
93
+ const metadata = {};
94
+ expect(getEnvironmentInput(testConfig, metadata)).toBe(void 0);
95
+ });
96
+ });
97
+ describe("createLabels", () => {
98
+ test("should return labels from metadata", () => {
99
+ const metadata = {
100
+ name: "circle",
101
+ run: "circle_1234",
102
+ url: "http://circleci.com/workflow/123",
103
+ actor: "circle_actor",
104
+ commitSha: "circle_sha"
105
+ };
106
+ const labels = [
107
+ 'circle-runId="circle_1234"',
108
+ 'url="http://circleci.com/workflow/123"',
109
+ 'user="circle_actor"',
110
+ 'version="circle_sha"'
111
+ ];
112
+ expect(createLabels(metadata)).toEqual(labels);
113
+ });
114
+ test("should return labels from metadata only for populated fields", () => {
115
+ const metadata = {
116
+ name: "circle",
117
+ run: "circle_run"
118
+ };
119
+ const labels = ['circle-runId="circle_run"'];
120
+ expect(createLabels(metadata)).toEqual(labels);
121
+ });
122
+ test("should throw when user metadata exceeds maximum length", () => {
123
+ const metadata = {
124
+ actor: "a".repeat(101),
125
+ name: "unknown"
126
+ };
127
+ expect(() => createLabels(metadata)).toThrow(
128
+ `Provided user metadata exceeds maximum length (max ${maxLabelLength} characters).`
129
+ );
130
+ });
131
+ });
@@ -0,0 +1,52 @@
1
+ interface Build {
2
+ id: string;
3
+ assetPath: string;
4
+ }
5
+ interface ClientError extends Error {
6
+ statusCode: number;
7
+ }
8
+ interface DeployConfig {
9
+ assetsDir?: string;
10
+ buildCommand: string;
11
+ deploymentToken: DeploymentToken;
12
+ deploymentUrl: string;
13
+ environmentTag?: string;
14
+ metadata: {
15
+ user?: string;
16
+ version?: string;
17
+ url?: string;
18
+ };
19
+ rootPath?: string;
20
+ skipBuild: boolean;
21
+ workerDir?: string;
22
+ workerOnly: boolean;
23
+ }
24
+ interface DeploymentToken {
25
+ accessToken: string;
26
+ allowedResource: string;
27
+ appId: string;
28
+ client: string;
29
+ expiresAt: string;
30
+ namespace: string;
31
+ namespaceId: string;
32
+ }
33
+ interface DeploymentManifestFile {
34
+ filePath: string;
35
+ fileSize: number;
36
+ mimeType: string;
37
+ fileHash: string;
38
+ fileType: string;
39
+ }
40
+ interface EnvironmentInput {
41
+ handle?: string;
42
+ tag?: string;
43
+ }
44
+ declare enum FileType {
45
+ Worker = "WORKER",
46
+ Asset = "ASSET"
47
+ }
48
+ interface OxygenError {
49
+ message: string;
50
+ }
51
+
52
+ export { Build, ClientError, DeployConfig, DeploymentManifestFile, DeploymentToken, EnvironmentInput, FileType, OxygenError };
@@ -0,0 +1,7 @@
1
+ var FileType = /* @__PURE__ */ ((FileType2) => {
2
+ FileType2["Worker"] = "WORKER";
3
+ FileType2["Asset"] = "ASSET";
4
+ return FileType2;
5
+ })(FileType || {});
6
+
7
+ export { FileType };
@@ -0,0 +1,6 @@
1
+ import { DeployConfig } from './types.js';
2
+ import { DeploymentTargetResponse } from './graphql/deployment-initiate.js';
3
+
4
+ declare function uploadFiles(config: DeployConfig, targets: DeploymentTargetResponse[]): Promise<void>;
5
+
6
+ export { uploadFiles };
@@ -0,0 +1,156 @@
1
+ import { formData, fetch } from '@shopify/cli-kit/node/http';
2
+ import { createFileReadStream } from '@shopify/cli-kit/node/fs';
3
+ import { outputInfo, outputCompleted } from '@shopify/cli-kit/node/output';
4
+ import { joinPath } from '@shopify/cli-kit/node/path';
5
+ import { mapLimit } from 'async';
6
+ import { deployDefaults } from '../utils/utils.js';
7
+
8
+ async function uploadFiles(config, targets) {
9
+ outputInfo(`Uploading ${targets.length} files...`);
10
+ return mapLimit(targets, 6, async (target) => {
11
+ await uploadFile(config, target);
12
+ }).then(() => {
13
+ outputCompleted(`Files uploaded successfully`);
14
+ });
15
+ }
16
+ async function uploadFile(config, target) {
17
+ const localFolderPath = target.fileType === "WORKER" ? joinPath(config.rootPath, config.workerDir) : joinPath(config.rootPath, config.assetsDir);
18
+ if (target.parameters !== null && target.parameters.length > 0) {
19
+ const form = formData();
20
+ target.parameters.forEach((param) => {
21
+ form.append(param.name, param.value);
22
+ });
23
+ form.append(
24
+ "file",
25
+ createFileReadStream(joinPath(localFolderPath, target.filePath))
26
+ );
27
+ await formUpload(form, target);
28
+ } else {
29
+ const initData = await initiateResumableUpload(target);
30
+ await performResumableUpload(
31
+ joinPath(localFolderPath, target.filePath),
32
+ initData
33
+ );
34
+ }
35
+ }
36
+ async function formUpload(form, target, attemptNumber = 0) {
37
+ try {
38
+ const timeoutDuration = 6e4;
39
+ const controller = new AbortController();
40
+ const timeout = setTimeout(() => {
41
+ controller.abort();
42
+ }, timeoutDuration);
43
+ const response = await fetch(target.uploadUrl, {
44
+ method: "POST",
45
+ body: form,
46
+ signal: controller.signal,
47
+ headers: {
48
+ Connection: "keep-alive"
49
+ }
50
+ });
51
+ clearTimeout(timeout);
52
+ if (!response.ok) {
53
+ throw new Error(`${response.status}`);
54
+ }
55
+ } catch (err) {
56
+ if (isErrorCode(err, "ENOENT")) {
57
+ throw new Error(`File not found: ${target.filePath}`);
58
+ }
59
+ if (attemptNumber < Number(deployDefaults.maxUploadAttempts)) {
60
+ await formUpload(form, target, attemptNumber + 1);
61
+ } else {
62
+ if (err instanceof Error && err.name === "AbortError") {
63
+ throw new Error(`Request timeout whilst uploading ${target.filePath}`);
64
+ }
65
+ throw new Error(`Failed to upload file ${target.filePath}`);
66
+ }
67
+ }
68
+ }
69
+ async function initiateResumableUpload(target) {
70
+ return fetch(target.uploadUrl, {
71
+ method: "POST",
72
+ headers: {
73
+ "x-goog-resumable": "start",
74
+ "X-Goog-Content-Length-Range": `0,${target.fileSize}`,
75
+ "User-Agent": "oxygen-cli"
76
+ }
77
+ }).then((res) => {
78
+ return {
79
+ sessionUri: res.headers.get("x-guploader-uploadid"),
80
+ location: res.headers.get("location"),
81
+ target
82
+ };
83
+ }).catch((err) => {
84
+ throw new Error(
85
+ `Failed to initiate resumable upload for file ${target.filePath} (status code ${err.statusCode})`
86
+ );
87
+ });
88
+ }
89
+ async function performResumableUpload(localFilePath, initData, startByte = 0, attemptNumber = 0) {
90
+ await uploadResumable(initData.location, localFilePath, startByte).catch(
91
+ async (err) => {
92
+ if (isErrorCode(err, "ENOENT")) {
93
+ throw new Error(`File not found: ${initData.target.filePath}`);
94
+ }
95
+ if (err && attemptNumber >= Number(deployDefaults.maxResumabeUploadAttempts)) {
96
+ throw new Error(
97
+ `Failed to upload file ${initData.target.filePath} after ${deployDefaults.maxResumabeUploadAttempts} attempts`
98
+ );
99
+ }
100
+ const status = await resumableUploadStatus(
101
+ initData.location,
102
+ initData.target.fileSize
103
+ );
104
+ if (!status.complete) {
105
+ const nextAttemptStartByte = status.lastReceivedByte;
106
+ const attempt = attemptNumber + 1;
107
+ await performResumableUpload(
108
+ localFilePath,
109
+ initData,
110
+ nextAttemptStartByte,
111
+ attempt
112
+ );
113
+ }
114
+ }
115
+ );
116
+ }
117
+ async function uploadResumable(location, filePath, lastReceivedByte) {
118
+ const file = createFileReadStream(filePath, { start: lastReceivedByte });
119
+ return fetch(location, {
120
+ method: "PUT",
121
+ body: file
122
+ }).then((res) => {
123
+ return res.status;
124
+ });
125
+ }
126
+ async function resumableUploadStatus(location, fileSize) {
127
+ const getLastByte = (range) => {
128
+ if (!range || range.split("-").length !== 2)
129
+ return 0;
130
+ const rangeParts = range.split("-");
131
+ return parseInt(rangeParts[1], 10);
132
+ };
133
+ return fetch(location, {
134
+ method: "PUT",
135
+ headers: {
136
+ "Content-Length": "0",
137
+ "Content-Range": `bytes */${fileSize}`
138
+ }
139
+ }).then((res) => {
140
+ return {
141
+ complete: res.status === 200,
142
+ lastReceivedByte: getLastByte(res.headers.get("range"))
143
+ };
144
+ }).catch((err) => {
145
+ console.error(err);
146
+ return {
147
+ complete: false,
148
+ lastReceivedByte: 0
149
+ };
150
+ });
151
+ }
152
+ function isErrorCode(err, code) {
153
+ return err instanceof Error && "code" in err && err.code === code;
154
+ }
155
+
156
+ export { uploadFiles };
@@ -0,0 +1,2 @@
1
+
2
+ export { }