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.
@@ -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
- const isStatic = hasDynamic && fileContent.includes("force-static");
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 default exports!
277
- const imports = routes.map(
278
- (r) =>
279
- `import ${r.componentId} from "${importPathPrefix}/${r.importPath}";`,
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.1",
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.2.0",
66
- "@types/react-dom": "^18.2.0",
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",