@fugood/bricks-ctor 2.25.0-beta.49 → 2.25.0-beta.50
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.
|
@@ -171,6 +171,40 @@ describe('compile animations', () => {
|
|
|
171
171
|
})
|
|
172
172
|
})
|
|
173
173
|
|
|
174
|
+
describe('compile event handlers', () => {
|
|
175
|
+
const BRICK_ID = 'BRICK_00000000-0000-0000-0000-000000000002'
|
|
176
|
+
|
|
177
|
+
test('preserves item-brick string handler casing and normalizes only system', async () => {
|
|
178
|
+
// A mixed-case ItemBrickID handler — must survive compile verbatim because the
|
|
179
|
+
// runtime resolves handlers case-sensitively (mapEventMapHandlersWithNewId).
|
|
180
|
+
const itemHandlerId = 'itemBrickHandlerId'
|
|
181
|
+
const app = makeApp()
|
|
182
|
+
app.rootSubspace.bricks = [
|
|
183
|
+
{
|
|
184
|
+
__typename: 'Brick',
|
|
185
|
+
id: BRICK_ID,
|
|
186
|
+
templateKey: 'BRICK_VIEW',
|
|
187
|
+
property: {},
|
|
188
|
+
events: {
|
|
189
|
+
onPress: [
|
|
190
|
+
{ handler: itemHandlerId, action: { __actionName: 'SCROLL_TO_INDEX' } },
|
|
191
|
+
{ handler: 'system', action: { __actionName: 'NAVIGATE' } },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
const config = await compile(app)
|
|
198
|
+
const eventMap = config.subspace_map[SUBSPACE_ID].brick_map[BRICK_ID].event_map
|
|
199
|
+
const events = eventMap.BRICK_VIEW_ON_PRESS
|
|
200
|
+
|
|
201
|
+
// ItemBrickID handler kept verbatim (was wrongly uppercased to ITEMBRICKHANDLERID).
|
|
202
|
+
expect(events[0].handler).toBe(itemHandlerId)
|
|
203
|
+
// The literal 'system' handler still normalizes to SYSTEM.
|
|
204
|
+
expect(events[1].handler).toBe('SYSTEM')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
174
208
|
describe('compile data remote update', () => {
|
|
175
209
|
test.each([
|
|
176
210
|
[undefined, { bank_type: 'none' }],
|
|
@@ -200,6 +234,39 @@ describe('compile data remote update', () => {
|
|
|
200
234
|
})
|
|
201
235
|
})
|
|
202
236
|
|
|
237
|
+
describe('compile asset preload normalization', () => {
|
|
238
|
+
const makeAssetData = (id, kindType) => ({
|
|
239
|
+
__typename: 'Data',
|
|
240
|
+
id,
|
|
241
|
+
type: 'string',
|
|
242
|
+
kind: {
|
|
243
|
+
type: kindType,
|
|
244
|
+
preload: { type: 'url', hashType: 'sha256', hash: 'abc123' },
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('rive-file-uri normalizes preload like lottie-file-uri (hash re-keyed by hashType)', async () => {
|
|
249
|
+
const RIVE_ID = 'PROPERTY_BANK_DATA_NODE_00000000-0000-0000-0000-0000000000a1'
|
|
250
|
+
const LOTTIE_ID = 'PROPERTY_BANK_DATA_NODE_00000000-0000-0000-0000-0000000000a2'
|
|
251
|
+
const config = await compile(
|
|
252
|
+
makeApp(
|
|
253
|
+
[],
|
|
254
|
+
[makeAssetData(RIVE_ID, 'rive-file-uri'), makeAssetData(LOTTIE_ID, 'lottie-file-uri')],
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const rive = config.subspace_map[SUBSPACE_ID].property_bank_map[RIVE_ID]
|
|
259
|
+
const lottie = config.subspace_map[SUBSPACE_ID].property_bank_map[LOTTIE_ID]
|
|
260
|
+
|
|
261
|
+
// Rive must get the same normalized preload as every sibling asset kind: the hash
|
|
262
|
+
// re-keyed under its hashType (the runtime reads preload[hashType]), not left raw under
|
|
263
|
+
// `hash`. Regression: rive was missing from preloadTypes, so it fell to the raw else
|
|
264
|
+
// branch and the runtime preload[hashType] lookup missed.
|
|
265
|
+
expect(rive.preload).toEqual({ type: 'url', hashType: 'sha256', sha256: 'abc123' })
|
|
266
|
+
expect(rive.preload).toEqual(lottie.preload)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
203
270
|
describe('compile config-change audit log', () => {
|
|
204
271
|
const readAudit = async (projectDir) =>
|
|
205
272
|
(await readFile(path.join(projectDir, '.bricks/edits.jsonl'), 'utf8'))
|
|
@@ -263,3 +330,36 @@ describe('compile config-change audit log', () => {
|
|
|
263
330
|
}
|
|
264
331
|
})
|
|
265
332
|
})
|
|
333
|
+
|
|
334
|
+
describe('compile linked-module subspace', () => {
|
|
335
|
+
const LINKED_SUBSPACE_ID = 'SUBSPACE_00000000-0000-0000-0000-000000000002'
|
|
336
|
+
const makeLinkedApp = () => {
|
|
337
|
+
const app = makeApp()
|
|
338
|
+
app.subspaces = [
|
|
339
|
+
app.rootSubspace,
|
|
340
|
+
{
|
|
341
|
+
__typename: 'Subspace',
|
|
342
|
+
id: LINKED_SUBSPACE_ID,
|
|
343
|
+
title: 'Linked Module',
|
|
344
|
+
layout: { width: 96, height: 54 },
|
|
345
|
+
module: { link: true, id: 'MODULE_00000000-0000-0000-0000-000000000001', version: 1 },
|
|
346
|
+
},
|
|
347
|
+
]
|
|
348
|
+
return app
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
test('uses a deterministic placeholder canvas id stable across recompiles', async () => {
|
|
352
|
+
// Regression: the placeholder canvas id came from makeId('canvas'), whose count-fallback
|
|
353
|
+
// branch uses a never-reset process-global counter, so recompiling identical source produced
|
|
354
|
+
// a different id and broke compile's byte-stable-output contract (phantom config-change ops).
|
|
355
|
+
const first = await compile(makeLinkedApp())
|
|
356
|
+
const second = await compile(makeLinkedApp())
|
|
357
|
+
|
|
358
|
+
const firstKeys = Object.keys(first.subspace_map[LINKED_SUBSPACE_ID].canvas_map)
|
|
359
|
+
const secondKeys = Object.keys(second.subspace_map[LINKED_SUBSPACE_ID].canvas_map)
|
|
360
|
+
expect(firstKeys).toHaveLength(1)
|
|
361
|
+
expect(secondKeys).toEqual(firstKeys)
|
|
362
|
+
expect(first.subspace_map[LINKED_SUBSPACE_ID].root_canvas_id).toBe(firstKeys[0])
|
|
363
|
+
expect(second.subspace_map[LINKED_SUBSPACE_ID].root_canvas_id).toBe(firstKeys[0])
|
|
364
|
+
})
|
|
365
|
+
})
|
package/compile/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import omit from 'lodash/omit'
|
|
|
6
6
|
import { parse as parseAST } from 'acorn'
|
|
7
7
|
import type { ExportNamedDeclaration, FunctionDeclaration } from 'acorn'
|
|
8
8
|
import escodegen from 'escodegen'
|
|
9
|
-
import {
|
|
9
|
+
import { makeSeededId } from '../utils/id'
|
|
10
10
|
import { generateCalulationMap } from './util'
|
|
11
11
|
import { templateActionNameMap } from './action-name-map'
|
|
12
12
|
import { templateEventPropsMap } from '../utils/event-props'
|
|
@@ -185,10 +185,17 @@ const compileEvents = (
|
|
|
185
185
|
|
|
186
186
|
let handlerKey
|
|
187
187
|
let handlerTemplateKey
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
if (typeof handler === 'string') {
|
|
189
|
+
// Only the literal 'system' handler is normalized to the SYSTEM template key.
|
|
190
|
+
// SubspaceID (SUBSPACE_*) and ItemBrickID handlers are kept verbatim: the runtime
|
|
191
|
+
// resolves them case-sensitively (see mapEventMapHandlersWithNewId), so uppercasing
|
|
192
|
+
// a mixed-case ItemBrickID would break handler-to-item event wiring.
|
|
193
|
+
if (handler === 'system') {
|
|
194
|
+
handlerKey = 'SYSTEM'
|
|
195
|
+
handlerTemplateKey = 'SYSTEM'
|
|
196
|
+
} else {
|
|
197
|
+
handlerKey = handler
|
|
198
|
+
}
|
|
192
199
|
} else if (typeof handler === 'function') {
|
|
193
200
|
let instance = handler()
|
|
194
201
|
if (instance?.id) {
|
|
@@ -486,6 +493,7 @@ const preloadTypes = [
|
|
|
486
493
|
'media-resource-audio',
|
|
487
494
|
'media-resource-file',
|
|
488
495
|
'lottie-file-uri',
|
|
496
|
+
'rive-file-uri',
|
|
489
497
|
'ggml-model-asset',
|
|
490
498
|
'gguf-model-asset',
|
|
491
499
|
'binary-asset',
|
|
@@ -755,7 +763,14 @@ export const compile = async (app: Application) => {
|
|
|
755
763
|
// validation (root_canvas_id is required before the conditional
|
|
756
764
|
// schema fix is published).
|
|
757
765
|
if (subspace.module?.link) {
|
|
758
|
-
|
|
766
|
+
// Seed the placeholder id from the (stable) subspace id. `makeId('canvas')` would take
|
|
767
|
+
// the count-fallback branch (a process-global counter that is never reset), so the
|
|
768
|
+
// placeholder id depended on how many prior count-fallback ids had been minted — making
|
|
769
|
+
// it differ between recompiles and breaking compile's byte-stable-output contract
|
|
770
|
+
// (phantom config-change ops). `makeSeededId` keeps no global state, so identical source
|
|
771
|
+
// recompiles to an identical id. (`makeId('canvas', alias)` would instead throw
|
|
772
|
+
// "Duplicate makeId alias" on the second compile in a long-lived process.)
|
|
773
|
+
const placeholderCanvasId = makeSeededId('canvas', `${subspaceId}:module-placeholder`)
|
|
759
774
|
subspaceMap[subspaceId] = {
|
|
760
775
|
title: subspace.title,
|
|
761
776
|
description: subspace.description,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.25.0-beta.
|
|
3
|
+
"version": "2.25.0-beta.50",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@babel/parser": "7.28.5",
|
|
12
12
|
"@babel/traverse": "7.28.5",
|
|
13
13
|
"@babel/types": "7.28.5",
|
|
14
|
-
"@fugood/bricks-cli": "^2.25.0-beta.
|
|
14
|
+
"@fugood/bricks-cli": "^2.25.0-beta.50",
|
|
15
15
|
"@huggingface/gguf": "^0.3.2",
|
|
16
16
|
"@iarna/toml": "^3.0.0",
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"oxfmt": "^0.36.0"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "3614445cd0166e40634f69c6238299bb63f4f597"
|
|
33
33
|
}
|
|
@@ -229,6 +229,36 @@ describe('ctor MCP entry-editing tools', () => {
|
|
|
229
229
|
expect(gitignore).toContain('.bricks/edits.jsonl')
|
|
230
230
|
})
|
|
231
231
|
|
|
232
|
+
test('edit_entry removes the correct elements when unsetting multiple array indices', async () => {
|
|
233
|
+
// Regression: unset paths were applied in input order, but unsetPathValue
|
|
234
|
+
// splices arrays in place — removing items[1] shifts items[2] down, so the
|
|
235
|
+
// second unset hit the wrong element (or no-op'd). Both requested elements
|
|
236
|
+
// must be removed regardless of order.
|
|
237
|
+
projectDir = await writeFixtureProject()
|
|
238
|
+
const target = { file: 'subspaces/subspace-0/bricks.ts', entry: 'bButton', verify: false }
|
|
239
|
+
|
|
240
|
+
const built = await __test__.editEntry(projectDir, {
|
|
241
|
+
...target,
|
|
242
|
+
set: {
|
|
243
|
+
'property.list[0]': 'ITEM_A',
|
|
244
|
+
'property.list[1]': 'ITEM_B',
|
|
245
|
+
'property.list[2]': 'ITEM_C',
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
expect(built.outcome).toBe('ok')
|
|
249
|
+
|
|
250
|
+
const unset = await __test__.editEntry(projectDir, {
|
|
251
|
+
...target,
|
|
252
|
+
unset: ['property.list[1]', 'property.list[2]'],
|
|
253
|
+
})
|
|
254
|
+
expect(unset.outcome).toBe('ok')
|
|
255
|
+
|
|
256
|
+
const source = await readProjectFile(projectDir, 'subspaces/subspace-0/bricks.ts')
|
|
257
|
+
expect(source).toContain('ITEM_A')
|
|
258
|
+
expect(source).not.toContain('ITEM_B') // items[1] removed
|
|
259
|
+
expect(source).not.toContain('ITEM_C') // items[2] removed (survived before the fix)
|
|
260
|
+
})
|
|
261
|
+
|
|
232
262
|
test('edit_events adds a typed system event action', async () => {
|
|
233
263
|
projectDir = await writeFixtureProject()
|
|
234
264
|
|
|
@@ -924,7 +924,20 @@ const editEntry = async (projectDir: string, input: any) =>
|
|
|
924
924
|
for (const [pathValue, value] of Object.entries(input.set || {})) {
|
|
925
925
|
await setPathValue(targetObject, pathValue, value, ctx)
|
|
926
926
|
}
|
|
927
|
-
|
|
927
|
+
// Apply unset paths highest array-index first: unsetPathValue splices arrays
|
|
928
|
+
// in place, so removing a lower index would shift the higher indices down and
|
|
929
|
+
// a later unset would then hit the wrong element (or no-op). Sorting descending
|
|
930
|
+
// by the numeric indices in each path keeps same-array removals correct;
|
|
931
|
+
// object-key and distinct-array removals are order-independent.
|
|
932
|
+
const orderedUnset = [...(input.unset || [])].sort((a: string, b: string) => {
|
|
933
|
+
const ka = parsePath(a).map((token) => ('index' in token ? token.index : -1))
|
|
934
|
+
const kb = parsePath(b).map((token) => ('index' in token ? token.index : -1))
|
|
935
|
+
for (let i = 0; i < Math.min(ka.length, kb.length); i += 1) {
|
|
936
|
+
if (ka[i] !== kb[i]) return kb[i] - ka[i]
|
|
937
|
+
}
|
|
938
|
+
return kb.length - ka.length
|
|
939
|
+
})
|
|
940
|
+
for (const pathValue of orderedUnset) {
|
|
928
941
|
unsetPathValue(targetObject, pathValue)
|
|
929
942
|
}
|
|
930
943
|
|