blumenjs 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { renderToString } from "react-dom/server";
3
3
  import React from "react";
4
4
 
5
5
  // Auto-generated route map (run `npm run routes` to regenerate)
6
- import { routes, App, Document } from "./generated-routes";
6
+ import { routes, App, Document, serverPropsMap } from "./generated-routes";
7
7
  import { matchRoute } from "../app/shared/router";
8
8
  import NotFoundPage from "../app/pages/NotFound";
9
9
 
@@ -21,6 +21,19 @@ interface SSRResponse {
21
21
  props: Record<string, any>;
22
22
  }
23
23
 
24
+ interface DataRequest {
25
+ path: string;
26
+ query: Record<string, string[]>;
27
+ params: Record<string, any>;
28
+ headers?: Record<string, string>;
29
+ }
30
+
31
+ interface DataResponse {
32
+ props: Record<string, any>;
33
+ hasServerProps: boolean;
34
+ revalidate: number; // TTL in seconds; 0 = no caching
35
+ }
36
+
24
37
  const PORT = process.env.PORT || 4000;
25
38
 
26
39
  const server = http.createServer(async (req, res) => {
@@ -36,12 +49,97 @@ const server = http.createServer(async (req, res) => {
36
49
  return;
37
50
  }
38
51
 
39
- if (req.method !== "POST" || req.url !== "/render") {
52
+ if (req.method !== "POST") {
40
53
  res.writeHead(404);
41
54
  res.end(JSON.stringify({ error: "Not Found" }));
42
55
  return;
43
56
  }
44
57
 
58
+ // Route to the correct handler
59
+ if (req.url === "/data") {
60
+ return handleData(req, res);
61
+ } else if (req.url === "/render") {
62
+ return handleRender(req, res);
63
+ } else {
64
+ res.writeHead(404);
65
+ res.end(JSON.stringify({ error: "Not Found" }));
66
+ }
67
+ });
68
+
69
+ /**
70
+ * POST /data - Execute getServerProps for a matched route.
71
+ *
72
+ * Called by the Go server before /render to fetch TypeScript-defined
73
+ * server data. If the page doesn't export getServerProps, returns
74
+ * { hasServerProps: false } and Go skips the data merge.
75
+ */
76
+ async function handleData(req: http.IncomingMessage, res: http.ServerResponse) {
77
+ try {
78
+ const body = await readBody(req);
79
+ const dataReq: DataRequest = JSON.parse(body);
80
+
81
+ // Match the route to find the page
82
+ const match = matchRoute(dataReq.path, routes);
83
+
84
+ if (!match) {
85
+ res.writeHead(200);
86
+ res.end(JSON.stringify({ props: {}, hasServerProps: false }));
87
+ return;
88
+ }
89
+
90
+ // Find the route definition to look up in serverPropsMap
91
+ const routeDef = routes.find(r => r.component === match.component);
92
+ const getServerProps = routeDef ? serverPropsMap[routeDef.path] : undefined;
93
+
94
+ if (!getServerProps) {
95
+ // This page doesn't export getServerProps
96
+ res.writeHead(200);
97
+ res.end(JSON.stringify({ props: {}, hasServerProps: false }));
98
+ return;
99
+ }
100
+
101
+ // Build the BlumenContext
102
+ const ctx = {
103
+ params: { ...(dataReq.params || {}), ...match.params },
104
+ query: dataReq.query || {},
105
+ path: dataReq.path,
106
+ headers: dataReq.headers || {},
107
+ };
108
+
109
+ // Run getServerProps
110
+ const result = await getServerProps(ctx);
111
+
112
+ const dataResp: DataResponse = {
113
+ props: result?.props || {},
114
+ hasServerProps: true,
115
+ revalidate: result?.revalidate || 0,
116
+ };
117
+
118
+ res.writeHead(200);
119
+ res.end(JSON.stringify(dataResp));
120
+ } catch (error) {
121
+ console.error("getServerProps Error:", error);
122
+ res.writeHead(500);
123
+ res.end(
124
+ JSON.stringify({
125
+ error: "getServerProps failed",
126
+ message:
127
+ process.env.NODE_ENV === "development" ? String(error) : undefined,
128
+ }),
129
+ );
130
+ }
131
+ }
132
+
133
+ /**
134
+ * POST /render - Render the matched page to HTML.
135
+ *
136
+ * This is the existing SSR endpoint. It receives props (possibly
137
+ * enriched by /data) and renders the React component tree to HTML.
138
+ */
139
+ async function handleRender(
140
+ req: http.IncomingMessage,
141
+ res: http.ServerResponse,
142
+ ) {
45
143
  try {
46
144
  // Parse request body
47
145
  const body = await readBody(req);
@@ -57,7 +155,11 @@ const server = http.createServer(async (req, res) => {
57
155
  status: 404,
58
156
  serverRendered: true,
59
157
  };
60
- const element = React.createElement(NotFound, props);
158
+ const element = React.createElement(
159
+ "div",
160
+ { className: "page-transition page-transition-active" },
161
+ React.createElement(NotFound, props),
162
+ );
61
163
  const html = renderToString(element);
62
164
 
63
165
  res.writeHead(200);
@@ -76,10 +178,16 @@ const server = http.createServer(async (req, res) => {
76
178
  };
77
179
 
78
180
  // Render React to HTML
79
- const appElement = React.createElement(App, {
80
- Component: match.component,
81
- pageProps: props,
82
- });
181
+ // Wrap in the same page-transition div that the client-side
182
+ // RouterProvider renders, so hydration trees match exactly.
183
+ const appElement = React.createElement(
184
+ "div",
185
+ { className: "page-transition page-transition-active" },
186
+ React.createElement(App, {
187
+ Component: match.component,
188
+ pageProps: props,
189
+ }),
190
+ );
83
191
 
84
192
  const documentElement = React.createElement(Document, {
85
193
  initialProps: props,
@@ -106,7 +214,7 @@ const server = http.createServer(async (req, res) => {
106
214
  }),
107
215
  );
108
216
  }
109
- });
217
+ }
110
218
 
111
219
  function readBody(req: http.IncomingMessage): Promise<string> {
112
220
  return new Promise((resolve, reject) => {
@@ -123,6 +231,10 @@ function readBody(req: http.IncomingMessage): Promise<string> {
123
231
 
124
232
  server.listen(PORT, () => {
125
233
  console.log(`Node SSR server running on http://localhost:${PORT}`);
234
+ const gspCount = Object.keys(serverPropsMap).length;
235
+ if (gspCount > 0) {
236
+ console.log(` ${gspCount} page(s) with getServerProps detected`);
237
+ }
126
238
  });
127
239
 
128
240
  // Handle graceful shutdown
@@ -40,6 +40,8 @@ interface RouteEntry {
40
40
  patternStr: string;
41
41
  /** Extracted parameter keys */
42
42
  keys: string[];
43
+ /** Whether this page exports a getServerProps function */
44
+ hasGetServerProps: boolean;
43
45
  }
44
46
 
45
47
  function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
@@ -87,12 +89,17 @@ function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
87
89
  patternStr = `^${patternStr}$`;
88
90
  }
89
91
 
92
+ // Check if the page exports getServerProps
93
+ const fileContent = fs.readFileSync(fullPath, "utf-8");
94
+ const hasGetServerProps = /export\s+(async\s+)?function\s+getServerProps/.test(fileContent);
95
+
90
96
  results.push({
91
97
  route: routePath,
92
98
  componentId,
93
99
  importPath: relPath.replace(".tsx", "").replace(/\\/g, "/"),
94
100
  patternStr,
95
- keys
101
+ keys,
102
+ hasGetServerProps
96
103
  });
97
104
  }
98
105
  }
@@ -139,6 +146,18 @@ function generateRouteFile(
139
146
  `import ${r.componentId} from "${importPathPrefix}/${r.importPath}";`,
140
147
  );
141
148
 
149
+ // For SSR: import getServerProps from pages that export it
150
+ const gspImports: string[] = [];
151
+ const gspRoutes = routes.filter(r => r.hasGetServerProps);
152
+ if (isServer && gspRoutes.length > 0) {
153
+ for (const r of gspRoutes) {
154
+ const gspId = "gsp_" + r.componentId.replace("Page_", "");
155
+ gspImports.push(
156
+ `import { getServerProps as ${gspId} } from "${importPathPrefix}/${r.importPath}";`,
157
+ );
158
+ }
159
+ }
160
+
142
161
  const hasApp = fs.existsSync(path.join(PAGES_DIR, "_app.tsx"));
143
162
  const hasDoc = fs.existsSync(path.join(PAGES_DIR, "_document.tsx"));
144
163
 
@@ -163,6 +182,25 @@ function generateRouteFile(
163
182
  \t}`;
164
183
  });
165
184
 
185
+ // Generate serverPropsMap for SSR routes
186
+ let serverPropsMapStr = "";
187
+ if (isServer && gspRoutes.length > 0) {
188
+ const entries = gspRoutes.map(r => {
189
+ const gspId = "gsp_" + r.componentId.replace("Page_", "");
190
+ return `\t"${r.route}": ${gspId},`;
191
+ });
192
+ serverPropsMapStr = [
193
+ "",
194
+ "// Map of routes that export getServerProps",
195
+ "// Used by the SSR server to run data fetching before rendering",
196
+ "export const serverPropsMap: Record<string, Function> = {",
197
+ ...entries,
198
+ "};",
199
+ ].join("\n");
200
+ } else if (isServer) {
201
+ serverPropsMapStr = "\nexport const serverPropsMap: Record<string, Function> = {};";
202
+ }
203
+
166
204
  const map = [
167
205
  "",
168
206
  "export interface RouteDef {",
@@ -178,10 +216,11 @@ function generateRouteFile(
178
216
  "",
179
217
  appImport,
180
218
  docImport,
219
+ serverPropsMapStr,
181
220
  "",
182
221
  ];
183
222
 
184
- return [...header, ...imports, ...map].join("\n");
223
+ return [...header, ...imports, ...gspImports, ...map].join("\n");
185
224
  }
186
225
 
187
226
  function main() {
@@ -197,7 +236,8 @@ function main() {
197
236
 
198
237
  console.log(` Found ${routes.length} route(s):`);
199
238
  for (const r of routes) {
200
- console.log(` ${r.route.padEnd(25)} ${r.importPath}.tsx`);
239
+ const gspTag = r.hasGetServerProps ? " [getServerProps]" : "";
240
+ console.log(` ${r.route.padEnd(25)} → ${r.importPath}.tsx${gspTag}`);
201
241
  }
202
242
 
203
243
  // Generate SSR routes (node-ssr/ → ../app/pages)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blumenjs",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "The React framework powered by Go. Lightning-fast SSR with the DX you deserve.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,9 +26,9 @@
26
26
  "dev:legacy": "npm run routes && concurrently \"npm run dev:client\" \"npm run dev:ssr\" \"npm run dev:go\"",
27
27
  "dev:client": "webpack serve --mode development",
28
28
  "dev:ssr": "NODE_ENV=development tsx watch node-ssr/server.ts",
29
- "dev:go": "go run go-server/main.go",
29
+ "dev:go": "go run go-server/main.go go-server/image.go go-server/cache.go",
30
30
  "build:client": "webpack --mode production",
31
- "build:ssr": "esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --external:react --external:react-dom",
31
+ "build:ssr": "esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external",
32
32
  "clean": "rm -rf dist static/js/bundle.js"
33
33
  },
34
34
  "keywords": [
@@ -64,12 +64,16 @@
64
64
  "@types/react-dom": "^18.2.0",
65
65
  "babel-loader": "^10.1.1",
66
66
  "concurrently": "^8.2.2",
67
+ "css-loader": "^7.1.4",
67
68
  "esbuild": "^0.19.0",
69
+ "mini-css-extract-plugin": "^2.10.2",
68
70
  "react-refresh": "^0.18.0",
71
+ "style-loader": "^4.0.0",
69
72
  "ts-loader": "^9.5.1",
70
73
  "tsx": "^4.6.0",
71
74
  "typescript": "^5.3.0",
72
75
  "webpack": "^5.89.0",
76
+ "webpack-bundle-analyzer": "^5.3.0",
73
77
  "webpack-cli": "^5.1.4",
74
78
  "webpack-dev-server": "^5.2.3"
75
79
  },