1import { spawn } from 'node:child_process'2import { readFile, readdir } from 'node:fs/promises'3import { join, resolve } from 'node:path'45const root = resolve(import.meta.dirname, '../..')6const configDirectory = join(root, 'config/units')7const wrangler = join(root, 'node_modules/.bin/wrangler')8const message = process.env.DIAGRAMZIP_RELEASE_MESSAGE910function parseJsonc(source) {11 let stripped = ''12 let string = false13 let escaped = false14 for (let index = 0; index < source.length; index += 1) {15 const character = source[index]16 const next = source[index + 1]17 if (string) {18 stripped += character19 if (escaped) escaped = false20 else if (character === '\\') escaped = true21 else if (character === '"') string = false22 } else if (character === '"') {23 string = true24 stripped += character25 } else if (character === '/' && next === '/') {26 while (index < source.length && source[index] !== '\n') index += 127 stripped += '\n'28 } else if (character === '/' && next === '*') {29 index += 230 while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) index += 131 index += 132 } else {33 stripped += character34 }35 }36 let normalized = ''37 string = false38 escaped = false39 for (let index = 0; index < stripped.length; index += 1) {40 const character = stripped[index]41 if (string) {42 normalized += character43 if (escaped) escaped = false44 else if (character === '\\') escaped = true45 else if (character === '"') string = false46 } else if (character === '"') {47 string = true48 normalized += character49 } else if (character === ',') {50 let next = index + 151 while (/\s/.test(stripped[next] ?? '')) next += 152 if (stripped[next] !== '}' && stripped[next] !== ']') normalized += character53 } else {54 normalized += character55 }56 }57 return JSON.parse(normalized)58}5960const configs = []61for (const filename of (await readdir(configDirectory)).filter(name => name.endsWith('.jsonc')).sort()) {62 const path = join(configDirectory, filename)63 const config = parseJsonc(await readFile(path, 'utf8'))64 if (!config.name) throw new Error(`${filename} does not declare a Worker name.`)65 configs.push({66 filename,67 path,68 name: config.name,69 dependencies: (config.services ?? []).map(service => service.service),70 })71}7273const names = new Set(configs.map(config => config.name))74if (names.size !== configs.length) throw new Error('Edge Worker configuration names must be unique.')7576async function deploy(config) {77 await new Promise((resolvePromise, reject) => {78 const args = ['deploy', '--strict', '--config', config.path]79 if (message) args.push('--message', message)80 const child = spawn(wrangler, args, {81 cwd: root,82 stdio: 'inherit',83 })84 child.on('error', reject)85 child.on('exit', code => code === 0 ? resolvePromise() : reject(new Error(`${config.filename} deployment exited ${code}`)))86 })87}8889async function deployBatch(configs) {90 const pending = [...configs]91 const failures = []92 async function worker() {93 while (pending.length > 0) {94 const config = pending.shift()95 try {96 await deploy(config)97 } catch (error) {98 failures.push(error instanceof Error ? error.message : String(error))99 }100 }101 }102 await Promise.all(Array.from({ length: Math.min(4, configs.length) }, worker))103 if (failures.length > 0) throw new Error(failures.join('\n'))104}105106const remaining = new Map(configs.map(config => [config.name, config]))107let deployed = 0108while (remaining.size > 0) {109 const ready = [...remaining.values()].filter(config =>110 config.dependencies.every(dependency => !names.has(dependency) || !remaining.has(dependency)),111 )112 if (ready.length === 0) {113 throw new Error(`Cyclic Worker service dependencies: ${[...remaining.keys()].join(', ')}`)114 }115 await deployBatch(ready)116 for (const config of ready) remaining.delete(config.name)117 deployed += ready.length118}119120console.log(`${deployed}/${configs.length} Worker renderer units deployed`)