41 lines
3.6 KiB
JavaScript
41 lines
3.6 KiB
JavaScript
import { build } from 'esbuild'
|
||
import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises'
|
||
import { dirname, resolve, join } from 'node:path'
|
||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||
import { tmpdir } from 'node:os'
|
||
import { gzipSync } from 'node:zlib'
|
||
|
||
const here = dirname(fileURLToPath(import.meta.url)), root = resolve(here, '../..')
|
||
const temp = await mkdtemp(join(tmpdir(), 'reformer-build-'))
|
||
try {
|
||
await build({ entryPoints: [join(here, 'src/geometry.ts')], bundle: true, platform: 'node', format: 'esm', outfile: join(temp, 'geometry.mjs') })
|
||
const { createFigure, partsInventory } = await import(pathToFileURL(join(temp, 'geometry.mjs')).href)
|
||
await build({ entryPoints: [join(root, 'packages/app/src/content/reformer.ts')], bundle: true, platform: 'node', format: 'esm', outfile: join(temp, 'content.mjs') })
|
||
const { reformerParts, reformerLesson } = await import(pathToFileURL(join(temp, 'content.mjs')).href)
|
||
const live = await build({ entryPoints: [join(here, 'src/controller.ts')], bundle: true, platform: 'browser', format: 'iife', target: ['es2019'], minify: true, write: false })
|
||
const [template, css, illustration] = await Promise.all(['page.html', 'page.css', 'illustration.css'].map(name => readFile(join(here, 'src', name), 'utf8')))
|
||
const approved = reformerLesson.review.status === 'approved' && reformerLesson.review.coach && reformerLesson.review.reviewedAt
|
||
if (process.argv.includes('--release') && !approved) throw new Error('正式发布构建需要教练审核记录:status=approved、coach、reviewedAt。审阅预览请使用 pnpm build:education。')
|
||
const figure = createFigure()
|
||
const replacements = {
|
||
__THIRD_PARTY_LICENSE__: `Anatomy SVG kit · https://github.com/wheresryan22/anatomy\n${await readFile(join(here, 'vendor/anatomy/LICENSE'), 'utf8')}`,
|
||
'/*__CSS__*/': `${css}\n${illustration}`,
|
||
'/*__SCRIPT__*/': live.outputFiles[0].text.replaceAll('</script', '<\\/script'),
|
||
__FIGURE__: figure,
|
||
__PART_BUTTONS__: reformerParts.map(p => `<button data-part-button="${p.id}" aria-pressed="${p.id === 'carriage'}">${p.name}</button>`).join(''),
|
||
__STATIC_PARTS__: reformerParts.map(p => `<dt>${p.number} · ${p.name}</dt><dd>${p.description} ${p.observation}</dd>`).join(''),
|
||
__SPRING_BUTTONS__: reformerLesson.springs.map((s, i) => `<button class="spring-button" data-spring-button="${s.id}" aria-pressed="${i === 0 || i === 3}" aria-label="${s.label}弹簧,${s.level},切换连接" style="--spring-color:${s.color}"><span class="spring-color"></span><span>${s.label}</span><span class="spring-state">${i === 0 || i === 3 ? '已连接' : '未连接'}</span></button>`).join(''),
|
||
__REFERENCE__: reformerLesson.reference,
|
||
__SOURCES__: reformerLesson.sources.map(s => `<a href="${s.url}" target="_blank" rel="noopener noreferrer">${s.title} ↗</a>`).join(''),
|
||
}
|
||
let html = template
|
||
for (const [key, value] of Object.entries(replacements)) html = html.replace(key, () => value)
|
||
const output = join(root, 'packages/server/public/learn/reformer')
|
||
await mkdir(output, { recursive: true })
|
||
await writeFile(join(output, 'index.html'), html)
|
||
// Keep an inspectable standalone SVG. Native mini-programs use a checked-in PNG raster.
|
||
const svg = figure.replace('><title', `><style>${illustration}</style><title`)
|
||
await writeFile(join(output, 'reformer.svg'), svg)
|
||
console.log(`Reformer built: ${Math.round(Buffer.byteLength(html) / 1024)} KiB HTML, ${Math.round(gzipSync(html).length / 1024)} KiB gzip; no runtime requests. ${partsInventory.length} part categories.`)
|
||
} finally { await rm(temp, { recursive: true, force: true }) }
|