feat: load UI pages from lua packages

This commit is contained in:
2025-12-29 23:19:02 +00:00
parent 1f88b32d0c
commit d0dbf45d24
3 changed files with 104 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { UIPageRenderer } from '@/components/ui-page-renderer/UIPageRenderer'
import { loadPageFromLuaPackages } from '@/lib/ui-pages/load-page-from-lua-packages'
import { loadPageFromDB } from '@/lib/ui-pages/load-page-from-db'
interface PageProps {
@@ -25,8 +26,8 @@ export default async function DynamicUIPage({ params }: PageProps) {
const slug = resolvedParams.slug || []
const path = '/' + slug.join('/')
// Load page from database
const pageData = await loadPageFromDB(path)
// Prefer Lua package-based UI pages, fallback to database-backed pages
const pageData = (await loadPageFromLuaPackages(path)) ?? (await loadPageFromDB(path))
if (!pageData) {
notFound()
@@ -46,7 +47,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
const slug = resolvedParams.slug || []
const path = '/' + slug.join('/')
const pageData = await loadPageFromDB(path)
const pageData = (await loadPageFromLuaPackages(path)) ?? (await loadPageFromDB(path))
if (!pageData) {
return {

View File

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'bun:test'
import { loadPageFromLuaPackages } from './load-page-from-lua-packages'
describe('loadPageFromLuaPackages', () => {
it('loads a page definition from Lua UI packages', async () => {
const page = await loadPageFromLuaPackages('/example-form')
expect(page).not.toBeNull()
expect(page?.title).toBe('Example Form')
expect(page?.layout).toBeDefined()
expect(Object.keys(page?.actions ?? {})).toContain('handleFormSubmit')
})
it('returns null for paths that are not in Lua packages', async () => {
const page = await loadPageFromLuaPackages('/does-not-exist')
expect(page).toBeNull()
})
})

View File

@@ -0,0 +1,80 @@
import { access, readFile, readdir } from 'fs/promises'
import { constants } from 'fs'
import { join } from 'path'
import type { JsonObject } from '@/types/utility-types'
import { loadLuaUIPackage } from '@/lib/lua/ui/load-lua-ui-package'
import type { UIPageData } from './load-page-from-db'
const LUA_UI_PACKAGES_ROOT = join(process.cwd(), 'src/lib/packages/lua-ui')
/**
* Load a UI page directly from Lua UI packages on disk.
*
* - Scans packages in src/lib/packages/lua-ui
* - Uses manifest.json + Lua files to build the page definition
* - Returns the same UIPageData shape used by database-backed pages
*/
export async function loadPageFromLuaPackages(path: string): Promise<UIPageData | null> {
let packageEntries: Awaited<ReturnType<typeof readdir>>
try {
packageEntries = await readdir(LUA_UI_PACKAGES_ROOT, { withFileTypes: true })
} catch (error) {
console.error('Failed to read Lua UI packages directory', error)
return null
}
for (const entry of packageEntries) {
if (!entry.isDirectory()) {
continue
}
const packagePath = join(LUA_UI_PACKAGES_ROOT, entry.name)
const manifestPath = join(packagePath, 'manifest.json')
try {
await access(manifestPath, constants.F_OK)
} catch (_error) {
continue
}
// Quickly inspect manifest to see if this package provides the requested path
try {
const manifestContent = await readFile(manifestPath, 'utf-8')
const manifest = JSON.parse(manifestContent) as { pages?: Array<{ path: string }> }
const hasMatchingPage = manifest.pages?.some(page => page.path === path)
if (!hasMatchingPage) {
continue
}
} catch (error) {
console.warn(`Skipping Lua UI package at ${packagePath}:`, error)
continue
}
try {
const uiPackage = await loadLuaUIPackage(packagePath)
const page = uiPackage.pages.find(p => p.path === path)
if (page) {
return {
path: page.path,
title: page.title,
level: page.level,
requireAuth: page.requiresAuth ?? false,
requiredRole: page.requiredRole,
layout: page.layout as JsonObject,
actions: uiPackage.actions,
}
}
} catch (error) {
// Skip packages that fail to load so other packages can still resolve the page
console.warn(`Skipping Lua UI package at ${packagePath}:`, error)
}
}
return null
}