@taboo-avalanche/andesite-compiler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@taboo-avalanche/andesite-compiler",
3
+ "version": "1.0.0",
4
+ "description": "Rasterize Andesite React pages to Bedrock chest UI zip",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/compile.d.ts",
9
+ "import": "./dist/compile.js"
10
+ }
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "files": [
15
+ "dist"
16
+ ]
17
+ },
18
+ "dependencies": {
19
+ "archiver": "^7.0.1",
20
+ "puppeteer": "^23.0.0",
21
+ "@taboo-avalanche/andesite": "1.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^26.6.2",
25
+ "tsup": "^8.0.0"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup"
29
+ }
30
+ }
package/src/compile.ts ADDED
@@ -0,0 +1,142 @@
1
+ import * as fs from 'fs'
2
+ import * as path from 'path'
3
+ import { createHash } from 'node:crypto'
4
+ import archiver from 'archiver'
5
+ import type { Browser } from 'puppeteer'
6
+ import { rasterizeAll, rasterizeAllInBrowser } from './rasterize/puppeteer'
7
+ import type { AndesiteMenuPageDefinition, AndesiteHudPageDefinition, AndesitePageDefinition } from '@taboo-avalanche/andesite'
8
+
9
+ export { launchRasterBrowser } from './rasterize/puppeteer'
10
+
11
+ export interface CompilePageOptions {
12
+ /** 传入则复用浏览器,多页 build-all 时显著提速 */
13
+ browser?: Browser
14
+ verbose?: boolean
15
+ }
16
+
17
+ /**
18
+ * 编译 React 页面为 andesite.zip。
19
+ * 先栅格化所有 APanel 和 AButton 产出 PNG,再从 DOM 读取 ALabel 声明信息,
20
+ * 最后组装 andesite.json 并打包成 zip。
21
+ *
22
+ * @param pageUrl Vite dev server 或静态构建产物的 URL。
23
+ * @param pageId 安山岩页面 id。
24
+ * @param outputZipPath 输出 zip 文件路径。
25
+ */
26
+ export async function compilePage(
27
+ pageUrl: string,
28
+ pageId: string,
29
+ outputZipPath: string,
30
+ options?: CompilePageOptions,
31
+ ): Promise<void> {
32
+ const tempDir = path.join(path.dirname(outputZipPath), `.andesite-temp-${pageId.replace(/[^a-zA-Z0-9_-]/g, '_')}`)
33
+ fs.mkdirSync(tempDir, { recursive: true })
34
+ const rasterOpts = { verbose: options?.verbose }
35
+ const rasterResult = options?.browser
36
+ ? await rasterizeAllInBrowser(options.browser, pageUrl, tempDir, rasterOpts)
37
+ : await rasterizeAll(pageUrl, tempDir, rasterOpts)
38
+ // 从 DOM 读取 ALabel 信息由栅格化器在浏览器内收集,这里用 page.$$ 收集
39
+ // 但由于 rasterizeAll 关闭了浏览器,需要在栅格化阶段收集 label 信息
40
+ // 修正:让 rasterizeAll 同时返回 labels
41
+ const pageControls = {
42
+ viewStacks: rasterResult.viewStacks,
43
+ panels: rasterResult.panels.map(p => ({
44
+ id: p.id,
45
+ mountId: p.mountId,
46
+ texture: p.texturePath,
47
+ x: p.x,
48
+ y: p.y,
49
+ width: p.width,
50
+ height: p.height,
51
+ ...(p.overflow == null ? {} : { overflow: p.overflow }),
52
+ ...(p.defaultVisible == null ? {} : { defaultVisible: p.defaultVisible }),
53
+ })),
54
+ buttons: rasterResult.buttons.map(b => ({
55
+ id: b.id,
56
+ mountId: b.mountId,
57
+ defaultTexture: b.defaultTexturePath,
58
+ pressedTexture: b.pressedTexturePath,
59
+ x: b.x,
60
+ y: b.y,
61
+ width: b.width,
62
+ height: b.height,
63
+ pressDuration: b.pressDuration,
64
+ clickable: b.clickable,
65
+ ...(b.layer == null ? {} : { layer: b.layer }),
66
+ ...(b.defaultVisible == null ? {} : { defaultVisible: b.defaultVisible }),
67
+ labels: b.labels,
68
+ states: b.states,
69
+ })),
70
+ labels: rasterResult.labels,
71
+ inputs: rasterResult.inputs,
72
+ }
73
+ const definition: AndesitePageDefinition = rasterResult.kind === 'hud'
74
+ ? ({
75
+ id: pageId,
76
+ kind: 'hud',
77
+ width: rasterResult.width,
78
+ height: rasterResult.height,
79
+ mounts: rasterResult.mounts,
80
+ ...pageControls,
81
+ slots: [],
82
+ progresses: rasterResult.progresses,
83
+ sprites: rasterResult.sprites,
84
+ scrollViews: rasterResult.scrollViews,
85
+ } satisfies AndesiteHudPageDefinition)
86
+ : ({
87
+ id: pageId,
88
+ rows: 6,
89
+ // 页面整体尺寸与锚点:anchor=center 时运行时按屏幕把整体包围盒居中,解决左上锚定导致的偏移
90
+ width: rasterResult.width,
91
+ height: rasterResult.height,
92
+ anchor: 'center',
93
+ ...pageControls,
94
+ slots: rasterResult.slots,
95
+ progresses: rasterResult.progresses,
96
+ sprites: rasterResult.sprites,
97
+ scrollViews: rasterResult.scrollViews,
98
+ } satisfies AndesiteMenuPageDefinition)
99
+ // 只合并逐字节相同的 PNG,保留控件与帧索引;嵌套滚动区使用同一纹理映射。
100
+ const textures = new Map<string, string>()
101
+ const hashes = new Map<string, string>()
102
+ const pending: object[] = [definition]
103
+ const textureFields = new Set(['texture', 'defaultTexture', 'pressedTexture', 'backgroundTexture'])
104
+ while (pending.length) {
105
+ const item = pending.pop() as Record<string, unknown>
106
+ for (const [key, value] of Object.entries(item)) {
107
+ if (typeof value === 'string' && textureFields.has(key)) {
108
+ let canonical = textures.get(value)
109
+ if (!canonical) {
110
+ const bytes = fs.readFileSync(path.join(tempDir, value))
111
+ const digest = createHash('sha256').update(bytes).digest('hex')
112
+ canonical = hashes.get(digest) ?? value
113
+ hashes.set(digest, canonical)
114
+ textures.set(value, canonical)
115
+ }
116
+ item[key] = canonical
117
+ } else if (value && typeof value === 'object') {
118
+ pending.push(value)
119
+ }
120
+ }
121
+ }
122
+ const jsonPath = path.join(tempDir, 'andesite.json')
123
+ fs.writeFileSync(jsonPath, JSON.stringify(definition, null, 2))
124
+ await packZip(tempDir, outputZipPath, [...hashes.values()])
125
+ fs.rmSync(tempDir, { recursive: true, force: true })
126
+ }
127
+
128
+ async function packZip(sourceDir: string, outputZipPath: string, textures: string[]): Promise<void> {
129
+ return new Promise((resolve, reject) => {
130
+ const output = fs.createWriteStream(outputZipPath)
131
+ const archive = archiver('zip', { zlib: { level: 6 } })
132
+ output.on('close', () => resolve())
133
+ output.on('error', reject)
134
+ archive.on('error', reject)
135
+ archive.pipe(output)
136
+ // 只打包清单引用的唯一纹理,不把临时目录内重复图片带进产物。
137
+ for (const name of ['andesite.json', ...textures]) {
138
+ archive.file(path.join(sourceDir, name), { name })
139
+ }
140
+ archive.finalize()
141
+ })
142
+ }