diff --git a/frontends/nextjs/src/app/ui/[[...slug]]/page.tsx b/frontends/nextjs/src/app/ui/[[...slug]]/page.tsx index 27ce983a4..9af2978ad 100644 --- a/frontends/nextjs/src/app/ui/[[...slug]]/page.tsx +++ b/frontends/nextjs/src/app/ui/[[...slug]]/page.tsx @@ -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 const slug = resolvedParams.slug || [] const path = '/' + slug.join('/') - const pageData = await loadPageFromDB(path) + const pageData = (await loadPageFromLuaPackages(path)) ?? (await loadPageFromDB(path)) if (!pageData) { return { diff --git a/frontends/nextjs/src/lib/ui-pages/load-page-from-lua-packages.test.ts b/frontends/nextjs/src/lib/ui-pages/load-page-from-lua-packages.test.ts new file mode 100644 index 000000000..8198a2ddd --- /dev/null +++ b/frontends/nextjs/src/lib/ui-pages/load-page-from-lua-packages.test.ts @@ -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() + }) +}) diff --git a/frontends/nextjs/src/lib/ui-pages/load-page-from-lua-packages.ts b/frontends/nextjs/src/lib/ui-pages/load-page-from-lua-packages.ts new file mode 100644 index 000000000..72539e380 --- /dev/null +++ b/frontends/nextjs/src/lib/ui-pages/load-page-from-lua-packages.ts @@ -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 { + let packageEntries: Awaited> + + 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 +}