Skip to content

Commit fd3d0b6

Browse files
committed
add support for workers paths
1 parent 5d3768f commit fd3d0b6

File tree

2 files changed

+131
-27
lines changed

2 files changed

+131
-27
lines changed

src/commands/deploy.mjs

+57-25
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { join, resolve, relative } from 'pathe'
88
import { execa } from 'execa'
99
import { setupDotenv } from 'c12'
1010
import { $api, fetchUser, selectTeam, selectProject, projectPath, fetchProject, linkProject, gitInfo, getPackageJson } from '../utils/index.mjs'
11-
import { getStorage, getPathsToDeploy, getFile, uploadAssetsToCloudflare, isMetaPath, isServerPath, getPublicFiles } from '../utils/deploy.mjs'
11+
import { getStorage, getPathsToDeploy, getFile, uploadAssetsToCloudflare, uploadWorkersAssetsToCloudflare, isMetaPath, isWorkerMetaPath, isServerPath, isWorkerServerPath, getPublicFiles, getWorkerPublicFiles } from '../utils/deploy.mjs'
1212
import { createMigrationsTable, fetchRemoteMigrations, queryDatabase } from '../utils/database.mjs'
1313
import login from './login.mjs'
1414

@@ -137,6 +137,7 @@ export default defineCommand({
137137
const fileKeys = await storage.getKeys()
138138
const pathsToDeploy = getPathsToDeploy(fileKeys)
139139
const config = await storage.getItem('hub.config.json')
140+
const isWorkerPreset = ['cloudflare_module', 'cloudflare_durable'].includes(config.nitroPreset)
140141
const { format: formatNumber } = new Intl.NumberFormat('en-US')
141142

142143
let spinner = ora(`Preparing ${colors.blueBright(linkedProject.slug)} deployment for ${deployEnvColored}...`).start()
@@ -147,40 +148,64 @@ export default defineCommand({
147148
spinnerColorIndex = (spinnerColorIndex + 1) % spinnerColors.length
148149
}, 2500)
149150

150-
let deploymentKey, serverFiles, metaFiles
151+
let deploymentKey, serverFiles, metaFiles, completionToken
151152
try {
152-
const publicFiles = await getPublicFiles(storage, pathsToDeploy)
153+
let url = `/teams/${linkedProject.teamSlug}/projects/${linkedProject.slug}/${deployEnv}/deploy/prepare`
154+
let publicFiles, publicManifest
153155

154-
const deploymentInfo = await $api(`/teams/${linkedProject.teamSlug}/projects/${linkedProject.slug}/${deployEnv}/deploy/prepare`, {
156+
if (isWorkerPreset) {
157+
url = `/teams/${linkedProject.teamSlug}/projects/${linkedProject.slug}/${deployEnv}/deploy/worker/prepare`
158+
publicFiles = await getWorkerPublicFiles(storage, pathsToDeploy)
159+
/**
160+
* { "/index.html": { hash: "hash", size: 30 }
161+
*/
162+
publicManifest = publicFiles.reduce((acc, file) => {
163+
acc[file.path] = {
164+
hash: file.hash,
165+
size: file.size
166+
}
167+
return acc
168+
}, {})
169+
} else {
170+
publicFiles = await getPublicFiles(storage, pathsToDeploy)
171+
/**
172+
* { "/index.html": "hash" }
173+
*/
174+
publicManifest = publicFiles.reduce((acc, file) => {
175+
acc[file.path] = file.hash
176+
return acc
177+
}, {})
178+
}
179+
// Get deployment info by preparing the deployment
180+
const deploymentInfo = await $api(url, {
155181
method: 'POST',
156182
body: {
157183
config,
158-
/**
159-
* Public manifest is a map of file paths to their unique hash (SHA256 sliced to 32 characters).
160-
* @example
161-
* {
162-
* "/index.html": "hash",
163-
* "/assets/image.png": "hash"
164-
* }
165-
*/
166-
publicManifest: publicFiles.reduce((acc, file) => {
167-
acc[file.path] = file.hash
168-
return acc
169-
}, {})
184+
publicManifest
170185
}
171186
})
172187
spinner.succeed(`${colors.blueBright(linkedProject.slug)} ready to deploy.`)
173-
const { missingPublicHashes, cloudflareUploadJwt } = deploymentInfo
174188
deploymentKey = deploymentInfo.deploymentKey
189+
190+
const { cloudflareUploadJwt, buckets, accountId } = deploymentInfo
191+
// missingPublicHash is sent for pages & buckets for worker
192+
let missingPublicHashes = deploymentInfo.missingPublicHashes || buckets.flat()
175193
const publicFilesToUpload = publicFiles.filter(file => missingPublicHashes.includes(file.hash))
176194

177195
if (publicFilesToUpload.length) {
178196
const totalSizeToUpload = publicFilesToUpload.reduce((acc, file) => acc + file.size, 0)
179197
spinner = ora(`Uploading ${colors.blueBright(formatNumber(publicFilesToUpload.length))} new static assets (${colors.blueBright(prettyBytes(totalSizeToUpload))})...`).start()
180-
await uploadAssetsToCloudflare(publicFilesToUpload, cloudflareUploadJwt, ({ progressSize, totalSize }) => {
181-
const percentage = Math.round((progressSize / totalSize) * 100)
182-
spinner.text = `${percentage}% uploaded (${prettyBytes(progressSize)}/${prettyBytes(totalSize)})...`
183-
})
198+
if (linkedProject.type === 'pages') {
199+
await uploadAssetsToCloudflare(publicFilesToUpload, cloudflareUploadJwt, ({ progressSize, totalSize }) => {
200+
const percentage = Math.round((progressSize / totalSize) * 100)
201+
spinner.text = `${percentage}% uploaded (${prettyBytes(progressSize)}/${prettyBytes(totalSize)})...`
202+
})
203+
} else {
204+
completionToken = await uploadWorkersAssetsToCloudflare(accountId, publicFilesToUpload, cloudflareUploadJwt, ({ progressSize, totalSize }) => {
205+
const percentage = Math.round((progressSize / totalSize) * 100)
206+
spinner.text = `${percentage}% uploaded (${prettyBytes(progressSize)}/${prettyBytes(totalSize)})...`
207+
})
208+
}
184209
spinner.succeed(`${colors.blueBright(formatNumber(publicFilesToUpload.length))} new static assets uploaded (${colors.blueBright(prettyBytes(totalSizeToUpload))})`)
185210
}
186211

@@ -190,8 +215,14 @@ export default defineCommand({
190215
consola.info(`${colors.blueBright(formatNumber(publicFiles.length))} static assets (${colors.blueBright(prettyBytes(totalSize))} / ${colors.blueBright(prettyBytes(totalGzipSize))} gzip)`)
191216
}
192217

193-
metaFiles = await Promise.all(pathsToDeploy.filter(isMetaPath).map(p => getFile(storage, p, 'base64')))
194-
serverFiles = await Promise.all(pathsToDeploy.filter(isServerPath).map(p => getFile(storage, p, 'base64')))
218+
metaFiles = await Promise.all(pathsToDeploy.filter(isWorkerPreset ? isWorkerMetaPath : isMetaPath).map(p => getFile(storage, p, 'base64')))
219+
serverFiles = await Promise.all(pathsToDeploy.filter(isWorkerPreset ? isWorkerServerPath : isServerPath).map(p => getFile(storage, p, 'base64')))
220+
if (isWorkerPreset) {
221+
serverFiles = serverFiles.map(file => ({
222+
...file,
223+
path: file.path.replace('/server/', '/')
224+
}))
225+
}
195226
const serverFilesSize = serverFiles.reduce((acc, file) => acc + file.size, 0)
196227
const serverFilesGzipSize = serverFiles.reduce((acc, file) => acc + file.gzipSize, 0)
197228
consola.info(`${colors.blueBright(formatNumber(serverFiles.length))} server files (${colors.blueBright(prettyBytes(serverFilesSize))} / ${colors.blueBright(prettyBytes(serverFilesGzipSize))} gzip)...`)
@@ -286,13 +317,14 @@ export default defineCommand({
286317

287318
// #region Complete deployment
288319
spinner = ora(`Deploying ${colors.blueBright(linkedProject.slug)} to ${deployEnvColored}...`).start()
289-
const deployment = await $api(`/teams/${linkedProject.teamSlug}/projects/${linkedProject.slug}/${deployEnv}/deploy/complete`, {
320+
const deployment = await $api(`/teams/${linkedProject.teamSlug}/projects/${linkedProject.slug}/${deployEnv}/deploy/${isWorkerPreset ? 'worker/complete' : 'complete'}`, {
290321
method: 'POST',
291322
body: {
292323
deploymentKey,
293324
git,
294325
serverFiles,
295-
metaFiles
326+
metaFiles,
327+
completionToken
296328
},
297329
}).catch((err) => {
298330
spinner.fail(`Failed to deploy ${colors.blueBright(linkedProject.slug)} to ${deployEnvColored}.`)

src/utils/deploy.mjs

+74-2
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,18 @@ export const META_PATHS = [
126126
'/nitro.json',
127127
'/hub.config.json',
128128
'/wrangler.toml',
129+
'/package-lock.json',
130+
'/package.json'
129131
]
130132

131133
export const isMetaPath = (path) => META_PATHS.includes(path)
132134
export const isServerPath = (path) => path.startsWith('/_worker.js/')
133135
export const isPublicPath = (path) => !isMetaPath(path) && !isServerPath(path)
134136

137+
export const isWorkerMetaPath = (path) => META_PATHS.includes(path)
138+
export const isWorkerPublicPath = (path) => path.startsWith('/public/')
139+
export const isWorkerServerPath = (path) => path.startsWith('/server/')
140+
135141
/**
136142
* Get all public files with their metadata
137143
* @param {import('unstorage').Storage} storage - Storage instance
@@ -143,9 +149,18 @@ export async function getPublicFiles(storage, paths) {
143149
paths.filter(isPublicPath).map(p => getFile(storage, p, 'base64'))
144150
)
145151
}
152+
export async function getWorkerPublicFiles(storage, paths) {
153+
const files = await Promise.all(
154+
paths.filter(isWorkerPublicPath).map(p => getFile(storage, p, 'base64'))
155+
)
156+
return files.map((file) => ({
157+
...file,
158+
path: file.path.replace('/public/', '/')
159+
}))
160+
}
146161

147162
/**
148-
* Upload assets to Cloudflare with concurrent uploads
163+
* Upload assets to Cloudflare Pages with concurrent uploads
149164
* @param {Array<{ path: string, data: string, hash: string, contentType: string }>} files - Files to upload
150165
* @param {string} cloudflareUploadJwt - Cloudflare upload JWT
151166
* @param {Function} onProgress - Callback function to update progress
@@ -200,4 +215,61 @@ export async function uploadAssetsToCloudflare(files, cloudflareUploadJwt, onPro
200215
}
201216
}
202217

203-
// async function uploadToCloudflare(body, cloudflareUploadJwt) {
218+
219+
/**
220+
* Upload assets to Cloudflare Workers with concurrent uploads
221+
* @param {Array<string<string>} buckets - Buckets of hashes to upload
222+
* @param {Array<{ path: string, data: string, hash: string, contentType: string }>} files - Files to upload
223+
* @param {string} cloudflareUploadJwt - Cloudflare upload JWT
224+
* @param {Function} onProgress - Callback function to update progress
225+
*/
226+
export async function uploadWorkersAssetsToCloudflare(accountId, files, cloudflareUploadJwt, onProgress) {
227+
const chunks = await createChunks(files)
228+
if (!chunks.length) {
229+
return
230+
}
231+
232+
let filesUploaded = 0
233+
let progressSize = 0
234+
let completionToken
235+
const totalSize = files.reduce((acc, file) => acc + file.size, 0)
236+
for (let i = 0; i < chunks.length; i += CONCURRENT_UPLOADS) {
237+
const chunkGroup = chunks.slice(i, i + CONCURRENT_UPLOADS)
238+
239+
await Promise.all(chunkGroup.map(async (filesInChunk) => {
240+
const form = new FormData()
241+
for (const file of filesInChunk) {
242+
form.append(file.hash, new File([file.data], file.hash, { type: file.contentType}), file.hash)
243+
}
244+
return ofetch(`/accounts/${accountId}/workers/assets/upload?base64=true`, {
245+
baseURL: 'https://api.cloudflare.com/client/v4/',
246+
method: 'POST',
247+
headers: {
248+
Authorization: `Bearer ${cloudflareUploadJwt}`
249+
},
250+
retry: MAX_UPLOAD_ATTEMPTS,
251+
retryDelay: UPLOAD_RETRY_DELAY,
252+
body: form
253+
})
254+
.then((data) => {
255+
if (data && data.result?.jwt) {
256+
completionToken = data.result.jwt
257+
}
258+
if (typeof onProgress === 'function') {
259+
filesUploaded += filesInChunk.length
260+
progressSize += filesInChunk.reduce((acc, file) => acc + file.size, 0)
261+
onProgress({ progress: filesUploaded, progressSize, total: files.length, totalSize })
262+
}
263+
})
264+
.catch((err) => {
265+
if (err.data) {
266+
throw new Error(`Error while uploading assets to Cloudflare: ${JSON.stringify(err.data)} - ${err.message}`)
267+
}
268+
else {
269+
throw new Error(`Error while uploading assets to Cloudflare: ${err.message.split(' - ')[1] || err.message}`)
270+
}
271+
})
272+
}))
273+
}
274+
return completionToken
275+
}

0 commit comments

Comments
 (0)