cardbuddy 0.23.0 → 0.24.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.
@@ -8,8 +8,8 @@
8
8
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16-Cg6Nrosk.png" />
9
9
  <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32-B_1744sp.png" />
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-BQcES9QK.png" />
11
- <script type="module" crossorigin src="/assets/index-C7N5n_qW.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-7RzNd7tf.css">
11
+ <script type="module" crossorigin src="/assets/index-BumNBo-n.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-MWKPP9nd.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="app"></div>
@@ -0,0 +1,202 @@
1
+ /**
2
+ * 卡片编译端点:把本地 schema.json 编译成两套可投放产物
3
+ *
4
+ * POST /__compile-cards body { cardIds: string[], targetDir?: string }
5
+ * → { ok, outDir, cards: [{ id, name, ok, version, templates, nodes, clientBytes, h5Bytes, warnings, error }] }
6
+ *
7
+ * 产物布局(客户端 / H5 分两个目录,同属一个父目录):
8
+ * <targetDir>/ ← 默认 <工程根>/flame-build
9
+ * client/<cardCode>/<templateCode>/index.{json,css,databinding,data,js,style}
10
+ * h5/<cardCode>.js
11
+ * manifest.json ← 目录内产物清单(累计)+ 本次编译批次(lastBatch)
12
+ *
13
+ * 编译逻辑本身在 flameCompile/ 下(1:1 迁移自 flame-release-mange 的 card/exporter),
14
+ * 本文件只负责「读盘 → 编译 → 落盘 → 汇报」,不含任何渲染规则。
15
+ *
16
+ * 覆盖语义:产物是构建缓存,允许直接覆盖——
17
+ * - 逐文件 writeFileSync 截断重写,同卡重复编译结果字节一致;
18
+ * - 写之前先删该卡的整个模板目录,模板数量变少(如删掉列表)时不会残留 _sub_/_item;
19
+ * - 编译失败不删旧产物(先编译成功再落盘),上一次的好产物不会被半途失败搞坏;
20
+ * - manifest 同理不丢信息:按 id 并入已有记录(本次为准),只保留磁盘上仍有产物的卡。
21
+ */
22
+
23
+ import fs from 'node:fs'
24
+ import path from 'node:path'
25
+ import { compileCard } from './flameCompile/index.js'
26
+ import { log } from './logger.js'
27
+
28
+ /** 单次编译上限(与导出的 500 同口径) */
29
+ const MAX_COMPILE_CARDS = 500
30
+
31
+ /** 卡片 id 白名单:只允许字母数字下划线中划线,杜绝目录穿越 */
32
+ const CARD_ID_RE = /^[A-Za-z0-9_-]+$/
33
+
34
+ /**
35
+ * 读已有 manifest 的 cards 段。缺失 / 损坏 / 结构不对一律当空数组 ——
36
+ * 一份旧清单坏掉不该让整批编译失败(它是构建产物,不是输入)。
37
+ */
38
+ function readPrevManifestCards(manifestPath) {
39
+ try {
40
+ const mf = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
41
+ return Array.isArray(mf.cards) ? mf.cards : []
42
+ } catch {
43
+ return []
44
+ }
45
+ }
46
+
47
+ export function createCompileMiddleware({ root }) {
48
+ const readJsonBody = (req, res, cb) => {
49
+ let body = ''
50
+ req.setEncoding('utf8')
51
+ req.on('data', (c) => {
52
+ body += c
53
+ if (body.length > 1024 * 1024) req.destroy()
54
+ })
55
+ req.on('end', () => {
56
+ try {
57
+ cb(JSON.parse(body || '{}'))
58
+ } catch (e) {
59
+ res.statusCode = 400
60
+ res.setHeader('Content-Type', 'application/json')
61
+ res.end(JSON.stringify({ ok: false, error: 'invalid json', message: String(e.message) }))
62
+ }
63
+ })
64
+ }
65
+
66
+ const sendJson = (res, status, obj) => {
67
+ res.statusCode = status
68
+ res.setHeader('Content-Type', 'application/json')
69
+ res.setHeader('Cache-Control', 'no-store')
70
+ res.end(JSON.stringify(obj))
71
+ }
72
+
73
+ return function compileMiddleware(req, res, next) {
74
+ const pathname = decodeURIComponent((req.url || '').split('?')[0])
75
+ const method = (req.method || 'GET').toUpperCase()
76
+ if (pathname !== '/__compile-cards' || method !== 'POST') return next()
77
+
78
+ readJsonBody(req, res, (body) => {
79
+ const rawIds = Array.isArray(body.cardIds)
80
+ ? body.cardIds.filter((x) => typeof x === 'string')
81
+ : []
82
+ // 去重:同一张卡在同一批里传两次没有意义(第二次会把第一次刚写的产物删掉再写一遍),只留首个
83
+ const ids = [...new Set(rawIds)]
84
+ if (!ids.length) return sendJson(res, 400, { ok: false, error: 'cardIds 不能为空' })
85
+ if (ids.length > MAX_COMPILE_CARDS) {
86
+ return sendJson(res, 400, { ok: false, error: `单次最多编译 ${MAX_COMPILE_CARDS} 张卡片` })
87
+ }
88
+
89
+ const bad = ids.filter((id) => !CARD_ID_RE.test(id))
90
+ if (bad.length) return sendJson(res, 400, { ok: false, error: `非法卡片 id: ${bad.join(', ')}` })
91
+
92
+ // 目标目录:默认工程根下 flame-build;自定义时限制在工程根内,避免误写到系统目录
93
+ const outDir = body.targetDir
94
+ ? path.resolve(root, String(body.targetDir))
95
+ : path.join(root, 'flame-build')
96
+ const rootResolved = path.resolve(root)
97
+ if (!outDir.startsWith(rootResolved + path.sep)) {
98
+ return sendJson(res, 400, { ok: false, error: 'targetDir 必须位于工程目录内' })
99
+ }
100
+
101
+ const clientDir = path.join(outDir, 'client')
102
+ const h5Dir = path.join(outDir, 'h5')
103
+ fs.mkdirSync(clientDir, { recursive: true })
104
+ fs.mkdirSync(h5Dir, { recursive: true })
105
+
106
+ const results = []
107
+ const manifestCards = []
108
+
109
+ for (const id of ids) {
110
+ const schemaPath = path.join(root, 'src', 'cards', id, 'schema.json')
111
+ if (!fs.existsSync(schemaPath)) {
112
+ results.push({ id, ok: false, error: 'schema.json 不存在' })
113
+ continue
114
+ }
115
+
116
+ try {
117
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
118
+ const compiled = compileCard(schema, { cardCode: id })
119
+
120
+ // 清掉该卡的旧产物(模板数量可能变化)
121
+ const cardClientDir = path.join(clientDir, id)
122
+ if (fs.existsSync(cardClientDir)) fs.rmSync(cardClientDir, { recursive: true, force: true })
123
+
124
+ for (const template of compiled.client) {
125
+ const dir = path.join(cardClientDir, template.code)
126
+ fs.mkdirSync(dir, { recursive: true })
127
+ for (const [file, content] of Object.entries(template.files)) {
128
+ // index.style 是 SM4 密文(Buffer),必须按字节原样落盘;
129
+ // 其余是 utf8 文本,显式声明编码便于排查乱码。
130
+ if (Buffer.isBuffer(content)) fs.writeFileSync(path.join(dir, file), content)
131
+ else fs.writeFileSync(path.join(dir, file), content, 'utf8')
132
+ }
133
+ }
134
+
135
+ fs.writeFileSync(path.join(h5Dir, `${id}.js`), compiled.h5, 'utf8')
136
+
137
+ results.push({
138
+ id,
139
+ ok: true,
140
+ version: compiled.version,
141
+ templates: compiled.client.length,
142
+ nodes: compiled.stats.nodes,
143
+ clientBytes: compiled.stats.clientBytes,
144
+ h5Bytes: compiled.stats.h5Bytes,
145
+ warnings: compiled.warnings,
146
+ })
147
+ manifestCards.push({
148
+ id,
149
+ name: schema.name || '',
150
+ version: compiled.version,
151
+ templates: compiled.client.map((t) => t.code),
152
+ nodes: compiled.stats.nodes,
153
+ clientBytes: compiled.stats.clientBytes,
154
+ h5Bytes: compiled.stats.h5Bytes,
155
+ warnings: compiled.warnings,
156
+ })
157
+ } catch (e) {
158
+ log.warn(`[compile] ${id} 编译失败: ${e.message}`)
159
+ results.push({ id, ok: false, error: String(e.message || e) })
160
+ }
161
+ }
162
+
163
+ const okCount = results.filter((r) => r.ok).length
164
+ // manifest 描述的是「这个目录里有什么」,因此跨批次累计:按 id 合并,本次记录覆盖同 id 旧记录。
165
+ // 只按本次批次重写会得到一份自相矛盾的清单——磁盘上 10 张卡,manifest 里只有最后勾的 2 张。
166
+ // 旧记录还要过一道「磁盘上仍有产物」的存在性检查:换过 targetDir、手工删过文件时它会失真。
167
+ const manifestPath = path.join(outDir, 'manifest.json')
168
+ const merged = new Map()
169
+ for (const c of readPrevManifestCards(manifestPath)) {
170
+ if (!c || typeof c.id !== 'string' || !c.id) continue
171
+ const hasArtifacts =
172
+ fs.existsSync(path.join(clientDir, c.id)) || fs.existsSync(path.join(h5Dir, `${c.id}.js`))
173
+ if (hasArtifacts) merged.set(c.id, c)
174
+ }
175
+ for (const c of manifestCards) merged.set(c.id, c)
176
+ const manifest = {
177
+ generatedAt: new Date().toISOString(),
178
+ outDir,
179
+ // 目录内卡片总数(累计),不是本批张数
180
+ count: merged.size,
181
+ cards: [...merged.values()].sort((a, b) => String(a.id).localeCompare(String(b.id))),
182
+ // 本次请求的批次结果,便于回溯「这一次又编了什么」
183
+ lastBatch: {
184
+ requested: ids.length,
185
+ ok: okCount,
186
+ failed: ids.length - okCount,
187
+ ids,
188
+ },
189
+ }
190
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
191
+
192
+ log.info(`[compile] 编译完成 ${okCount}/${ids.length} → ${outDir}`)
193
+ return sendJson(res, 200, {
194
+ ok: okCount > 0,
195
+ outDir,
196
+ clientDir,
197
+ h5Dir,
198
+ cards: results,
199
+ })
200
+ })
201
+ }
202
+ }
@@ -0,0 +1,466 @@
1
+ /**
2
+ * 客户端产物编译(GaiaX 明文模板包)
3
+ *
4
+ * 1:1 迁移自 flame-release-mange(Java)的
5
+ * client/NativeEngine.java
6
+ * client/convert/{CardConvert,ComponentConvert,StyleConvert,PropertyConvert,
7
+ * EventActionConvert,DirectiveConvert,DataConvert,ConditionConvert}
8
+ * client/entity/*(TemplateEntity / JsonFileEntity / CssFileEntity / DataBindingFileEntity ...)
9
+ *
10
+ * 产物(每个模板一个目录):
11
+ * index.json 视图层级 { id, type:'gaia-template', sub-type?, package, layers }
12
+ * index.css `#nodeId { prop:value; }`
13
+ * index.databinding { data:{ nodeId:{...} }, event:{ nodeId:{...} } }
14
+ * index.data 卡片入参默认值(仅主模板)
15
+ * index.js Component({ onShow/onReady/... })
16
+ * index.style index.js 的 SM4-CBC 密文(iOS 侧优先读它,见 GXTemplateReader)
17
+ *
18
+ * 列表类组件(ListLayout / GridLayout)会拆成三级模板:
19
+ * <cardCode> ← 外层占位(grid 节点 + 属性绑定)
20
+ * <cardCode>_sub_<cid> ← 真正的 grid 模板(含 column 等)
21
+ * <cardCode>_sub_<cid>_item ← 循环项模板(子组件挂这里)
22
+ * 该结构由平台侧历史产物固化,客户端只认这套约定,不要简化成单层。
23
+ */
24
+
25
+ import { elementOf, styleConst, styleVar } from './clientElements.js'
26
+ import { convertCondition, convertPropsClient, convertExpression, hasText } from './common.js'
27
+
28
+ /* ------------------------------- 文件实体 ------------------------------- */
29
+
30
+ function createTemplate(code) {
31
+ return {
32
+ code,
33
+ cssRules: [], // [{ name, contents: [] }]
34
+ dataProps: {}, // index.data
35
+ dataBind: { data: {}, event: {} }, // index.databinding
36
+ js: { onShow: [], onDataInit: [], onReady: [], onHide: [], onReuse: [], onDestroy: [] },
37
+ json: { code: '', type: '', subType: '', packageInfo: {}, other: {}, layers: [] },
38
+ }
39
+ }
40
+
41
+ function addDataBind(template, componentId, map) {
42
+ const bucket = template.dataBind.data
43
+ bucket[componentId] = Object.assign(bucket[componentId] || {}, map)
44
+ }
45
+
46
+ function ensureDataBind(template, componentId) {
47
+ const bucket = template.dataBind.data
48
+ if (!bucket[componentId]) bucket[componentId] = {}
49
+ return bucket[componentId]
50
+ }
51
+
52
+ function addEventBind(template, componentId, map) {
53
+ const bucket = template.dataBind.event
54
+ bucket[componentId] = Object.assign(bucket[componentId] || {}, map)
55
+ }
56
+
57
+ const LIFECYCLES = ['onShow', 'onDataInit', 'onReady', 'onHide', 'onReuse', 'onDestroy']
58
+
59
+ function addLifecycleCode(template, lifeCycle, code) {
60
+ if (LIFECYCLES.includes(lifeCycle)) template.js[lifeCycle].push(code)
61
+ }
62
+
63
+ /* ------------------------------- 属性转换 ------------------------------- */
64
+
65
+ /** 常量域:标量 JSON 编码,Map/List 递归(与 Java convertConstField 等价) */
66
+ function normalizeConst(value) {
67
+ if (value === null || value === undefined) return undefined
68
+ if (Array.isArray(value)) return value.map((item) => (item && typeof item === 'object' ? normalizeConstMap(item) : item))
69
+ if (typeof value === 'object') return normalizeConstMap(value)
70
+ return JSON.stringify(value)
71
+ }
72
+
73
+ function normalizeConstMap(map) {
74
+ const out = {}
75
+ for (const [k, v] of Object.entries(map)) {
76
+ const normalized = normalizeConst(v)
77
+ if (normalized !== undefined) out[k] = normalized
78
+ }
79
+ return out
80
+ }
81
+
82
+ /**
83
+ * 变量域合并:字符串非空才写入(表达式为空表示「沿用静态值」),
84
+ * 对象/数组递归合并,保证同一节点既有静态也有动态属性时不互相覆盖。
85
+ */
86
+ function mergeVarField(target, key, value) {
87
+ if (value === null || value === undefined) return
88
+ if (Array.isArray(value)) {
89
+ const list = Array.isArray(target[key]) ? target[key] : []
90
+ target[key] = list
91
+ value.forEach((item, index) => {
92
+ if (!list[index]) list[index] = {}
93
+ if (item && typeof item === 'object') {
94
+ for (const [k, v] of Object.entries(item)) mergeVarField(list[index], k, v)
95
+ }
96
+ })
97
+ return
98
+ }
99
+ if (typeof value === 'object') {
100
+ const bucket = target[key] && typeof target[key] === 'object' ? target[key] : {}
101
+ target[key] = bucket
102
+ for (const [k, v] of Object.entries(value)) mergeVarField(bucket, k, v)
103
+ return
104
+ }
105
+ if (typeof value === 'string') {
106
+ if (hasText(value)) target[key] = value
107
+ return
108
+ }
109
+ target[key] = value
110
+ }
111
+
112
+ /** 组件属性 → databinding(静态走 const 归一化,动态覆盖) */
113
+ function convertComponentProps(component, template) {
114
+ const { const: constProps, var: varProps } = convertPropsClient(component.properties)
115
+ const out = normalizeConstMap(constProps)
116
+
117
+ for (const [key, value] of Object.entries(varProps)) mergeVarField(out, key, value)
118
+
119
+ addDataBind(template, component.componentId, out)
120
+ return { constProps, varProps, merged: out }
121
+ }
122
+
123
+ /* ------------------------------- 样式转换 ------------------------------- */
124
+
125
+ function convertStyle(className, style, template, componentId) {
126
+ const { const: constProps, var: varProps } = convertPropsClient(style.properties)
127
+
128
+ template.cssRules.push({ name: `#${componentId}`, contents: styleConst(className, constProps) })
129
+
130
+ const dynamic = {}
131
+ const vars = styleVar(varProps)
132
+ for (const [k, v] of Object.entries(vars)) {
133
+ if (hasText(v)) dynamic[k] = v
134
+ }
135
+ addDataBind(template, componentId, { extend: dynamic })
136
+ }
137
+
138
+ /* ------------------------------- 事件转换 ------------------------------- */
139
+
140
+ function convertEventBody(eventAction, component, engine, warnings) {
141
+ const actionType = eventAction.actionType
142
+ const { const: constProps } = convertPropsClient(eventAction.properties)
143
+ const desc = component ? elementOf(component.className) : null
144
+ let out = ''
145
+
146
+ // Canvas 的 onMeasured 前置:把画布尺寸换算进缩放坐标系
147
+ if (
148
+ component &&
149
+ desc &&
150
+ desc.type === 'canvas' &&
151
+ eventAction.eventKey === 'onMeasured' &&
152
+ actionType === 'customAction'
153
+ ) {
154
+ out +=
155
+ 'event.canvas = new Canvas(event.canvasId);' +
156
+ 'event.canvas.zoom = (event.screenWidth || 375)/375;' +
157
+ 'event.canvas.width = event.width/event.canvas.zoom;' +
158
+ 'event.canvas.height = event.height/event.canvas.zoom;' +
159
+ 'event.width = event.canvas.width;' +
160
+ 'event.height = event.canvas.height;'
161
+ }
162
+
163
+ if (actionType === 'customAction') {
164
+ const fn = constProps.function
165
+ const code = fn && typeof fn === 'object' ? fn.code : null
166
+ if (typeof code === 'string') out += code
167
+ else warnings.push(`事件 ${eventAction.eventKey} 的 customAction 缺少 function.code,已跳过`)
168
+ } else if (actionType === 'closeAlertCard') {
169
+ out += `ynet.callJSAPI('${actionType}',${JSON.stringify({ cardCode: engine.code })});`
170
+ } else if (hasText(actionType)) {
171
+ out += `ynet.callJSAPI('${actionType}',${JSON.stringify(constProps)});`
172
+ }
173
+
174
+ if (
175
+ component &&
176
+ desc &&
177
+ desc.type === 'canvas' &&
178
+ eventAction.eventKey === 'onMeasured' &&
179
+ actionType === 'customAction'
180
+ ) {
181
+ out += "\nevent.canvas.getContext('2d').endDraw();"
182
+ }
183
+
184
+ return out
185
+ }
186
+
187
+ function convertEventAction(eventAction, template, engine, component, warnings) {
188
+ if (!hasText(eventAction.eventKey) || !hasText(eventAction.actionType)) return
189
+
190
+ if (!component) {
191
+ // 卡片生命周期:包一层箭头函数隔离作用域,再在生命周期里立即执行
192
+ const body = convertEventBody(eventAction, null, engine, warnings)
193
+ addLifecycleCode(template, eventAction.eventKey, `((event) => {${body}\n})(event);`)
194
+ return
195
+ }
196
+
197
+ const cid = component.componentId
198
+ const body = convertEventBody(eventAction, component, engine, warnings)
199
+ const code =
200
+ `let ${cid}=this.getElementById('${cid}');` +
201
+ `if(${cid}){` +
202
+ `${cid}.addEventListener('${eventAction.eventKey}',(event)=>{${body}\n})` +
203
+ `}`
204
+ template.js.onReady.push(code)
205
+ }
206
+
207
+ /** 双向绑定:value 绑定到变量时补 input 监听,把输入写回状态 */
208
+ function convertTwoWayBinding(component, varProps, template) {
209
+ const desc = elementOf(component.className)
210
+ if (!desc.twoWayBinding) return
211
+ const modelVar = varProps.value
212
+ if (!hasText(modelVar)) return
213
+ const cid = component.componentId
214
+ template.js.onReady.push(
215
+ `let _${cid}_=this.getElementById('${cid}');` +
216
+ `if(_${cid}_){` +
217
+ `_${cid}_.addEventListener('input',(event)=>{` +
218
+ `this._syncData_(${JSON.stringify(modelVar)}, event)` +
219
+ `})` +
220
+ `}`
221
+ )
222
+ }
223
+
224
+ /* ------------------------------- 指令转换 ------------------------------- */
225
+
226
+ function convertDirectives(component, template, warnings) {
227
+ const { const: constProps } = convertPropsClient(component.directives)
228
+ const showProps = constProps['v-show']
229
+ if (!showProps || typeof showProps !== 'object') return
230
+
231
+ const showType = showProps.showType
232
+ if (showType === '00') return
233
+
234
+ const conditionMap = showProps.condition
235
+ if (!conditionMap || typeof conditionMap !== 'object') return
236
+
237
+ const bucket = ensureDataBind(template, component.componentId)
238
+ const extend = bucket.extend && typeof bucket.extend === 'object' ? bucket.extend : {}
239
+ bucket.extend = extend
240
+
241
+ const expression = conditionMap.expression
242
+ if (hasText(expression)) {
243
+ if (showType === '01') extend.display = `(${expression}) ? 'block' : 'none'`
244
+ else if (showType === '02') extend.display = `(${expression}) ? 'none' : 'block'`
245
+ return
246
+ }
247
+
248
+ if (Array.isArray(conditionMap.group) && conditionMap.group.length > 0) {
249
+ const expr = convertCondition(conditionMap.group)
250
+ if (showType === '01') extend.display = `${expr} ? 'block' : 'none'`
251
+ else if (showType === '02') extend.display = `${expr} ? 'none' : 'block'`
252
+ return
253
+ }
254
+
255
+ warnings.push(`组件 ${component.componentId} 的 v-show 条件为空,已忽略`)
256
+ }
257
+
258
+ /* -------------------------------- 引擎 --------------------------------- */
259
+
260
+ export function createClientCompiler(card, { version = '', warnings = [] } = {}) {
261
+ const engine = { code: card.code, version, templates: [] }
262
+
263
+ const newTemplate = () => {
264
+ const t = createTemplate('')
265
+ engine.templates.push(t)
266
+ return t
267
+ }
268
+
269
+ const convertBaseInfo = (code, subType, template, width) => {
270
+ template.json.code = code
271
+ template.json.type = 'gaia-template'
272
+ if (hasText(subType)) template.json.subType = subType
273
+ template.json.packageInfo = {
274
+ id: code,
275
+ version: engine.version,
276
+ dependencies: {},
277
+ }
278
+ if (width !== undefined) template.json.packageInfo.width = width
279
+ }
280
+
281
+ /* ---------------------------- 组件递归 ---------------------------- */
282
+
283
+ const convertArray = (components, template, layers) => {
284
+ for (const component of components) convert(component, template, layers)
285
+ }
286
+
287
+ const convert = (component, template, layers) => {
288
+ const desc = elementOf(component.className)
289
+ if (desc.unknown) warnings.push(`未识别的组件类型 ${component.className}(${component.componentId}),按 view 输出`)
290
+
291
+ if (desc.isNewTemplate) {
292
+ createComponentTemplate(component, template, layers, desc)
293
+ return
294
+ }
295
+ convertComponent(component, template, layers, desc)
296
+ }
297
+
298
+ const convertComponent = (component, template, layers, desc) => {
299
+ const layer = { id: component.componentId, type: desc.type, layers: [] }
300
+ layers.push(layer)
301
+
302
+ convertStyle(component.className, component.style, template, component.componentId)
303
+
304
+ const { varProps } = convertComponentProps(component, template)
305
+ convertTwoWayBinding(component, varProps, template)
306
+
307
+ for (const ea of component.eventactions) convertEventAction(ea, template, engine, component, warnings)
308
+
309
+ convertDirectives(component, template, warnings)
310
+
311
+ const slot = component.slots && component.slots[0]
312
+ if (slot) convertArray(slot.components, template, layer.layers)
313
+ }
314
+
315
+ /**
316
+ * 列表 / 网格:外层占位 + 子模板 + 循环项模板(三级)
317
+ * 注意:平台侧会把 componentId 改写成子模板编码,后续样式与绑定的 key 都用新编码。
318
+ */
319
+ const createComponentTemplate = (component, template, layers, desc) => {
320
+ const newTemplateCode = `${engine.code}_sub_${component.componentId}`
321
+ component.componentId = newTemplateCode
322
+
323
+ layers.push({ id: newTemplateCode, type: 'gaia-template', subType: 'custom', layers: [] })
324
+
325
+ convertComponentProps(component, template)
326
+ template.json.packageInfo.dependencies[newTemplateCode] = engine.version
327
+
328
+ const componentTemplate = newTemplate()
329
+ componentTemplate.code = newTemplateCode
330
+ addDataBind(componentTemplate, newTemplateCode, { value: '$$' })
331
+ convertBaseInfo(newTemplateCode, desc.type, componentTemplate)
332
+
333
+ convertTemplateComponent(component, componentTemplate, desc)
334
+ }
335
+
336
+ const convertTemplateComponent = (component, template, desc) => {
337
+ convertStyle(component.className, component.style, template, component.componentId)
338
+
339
+ const { const: constProps } = convertPropsClient(component.properties)
340
+ const constTransferred = desc.newTemplatePropsConst
341
+ ? desc.newTemplatePropsConst(constProps)
342
+ : constProps
343
+ for (const [k, v] of Object.entries(constTransferred)) template.json.other[k] = v
344
+ // 循环项数据由 databinding 的 `$$` 提供,模板顶层不保留 value
345
+ delete template.json.other.value
346
+
347
+ for (const ea of component.eventactions) convertEventAction(ea, template, engine, component, warnings)
348
+ convertDirectives(component, template, warnings)
349
+
350
+ const itemTemplateCode = `${component.componentId}_item`
351
+ template.json.layers.push({ id: itemTemplateCode, type: 'gaia-template', subType: 'custom', layers: [] })
352
+ template.json.packageInfo.dependencies[itemTemplateCode] = engine.version
353
+
354
+ const itemTemplate = newTemplate()
355
+ itemTemplate.code = itemTemplateCode
356
+ convertBaseInfo(itemTemplateCode, '', itemTemplate)
357
+
358
+ // 循环项样式:宽度由布局方向决定(横向列表 auto,其余撑满)
359
+ // 注意归属 —— 平台侧 StyleConvert.convertItem 收的是 itemComponentTemplate,
360
+ // 规则落在**第三层(_item)模板自己的 index.css** 里,不能挂到第二层。
361
+ if (desc.itemStyleConst) {
362
+ const contents = desc.itemStyleConst(constProps)
363
+ if (contents && contents.length) {
364
+ itemTemplate.cssRules.push({ name: `#${itemTemplateCode}`, contents })
365
+ }
366
+ }
367
+
368
+ const slot = component.slots && component.slots[0]
369
+ if (slot) convertArray(slot.components, itemTemplate, itemTemplate.json.layers)
370
+ }
371
+
372
+ /* ------------------------------ 卡片 ------------------------------ */
373
+
374
+ const convertCard = () => {
375
+ const template = newTemplate()
376
+
377
+ template.code = card.code
378
+ template.json.code = card.code
379
+ template.json.type = 'gaia-template'
380
+ template.json.packageInfo = {
381
+ id: card.code,
382
+ version: engine.version,
383
+ dependencies: {},
384
+ params: [],
385
+ }
386
+ if (card.width) template.json.packageInfo.width = card.width
387
+
388
+ // 数据定义:入参树进 package.params,默认值进 index.data
389
+ template.json.packageInfo.params = card.data.props.map(toParamField)
390
+ for (const field of card.data.props) {
391
+ template.dataProps[field.key] = field.defaultValue
392
+ }
393
+
394
+ convertStyle(card.className, card.style, template, card.code)
395
+
396
+ for (const ea of card.eventactions) convertEventAction(ea, template, engine, null, warnings)
397
+
398
+ convertArray(card.components, template, template.json.layers)
399
+ }
400
+
401
+ convertCard()
402
+ return engine
403
+ }
404
+
405
+ function toParamField(field) {
406
+ return {
407
+ name: field.key,
408
+ remark: field.name,
409
+ type: field.type,
410
+ child: (field.children || []).map(toParamField),
411
+ }
412
+ }
413
+
414
+ /* ------------------------------ 文件序列化 ------------------------------ */
415
+
416
+ export function serializeTemplateJson(template) {
417
+ const out = { ...template.json.other }
418
+ out.id = template.json.code
419
+ out.type = template.json.type
420
+ if (hasText(template.json.subType)) out['sub-type'] = template.json.subType
421
+ out.package = template.json.packageInfo
422
+ out.layers = serializeLayers(template.json.layers)
423
+ // 紧凑输出:平台侧走 fastjson 的 JSON.toJSONString,同样无缩进。
424
+ // 若这里加缩进,产物虽语义相同但字节全行不同,与线上产物对拍就失去意义。
425
+ return JSON.stringify(out)
426
+ }
427
+
428
+ function serializeLayers(layers) {
429
+ return layers.map((layer) => {
430
+ const out = { id: layer.id, type: layer.type }
431
+ if (hasText(layer.subType)) out['sub-type'] = layer.subType
432
+ if (layer.layers && layer.layers.length) out.layers = serializeLayers(layer.layers)
433
+ return out
434
+ })
435
+ }
436
+
437
+ export function serializeTemplateCss(template) {
438
+ return template.cssRules
439
+ .map((rule) => {
440
+ const body = rule.contents.map((line) => `${line};\n`).join('')
441
+ return `${rule.name}{\n${body}}\n`
442
+ })
443
+ .join('')
444
+ }
445
+
446
+ export function serializeTemplateDataBinding(template) {
447
+ const result = {}
448
+ if (Object.keys(template.dataBind.data).length) result.data = template.dataBind.data
449
+ if (Object.keys(template.dataBind.event).length) result.event = template.dataBind.event
450
+ return JSON.stringify(result)
451
+ }
452
+
453
+ export function serializeTemplateData(template) {
454
+ return JSON.stringify(template.dataProps)
455
+ }
456
+
457
+ export function serializeTemplateJs(template) {
458
+ let out = 'Component({'
459
+ for (const lifeCycle of LIFECYCLES) {
460
+ const codes = template.js[lifeCycle]
461
+ if (!codes.length) continue
462
+ out += `${lifeCycle}: function(event){${codes.join('')}},`
463
+ }
464
+ out += '});'
465
+ return out
466
+ }