Skip to content

Commit 0d0eef5

Browse files
committed
feat: add server and site manifest output paths, enhance server-side rendering handling
1 parent 4a4b119 commit 0d0eef5

File tree

5 files changed

+112
-26
lines changed

5 files changed

+112
-26
lines changed

packages/pranx/src/build/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@ const SOURCE_PAGES_DIR = join(SOURCE_DIR, "pages");
1313

1414
const PUBLIC_USER_DIR = join(CWD, "public");
1515

16+
const SERVER_MANIFEST_OUTPUT_PATH = join(OUTPUT_BUNDLE_SERVER_DIR, "server.manifest.json");
17+
const SITE_MANIFEST_OUTPUT_PATH = join(OUTPUT_BUNDLE_BROWSER_DIR, "site.manifest.json");
18+
1619
export {
1720
CWD,
1821
OUTPUT_BUNDLE_BROWSER_DIR,
1922
OUTPUT_BUNDLE_SERVER_DIR,
2023
OUTPUT_PAGES_DIR,
2124
OUTPUT_PRANX_DIR,
2225
PUBLIC_USER_DIR,
26+
SERVER_MANIFEST_OUTPUT_PATH,
27+
SITE_MANIFEST_OUTPUT_PATH,
2328
SOURCE_DIR,
2429
SOURCE_PAGES_DIR,
2530
};

packages/pranx/src/client/StartApp.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import type { VNode } from "preact";
12
import { ErrorBoundary, lazy, LocationProvider, Route, Router } from "preact-iso";
3+
import { Children, cloneElement, type PropsWithChildren } from "preact/compat";
24
import type { HydrateDataRoute } from "types/index.js";
35
import { useHead } from "unhead";
46
import { exec_route_match } from "./exec-match.js";
@@ -57,7 +59,13 @@ export function StartApp() {
5759
let props = r.props;
5860

5961
if (!r.is_dynamic) {
60-
return <Page {...props} />;
62+
return (
63+
<ServerSidePage
64+
// biome-ignore lint/correctness/noChildrenProp: <>
65+
children={<Page />}
66+
route_data={r}
67+
/>
68+
);
6169
}
6270

6371
for (const route of r.static_generated_routes) {
@@ -81,3 +89,9 @@ export function StartApp() {
8189
</LocationProvider>
8290
);
8391
}
92+
93+
const ServerSidePage = (props: PropsWithChildren & { route_data: HydrateDataRoute }) => {
94+
const child = Children.only(props.children);
95+
96+
return cloneElement(child as VNode, props.route_data.props);
97+
};

packages/pranx/src/cmd/build.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { logger } from "@/utils/logger.js";
33
import { measureTime } from "@/utils/time-perf.js";
44
import fse from "fs-extra";
55
import kleur from "kleur";
6-
import { join, resolve } from "pathe";
6+
import { join } from "pathe";
77
import { Fragment, h } from "preact";
88
import { renderToStringAsync } from "preact-render-to-string";
99
import type {
@@ -20,6 +20,8 @@ import {
2020
OUTPUT_BUNDLE_BROWSER_DIR,
2121
OUTPUT_BUNDLE_SERVER_DIR,
2222
OUTPUT_PRANX_DIR,
23+
SERVER_MANIFEST_OUTPUT_PATH,
24+
SITE_MANIFEST_OUTPUT_PATH,
2325
} from "../build/constants.js";
2426
import { generate_html_template } from "../build/generate_html_template.js";
2527

@@ -38,14 +40,14 @@ export async function build() {
3840
optimize: true,
3941
});
4042

41-
const site_manifest: SERVER_MANIFEST = {
43+
const server_site_manifest: SERVER_MANIFEST = {
4244
entry_server: join(OUTPUT_BUNDLE_SERVER_DIR, "entry-server.js"),
4345
routes: [],
4446
};
4547

4648
let server_entry_module: ServerEntryModule | null = null;
4749

48-
server_entry_module = (await import(site_manifest.entry_server)) as ServerEntryModule;
50+
server_entry_module = (await import(server_site_manifest.entry_server)) as ServerEntryModule;
4951

5052
const pranx_bundle_replace_path = join(".pranx", "browser");
5153

@@ -56,6 +58,7 @@ export async function build() {
5658
entry: "",
5759
};
5860

61+
// Calculating css files
5962
for (const [file, _output] of Object.entries(browser_bundle_metafile.metafile.outputs)) {
6063
if (file.endsWith("entry-client.css")) {
6164
css_output.entry = file.replace(pranx_bundle_replace_path, "");
@@ -73,6 +76,7 @@ export async function build() {
7376
}
7477
}
7578

79+
// Generating Manifest and generating static pages
7680
for (const [file, _output] of Object.entries(browser_bundle_metafile.metafile.outputs)) {
7781
if (!file.endsWith("page.js")) continue;
7882

@@ -132,7 +136,7 @@ path: ${final_path}`);
132136
const static_paths_result = await getStaticPaths();
133137
const new_final_path = final_path;
134138

135-
site_manifest.routes.push({
139+
server_site_manifest.routes.push({
136140
path: final_path,
137141
module: pages_relative_path,
138142
props: statics_fn_result.props,
@@ -142,6 +146,7 @@ path: ${final_path}`);
142146
dynamic_params: dynamic_params,
143147
css: [css_output.entry, css_output[final_path] || ""].filter(Boolean),
144148
static_generated_routes: [],
149+
absolute_module_path: module_path,
145150
});
146151

147152
for (const static_path of static_paths_result.paths) {
@@ -172,7 +177,7 @@ params returned by getStaticPaths: ${JSON.stringify(static_path.params)}`);
172177
});
173178
}
174179

175-
site_manifest.routes.at(-1)?.static_generated_routes.push({
180+
server_site_manifest.routes.at(-1)?.static_generated_routes.push({
176181
path: replaced_path,
177182
props: statics_fn_result.props || {},
178183
revalidate: statics_fn_result.revalidate || -1,
@@ -187,7 +192,7 @@ params returned by getStaticPaths: ${JSON.stringify(static_path.params)}`);
187192
});
188193
}
189194

190-
site_manifest.routes.push({
195+
server_site_manifest.routes.push({
191196
path: final_path,
192197
module: pages_relative_path,
193198
props: statics_fn_result.props,
@@ -197,11 +202,12 @@ params returned by getStaticPaths: ${JSON.stringify(static_path.params)}`);
197202
is_dynamic: isUrlDynamic,
198203
dynamic_params: dynamic_params,
199204
css: [css_output.entry, css_output[final_path] || ""].filter(Boolean) as string[],
205+
absolute_module_path: module_path,
200206
});
201207
}
202208

203209
const hydrate_data: HYDRATE_DATA = {
204-
routes: site_manifest.routes.map((r) => {
210+
routes: server_site_manifest.routes.map((r) => {
205211
return {
206212
module: r.module,
207213
path: r.path,
@@ -222,11 +228,11 @@ params returned by getStaticPaths: ${JSON.stringify(static_path.params)}`);
222228

223229
const hydrate_data_as_string = JSON.stringify(hydrate_data);
224230

225-
for (const route of site_manifest.routes) {
231+
// Writing static files and prerender pages
232+
for (const route of server_site_manifest.routes) {
226233
if (route.rendering_kind === "server-side") continue;
227234

228-
const file_absolute = resolve(join(OUTPUT_BUNDLE_SERVER_DIR, "pages", route.module));
229-
const page_module = (await import(file_absolute)) as PageModule;
235+
const page_module = (await import(route.absolute_module_path)) as PageModule;
230236

231237
if (route.static_generated_routes.length > 0) {
232238
for (const static_route of route.static_generated_routes) {
@@ -272,19 +278,12 @@ params returned by getStaticPaths: ${JSON.stringify(static_path.params)}`);
272278
await fse.writeFile(output_html_path, html);
273279
}
274280

275-
await fse.writeFile(
276-
join(OUTPUT_BUNDLE_SERVER_DIR, "server.manifest.json"),
277-
JSON.stringify(site_manifest)
278-
);
279-
280-
await fse.writeFile(
281-
join(OUTPUT_BUNDLE_BROWSER_DIR, "site.manifest.json"),
282-
JSON.stringify(hydrate_data)
283-
);
281+
await fse.writeFile(SERVER_MANIFEST_OUTPUT_PATH, JSON.stringify(server_site_manifest));
282+
await fse.writeFile(SITE_MANIFEST_OUTPUT_PATH, JSON.stringify(hydrate_data));
284283

285284
const BUILD_TIME = measureTime("build_measure_time");
286285

287-
printRoutesTreeForUser(site_manifest.routes);
286+
printRoutesTreeForUser(server_site_manifest.routes);
288287

289288
logger.success(`Project builded in ${BUILD_TIME} ms\n`);
290289
}

packages/pranx/src/cmd/start.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,89 @@
1-
import { OUTPUT_BUNDLE_BROWSER_DIR, PUBLIC_USER_DIR } from "@/build/constants.js";
1+
import {
2+
OUTPUT_BUNDLE_BROWSER_DIR,
3+
OUTPUT_BUNDLE_SERVER_DIR,
4+
PUBLIC_USER_DIR,
5+
SERVER_MANIFEST_OUTPUT_PATH,
6+
SITE_MANIFEST_OUTPUT_PATH,
7+
} from "@/build/constants.js";
8+
import { filePathToRoutingPath } from "@/build/filepath-to-routing-path.js";
9+
import { generate_html_template } from "@/build/generate_html_template.js";
210
import { logger } from "@/utils/logger.js";
311
import { measureTime } from "@/utils/time-perf.js";
412
import fse from "fs-extra";
5-
import { H3, serve, serveStatic } from "h3";
13+
import { defineHandler, H3, html, serve, serveStatic } from "h3";
614
import kleur from "kleur";
715
import { readFile, stat } from "node:fs/promises";
8-
import { join } from "node:path";
16+
import { join, resolve } from "pathe";
17+
import { Fragment, h } from "preact";
18+
import { renderToStringAsync } from "preact-render-to-string";
19+
import type { HYDRATE_DATA, PageModule, SERVER_MANIFEST, ServerEntryModule } from "types/index.js";
920

1021
export async function start() {
22+
measureTime("pranx-start");
23+
1124
logger.log(kleur.bold().magenta("Pranx Start"));
1225

13-
measureTime("pranx-start");
26+
const server_manifest = (await fse.readJSON(SERVER_MANIFEST_OUTPUT_PATH)) as SERVER_MANIFEST;
1427

1528
const PORT = Number(process.env.PORT) || 3030;
1629

1730
const app = new H3();
1831

19-
app.use("**", (event) => {
32+
for (const route of server_manifest.routes) {
33+
if (route.rendering_kind === "server-side") {
34+
app.on(
35+
"GET",
36+
filePathToRoutingPath(route.path, false),
37+
defineHandler({
38+
middleware: [],
39+
meta: {},
40+
handler: async (event) => {
41+
let server_entry_module: ServerEntryModule | null = null;
42+
43+
server_entry_module = (await import(server_manifest.entry_server)) as ServerEntryModule;
44+
45+
const file_absolute = resolve(join(OUTPUT_BUNDLE_SERVER_DIR, "pages", route.module));
46+
47+
const { default: page, getServerSideProps } = (await import(
48+
file_absolute
49+
)) as PageModule;
50+
51+
let props = {};
52+
53+
if (getServerSideProps) {
54+
props = await getServerSideProps();
55+
}
56+
57+
const hydrate_data = (await fse.readJSON(SITE_MANIFEST_OUTPUT_PATH)) as HYDRATE_DATA;
58+
59+
const target_route = hydrate_data.routes.find((r) => r.path === route.path);
60+
if (!target_route) {
61+
logger.error(`Route not found in hydrate data: ${route.path}`);
62+
event.res.status = 500;
63+
return html(event, "Internal Server Error");
64+
}
65+
66+
const page_prerendered = await renderToStringAsync(
67+
h(server_entry_module?.default || Fragment, {}, h(page, props, null))
68+
);
69+
70+
const html_string = generate_html_template({
71+
page_prerendered,
72+
hydrate_data_as_string: JSON.stringify(hydrate_data),
73+
minify: true,
74+
css: route.css,
75+
});
76+
77+
event.res.headers.set("Content-Type", "text/html");
78+
79+
return html(event, html_string);
80+
},
81+
})
82+
);
83+
}
84+
}
85+
86+
app.on("GET", "**", (event) => {
2087
return serveStatic(event, {
2188
indexNames: ["/index.html"],
2289

packages/pranx/types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export type RouteRenderingKind = "static" | "server-side";
6565
export type ServerManifestRoute = {
6666
path: string;
6767
module: string;
68+
absolute_module_path: string;
6869
props: Record<string, any>;
6970
static_generated_routes: Array<{
7071
path: string;

0 commit comments

Comments
 (0)