Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions demo/src/routes/hello.data.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export function getRouteData() {
return {
props: {
msg: "hello world",
},
msg: "hello world",
};
}
12 changes: 10 additions & 2 deletions demo/src/routes/hello.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof import("./hello.data")>) {
return (
<App title="Hello">
<div>Hello {url}!</div>
<div>
<>
{routeData?.msg} {path}!
</>
</div>
</App>
);
}
5 changes: 3 additions & 2 deletions demo/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<App title="Home">
<div>Home {url}!</div>
<div>Home {path}!</div>
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</div>
Expand Down
9 changes: 6 additions & 3 deletions demo/src/routes/world/[id].data.ts
Original file line number Diff line number Diff line change
@@ -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" },
},
],
};
}
44 changes: 12 additions & 32 deletions demo/src/routes/world/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,20 @@
import { DynamicRouteProps } from "@impalajs/core";
import { App } from "../../App";

// Don't mind me. Just testing types
export interface PathInfo<TData = Record<string, string>> {
params: Record<string, string>;
data?: TData;
}

export interface DataModule<
TPathData extends Record<string, unknown> = Record<string, string>,
TRouteData extends Record<string, unknown> = Record<string, string>
> {
getStaticPaths: () =>
| Promise<{ paths: Array<PathInfo<TPathData>> }>
| { paths: Array<PathInfo<TPathData>> };
getRouteData?: () => Promise<TRouteData>;
}
type DataType<Mod extends DataModule> =
StaticPaths<Mod>["paths"][number]["params"];

type StaticPaths<Mod extends DataModule> = (ReturnType<
Mod["getStaticPaths"]
> extends PromiseLike<any>
? Awaited<ReturnType<Mod["getStaticPaths"]>>
: ReturnType<Mod["getStaticPaths"]>) &
(unknown & {});
import { Head } from "@impalajs/react/head";

export default function Hello({
url,
path,
params,
}: {
url: string;
params: DataType<typeof import("./[id].data")>;
}) {
data,
}: DynamicRouteProps<typeof import("./[id].data")>) {
return (
<App title={params.title}>
<div>Hello {url}!</div>
<App title={data?.title}>
<Head>
<meta name="description" content={data.description || "A page"} />
</Head>
<div>
Hello {path} {params.id}!
</div>
</App>
);
}
54 changes: 51 additions & 3 deletions packages/core/src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,38 @@ function stripExtension(path: string) {
return path.replace(/\.[^/.]+$/, "");
}

function shallowCompare(
obj1?: Record<string, unknown>,
obj2?: Record<string, unknown>
): 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();
Expand Down Expand Up @@ -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`];
Expand All @@ -53,16 +86,31 @@ export async function createServer() {

const routeData = await getRouteData?.();

let data: Awaited<
ReturnType<typeof getStaticPaths>
>["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, []);
Expand Down
25 changes: 8 additions & 17 deletions packages/core/src/prerender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,7 +29,7 @@ export async function prerender(root: string) {
.replace("<!--app-content-->", body);

const filePath = `dist/static${
context.url === "/" ? "/index" : context.url
context.path === "/" ? "/index" : context.path
}.html`;

const dir = path.dirname(path.resolve(root, filePath));
Expand Down Expand Up @@ -91,27 +82,27 @@ 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
);
})
);
} else {
await prerenderRoute(
{ url: routePattern, chunk: route, routeData },
{ path: routePattern, chunk: route, routeData },
mod
);
}
Expand Down
52 changes: 45 additions & 7 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,31 @@ export type ModuleImports<TModule = unknown> = Record<
() => Promise<TModule>
>;

export interface Context<TData = unknown> {
url: string;
export interface Context<TData = unknown, TRouteData = unknown> {
path: string;
chunk: string;
data?: TData;
routeData?: Record<string, string>;
routeData?: TRouteData;
params?: Record<string, string>;
}

export interface RouteModule<TElement = HTMLElement> {
default: TElement;
}

export interface DataModule<
export interface StaticDataModule<TRouteData = unknown> {
getRouteData?: () => Promise<TRouteData> | TRouteData;
}

export interface DynamicDataModule<
TPathData extends Record<string, unknown> = Record<string, string>,
TRouteData extends Record<string, unknown> = Record<string, string>
> {
getStaticPaths: () => Promise<{ paths: Array<PathInfo<TPathData>> }>;
getRouteData: () => Promise<TRouteData>;
> extends StaticDataModule<TRouteData> {
getStaticPaths: () =>
| Promise<{ paths: Array<PathInfo<TPathData>> }>
| { paths: Array<PathInfo<TPathData>> };
}

export interface PathInfo<TData = Record<string, string>> {
params: Record<string, string>;
data?: TData;
Expand All @@ -43,3 +49,35 @@ export interface ServerEntry<TElement = HTMLElement> {
head: string;
}>;
}

export type DataModule = DynamicDataModule;

export type DataType<Mod extends DynamicDataModule> =
StaticPaths<Mod>["paths"][number];

export type ReturnTypeIfDefined<T extends ((...args: any) => any) | undefined> =
T extends undefined ? undefined : ReturnType<Exclude<T, undefined>>;

export type AwaitedIfPromise<T> = T extends PromiseLike<any> ? Awaited<T> : T;
export type StaticPaths<Mod extends DynamicDataModule> = AwaitedIfPromise<
ReturnTypeIfDefined<Mod["getStaticPaths"]>
>;

export type RouteData<Mod extends StaticDataModule> = AwaitedIfPromise<
ReturnTypeIfDefined<Mod["getRouteData"]>
>;

export interface StaticRouteProps<
Mod extends StaticDataModule | undefined = undefined
> {
path: string;
routeData?: Mod extends undefined
? undefined
: RouteData<Exclude<Mod, undefined>>;
}

export interface DynamicRouteProps<Mod extends DynamicDataModule>
extends StaticRouteProps<Mod> {
params: DataType<Mod>["params"];
data: DataType<Mod>["data"];
}
2 changes: 1 addition & 1 deletion packages/react/head.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./dist/head";
export { Head } from "./dist/head";