blumenjs 0.2.1 → 0.2.3
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 +875 -62
- package/dist/cli/commands/audit.js +204 -0
- package/dist/cli/commands/bench.js +227 -0
- package/dist/cli/commands/build.js +47 -6
- package/dist/cli/commands/export.js +241 -0
- package/dist/cli/commands/migrate.js +267 -0
- package/dist/cli/commands/test.js +118 -0
- package/dist/templates/app/client/entry.tsx +5 -1
- package/dist/templates/app/shared/DefaultDocument.tsx +19 -5
- package/dist/templates/app/shared/RouterContext.tsx +4 -1
- package/dist/templates/go-server/actions.go +147 -0
- package/dist/templates/go-server/main.go +107 -4
- package/dist/templates/go-server/middleware.go +1 -1
- package/dist/templates/go-server/redirects.go +203 -0
- package/dist/templates/go-server/ssg.go +230 -0
- package/dist/templates/node-ssr/server.ts +222 -2
- package/dist/templates/scripts/generate-routes.ts +141 -9
- package/package.json +16 -4
|
@@ -55,6 +55,14 @@ interface RouteEntry {
|
|
|
55
55
|
loadingImportPath?: string;
|
|
56
56
|
/** Whether this page exports dynamic = 'force-static' for SSG */
|
|
57
57
|
isStatic: boolean;
|
|
58
|
+
/** Whether this page exports a getStaticProps function */
|
|
59
|
+
hasGetStaticProps: boolean;
|
|
60
|
+
/** Whether this page exports a getStaticPaths function */
|
|
61
|
+
hasGetStaticPaths: boolean;
|
|
62
|
+
/** Whether this page starts with 'use client' directive */
|
|
63
|
+
isClientComponent: boolean;
|
|
64
|
+
/** Whether this page exports runtime = 'edge' */
|
|
65
|
+
isEdge: boolean;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
interface LayoutEntry {
|
|
@@ -162,10 +170,20 @@ function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
|
|
|
162
170
|
// Check if the page exports getServerProps, metadata, or generateMetadata
|
|
163
171
|
const fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
164
172
|
const hasGetServerProps = hasNamedExport(fullPath, fileContent, "getServerProps");
|
|
173
|
+
const hasGetStaticProps = hasNamedExport(fullPath, fileContent, "getStaticProps");
|
|
174
|
+
const hasGetStaticPaths = hasNamedExport(fullPath, fileContent, "getStaticPaths");
|
|
165
175
|
const hasMetadata = hasNamedExport(fullPath, fileContent, "metadata");
|
|
166
176
|
const hasGenerateMetadata = hasNamedExport(fullPath, fileContent, "generateMetadata");
|
|
167
177
|
const hasDynamic = hasNamedExport(fullPath, fileContent, "dynamic");
|
|
168
|
-
|
|
178
|
+
// Pages with getStaticProps are automatically static
|
|
179
|
+
const isStatic = hasGetStaticProps || (hasDynamic && fileContent.includes("force-static"));
|
|
180
|
+
|
|
181
|
+
// Detect 'use client' directive at the top of the file
|
|
182
|
+
const isClientComponent = /^\s*['"]use client['"]/.test(fileContent);
|
|
183
|
+
|
|
184
|
+
// Detect runtime = 'edge' for edge functions
|
|
185
|
+
const hasRuntime = hasNamedExport(fullPath, fileContent, "runtime");
|
|
186
|
+
const isEdge = hasRuntime && fileContent.includes("edge");
|
|
169
187
|
|
|
170
188
|
// Check for a loading.tsx file in the same directory
|
|
171
189
|
const pageDir = path.dirname(fullPath);
|
|
@@ -187,6 +205,10 @@ function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
|
|
|
187
205
|
hasLoading,
|
|
188
206
|
loadingImportPath,
|
|
189
207
|
isStatic,
|
|
208
|
+
hasGetStaticProps,
|
|
209
|
+
hasGetStaticPaths,
|
|
210
|
+
isClientComponent,
|
|
211
|
+
isEdge,
|
|
190
212
|
});
|
|
191
213
|
}
|
|
192
214
|
}
|
|
@@ -257,6 +279,18 @@ function discoverPages(): RouteEntry[] {
|
|
|
257
279
|
return routes;
|
|
258
280
|
}
|
|
259
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Generate a webpack chunk name for a route entry.
|
|
284
|
+
* Produces clean names like "page-home", "page-docs", "page-users-id".
|
|
285
|
+
*/
|
|
286
|
+
function getChunkName(r: RouteEntry): string {
|
|
287
|
+
return "page-" + r.importPath
|
|
288
|
+
.replace(/\/index$/, "") // "docs/index" → "docs"
|
|
289
|
+
.replace(/\[([^\]]+)\]/g, "$1") // "[id]" → "id"
|
|
290
|
+
.replace(/\//g, "-") // "dashboard/settings" → "dashboard-settings"
|
|
291
|
+
.toLowerCase();
|
|
292
|
+
}
|
|
293
|
+
|
|
260
294
|
function generateRouteFile(
|
|
261
295
|
routes: RouteEntry[],
|
|
262
296
|
layouts: LayoutEntry[],
|
|
@@ -273,11 +307,18 @@ function generateRouteFile(
|
|
|
273
307
|
"",
|
|
274
308
|
];
|
|
275
309
|
|
|
276
|
-
// Import
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
310
|
+
// Import page components
|
|
311
|
+
// Server: static imports (SSR needs all components loaded)
|
|
312
|
+
// Client: React.lazy() dynamic imports for code splitting
|
|
313
|
+
const imports = isServer
|
|
314
|
+
? routes.map(
|
|
315
|
+
(r) =>
|
|
316
|
+
`import ${r.componentId} from "${importPathPrefix}/${r.importPath}";`,
|
|
317
|
+
)
|
|
318
|
+
: routes.map((r) => {
|
|
319
|
+
const chunkName = getChunkName(r);
|
|
320
|
+
return `const ${r.componentId} = React.lazy(() => import(/* webpackChunkName: "${chunkName}" */ "${importPathPrefix}/${r.importPath}"));`;
|
|
321
|
+
});
|
|
281
322
|
|
|
282
323
|
// For SSR: import getServerProps from pages that export it
|
|
283
324
|
const gspImports: string[] = [];
|
|
@@ -291,6 +332,31 @@ function generateRouteFile(
|
|
|
291
332
|
}
|
|
292
333
|
}
|
|
293
334
|
|
|
335
|
+
// For SSR: import getStaticProps from pages that export it
|
|
336
|
+
// getStaticProps works like getServerProps but the page is pre-rendered at build time
|
|
337
|
+
const staticPropsImports: string[] = [];
|
|
338
|
+
const staticPropsRoutes = routes.filter(r => r.hasGetStaticProps);
|
|
339
|
+
if (isServer && staticPropsRoutes.length > 0) {
|
|
340
|
+
for (const r of staticPropsRoutes) {
|
|
341
|
+
const spId = "staticProps_" + r.componentId.replace("Page_", "");
|
|
342
|
+
staticPropsImports.push(
|
|
343
|
+
`import { getStaticProps as ${spId} } from "${importPathPrefix}/${r.importPath}";`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// For SSR: import getStaticPaths from pages that export it (dynamic SSG)
|
|
349
|
+
const staticPathsImports: string[] = [];
|
|
350
|
+
const staticPathsRoutes = routes.filter(r => r.hasGetStaticPaths);
|
|
351
|
+
if (isServer && staticPathsRoutes.length > 0) {
|
|
352
|
+
for (const r of staticPathsRoutes) {
|
|
353
|
+
const pathsId = "staticPaths_" + r.componentId.replace("Page_", "");
|
|
354
|
+
staticPathsImports.push(
|
|
355
|
+
`import { getStaticPaths as ${pathsId} } from "${importPathPrefix}/${r.importPath}";`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
294
360
|
// For SSR: import metadata and generateMetadata from pages that export them
|
|
295
361
|
const metaImports: string[] = [];
|
|
296
362
|
const metaRoutes = routes.filter(r => r.hasMetadata);
|
|
@@ -369,23 +435,66 @@ function generateRouteFile(
|
|
|
369
435
|
|
|
370
436
|
// Generate serverPropsMap for SSR routes
|
|
371
437
|
let serverPropsMapStr = "";
|
|
372
|
-
if (isServer && gspRoutes.length > 0) {
|
|
438
|
+
if (isServer && (gspRoutes.length > 0 || staticPropsRoutes.length > 0)) {
|
|
439
|
+
// Merge getServerProps + getStaticProps into one map
|
|
440
|
+
// getStaticProps pages are treated identically at render time
|
|
373
441
|
const entries = gspRoutes.map(r => {
|
|
374
442
|
const gspId = "gsp_" + r.componentId.replace("Page_", "");
|
|
375
443
|
return `\t"${r.route}": ${gspId},`;
|
|
376
444
|
});
|
|
445
|
+
const staticEntries = staticPropsRoutes.map(r => {
|
|
446
|
+
const spId = "staticProps_" + r.componentId.replace("Page_", "");
|
|
447
|
+
return `\t"${r.route}": ${spId},`;
|
|
448
|
+
});
|
|
377
449
|
serverPropsMapStr = [
|
|
378
450
|
"",
|
|
379
|
-
"// Map of routes that export getServerProps",
|
|
451
|
+
"// Map of routes that export getServerProps or getStaticProps",
|
|
380
452
|
"// Used by the SSR server to run data fetching before rendering",
|
|
381
453
|
"export const serverPropsMap: Record<string, Function> = {",
|
|
382
454
|
...entries,
|
|
455
|
+
...staticEntries,
|
|
383
456
|
"};",
|
|
384
457
|
].join("\n");
|
|
385
458
|
} else if (isServer) {
|
|
386
459
|
serverPropsMapStr = "\nexport const serverPropsMap: Record<string, Function> = {};";
|
|
387
460
|
}
|
|
388
461
|
|
|
462
|
+
// Generate staticPropsMap (for ISR — tracks which pages use getStaticProps)
|
|
463
|
+
let staticMapsStr = "";
|
|
464
|
+
if (isServer) {
|
|
465
|
+
if (staticPropsRoutes.length > 0) {
|
|
466
|
+
const entries = staticPropsRoutes.map(r => {
|
|
467
|
+
const spId = "staticProps_" + r.componentId.replace("Page_", "");
|
|
468
|
+
return `\t"${r.route}": ${spId},`;
|
|
469
|
+
});
|
|
470
|
+
staticMapsStr += [
|
|
471
|
+
"",
|
|
472
|
+
"// Map of routes with getStaticProps (for ISR revalidation)",
|
|
473
|
+
"export const staticPropsMap: Record<string, Function> = {",
|
|
474
|
+
...entries,
|
|
475
|
+
"};",
|
|
476
|
+
].join("\n");
|
|
477
|
+
} else {
|
|
478
|
+
staticMapsStr += "\nexport const staticPropsMap: Record<string, Function> = {};";
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (staticPathsRoutes.length > 0) {
|
|
482
|
+
const entries = staticPathsRoutes.map(r => {
|
|
483
|
+
const pathsId = "staticPaths_" + r.componentId.replace("Page_", "");
|
|
484
|
+
return `\t"${r.route}": ${pathsId},`;
|
|
485
|
+
});
|
|
486
|
+
staticMapsStr += [
|
|
487
|
+
"",
|
|
488
|
+
"// Map of routes with getStaticPaths (for dynamic SSG)",
|
|
489
|
+
"export const staticPathsMap: Record<string, Function> = {",
|
|
490
|
+
...entries,
|
|
491
|
+
"};",
|
|
492
|
+
].join("\n");
|
|
493
|
+
} else {
|
|
494
|
+
staticMapsStr += "\nexport const staticPathsMap: Record<string, Function> = {};";
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
389
498
|
// Generate metadataMap and generateMetadataMap for SSR routes
|
|
390
499
|
let metadataMapStr = "";
|
|
391
500
|
if (isServer) {
|
|
@@ -460,6 +569,22 @@ function generateRouteFile(
|
|
|
460
569
|
}
|
|
461
570
|
}
|
|
462
571
|
|
|
572
|
+
// Generate route-to-chunk name map (client-side only, for prefetching)
|
|
573
|
+
let routeChunkMapStr = "";
|
|
574
|
+
if (!isServer) {
|
|
575
|
+
const entries = routes.map(r => {
|
|
576
|
+
const chunkName = getChunkName(r);
|
|
577
|
+
return `\t"${r.route}": "${chunkName}",`;
|
|
578
|
+
});
|
|
579
|
+
routeChunkMapStr = [
|
|
580
|
+
"",
|
|
581
|
+
"// Map of route paths to their webpack chunk names (for prefetching)",
|
|
582
|
+
"export const routeChunkMap: Record<string, string> = {",
|
|
583
|
+
...entries,
|
|
584
|
+
"};",
|
|
585
|
+
].join("\n");
|
|
586
|
+
}
|
|
587
|
+
|
|
463
588
|
const map = [
|
|
464
589
|
"",
|
|
465
590
|
"export interface RouteDef {",
|
|
@@ -477,11 +602,13 @@ function generateRouteFile(
|
|
|
477
602
|
appImport,
|
|
478
603
|
docImport,
|
|
479
604
|
serverPropsMapStr,
|
|
605
|
+
staticMapsStr,
|
|
480
606
|
metadataMapStr,
|
|
607
|
+
routeChunkMapStr,
|
|
481
608
|
"",
|
|
482
609
|
];
|
|
483
610
|
|
|
484
|
-
return [...header, ...imports, ...gspImports, ...metaImports, ...layoutImports, ...loadingImports, ...map].join("\n");
|
|
611
|
+
return [...header, ...imports, ...gspImports, ...staticPropsImports, ...staticPathsImports, ...metaImports, ...layoutImports, ...loadingImports, ...map].join("\n");
|
|
485
612
|
}
|
|
486
613
|
|
|
487
614
|
function main() {
|
|
@@ -512,10 +639,13 @@ function main() {
|
|
|
512
639
|
for (const r of routes) {
|
|
513
640
|
const tags: string[] = [];
|
|
514
641
|
if (r.hasGetServerProps) tags.push("getServerProps");
|
|
642
|
+
if (r.hasGetStaticProps) tags.push("getStaticProps");
|
|
515
643
|
if (r.hasMetadata) tags.push("metadata");
|
|
516
644
|
if (r.hasGenerateMetadata) tags.push("generateMetadata");
|
|
517
645
|
if (r.hasLoading) tags.push("loading");
|
|
518
646
|
if (r.isStatic) tags.push("ssg");
|
|
647
|
+
if (r.isClientComponent) tags.push("use client");
|
|
648
|
+
if (r.isEdge) tags.push("edge");
|
|
519
649
|
const tagStr = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
|
|
520
650
|
console.log(` ${r.route.padEnd(25)} → ${r.importPath}.tsx${tagStr}`);
|
|
521
651
|
}
|
|
@@ -555,6 +685,8 @@ function main() {
|
|
|
555
685
|
route: r.route,
|
|
556
686
|
componentId: r.componentId,
|
|
557
687
|
hasGetServerProps: r.hasGetServerProps,
|
|
688
|
+
hasGetStaticProps: r.hasGetStaticProps,
|
|
689
|
+
hasGetStaticPaths: r.hasGetStaticPaths,
|
|
558
690
|
}))
|
|
559
691
|
};
|
|
560
692
|
fs.mkdirSync("dist", { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blumenjs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "The React framework powered by Go. Lightning-fast SSR with the DX you deserve.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,10 @@
|
|
|
29
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
31
|
"build:ssr": "esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external",
|
|
32
|
-
"clean": "rm -rf dist static/js/bundle.js"
|
|
32
|
+
"clean": "rm -rf dist static/js/bundle.js",
|
|
33
|
+
"test": "tsx cli/blumen.ts test",
|
|
34
|
+
"test:coverage": "tsx cli/blumen.ts test --coverage",
|
|
35
|
+
"bench": "tsx cli/blumen.ts bench"
|
|
33
36
|
},
|
|
34
37
|
"keywords": [
|
|
35
38
|
"react",
|
|
@@ -61,19 +64,28 @@
|
|
|
61
64
|
"@babel/preset-react": "^7.28.5",
|
|
62
65
|
"@babel/preset-typescript": "^7.28.5",
|
|
63
66
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
|
67
|
+
"@testing-library/dom": "^10.4.1",
|
|
68
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
69
|
+
"@testing-library/react": "^16.3.2",
|
|
64
70
|
"@types/node": "^20.10.0",
|
|
65
|
-
"@types/react": "^18.
|
|
66
|
-
"@types/react-dom": "^18.
|
|
71
|
+
"@types/react": "^18.3.29",
|
|
72
|
+
"@types/react-dom": "^18.3.7",
|
|
73
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
74
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
67
75
|
"babel-loader": "^10.1.1",
|
|
68
76
|
"concurrently": "^8.2.2",
|
|
69
77
|
"css-loader": "^7.1.4",
|
|
70
78
|
"esbuild": "^0.19.0",
|
|
79
|
+
"jsdom": "^29.1.1",
|
|
71
80
|
"mini-css-extract-plugin": "^2.10.2",
|
|
81
|
+
"react": "^19.2.6",
|
|
82
|
+
"react-dom": "^19.2.6",
|
|
72
83
|
"react-refresh": "^0.18.0",
|
|
73
84
|
"style-loader": "^4.0.0",
|
|
74
85
|
"ts-loader": "^9.5.1",
|
|
75
86
|
"tsx": "^4.6.0",
|
|
76
87
|
"typescript": "^5.3.0",
|
|
88
|
+
"vitest": "^4.1.7",
|
|
77
89
|
"webpack": "^5.89.0",
|
|
78
90
|
"webpack-bundle-analyzer": "^5.3.0",
|
|
79
91
|
"webpack-cli": "^5.1.4",
|