diff --git a/demo/src/routes/hello.data.ts b/demo/src/routes/hello.data.ts index e28ee2f..05f296a 100644 --- a/demo/src/routes/hello.data.ts +++ b/demo/src/routes/hello.data.ts @@ -1,7 +1,5 @@ export function getRouteData() { return { - props: { - msg: "hello world", - }, + msg: "hello world", }; } diff --git a/demo/src/routes/hello.tsx b/demo/src/routes/hello.tsx index eebdcbd..b8e143d 100644 --- a/demo/src/routes/hello.tsx +++ b/demo/src/routes/hello.tsx @@ -1,9 +1,17 @@ import { App } from "../App"; +import type { StaticRouteProps } from "@impalajs/core"; -export default function Hello({ url }: { url: string }) { +export default function Hello({ + path, + routeData, +}: StaticRouteProps) { return ( -
Hello {url}!
+
+ <> + {routeData?.msg} {path}! + +
); } diff --git a/demo/src/routes/index.tsx b/demo/src/routes/index.tsx index 4437770..b2aaa87 100644 --- a/demo/src/routes/index.tsx +++ b/demo/src/routes/index.tsx @@ -1,12 +1,13 @@ +import type { StaticRouteProps } from "@impalajs/core"; import { useState } from "react"; import { App } from "../App"; -export default function Hello({ url }: { url: string }) { +export default function Hello({ path }: StaticRouteProps) { const [count, setCount] = useState(0); return ( -
Home {url}!
+
Home {path}!
diff --git a/demo/src/routes/world/[id].data.ts b/demo/src/routes/world/[id].data.ts index 7389486..d7d503b 100644 --- a/demo/src/routes/world/[id].data.ts +++ b/demo/src/routes/world/[id].data.ts @@ -1,9 +1,12 @@ export function getStaticPaths() { return { paths: [ - { params: { id: "1", title: "One" } }, - { params: { id: "2", title: "Two" } }, - { params: { id: "3", title: "Three" } }, + { params: { id: "1" }, data: { title: "One", description: "Page one" } }, + { params: { id: "2" }, data: { title: "Two", description: "Page two" } }, + { + params: { id: "3" }, + data: { title: "Three", description: "Page three" }, + }, ], }; } diff --git a/demo/src/routes/world/[id].tsx b/demo/src/routes/world/[id].tsx index 60d3ff4..bffe9f8 100644 --- a/demo/src/routes/world/[id].tsx +++ b/demo/src/routes/world/[id].tsx @@ -1,40 +1,20 @@ +import { DynamicRouteProps } from "@impalajs/core"; import { App } from "../../App"; - -// Don't mind me. Just testing types -export interface PathInfo> { - params: Record; - data?: TData; -} - -export interface DataModule< - TPathData extends Record = Record, - TRouteData extends Record = Record -> { - getStaticPaths: () => - | Promise<{ paths: Array> }> - | { paths: Array> }; - getRouteData?: () => Promise; -} -type DataType = - StaticPaths["paths"][number]["params"]; - -type StaticPaths = (ReturnType< - Mod["getStaticPaths"] -> extends PromiseLike - ? Awaited> - : ReturnType) & - (unknown & {}); +import { Head } from "@impalajs/react/head"; export default function Hello({ - url, + path, params, -}: { - url: string; - params: DataType; -}) { + data, +}: DynamicRouteProps) { return ( - -
Hello {url}!
+ + + + +
+ Hello {path} {params.id}! +
); } diff --git a/packages/core/src/dev.ts b/packages/core/src/dev.ts index f764e37..e94c321 100644 --- a/packages/core/src/dev.ts +++ b/packages/core/src/dev.ts @@ -13,6 +13,38 @@ function stripExtension(path: string) { return path.replace(/\.[^/.]+$/, ""); } +function shallowCompare( + obj1?: Record, + obj2?: Record +): boolean { + // Check if both objects are null or undefined + if (obj1 == obj2) { + return true; + } + + if (!obj1 || !obj2) { + return false; + } + + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + if (keys1.length !== keys2.length) { + return false; + } + for (const key of keys1) { + if (obj1[key] !== obj2[key]) { + return false; + } + } + + return true; +} + +/** + * We can't use the vite dev server directly, because we need to do SSR. + * Because of this, we create a custom server and use the vite server as middleware. + */ export async function createServer() { console.log("Starting dev server!"); const app = express(); @@ -45,6 +77,7 @@ export async function createServer() { const mod = routeModules[result.chunk]; const baseRoute = stripExtension(result.chunk); + // Try and find a data module for this route const dataMod = dataModules[`${baseRoute}.data.ts`] || dataModules[`${baseRoute}.data.js`]; @@ -53,16 +86,31 @@ export async function createServer() { const routeData = await getRouteData?.(); + let data: Awaited< + ReturnType + >["paths"][number]["data"]; + if (isDynamicRoute(result.chunk)) { - const paths = await getStaticPaths?.(); + const { paths } = (await getStaticPaths?.()) || {}; + const matched = paths?.find((p) => + shallowCompare(p.params, result.params) + ); + data = matched?.data; + console.log({ matched }); + + if (!matched) { + console.log("No match for dynamic route", result.chunk, paths); + res.status(404).end("404"); + return; + } } const context = { - url: req.originalUrl, + path: req.originalUrl, routeData, chunk: result.chunk, params: result.params, - // data: path.data, + data, }; const { body, head } = await render(context, mod, []); diff --git a/packages/core/src/prerender.ts b/packages/core/src/prerender.ts index 9fc61bb..700b790 100644 --- a/packages/core/src/prerender.ts +++ b/packages/core/src/prerender.ts @@ -12,15 +12,6 @@ function stripExtension(path: string) { return path.replace(/\.[^/.]+$/, ""); } export async function prerender(root: string) { - const manifestPath = path.resolve(root, "dist/static/manifest.json"); - if (!existsSync(manifestPath)) { - console.error( - `Cannot find manifest.json at ${manifestPath}. Did you build the site?` - ); - return; - } - const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8")); - const { render, routeModules, dataModules } = (await import( path.resolve(root, "./dist/server/entry-server.js") )) as ServerEntry; @@ -38,7 +29,7 @@ export async function prerender(root: string) { .replace("", body); const filePath = `dist/static${ - context.url === "/" ? "/index" : context.url + context.path === "/" ? "/index" : context.path }.html`; const dir = path.dirname(path.resolve(root, filePath)); @@ -91,19 +82,19 @@ export async function prerender(root: string) { const { paths } = await getStaticPaths(); await Promise.all( - paths.map((path) => { - const url = toPath(path.params); - if (!url) { + paths.map((pathInfo) => { + const path = toPath(pathInfo.params); + if (!path) { console.error(`Invalid path params for route: ${route}`); return; } return prerenderRoute( { - url, + path, routeData, chunk: route, - params: path.params, - data: path.data, + params: pathInfo.params, + data: pathInfo.data, }, mod ); @@ -111,7 +102,7 @@ export async function prerender(root: string) { ); } else { await prerenderRoute( - { url: routePattern, chunk: route, routeData }, + { path: routePattern, chunk: route, routeData }, mod ); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b397776..2301517 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -3,11 +3,11 @@ export type ModuleImports = Record< () => Promise >; -export interface Context { - url: string; +export interface Context { + path: string; chunk: string; data?: TData; - routeData?: Record; + routeData?: TRouteData; params?: Record; } @@ -15,13 +15,19 @@ export interface RouteModule { default: TElement; } -export interface DataModule< +export interface StaticDataModule { + getRouteData?: () => Promise | TRouteData; +} + +export interface DynamicDataModule< TPathData extends Record = Record, TRouteData extends Record = Record -> { - getStaticPaths: () => Promise<{ paths: Array> }>; - getRouteData: () => Promise; +> extends StaticDataModule { + getStaticPaths: () => + | Promise<{ paths: Array> }> + | { paths: Array> }; } + export interface PathInfo> { params: Record; data?: TData; @@ -43,3 +49,35 @@ export interface ServerEntry { head: string; }>; } + +export type DataModule = DynamicDataModule; + +export type DataType = + StaticPaths["paths"][number]; + +export type ReturnTypeIfDefined any) | undefined> = + T extends undefined ? undefined : ReturnType>; + +export type AwaitedIfPromise = T extends PromiseLike ? Awaited : T; +export type StaticPaths = AwaitedIfPromise< + ReturnTypeIfDefined +>; + +export type RouteData = AwaitedIfPromise< + ReturnTypeIfDefined +>; + +export interface StaticRouteProps< + Mod extends StaticDataModule | undefined = undefined +> { + path: string; + routeData?: Mod extends undefined + ? undefined + : RouteData>; +} + +export interface DynamicRouteProps + extends StaticRouteProps { + params: DataType["params"]; + data: DataType["data"]; +} diff --git a/packages/react/head.d.ts b/packages/react/head.d.ts index 406496a..7cd7bb1 100644 --- a/packages/react/head.d.ts +++ b/packages/react/head.d.ts @@ -1 +1 @@ -export * from "./dist/head"; +export { Head } from "./dist/head";