202 lines
5.9 KiB
JavaScript
202 lines
5.9 KiB
JavaScript
import crypto from 'node:crypto'
|
|
import fs from 'node:fs/promises'
|
|
import https from 'node:https'
|
|
import path from 'node:path'
|
|
import { spawn } from 'node:child_process'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
const root = path.resolve(__dirname, '..')
|
|
const args = new Set(process.argv.slice(2))
|
|
const skipUpload = args.has('--skip-upload')
|
|
|
|
const timestamp = process.env.BUILD_TIMESTAMP || formatTimestamp(new Date())
|
|
const bucket = process.env.OSS_BUCKET || 'yuedong-wechatapplet'
|
|
const region = process.env.OSS_REGION || 'oss-cn-shanghai'
|
|
const endpoint = normalizeEndpoint(process.env.OSS_ENDPOINT || `${bucket}.${region}.aliyuncs.com`)
|
|
const ossPrefix = normalizeObjectPrefix(process.env.OSS_PREFIX || 'static/web/samart-static')
|
|
const uploadPrefix = normalizeObjectPrefix(`${ossPrefix}/${timestamp}`)
|
|
const publicBase = normalizePublicBase(
|
|
process.env.OSS_PUBLIC_BASE || `https://${endpoint}/${uploadPrefix}/`
|
|
)
|
|
const outDir = path.resolve(root, process.env.BUILD_OUT_DIR || path.join('.oss-build', timestamp))
|
|
|
|
console.log(`[build] timestamp: ${timestamp}`)
|
|
console.log(`[build] output: ${path.relative(root, outDir)}`)
|
|
console.log(`[build] public base: ${publicBase}`)
|
|
|
|
await runViteBuild()
|
|
|
|
if (skipUpload) {
|
|
console.log('[oss] skip upload by --skip-upload')
|
|
process.exit(0)
|
|
}
|
|
|
|
const accessKeyId = 'LTAI5tAUZ6o8r8K2vkZX4shA'
|
|
const accessKeySecret = 'Mz898g6O3OQ8Kzsi7WaK2XPwZFKoRk'
|
|
|
|
if (!accessKeyId || !accessKeySecret) {
|
|
throw new Error(
|
|
'Missing OSS credentials. Please set OSS_ACCESS_KEY_ID and OSS_ACCESS_KEY_SECRET, or use `npm run build -- --skip-upload`.'
|
|
)
|
|
}
|
|
|
|
const files = await listFiles(outDir)
|
|
console.log(`[oss] uploading ${files.length} files to oss://${bucket}/${uploadPrefix}/`)
|
|
|
|
for (const file of files) {
|
|
const relativePath = toPosixPath(path.relative(outDir, file))
|
|
const objectKey = `${uploadPrefix}/${relativePath}`
|
|
await uploadObject({
|
|
file,
|
|
objectKey,
|
|
bucket,
|
|
endpoint,
|
|
accessKeyId,
|
|
accessKeySecret
|
|
})
|
|
console.log(`[oss] uploaded ${objectKey}`)
|
|
}
|
|
|
|
console.log(`[oss] done: ${publicBase}`)
|
|
|
|
function formatTimestamp(date) {
|
|
const parts = new Intl.DateTimeFormat('zh-CN', {
|
|
timeZone: process.env.BUILD_TIMEZONE || 'Asia/Shanghai',
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false
|
|
}).formatToParts(date)
|
|
|
|
const get = (type) => parts.find((item) => item.type === type)?.value || ''
|
|
return `${get('year')}${get('month')}${get('day')}${get('hour')}${get('minute')}`
|
|
}
|
|
|
|
function runViteBuild() {
|
|
const viteBin = path.resolve(root, 'node_modules/vite/bin/vite.js')
|
|
const env = {
|
|
...process.env,
|
|
BUILD_TIMESTAMP: timestamp,
|
|
VITE_BUILD_OUT_DIR: outDir,
|
|
VITE_APP_PUBLIC_BASE: publicBase
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [viteBin, 'build'], {
|
|
cwd: root,
|
|
env,
|
|
stdio: 'inherit'
|
|
})
|
|
|
|
child.on('error', reject)
|
|
child.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`vite build failed with exit code ${code}`))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
async function listFiles(dir) {
|
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
const files = []
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
files.push(...await listFiles(fullPath))
|
|
} else if (entry.isFile()) {
|
|
files.push(fullPath)
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
async function uploadObject({ file, objectKey, bucket, endpoint, accessKeyId, accessKeySecret }) {
|
|
const body = await fs.readFile(file)
|
|
const contentType = getContentType(file)
|
|
const date = new Date().toUTCString()
|
|
const resource = `/${bucket}/${objectKey}`
|
|
const signature = crypto
|
|
.createHmac('sha1', accessKeySecret)
|
|
.update(`PUT\n\n${contentType}\n${date}\n${resource}`)
|
|
.digest('base64')
|
|
|
|
const requestPath = `/${objectKey.split('/').map(encodeURIComponent).join('/')}`
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const req = https.request({
|
|
method: 'PUT',
|
|
hostname: endpoint,
|
|
path: requestPath,
|
|
headers: {
|
|
Authorization: `OSS ${accessKeyId}:${signature}`,
|
|
Date: date,
|
|
'Content-Type': contentType,
|
|
'Content-Length': body.length
|
|
}
|
|
}, (res) => {
|
|
const chunks = []
|
|
res.on('data', (chunk) => chunks.push(chunk))
|
|
res.on('end', () => {
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
resolve()
|
|
} else {
|
|
const message = Buffer.concat(chunks).toString('utf8')
|
|
reject(new Error(`OSS upload failed: ${res.statusCode} ${res.statusMessage}\n${message}`))
|
|
}
|
|
})
|
|
})
|
|
|
|
req.on('error', reject)
|
|
req.end(body)
|
|
})
|
|
}
|
|
|
|
function getContentType(file) {
|
|
const ext = path.extname(file).toLowerCase()
|
|
const map = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'application/javascript; charset=utf-8',
|
|
'.mjs': 'application/javascript; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.svg': 'image/svg+xml',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.gif': 'image/gif',
|
|
'.webp': 'image/webp',
|
|
'.ico': 'image/x-icon',
|
|
'.woff': 'font/woff',
|
|
'.woff2': 'font/woff2',
|
|
'.ttf': 'font/ttf',
|
|
'.otf': 'font/otf',
|
|
'.map': 'application/json; charset=utf-8',
|
|
'.txt': 'text/plain; charset=utf-8'
|
|
}
|
|
return map[ext] || 'application/octet-stream'
|
|
}
|
|
|
|
function normalizeEndpoint(value) {
|
|
return value.replace(/^https?:\/\//, '').replace(/\/+$/, '')
|
|
}
|
|
|
|
function normalizeObjectPrefix(value) {
|
|
return toPosixPath(value).replace(/^\/+|\/+$/g, '')
|
|
}
|
|
|
|
function normalizePublicBase(value) {
|
|
return value.endsWith('/') ? value : `${value}/`
|
|
}
|
|
|
|
function toPosixPath(value) {
|
|
return value.split(path.sep).join('/')
|
|
}
|