|
1 | 1 | import AdmZip from 'adm-zip'; |
2 | | -import { Command } from 'commander'; |
| 2 | +import { Command, Option } from 'commander'; |
3 | 3 | import { consola } from 'consola'; |
4 | | -import { access, readdir } from 'node:fs/promises'; |
| 4 | +import { access, readdir, readFile } from 'node:fs/promises'; |
5 | 5 | import { join } from 'node:path'; |
| 6 | +import { z } from 'zod'; |
| 7 | + |
| 8 | +const UploadSignatureSchema = z.object({ |
| 9 | + data: z.object({ |
| 10 | + upload_url: z.url(), |
| 11 | + upload_uuid: z.string(), |
| 12 | + }), |
| 13 | +}); |
6 | 14 |
|
7 | 15 | export const generateBundleZip = async (rootDir: string) => { |
8 | | - consola.info('Generating bundle.zip'); |
| 16 | + consola.info('Generating bundle...'); |
9 | 17 |
|
10 | 18 | const distDir = join(rootDir, '.bigcommerce/dist'); |
11 | 19 |
|
@@ -33,19 +41,121 @@ export const generateBundleZip = async (rootDir: string) => { |
33 | 41 | zip.addLocalFolder(distDir, 'output'); |
34 | 42 | zip.writeZip(outputZip); |
35 | 43 |
|
36 | | - consola.success(`Created ${outputZip}`); |
| 44 | + consola.success(`Bundle created at: ${outputZip}`); |
| 45 | +}; |
| 46 | + |
| 47 | +export const generateUploadSignature = async ( |
| 48 | + storeHash: string, |
| 49 | + accessToken: string, |
| 50 | + apiHost: string, |
| 51 | +) => { |
| 52 | + consola.info('Generating upload signature...'); |
| 53 | + |
| 54 | + try { |
| 55 | + const response = await fetch( |
| 56 | + `https://${apiHost}/stores/${storeHash}/v3/headless/deployments/uploads`, |
| 57 | + { |
| 58 | + method: 'POST', |
| 59 | + headers: { |
| 60 | + 'X-Auth-Token': accessToken, |
| 61 | + 'Content-Type': 'application/json', |
| 62 | + Accept: 'application/json', |
| 63 | + }, |
| 64 | + body: JSON.stringify({}), |
| 65 | + }, |
| 66 | + ); |
| 67 | + |
| 68 | + if (!response.ok) { |
| 69 | + consola.error(`Failed to fetch upload signature: ${response.status} ${response.statusText}`); |
| 70 | + process.exit(1); |
| 71 | + } |
| 72 | + |
| 73 | + const res: unknown = await response.json(); |
| 74 | + const { data } = UploadSignatureSchema.parse(res); |
| 75 | + |
| 76 | + consola.success('Upload signature generated.'); |
| 77 | + |
| 78 | + return data; |
| 79 | + } catch (error) { |
| 80 | + consola.error('Error in generateUploadSignature:', error); |
| 81 | + process.exit(1); |
| 82 | + } |
| 83 | +}; |
| 84 | + |
| 85 | +export const uploadBundleZip = async (uploadUrl: string, rootDir: string) => { |
| 86 | + consola.info('Uploading bundle...'); |
| 87 | + |
| 88 | + const zipPath = join(rootDir, '.bigcommerce/dist/bundle.zip'); |
| 89 | + |
| 90 | + // Read the zip file as a buffer |
| 91 | + const fileBuffer = await readFile(zipPath); |
| 92 | + |
| 93 | + try { |
| 94 | + const response = await fetch(uploadUrl, { |
| 95 | + method: 'PUT', |
| 96 | + headers: { |
| 97 | + 'Content-Type': 'application/zip', |
| 98 | + }, |
| 99 | + body: fileBuffer, |
| 100 | + }); |
| 101 | + |
| 102 | + if (!response.ok) { |
| 103 | + consola.error(`Failed to upload bundle: ${response.status} ${response.statusText}`); |
| 104 | + process.exit(1); |
| 105 | + } |
| 106 | + |
| 107 | + consola.success('Bundle uploaded successfully.'); |
| 108 | + |
| 109 | + return true; |
| 110 | + } catch (error) { |
| 111 | + consola.error('Error in uploadBundleZip:', error); |
| 112 | + process.exit(1); |
| 113 | + } |
37 | 114 | }; |
38 | 115 |
|
39 | 116 | interface DeployOptions { |
| 117 | + storeHash?: string; |
| 118 | + accessToken?: string; |
40 | 119 | rootDir: string; |
| 120 | + apiHost: string; |
41 | 121 | } |
42 | 122 |
|
43 | 123 | export const deploy = new Command('deploy') |
44 | 124 | .description('Deploy the application to Cloudflare') |
45 | | - .option('--root-dir <rootDir>', 'Root directory', process.cwd()) |
| 125 | + .addOption( |
| 126 | + new Option( |
| 127 | + '--store-hash <hash>', |
| 128 | + 'BigCommerce store hash. Can be found in the URL of your store Control Panel.', |
| 129 | + ).env('BIGCOMMERCE_STORE_HASH'), |
| 130 | + ) |
| 131 | + .addOption( |
| 132 | + new Option( |
| 133 | + '--access-token <token>', |
| 134 | + 'BigCommerce access token. Can be found after creating a store-level API account.', |
| 135 | + ).env('BIGCOMMERCE_ACCESS_TOKEN'), |
| 136 | + ) |
| 137 | + .addOption( |
| 138 | + new Option('--api-host <host>', 'BigCommerce API host. The default is api.bigcommerce.com.') |
| 139 | + .env('BIGCOMMERCE_API_HOST') |
| 140 | + .default('api.bigcommerce.com'), |
| 141 | + ) |
| 142 | + .option('--root-dir <rootDir>', 'Root directory to deploy from.', process.cwd()) |
46 | 143 | .action(async (opts: DeployOptions) => { |
| 144 | + if (!opts.storeHash || !opts.accessToken) { |
| 145 | + consola.error('Missing store hash and access token.'); |
| 146 | + process.exit(1); |
| 147 | + } |
| 148 | + |
47 | 149 | await generateBundleZip(opts.rootDir); |
48 | 150 |
|
| 151 | + const uploadSignature = await generateUploadSignature( |
| 152 | + opts.storeHash, |
| 153 | + opts.accessToken, |
| 154 | + opts.apiHost, |
| 155 | + ); |
| 156 | + |
| 157 | + await uploadBundleZip(uploadSignature.upload_url, opts.rootDir); |
| 158 | + |
49 | 159 | // @todo rest of upload flow |
50 | 160 | process.exit(0); |
51 | 161 | }); |
0 commit comments