| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- import { spawnSync } from 'node:child_process'
- import { brotliCompressSync, constants as zlibConstants } from 'node:zlib'
- import {
- cpSync,
- existsSync,
- mkdirSync,
- readdirSync,
- readFileSync,
- rmSync,
- statSync,
- writeFileSync
- } from 'node:fs'
- import { basename, extname, join, relative, resolve } from 'node:path'
- const projectName = process.env.CLOUDFLARE_PAGES_PROJECT || 'flyfish-file-viewer'
- // Wrangler direct upload publishes to the production deployment when no branch
- // is supplied. Keep branch deployments explicit so custom domains are updated
- // by the default release commands.
- const branch = process.env.CLOUDFLARE_PAGES_BRANCH
- const outputDir = process.env.CLOUDFLARE_PAGES_OUTPUT_DIR || 'apps/viewer-demo/dist'
- const dryRun = process.env.CLOUDFLARE_PAGES_DRY_RUN === '1' || process.argv.includes('--dry-run')
- function commandExists(command, args = ['--version']) {
- const result = spawnSync(command, args, {
- encoding: 'utf8',
- shell: false,
- stdio: 'pipe'
- })
- return result.status === 0
- }
- const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
- const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx'
- const usePnpmDlx = commandExists(pnpmCommand)
- const command = usePnpmDlx ? pnpmCommand : npxCommand
- const maxFileBytes = Number.parseInt(
- process.env.CLOUDFLARE_PAGES_MAX_FILE_BYTES || String(25 * 1024 * 1024),
- 10
- )
- const resolvedOutputDir = resolve(outputDir)
- const uploadDir = resolve('.release', 'cloudflare-pages', projectName)
- const skippedFiles = []
- const compressedFiles = []
- const toUrlPath = filePath => `/${filePath.replaceAll('\\', '/')}`
- function writeCompressedAssetHeaders(targetDir) {
- if (!compressedFiles.length) {
- return
- }
- const headersPath = join(targetDir, '_headers')
- const current = existsSync(headersPath) ? readFileSync(headersPath, 'utf8') : ''
- const prefix = current
- ? `${current}${current.endsWith('\n') ? '' : '\n'}\n`
- : ''
- const additions = compressedFiles
- .map(file => [
- toUrlPath(file.path),
- ' ! Content-Type',
- ' ! Cache-Control',
- ' ! X-Content-Type-Options',
- ' Content-Type: application/wasm',
- ' Content-Encoding: br',
- ' Vary: Accept-Encoding',
- ' X-Content-Type-Options: nosniff',
- ' Cache-Control: public, max-age=31536000, immutable'
- ].join('\n'))
- .join('\n\n')
- writeFileSync(headersPath, `${prefix}${additions}\n`)
- }
- function writeBrotliCompressedWasm(source, target, relativePath) {
- const raw = readFileSync(source)
- const compressed = brotliCompressSync(raw, {
- params: {
- [zlibConstants.BROTLI_PARAM_QUALITY]: 11
- }
- })
- if (compressed.length > maxFileBytes) {
- return false
- }
- mkdirSync(join(target, '..'), { recursive: true })
- writeFileSync(target, compressed)
- compressedFiles.push({
- path: relativePath,
- size: raw.length,
- compressedSize: compressed.length
- })
- return true
- }
- function copyDeployableFiles(sourceDir, targetDir) {
- if (!existsSync(sourceDir)) {
- throw new Error(`Cloudflare Pages output directory does not exist: ${sourceDir}`)
- }
- rmSync(targetDir, { force: true, recursive: true })
- mkdirSync(targetDir, { recursive: true })
- const visit = (source, target) => {
- const stat = statSync(source)
- if (stat.isDirectory()) {
- mkdirSync(target, { recursive: true })
- for (const entry of readdirSync(source)) {
- visit(join(source, entry), join(target, entry))
- }
- return
- }
- if (!stat.isFile()) {
- return
- }
- if (stat.size > maxFileBytes) {
- const relativePath = relative(sourceDir, source)
- if (extname(source).toLowerCase() === '.wasm' &&
- writeBrotliCompressedWasm(source, target, relativePath)) {
- return
- }
- skippedFiles.push({
- path: relativePath,
- size: stat.size
- })
- return
- }
- mkdirSync(join(target, '..'), { recursive: true })
- cpSync(source, target)
- }
- visit(sourceDir, targetDir)
- }
- copyDeployableFiles(resolvedOutputDir, uploadDir)
- writeCompressedAssetHeaders(uploadDir)
- if (compressedFiles.length) {
- console.warn(
- `[cloudflare-pages] Brotli-compressed ${compressedFiles.length} oversized WASM file(s) while preparing ${basename(uploadDir)}:`
- )
- for (const file of compressedFiles) {
- console.warn(` - ${file.path} (${file.size} bytes -> ${file.compressedSize} bytes)`)
- }
- }
- if (skippedFiles.length) {
- console.warn(
- `[cloudflare-pages] Skipped ${skippedFiles.length} oversized file(s) above ${maxFileBytes} bytes while preparing ${basename(uploadDir)}:`
- )
- for (const file of skippedFiles) {
- console.warn(` - ${file.path} (${file.size} bytes)`)
- }
- const fatalWasmFiles = skippedFiles.filter(file => file.path.toLowerCase().endsWith('.wasm'))
- if (fatalWasmFiles.length) {
- throw new Error(
- `Cloudflare Pages upload would miss required WASM assets: ${fatalWasmFiles.map(file => file.path).join(', ')}`
- )
- }
- }
- if (dryRun) {
- console.log(`[cloudflare-pages] Dry run prepared ${uploadDir}`)
- process.exit(0)
- }
- const args = [
- ...(usePnpmDlx ? ['dlx'] : ['--yes']),
- 'wrangler',
- 'pages',
- 'deploy',
- uploadDir,
- '--project-name',
- projectName,
- '--commit-dirty=true'
- ]
- if (branch) {
- args.push('--branch', branch)
- }
- const result = spawnSync(command, args, {
- stdio: 'inherit',
- shell: false
- })
- if (result.error) {
- throw result.error
- }
- process.exit(result.status ?? 1)
|