diff --git a/frontends/nextjs/src/lib/db/core/dbal-client/index.ts b/frontends/nextjs/src/lib/db/core/dbal-client/index.ts index 1cf332047..76b9a237c 100644 --- a/frontends/nextjs/src/lib/db/core/dbal-client/index.ts +++ b/frontends/nextjs/src/lib/db/core/dbal-client/index.ts @@ -8,6 +8,6 @@ * providing a migration path to the full DBAL when ready. */ -export { closeAdapter } from '../dbal-client/adapter/close-adapter' -export { getAdapter } from '../dbal-client/adapter/get-adapter' -export type { DBALAdapter, ListOptions, ListResult } from '../dbal-client/types' +export { closeAdapter } from '../../dbal-client/adapter/close-adapter' +export { getAdapter } from '../../dbal-client/adapter/get-adapter' +export type { DBALAdapter, ListOptions, ListResult } from '../../dbal-client/types' diff --git a/storybook/.storybook/main.ts b/storybook/.storybook/main.ts index 63b455e54..60d621c31 100644 --- a/storybook/.storybook/main.ts +++ b/storybook/.storybook/main.ts @@ -18,6 +18,8 @@ const config: StorybookConfig = { staticDirs: [ // Serve Lua packages from root { from: '../../packages', to: '/packages' }, + // Serve storybook config + { from: '..', to: '/' }, ], async viteFinal(config) { return mergeConfig(config, { diff --git a/storybook/src/lua/executor.ts b/storybook/src/lua/executor.ts new file mode 100644 index 000000000..989b30abf --- /dev/null +++ b/storybook/src/lua/executor.ts @@ -0,0 +1,274 @@ +/** + * Lua Executor + * + * Executes Lua scripts from packages using fengari-web. + * This allows rendering actual Lua packages in Storybook. + */ + +import * as fengari from 'fengari-web' +import type { LuaRenderContext, LuaUIComponent } from '../types/lua-types' + +const lua = fengari.lua +const lauxlib = fengari.lauxlib +const lualib = fengari.lualib + +export interface LuaExecutionResult { + success: boolean + result?: LuaUIComponent + error?: string + logs: string[] +} + +/** + * Convert a JavaScript value to Lua and push onto stack + */ +function pushToLua(L: unknown, value: unknown): void { + const state = L as fengari.lua_State + + if (value === null || value === undefined) { + lua.lua_pushnil(state) + } else if (typeof value === 'boolean') { + lua.lua_pushboolean(state, value ? 1 : 0) + } else if (typeof value === 'number') { + lua.lua_pushnumber(state, value) + } else if (typeof value === 'string') { + lua.lua_pushstring(state, fengari.to_luastring(value)) + } else if (Array.isArray(value)) { + lua.lua_createtable(state, value.length, 0) + value.forEach((item, index) => { + pushToLua(state, item) + lua.lua_rawseti(state, -2, index + 1) + }) + } else if (typeof value === 'object') { + lua.lua_createtable(state, 0, Object.keys(value as object).length) + for (const [key, val] of Object.entries(value as object)) { + lua.lua_pushstring(state, fengari.to_luastring(key)) + pushToLua(state, val) + lua.lua_rawset(state, -3) + } + } +} + +/** + * Convert a Lua value from stack to JavaScript + */ +function fromLua(L: unknown, index: number): unknown { + const state = L as fengari.lua_State + const type = lua.lua_type(state, index) + + switch (type) { + case lua.LUA_TNIL: + return null + case lua.LUA_TBOOLEAN: + return lua.lua_toboolean(state, index) !== 0 + case lua.LUA_TNUMBER: + return lua.lua_tonumber(state, index) + case lua.LUA_TSTRING: + return fengari.to_jsstring(lua.lua_tostring(state, index)) + case lua.LUA_TTABLE: { + // Check if it's an array (has consecutive integer keys starting at 1) + const result: Record = {} + const arrayPart: unknown[] = [] + let isArray = true + let maxIndex = 0 + + lua.lua_pushnil(state) + while (lua.lua_next(state, index < 0 ? index - 1 : index) !== 0) { + const keyType = lua.lua_type(state, -2) + + if (keyType === lua.LUA_TNUMBER) { + const idx = lua.lua_tonumber(state, -2) + if (Number.isInteger(idx) && idx > 0) { + arrayPart[idx - 1] = fromLua(state, -1) + maxIndex = Math.max(maxIndex, idx) + } else { + isArray = false + } + } else if (keyType === lua.LUA_TSTRING) { + isArray = false + const key = fengari.to_jsstring(lua.lua_tostring(state, -2)) + result[key] = fromLua(state, -1) + } + + lua.lua_pop(state, 1) + } + + // If all keys are consecutive integers, return as array + if (isArray && maxIndex === arrayPart.length && maxIndex > 0) { + return arrayPart + } + + // Mix array part into result + arrayPart.forEach((val, idx) => { + result[String(idx + 1)] = val + }) + + return result + } + default: + return null + } +} + +/** + * Normalize Lua output to proper component structure + */ +function normalizeComponent(raw: unknown): LuaUIComponent | null { + if (!raw || typeof raw !== 'object') return null + + const obj = raw as Record + + if (!obj.type || typeof obj.type !== 'string') return null + + const component: LuaUIComponent = { + type: obj.type, + } + + if (obj.props && typeof obj.props === 'object') { + component.props = obj.props as Record + } + + if (obj.children) { + if (Array.isArray(obj.children)) { + component.children = obj.children + .map(c => normalizeComponent(c)) + .filter((c): c is LuaUIComponent => c !== null) + } + } + + return component +} + +/** + * Create a new Lua state with standard libraries + */ +function createLuaState(): fengari.lua_State { + const L = lauxlib.luaL_newstate() + lualib.luaL_openlibs(L) + return L +} + +/** + * Execute a Lua script and call its render function with context + */ +export async function executeLuaRender( + luaCode: string, + context: LuaRenderContext, + functionName = 'render' +): Promise { + const logs: string[] = [] + let L: fengari.lua_State | null = null + + try { + L = createLuaState() + + // Override print to capture logs + lua.lua_pushcfunction(L, (state: fengari.lua_State) => { + const nargs = lua.lua_gettop(state) + const parts: string[] = [] + for (let i = 1; i <= nargs; i++) { + parts.push(String(fromLua(state, i))) + } + logs.push(parts.join('\t')) + return 0 + }) + lua.lua_setglobal(L, fengari.to_luastring('print')) + + // Load and execute the Lua code + const loadResult = lauxlib.luaL_loadstring(L, fengari.to_luastring(luaCode)) + if (loadResult !== lua.LUA_OK) { + const error = fengari.to_jsstring(lua.lua_tostring(L, -1)) + return { success: false, error: `Load error: ${error}`, logs } + } + + const execResult = lua.lua_pcall(L, 0, 1, 0) + if (execResult !== lua.LUA_OK) { + const error = fengari.to_jsstring(lua.lua_tostring(L, -1)) + return { success: false, error: `Execution error: ${error}`, logs } + } + + // Check what we got back + const returnType = lua.lua_type(L, -1) + + if (returnType === lua.LUA_TFUNCTION) { + // Script returned a function directly (render function) + pushToLua(L, context) + const callResult = lua.lua_pcall(L, 1, 1, 0) + if (callResult !== lua.LUA_OK) { + const error = fengari.to_jsstring(lua.lua_tostring(L, -1)) + return { success: false, error: `Call error: ${error}`, logs } + } + } else if (returnType === lua.LUA_TTABLE) { + // Script returned a module table, look for render function + lua.lua_getfield(L, -1, fengari.to_luastring(functionName)) + + if (lua.lua_isfunction(L, -1)) { + pushToLua(L, context) + const callResult = lua.lua_pcall(L, 1, 1, 0) + if (callResult !== lua.LUA_OK) { + const error = fengari.to_jsstring(lua.lua_tostring(L, -1)) + return { success: false, error: `Render error: ${error}`, logs } + } + } else { + // The table itself might be the component tree + lua.lua_pop(L, 1) // Remove the nil/non-function + // Keep the table on stack + } + } + + // Convert result to JavaScript + const rawResult = fromLua(L, -1) + const result = normalizeComponent(rawResult) + + if (!result) { + return { + success: false, + error: `Invalid component structure returned`, + logs + } + } + + return { success: true, result, logs } + + } catch (err) { + return { + success: false, + error: err instanceof Error ? err.message : String(err), + logs + } + } finally { + if (L) { + lua.lua_close(L) + } + } +} + +/** + * Load and execute a Lua file from a package + */ +export async function loadAndExecuteLuaFile( + packageId: string, + scriptPath: string, + context: LuaRenderContext +): Promise { + try { + const response = await fetch(`/packages/${packageId}/seed/scripts/${scriptPath}`) + if (!response.ok) { + return { + success: false, + error: `Failed to load script: ${response.statusText}`, + logs: [] + } + } + + const luaCode = await response.text() + return await executeLuaRender(luaCode, context) + + } catch (err) { + return { + success: false, + error: err instanceof Error ? err.message : String(err), + logs: [] + } + } +} diff --git a/storybook/src/types/fengari.d.ts b/storybook/src/types/fengari.d.ts new file mode 100644 index 000000000..9de85cb74 --- /dev/null +++ b/storybook/src/types/fengari.d.ts @@ -0,0 +1,51 @@ +// Type declarations for fengari-web + +declare module 'fengari-web' { + export interface lua_State {} + + export const lua: { + LUA_OK: number + LUA_TNIL: number + LUA_TBOOLEAN: number + LUA_TNUMBER: number + LUA_TSTRING: number + LUA_TTABLE: number + LUA_TFUNCTION: number + + lua_pushnil(L: lua_State): void + lua_pushboolean(L: lua_State, b: number): void + lua_pushnumber(L: lua_State, n: number): void + lua_pushstring(L: lua_State, s: Uint8Array): void + lua_pushcfunction(L: lua_State, fn: (L: lua_State) => number): void + + lua_createtable(L: lua_State, narr: number, nrec: number): void + lua_rawseti(L: lua_State, index: number, i: number): void + lua_rawset(L: lua_State, index: number): void + lua_setglobal(L: lua_State, name: Uint8Array): void + lua_getfield(L: lua_State, index: number, name: Uint8Array): void + + lua_type(L: lua_State, index: number): number + lua_gettop(L: lua_State): number + lua_toboolean(L: lua_State, index: number): number + lua_tonumber(L: lua_State, index: number): number + lua_tostring(L: lua_State, index: number): Uint8Array + lua_isfunction(L: lua_State, index: number): boolean + + lua_pop(L: lua_State, n: number): void + lua_pcall(L: lua_State, nargs: number, nresults: number, msgh: number): number + lua_next(L: lua_State, index: number): number + lua_close(L: lua_State): void + } + + export const lauxlib: { + luaL_newstate(): lua_State + luaL_loadstring(L: lua_State, s: Uint8Array): number + } + + export const lualib: { + luaL_openlibs(L: lua_State): void + } + + export function to_luastring(s: string): Uint8Array + export function to_jsstring(s: Uint8Array): string +}