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.
- package/dist/cli/blumen.js +258 -13
- package/dist/cli/commands/build.js +13 -4
- package/dist/cli/commands/create.js +1 -1
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/fonts.js +232 -0
- package/dist/cli/commands/start.js +1 -1
- package/dist/templates/app/client/entry.tsx +3 -1
- package/dist/templates/app/shared/RouterContext.tsx +4 -1
- package/dist/templates/go-server/cache.go +147 -0
- package/dist/templates/go-server/image.go +200 -0
- package/dist/templates/go-server/main.go +208 -40
- package/dist/templates/node-ssr/server.ts +120 -8
- package/dist/templates/scripts/generate-routes.ts +43 -3
- package/package.json +7 -3
|
@@ -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"
|
|
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(
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
},
|