Skip to content

Commit 7414e8f

Browse files
committed
feat(vite-plugin): emit store getters into the IR for typed reactive lists
storeGettersToIr surfaces array-returning get-accessors into GeaIrStore.getters[] (previously dropped). Each carries returnsArray, elementTypeName, this.<field> reactive deps, and a best-effort element shape. Consumed by the geatsc embedded backend to back reactive {this.getter.map(...)} lists; inert for the web path.
1 parent 51dc707 commit 7414e8f

3 files changed

Lines changed: 272 additions & 2 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@geajs/vite-plugin': patch
3+
---
4+
5+
### @geajs/vite-plugin (patch)
6+
7+
- **Store getters in the IR**: `storeGettersToIr` now emits `get`-accessors that return arrays into `GeaIrStore.getters[]`. Previously every getter was dropped — `storeFieldsToIr` only captured `ClassProperty` members and `storeMethodsToIr` skipped `kind: 'get'` — so a derived `get visible(): T[] { ... }` never reached downstream consumers. Each emitted getter carries `returnsArray`, `elementTypeName` (from the `T[]` / `Array<T>` return annotation), its `this.<field>` reactive dependencies, and a best-effort element shape (inferred from an object-literal `.map` callback or borrowed from a referenced array-of-objects field). Consumed by the geatsc embedded backend to back reactive `{this.getter.map(...)}` lists; inert for the web closure-codegen path.
8+
- `closure-codegen/ir.ts`: add the `GeaIrStoreGetter` type, `storeGettersToIr`, and element-shape inference helpers (`getterElementShape`, `getterBodyReturnsArray`, `collectThisFieldReads`, `arrayElementTypeNameFromTSType`).
9+
- `closure-codegen/transform/transform-store.ts`: thread `getters` into `buildStoreIr`.

packages/vite-plugin-gea/src/closure-codegen/ir.ts

Lines changed: 258 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,35 @@ export interface GeaIrStore {
6868
runtimeBase: 'compiled' | 'lean'
6969
fields: GeaIrStoreField[]
7070
methods?: GeaIrStoreMethod[]
71+
getters?: GeaIrStoreGetter[]
7172
constants?: GeaIrConstant[]
7273
sourceSpan?: GeaIrSourceSpan
7374
}
7475

76+
// A getter (`get x() { ... }`) on a Store. Surfaced into the IR so the embedded
77+
// target can back a reactive `{this.x.map(...)}` list with a derived array:
78+
// `deps` are the reactive fields whose change should recompute the list, and
79+
// `elementTypeName`/`shape` describe the row element type. (Getters are NOT
80+
// regular fields — geatsc must CALL the getter, not read stored state.)
81+
export interface GeaIrStoreGetter {
82+
name: string
83+
// True when the getter yields an array (array literal/spread, an
84+
// array-producing call like `.filter`/`.map`/`.slice`, or a `T[]`/`Array<T>`
85+
// return annotation). Only array getters can back a reactive list.
86+
returnsArray: boolean
87+
// The named element type from the return annotation (`get x(): Tile[]` →
88+
// 'Tile'); geatsc reuses its declared `__gea_type_<Name>` struct as the C++
89+
// element type.
90+
elementTypeName?: string
91+
shape?: GeaIrStoreValueShape
92+
// `this.<field>` reads inside the body — the reactive dependencies that
93+
// should re-run the list when they change.
94+
deps: string[]
95+
body: string
96+
ops?: GeaIrStoreStmt[]
97+
sourceSpan?: GeaIrSourceSpan
98+
}
99+
75100
export interface GeaIrStoreField {
76101
name: string
77102
initializer?: string
@@ -181,7 +206,13 @@ export function storeFieldsToIr(classDecl: ClassDeclaration): GeaIrStoreField[]
181206
function arrayElementTypeNameFromAnnotation(member: import('@babel/types').ClassProperty): string | undefined {
182207
const annotation = member.typeAnnotation
183208
if (!annotation || !t.isTSTypeAnnotation(annotation)) return undefined
184-
const typeNode = annotation.typeAnnotation
209+
return arrayElementTypeNameFromTSType(annotation.typeAnnotation)
210+
}
211+
212+
// Given a TS type node, return the bare element interface name for `T[]` /
213+
// `Array<T>` / `ReadonlyArray<T>` with a single named-type element; undefined
214+
// for anonymous element types, unions, or non-array types.
215+
function arrayElementTypeNameFromTSType(typeNode: import('@babel/types').TSType): string | undefined {
185216
if (t.isTSArrayType(typeNode)) {
186217
const element = typeNode.elementType
187218
if (t.isTSTypeReference(element) && t.isIdentifier(element.typeName)) return element.typeName.name
@@ -199,6 +230,232 @@ function arrayElementTypeNameFromAnnotation(member: import('@babel/types').Class
199230
return undefined
200231
}
201232

233+
const ARRAY_PRODUCING_METHODS = new Set([
234+
'filter',
235+
'map',
236+
'slice',
237+
'concat',
238+
'flat',
239+
'flatMap',
240+
'sort',
241+
'toSorted',
242+
'reverse',
243+
'toReversed',
244+
])
245+
246+
export function storeGettersToIr(classDecl: ClassDeclaration): GeaIrStoreGetter[] {
247+
const getters: GeaIrStoreGetter[] = []
248+
// Sibling field shapes, so a getter that derives from an array field
249+
// (`this.bricks.filter(...)`) can borrow that field's element shape.
250+
const fieldShapeByName = new Map<string, GeaIrStoreValueShape>()
251+
for (const field of storeFieldsToIr(classDecl)) {
252+
if (field.shape) fieldShapeByName.set(field.name, field.shape)
253+
}
254+
for (const member of classDecl.body.body) {
255+
if (!t.isClassMethod(member) || member.static || member.computed || member.kind !== 'get') continue
256+
if (!t.isIdentifier(member.key)) continue
257+
const returnType = member.returnType
258+
const elementTypeName =
259+
returnType && t.isTSTypeAnnotation(returnType)
260+
? arrayElementTypeNameFromTSType(returnType.typeAnnotation)
261+
: undefined
262+
const deps = collectThisFieldReads(member.body)
263+
const elementShape = getterElementShape(member.body, deps, elementTypeName, fieldShapeByName)
264+
const returnsArray = !!elementTypeName || !!elementShape || getterBodyReturnsArray(member.body)
265+
const shape: GeaIrStoreValueShape | undefined = returnsArray
266+
? {
267+
kind: 'array',
268+
...(elementShape ? { element: elementShape } : {}),
269+
...(elementTypeName ? { elementTypeName } : {}),
270+
}
271+
: undefined
272+
const ops = storeStmtsToIr(member.body.body)
273+
const getter: GeaIrStoreGetter = {
274+
name: member.key.name,
275+
returnsArray,
276+
deps,
277+
body: generate(member.body).code,
278+
...(elementTypeName ? { elementTypeName } : {}),
279+
...(shape ? { shape } : {}),
280+
...(ops ? { ops } : {}),
281+
...(sourceSpan(member) ? { sourceSpan: sourceSpan(member) } : {}),
282+
}
283+
getters.push(getter)
284+
}
285+
return getters
286+
}
287+
288+
// Best-effort element (row) object shape for an array-returning getter. Tries
289+
// the body first (an array literal, or a `.map(x => ({...}))` object literal),
290+
// then borrows from a referenced array-of-objects field (`this.bricks.filter`)
291+
// or any field whose element type name matches the getter's return annotation.
292+
function getterElementShape(
293+
body: unknown,
294+
deps: string[],
295+
elementTypeName: string | undefined,
296+
fieldShapeByName: Map<string, GeaIrStoreValueShape>,
297+
): GeaIrStoreValueShape | undefined {
298+
const arg = topLevelReturnArgument(body)
299+
const fromBody = arg ? elementShapeFromArrayExpression(arg, fieldShapeByName) : undefined
300+
if (fromBody) return fromBody
301+
if (elementTypeName) {
302+
for (const shape of fieldShapeByName.values()) {
303+
if (shape.kind === 'array' && shape.elementTypeName === elementTypeName && shape.element?.kind === 'object') {
304+
return shape.element
305+
}
306+
}
307+
}
308+
for (const dep of deps) {
309+
const shape = fieldShapeByName.get(dep)
310+
if (shape?.kind === 'array' && shape.element?.kind === 'object') return shape.element
311+
}
312+
return undefined
313+
}
314+
315+
function elementShapeFromArrayExpression(
316+
expr: unknown,
317+
fieldShapeByName: Map<string, GeaIrStoreValueShape>,
318+
): GeaIrStoreValueShape | undefined {
319+
if (t.isArrayExpression(expr)) {
320+
const first = expr.elements.find((element) => !!element && !t.isSpreadElement(element))
321+
if (!first) return undefined
322+
const shaped = shapeForExpression(first)
323+
return 'shape' in shaped ? shaped.shape : undefined
324+
}
325+
if (t.isCallExpression(expr) && t.isMemberExpression(expr.callee) && t.isIdentifier(expr.callee.property)) {
326+
const method = expr.callee.property.name
327+
if (method === 'map') {
328+
const objectLiteral = mapCallbackObjectLiteral(expr.arguments[0])
329+
if (!objectLiteral) return undefined
330+
const shaped = shapeForExpression(objectLiteral)
331+
return 'shape' in shaped ? shaped.shape : undefined
332+
}
333+
// filter/slice/sort/etc. preserve element type → recurse into the receiver.
334+
if (ARRAY_PRODUCING_METHODS.has(method)) {
335+
return elementShapeFromArrayExpression(expr.callee.object, fieldShapeByName)
336+
}
337+
}
338+
if (t.isMemberExpression(expr) && t.isThisExpression(expr.object) && t.isIdentifier(expr.property)) {
339+
const shape = fieldShapeByName.get(expr.property.name)
340+
if (shape?.kind === 'array') return shape.element
341+
}
342+
return undefined
343+
}
344+
345+
function mapCallbackObjectLiteral(callback: unknown): import('@babel/types').ObjectExpression | undefined {
346+
if (!callback || (!t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback))) return undefined
347+
const body = callback.body
348+
if (t.isObjectExpression(body)) return body
349+
if (t.isBlockStatement(body)) {
350+
for (const statement of body.body) {
351+
if (t.isReturnStatement(statement) && statement.argument && t.isObjectExpression(statement.argument)) {
352+
return statement.argument
353+
}
354+
}
355+
}
356+
return undefined
357+
}
358+
359+
// First `return` argument that is NOT inside a nested function (so a getter's
360+
// own return is found, not a `.map`/`.filter` callback's).
361+
function topLevelReturnArgument(body: unknown): unknown {
362+
let result: unknown
363+
let done = false
364+
const visit = (value: unknown): void => {
365+
if (done || !value || typeof value !== 'object') return
366+
if (Array.isArray(value)) {
367+
for (const item of value) visit(item)
368+
return
369+
}
370+
const node = value as Record<string, unknown>
371+
const type = node.type
372+
if (type === 'FunctionExpression' || type === 'ArrowFunctionExpression' || type === 'FunctionDeclaration') return
373+
if (type === 'ReturnStatement') {
374+
result = node.argument
375+
done = true
376+
return
377+
}
378+
for (const key of Object.keys(node)) {
379+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'range') continue
380+
visit(node[key])
381+
}
382+
}
383+
visit(body)
384+
return result
385+
}
386+
387+
// True when a getter body's own `return` yields an array. Nested functions
388+
// (e.g. a `.filter` callback) are skipped so their returns don't count.
389+
function getterBodyReturnsArray(body: unknown): boolean {
390+
let found = false
391+
const visit = (value: unknown): void => {
392+
if (found || !value || typeof value !== 'object') return
393+
if (Array.isArray(value)) {
394+
for (const item of value) visit(item)
395+
return
396+
}
397+
const node = value as Record<string, unknown>
398+
const type = node.type
399+
if (type === 'FunctionExpression' || type === 'ArrowFunctionExpression' || type === 'FunctionDeclaration') return
400+
if (type === 'ReturnStatement' && isArrayProducingExpression(node.argument)) {
401+
found = true
402+
return
403+
}
404+
for (const key of Object.keys(node)) {
405+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'range') continue
406+
visit(node[key])
407+
}
408+
}
409+
visit(body)
410+
return found
411+
}
412+
413+
function isArrayProducingExpression(expr: unknown): boolean {
414+
if (!expr || typeof expr !== 'object') return false
415+
const node = expr as Record<string, unknown>
416+
if (node.type === 'ArrayExpression') return true
417+
if (node.type === 'TSAsExpression' || node.type === 'TSNonNullExpression') return isArrayProducingExpression(node.expression)
418+
if (node.type === 'CallExpression') {
419+
const callee = node.callee as Record<string, unknown> | undefined
420+
const property = callee?.property as Record<string, unknown> | undefined
421+
if (callee?.type === 'MemberExpression' && property?.type === 'Identifier') {
422+
return ARRAY_PRODUCING_METHODS.has(property.name as string)
423+
}
424+
}
425+
return false
426+
}
427+
428+
// Collect the names of `this.<field>` reads anywhere in a node (deduped). These
429+
// are the reactive dependencies of a getter — when any changes, a list derived
430+
// from the getter must recompute.
431+
function collectThisFieldReads(node: unknown): string[] {
432+
const names = new Set<string>()
433+
const visit = (value: unknown): void => {
434+
if (!value || typeof value !== 'object') return
435+
if (Array.isArray(value)) {
436+
for (const item of value) visit(item)
437+
return
438+
}
439+
const rec = value as Record<string, unknown>
440+
const object = rec.object as Record<string, unknown> | undefined
441+
const property = rec.property as Record<string, unknown> | undefined
442+
if (
443+
rec.type === 'MemberExpression' &&
444+
object?.type === 'ThisExpression' &&
445+
rec.computed !== true &&
446+
property?.type === 'Identifier'
447+
) {
448+
names.add(property.name as string)
449+
}
450+
for (const key of Object.keys(rec)) {
451+
if (key === 'loc' || key === 'start' || key === 'end' || key === 'range') continue
452+
visit(rec[key])
453+
}
454+
}
455+
visit(node)
456+
return [...names]
457+
}
458+
202459
export function storeMethodsToIr(classDecl: ClassDeclaration): GeaIrStoreMethod[] {
203460
const methods: GeaIrStoreMethod[] = []
204461
for (const member of classDecl.body.body) {

packages/vite-plugin-gea/src/closure-codegen/transform/transform-store.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'node:fs'
44

55
import { COMPILER_RUNTIME_ID } from '../../virtual-modules.ts'
66
import { generate, t } from '../../utils/babel-interop.ts'
7-
import { sourceSpan, storeFieldsToIr, storeIrId, storeMethodsToIr, type GeaIrConstant, type GeaIrStore } from '../ir.ts'
7+
import { sourceSpan, storeFieldsToIr, storeGettersToIr, storeIrId, storeMethodsToIr, type GeaIrConstant, type GeaIrStore } from '../ir.ts'
88

99
export interface StoreTransformResult {
1010
code: string
@@ -272,6 +272,10 @@ function buildStoreIr(
272272
runtimeBase,
273273
fields: storeFieldsToIr(classDecl),
274274
methods: storeMethodsToIr(classDecl),
275+
...(() => {
276+
const getters = storeGettersToIr(classDecl)
277+
return getters.length > 0 ? { getters } : {}
278+
})(),
275279
...(constants.length > 0 ? { constants } : {}),
276280
...(sourceSpan(classDecl) ? { sourceSpan: sourceSpan(classDecl) } : {}),
277281
}

0 commit comments

Comments
 (0)