import { mkdir } from "node:fs/promises"; import path from "node:path"; import type { App } from "../commandDecorators"; export interface PageEntry { key: string; moduleAbsPath: string; seedAbsPaths?: string[]; } const LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/; async function appHasStModule(appCwdPath: string): Promise { return Bun.file(path.join(appCwdPath, "lib", "st.ts")).exists(); } const IMPLICIT_LAYOUT_DIR = path.join(".akan", "generated", "root-layouts"); const IMPLICIT_DICT_DIR = path.join(".akan", "generated", "dict"); const IMPLICIT_OVERRIDES_DIR = path.join(".akan", "generated", "overrides"); const OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/; /** * A `_overrides.tsx` manifest is a plain, server-safe module (`export default override({ Modal: BrandModal })`) * with no `"use client"` directive. The `UiOverrideProvider` that consumes it is a client component, so the * build emits a `"use client"` wrapper layout that reads the manifest's default map and mounts the provider * around the subtree. As a normal `"use client"` module the wrapper participates in client-entry discovery and * the RSC client manifest, so the server (as a client reference) and the client (as the real component) both * resolve it — and the author never writes `"use client"`. */ async function writeGeneratedOverridesLayoutFile(opts: { appCwdPath: string; key: string; userAbsPath: string; }): Promise { const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`; const absPath = path.join(path.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename); const userRel = path.relative(path.dirname(absPath), opts.userAbsPath).split(path.sep).join("/"); const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`; const source = `"use client"; import { UiOverrideProvider } from "akanjs/ui"; import { createElement, type ReactNode } from "react"; import value from ${JSON.stringify(userSpecifier)}; export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) { return createElement(UiOverrideProvider, { value }, children); } `; await Bun.write(absPath, source); return absPath; } interface RootBoundary { sourceKey: string | null; sourceAbsPath: string | null; segments: string[]; } function getRootBoundarySegments(key: string): string[] | null { const match = LAYOUT_KEY_RE.exec(key); if (!match) return null; const prefix = match[1]?.replace(/\/$/, ""); if (!prefix) return []; return prefix.split("/").filter(Boolean); } function implicitRootLayoutKey(segments: string[]): string { return `./${[...segments, "__root_layout"].join("/")}.tsx`; } function implicitRootLayoutAbsPath(appCwdPath: string, segments: string[]): string { const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx"; return path.join(path.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename); } function implicitDictionaryMacroAbsPath(appCwdPath: string): string { return path.join(path.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts"); } function isRootBoundarySegments(segments: string[], basePaths: Iterable): boolean { const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment)); if (firstVisibleIndex === -1) return segments.length <= 1; if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment))) return false; const visible = segments.slice(firstVisibleIndex); const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean)); return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? "")); } function findRootBoundaries(pageKeys: string[], appCwdPath: string, basePaths: Iterable): RootBoundary[] { const boundaries = new Map(); for (const key of pageKeys) { const segments = getRootBoundarySegments(key); if (!segments) continue; if (!isRootBoundarySegments(segments, basePaths)) continue; const id = segments.join("/"); boundaries.set(id, { sourceKey: key, sourceAbsPath: path.resolve(appCwdPath, "page", key.replace(/^\.\//, "")), segments, }); } const hasExplicitRootBoundary = [...boundaries.values()].some((boundary) => boundary.segments.length === 0); if (!hasExplicitRootBoundary && boundaries.size === 0) { boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] }); } return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/"))); } function hasAncestorRootBoundary(boundary: RootBoundary, boundaries: RootBoundary[]): boolean { return boundaries.some( (candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment), ); } function findExplicitRootLayoutAbsPath(pageKeys: string[], appCwdPath: string): string | null { const rootLayoutKey = pageKeys.find((key) => { const segments = getRootBoundarySegments(key); return segments !== null && segments.length === 0; }); return rootLayoutKey ? path.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null; } function routePrefixForSegments(segments: string[]): string | null { const visible = segments.filter((segment) => !/^\(.+\)$/.test(segment)); return visible[0] ?? null; } async function assertEnvClientConvention(appCwdPath: string, appName: string) { const envPath = path.join(appCwdPath, "env", "env.client.ts"); if (!(await Bun.file(envPath).exists())) { throw new Error( `[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`, ); } } async function writeGeneratedDictionaryMacroFile(appCwdPath: string, appName: string): Promise { const absPath = implicitDictionaryMacroAbsPath(appCwdPath); await mkdir(path.dirname(absPath), { recursive: true }); await Bun.write( absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" }; export const allDictionary = getAllDictionary(); `, ); return absPath; } async function writeGeneratedRootLayoutFile(opts: { appCwdPath: string; appName: string; boundary: RootBoundary; rootSourceAbsPath: string | null; includeStInit: boolean; includeSystemProvider: boolean; }): Promise { await assertEnvClientConvention(opts.appCwdPath, opts.appName); const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null; const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments); await mkdir(path.dirname(absPath), { recursive: true }); const dictMacroRel = dictMacroAbsPath ? path.relative(path.dirname(absPath), dictMacroAbsPath).split(path.sep).join("/") : null; const dictMacroSpecifier = dictMacroRel ? (dictMacroRel.startsWith(".") ? dictMacroRel : `./${dictMacroRel}`) : null; const sourceRel = opts.boundary.sourceAbsPath ? path.relative(path.dirname(absPath), opts.boundary.sourceAbsPath).split(path.sep).join("/") : null; const sourceSpecifier = sourceRel ? (sourceRel.startsWith(".") ? sourceRel : `./${sourceRel}`) : null; const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null; const inheritedSourceRel = inheritedSourceAbsPath ? path.relative(path.dirname(absPath), inheritedSourceAbsPath).split(path.sep).join("/") : null; const inheritedSourceSpecifier = inheritedSourceRel ? inheritedSourceRel.startsWith(".") ? inheritedSourceRel : `./${inheritedSourceRel}` : null; const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";\nvoid st;\n` : `import "@apps/${opts.appName}/client";\n`; const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedLayout from ${JSON.stringify(inheritedSourceSpecifier)};\n` : "const inheritedLayout = {};\n"; const prefix = routePrefixForSegments(opts.boundary.segments); const userImport = sourceSpecifier ? `import UserLayout, * as userLayout from ${JSON.stringify(sourceSpecifier)};\n` : "const UserLayout = ({ children }) => children;\nconst userLayout = {};\n"; const source = opts.includeSystemProvider ? `import type { LayoutProps, PageProps } from "akanjs/client"; import { loadFonts } from "akanjs/client"; import { System } from "akanjs/ui"; import { env } from "@apps/${opts.appName}/env/env.client"; import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)}; ${clientImport}${inheritedImport}${userImport} const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? []; const defaultFonts = userFonts.filter((font) => font.default); if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout"); const defaultFont = defaultFonts[0]; const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined; export async function generateHead(props: PageProps) { if (userLayout.generateHead) return userLayout.generateHead(props); if (userLayout.head !== undefined) return userLayout.head; if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props); return inheritedLayout.head; } export async function generateMetadata(props: PageProps) { if (userLayout.generateMetadata) return userLayout.generateMetadata(props); if (userLayout.metadata !== undefined) return userLayout.metadata; if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props); return inheritedLayout.metadata; } export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound; export const Error = userLayout.Error ?? inheritedLayout.Error; export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig; export default function GeneratedLayout({ children, params, searchParams }: LayoutProps) { return ( {children} ); } ` : `import type { LayoutProps, PageProps } from "akanjs/client"; ${inheritedImport}${userImport} export async function generateHead(props: PageProps) { if (userLayout.generateHead) return userLayout.generateHead(props); if (userLayout.head !== undefined) return userLayout.head; if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props); return inheritedLayout.head; } export async function generateMetadata(props: PageProps) { if (userLayout.generateMetadata) return userLayout.generateMetadata(props); if (userLayout.metadata !== undefined) return userLayout.metadata; if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props); return inheritedLayout.metadata; } export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound; export const Error = userLayout.Error ?? inheritedLayout.Error; export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig; export default function GeneratedLayout({ children, params, searchParams }: LayoutProps) { return {children}; } `; await Bun.write(absPath, source); return absPath; } /** * When no root `page/_layout.*` exists on disk, merge a generated implicit root layout * (with generated client runtime registration and optional `void st` when `lib/st.ts` exists). */ export async function resolveSsrPageEntries(opts: { appCwdPath: string; appName: string; pageKeys: string[]; basePaths?: Iterable; }): Promise { const absPageDir = path.resolve(opts.appCwdPath, "page"); const hasSt = await appHasStModule(opts.appCwdPath); const basePaths = opts.basePaths ?? []; const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath); const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths); const rootLayoutKeys = new Set( opts.pageKeys.filter((key) => { const segments = getRootBoundarySegments(key); return segments !== null && isRootBoundarySegments(segments, basePaths); }), ); const base = await Promise.all( opts.pageKeys .filter((key) => !rootLayoutKeys.has(key)) .map(async (key) => { const userAbsPath = path.resolve(absPageDir, key); // `_overrides.tsx` is served through a generated `"use client"` wrapper layout that mounts the provider, // so the author writes no directive; the raw manifest becomes a build seed so its slot components (and // the manifest itself) enter the client graph / RSC client manifest. if (OVERRIDES_KEY_RE.test(key)) { const moduleAbsPath = await writeGeneratedOverridesLayoutFile({ appCwdPath: opts.appCwdPath, key, userAbsPath, }); return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] }; } return { key, moduleAbsPath: userAbsPath }; }), ); const generated = await Promise.all( rootBoundaries.map(async (boundary) => ({ key: implicitRootLayoutKey(boundary.segments), moduleAbsPath: await writeGeneratedRootLayoutFile({ appCwdPath: opts.appCwdPath, appName: opts.appName, boundary, rootSourceAbsPath, includeStInit: hasSt && boundary.segments.length === 0, includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries), }), seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))], })), ); const entries = [...base, ...generated]; entries.sort((a, b) => a.key.localeCompare(b.key)); return entries; } export async function resolveSsrPageEntriesForApp(app: App, pageKeys: string[]): Promise { const config = await app.getConfig(); return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths }); }