blumenjs 0.2.0 → 0.2.1
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 +15 -5
- package/dist/cli/commands/build.js +13 -3
- package/dist/cli/commands/dev.js +1 -1
- package/dist/cli/commands/start.js +1 -1
- package/dist/templates/app/pages/BlumenStarter.tsx +1 -1
- package/dist/templates/app/shared/DefaultDocument.tsx +49 -4
- package/dist/templates/app/shared/Link.tsx +60 -6
- package/dist/templates/app/shared/RouterContext.tsx +80 -20
- package/dist/templates/app/shared/router.ts +2 -1
- package/dist/templates/go-server/main.go +188 -1
- package/dist/templates/go-server/middleware.go +546 -0
- package/dist/templates/go-server/websocket.go +430 -0
- package/dist/templates/node-ssr/server.ts +246 -2
- package/dist/templates/scripts/generate-routes.ts +318 -10
- package/package.json +6 -4
|
@@ -15,19 +15,22 @@
|
|
|
15
15
|
* - index.tsx → "/" (index route for folders)
|
|
16
16
|
* - NotFound.tsx → skipped (reserved 404)
|
|
17
17
|
* - _app.tsx → skipped (reserved prefix)
|
|
18
|
+
* - _layout.tsx → layout metadata (not a route, metadata cascades to children)
|
|
18
19
|
*
|
|
19
20
|
* Usage: npx tsx scripts/generate-routes.ts
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
import * as fs from "fs";
|
|
23
24
|
import * as path from "path";
|
|
25
|
+
import * as ts from "typescript";
|
|
26
|
+
import { generateAPIRoutes } from "./generate-api-routes";
|
|
24
27
|
|
|
25
28
|
const PAGES_DIR = path.resolve("app/pages");
|
|
26
29
|
const SSR_OUTPUT = path.resolve("node-ssr/generated-routes.ts");
|
|
27
30
|
const CLIENT_OUTPUT = path.resolve("app/client/generated-routes.ts");
|
|
28
31
|
|
|
29
32
|
// Reserved filenames that should not become routes
|
|
30
|
-
const RESERVED = new Set(["NotFound", "NotFound.tsx"]);
|
|
33
|
+
const RESERVED = new Set(["NotFound", "NotFound.tsx", "loading.tsx"]);
|
|
31
34
|
|
|
32
35
|
interface RouteEntry {
|
|
33
36
|
/** The URL path, e.g. "/" or "/users/[id]" */
|
|
@@ -42,6 +45,73 @@ interface RouteEntry {
|
|
|
42
45
|
keys: string[];
|
|
43
46
|
/** Whether this page exports a getServerProps function */
|
|
44
47
|
hasGetServerProps: boolean;
|
|
48
|
+
/** Whether this page exports a static metadata object */
|
|
49
|
+
hasMetadata: boolean;
|
|
50
|
+
/** Whether this page exports a generateMetadata function */
|
|
51
|
+
hasGenerateMetadata: boolean;
|
|
52
|
+
/** Whether a loading.tsx file exists in the same directory */
|
|
53
|
+
hasLoading: boolean;
|
|
54
|
+
/** Import path for the loading component (if hasLoading) */
|
|
55
|
+
loadingImportPath?: string;
|
|
56
|
+
/** Whether this page exports dynamic = 'force-static' for SSG */
|
|
57
|
+
isStatic: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface LayoutEntry {
|
|
61
|
+
/** Directory path relative to PAGES_DIR, e.g. "" (root) or "docs" */
|
|
62
|
+
dirPath: string;
|
|
63
|
+
/** The URL prefix this layout applies to, e.g. "/" or "/docs" */
|
|
64
|
+
routePrefix: string;
|
|
65
|
+
/** Import path for the layout file (without extension) */
|
|
66
|
+
importPath: string;
|
|
67
|
+
/** Safe identifier for imports */
|
|
68
|
+
layoutId: string;
|
|
69
|
+
/** Whether this layout exports a static metadata object */
|
|
70
|
+
hasMetadata: boolean;
|
|
71
|
+
/** Whether this layout exports a generateMetadata function */
|
|
72
|
+
hasGenerateMetadata: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a file exports a named identifier at the top level.
|
|
77
|
+
* Uses TypeScript AST to avoid false positives from strings/comments.
|
|
78
|
+
*/
|
|
79
|
+
function hasNamedExport(filePath: string, content: string, exportName: string): boolean {
|
|
80
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
81
|
+
let found = false;
|
|
82
|
+
|
|
83
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
84
|
+
// 1. Function declaration: export [async] function name() {}
|
|
85
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
86
|
+
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
87
|
+
if (isExported && node.name?.text === exportName) {
|
|
88
|
+
found = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 2. Variable declaration: export const name = ...
|
|
92
|
+
else if (ts.isVariableStatement(node)) {
|
|
93
|
+
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
94
|
+
if (isExported) {
|
|
95
|
+
for (const decl of node.declarationList.declarations) {
|
|
96
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === exportName) {
|
|
97
|
+
found = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 3. Export declaration: export { name } or export { x as name }
|
|
103
|
+
else if (ts.isExportDeclaration(node)) {
|
|
104
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
105
|
+
for (const element of node.exportClause.elements) {
|
|
106
|
+
if (element.name.text === exportName) {
|
|
107
|
+
found = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return found;
|
|
45
115
|
}
|
|
46
116
|
|
|
47
117
|
function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
|
|
@@ -89,9 +159,21 @@ function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
|
|
|
89
159
|
patternStr = `^${patternStr}$`;
|
|
90
160
|
}
|
|
91
161
|
|
|
92
|
-
// Check if the page exports getServerProps
|
|
162
|
+
// Check if the page exports getServerProps, metadata, or generateMetadata
|
|
93
163
|
const fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
94
|
-
const hasGetServerProps =
|
|
164
|
+
const hasGetServerProps = hasNamedExport(fullPath, fileContent, "getServerProps");
|
|
165
|
+
const hasMetadata = hasNamedExport(fullPath, fileContent, "metadata");
|
|
166
|
+
const hasGenerateMetadata = hasNamedExport(fullPath, fileContent, "generateMetadata");
|
|
167
|
+
const hasDynamic = hasNamedExport(fullPath, fileContent, "dynamic");
|
|
168
|
+
const isStatic = hasDynamic && fileContent.includes("force-static");
|
|
169
|
+
|
|
170
|
+
// Check for a loading.tsx file in the same directory
|
|
171
|
+
const pageDir = path.dirname(fullPath);
|
|
172
|
+
const loadingFile = path.join(pageDir, "loading.tsx");
|
|
173
|
+
const hasLoading = fs.existsSync(loadingFile);
|
|
174
|
+
const loadingImportPath = hasLoading
|
|
175
|
+
? path.relative(baseDir, loadingFile).replace(".tsx", "").replace(/\\/g, "/")
|
|
176
|
+
: undefined;
|
|
95
177
|
|
|
96
178
|
results.push({
|
|
97
179
|
route: routePath,
|
|
@@ -99,11 +181,61 @@ function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
|
|
|
99
181
|
importPath: relPath.replace(".tsx", "").replace(/\\/g, "/"),
|
|
100
182
|
patternStr,
|
|
101
183
|
keys,
|
|
102
|
-
hasGetServerProps
|
|
184
|
+
hasGetServerProps,
|
|
185
|
+
hasMetadata,
|
|
186
|
+
hasGenerateMetadata,
|
|
187
|
+
hasLoading,
|
|
188
|
+
loadingImportPath,
|
|
189
|
+
isStatic,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Scan for _layout.tsx files in the pages directory tree.
|
|
199
|
+
* These files can export metadata/generateMetadata that cascades to child pages.
|
|
200
|
+
*/
|
|
201
|
+
function scanLayouts(dir: string, baseDir: string = dir): LayoutEntry[] {
|
|
202
|
+
let results: LayoutEntry[] = [];
|
|
203
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
204
|
+
|
|
205
|
+
// Check for _layout.tsx in this directory
|
|
206
|
+
const layoutFile = path.join(dir, "_layout.tsx");
|
|
207
|
+
if (fs.existsSync(layoutFile)) {
|
|
208
|
+
const relDir = path.relative(baseDir, dir).replace(/\\/g, "/");
|
|
209
|
+
const routePrefix = relDir === "" ? "/" : "/" + relDir.toLowerCase();
|
|
210
|
+
const layoutId = relDir === "" ? "Layout_root" : "Layout_" + relDir.replace(/[^a-zA-Z0-9]/g, "_");
|
|
211
|
+
const importPath = relDir === "" ? "_layout" : relDir + "/_layout";
|
|
212
|
+
|
|
213
|
+
const content = fs.readFileSync(layoutFile, "utf-8");
|
|
214
|
+
const hasMetadata = hasNamedExport(layoutFile, content, "metadata");
|
|
215
|
+
const hasGenerateMetadata = hasNamedExport(layoutFile, content, "generateMetadata");
|
|
216
|
+
|
|
217
|
+
if (hasMetadata || hasGenerateMetadata) {
|
|
218
|
+
results.push({
|
|
219
|
+
dirPath: relDir,
|
|
220
|
+
routePrefix,
|
|
221
|
+
importPath,
|
|
222
|
+
layoutId,
|
|
223
|
+
hasMetadata,
|
|
224
|
+
hasGenerateMetadata,
|
|
103
225
|
});
|
|
104
226
|
}
|
|
105
227
|
}
|
|
106
228
|
|
|
229
|
+
// Recurse into subdirectories
|
|
230
|
+
for (const item of items) {
|
|
231
|
+
if (item.isDirectory() && !item.name.startsWith("_")) {
|
|
232
|
+
results = results.concat(scanLayouts(path.join(dir, item.name), baseDir));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Sort by depth (root first, then deeper segments)
|
|
237
|
+
results.sort((a, b) => a.dirPath.split("/").length - b.dirPath.split("/").length);
|
|
238
|
+
|
|
107
239
|
return results;
|
|
108
240
|
}
|
|
109
241
|
|
|
@@ -127,6 +259,7 @@ function discoverPages(): RouteEntry[] {
|
|
|
127
259
|
|
|
128
260
|
function generateRouteFile(
|
|
129
261
|
routes: RouteEntry[],
|
|
262
|
+
layouts: LayoutEntry[],
|
|
130
263
|
importPathPrefix: string,
|
|
131
264
|
isServer: boolean,
|
|
132
265
|
): string {
|
|
@@ -158,6 +291,44 @@ function generateRouteFile(
|
|
|
158
291
|
}
|
|
159
292
|
}
|
|
160
293
|
|
|
294
|
+
// For SSR: import metadata and generateMetadata from pages that export them
|
|
295
|
+
const metaImports: string[] = [];
|
|
296
|
+
const metaRoutes = routes.filter(r => r.hasMetadata);
|
|
297
|
+
const genMetaRoutes = routes.filter(r => r.hasGenerateMetadata);
|
|
298
|
+
if (isServer) {
|
|
299
|
+
for (const r of metaRoutes) {
|
|
300
|
+
const metaId = "meta_" + r.componentId.replace("Page_", "");
|
|
301
|
+
metaImports.push(
|
|
302
|
+
`import { metadata as ${metaId} } from "${importPathPrefix}/${r.importPath}";`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
for (const r of genMetaRoutes) {
|
|
306
|
+
const genMetaId = "genMeta_" + r.componentId.replace("Page_", "");
|
|
307
|
+
metaImports.push(
|
|
308
|
+
`import { generateMetadata as ${genMetaId} } from "${importPathPrefix}/${r.importPath}";`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// For SSR: import metadata and generateMetadata from layout files
|
|
314
|
+
const layoutImports: string[] = [];
|
|
315
|
+
if (isServer) {
|
|
316
|
+
for (const l of layouts) {
|
|
317
|
+
if (l.hasMetadata) {
|
|
318
|
+
const metaId = "layoutMeta_" + l.layoutId;
|
|
319
|
+
layoutImports.push(
|
|
320
|
+
`import { metadata as ${metaId} } from "${importPathPrefix}/${l.importPath}";`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (l.hasGenerateMetadata) {
|
|
324
|
+
const genMetaId = "layoutGenMeta_" + l.layoutId;
|
|
325
|
+
layoutImports.push(
|
|
326
|
+
`import { generateMetadata as ${genMetaId} } from "${importPathPrefix}/${l.importPath}";`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
161
332
|
const hasApp = fs.existsSync(path.join(PAGES_DIR, "_app.tsx"));
|
|
162
333
|
const hasDoc = fs.existsSync(path.join(PAGES_DIR, "_document.tsx"));
|
|
163
334
|
|
|
@@ -172,13 +343,27 @@ function generateRouteFile(
|
|
|
172
343
|
: `import { DefaultDocument } from "${importPathPrefix}/../shared/DefaultDocument";\nexport const Document = DefaultDocument;`;
|
|
173
344
|
}
|
|
174
345
|
|
|
346
|
+
// Import loading components (client-side only)
|
|
347
|
+
const loadingImports: string[] = [];
|
|
348
|
+
const loadingRoutes = routes.filter(r => r.hasLoading);
|
|
349
|
+
if (!isServer && loadingRoutes.length > 0) {
|
|
350
|
+
for (const r of loadingRoutes) {
|
|
351
|
+
const loadingId = "Loading_" + r.componentId.replace("Page_", "");
|
|
352
|
+
loadingImports.push(
|
|
353
|
+
`import ${loadingId} from "${importPathPrefix}/${r.loadingImportPath}";`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
175
358
|
const routeObjects = routes.map(r => {
|
|
176
359
|
const keysArray = r.keys.length > 0 ? `["${r.keys.join('", "')}"]` : "[]";
|
|
360
|
+
const loadingId = r.hasLoading ? "Loading_" + r.componentId.replace("Page_", "") : null;
|
|
361
|
+
const loadingLine = !isServer && loadingId ? `,\n\t\tloading: ${loadingId}` : "";
|
|
177
362
|
return `\t{
|
|
178
363
|
\t\tpath: "${r.route}",
|
|
179
364
|
\t\tpattern: new RegExp("${r.patternStr.replace(/\\/g, '\\\\')}"),
|
|
180
365
|
\t\tkeys: ${keysArray},
|
|
181
|
-
\t\tcomponent: ${r.componentId}
|
|
366
|
+
\t\tcomponent: ${r.componentId}${loadingLine}
|
|
182
367
|
\t}`;
|
|
183
368
|
});
|
|
184
369
|
|
|
@@ -201,6 +386,80 @@ function generateRouteFile(
|
|
|
201
386
|
serverPropsMapStr = "\nexport const serverPropsMap: Record<string, Function> = {};";
|
|
202
387
|
}
|
|
203
388
|
|
|
389
|
+
// Generate metadataMap and generateMetadataMap for SSR routes
|
|
390
|
+
let metadataMapStr = "";
|
|
391
|
+
if (isServer) {
|
|
392
|
+
// Static metadata map
|
|
393
|
+
if (metaRoutes.length > 0) {
|
|
394
|
+
const entries = metaRoutes.map(r => {
|
|
395
|
+
const metaId = "meta_" + r.componentId.replace("Page_", "");
|
|
396
|
+
return `\t"${r.route}": ${metaId},`;
|
|
397
|
+
});
|
|
398
|
+
metadataMapStr += [
|
|
399
|
+
"",
|
|
400
|
+
"// Map of routes with static metadata exports",
|
|
401
|
+
"export const metadataMap: Record<string, any> = {",
|
|
402
|
+
...entries,
|
|
403
|
+
"};",
|
|
404
|
+
].join("\n");
|
|
405
|
+
} else {
|
|
406
|
+
metadataMapStr += "\nexport const metadataMap: Record<string, any> = {};";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Dynamic generateMetadata map
|
|
410
|
+
if (genMetaRoutes.length > 0) {
|
|
411
|
+
const entries = genMetaRoutes.map(r => {
|
|
412
|
+
const genMetaId = "genMeta_" + r.componentId.replace("Page_", "");
|
|
413
|
+
return `\t"${r.route}": ${genMetaId},`;
|
|
414
|
+
});
|
|
415
|
+
metadataMapStr += [
|
|
416
|
+
"",
|
|
417
|
+
"// Map of routes with dynamic generateMetadata exports",
|
|
418
|
+
"export const generateMetadataMap: Record<string, Function> = {",
|
|
419
|
+
...entries,
|
|
420
|
+
"};",
|
|
421
|
+
].join("\n");
|
|
422
|
+
} else {
|
|
423
|
+
metadataMapStr += "\nexport const generateMetadataMap: Record<string, Function> = {};";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Layout static metadata map
|
|
427
|
+
const layoutMetaEntries = layouts.filter(l => l.hasMetadata);
|
|
428
|
+
if (layoutMetaEntries.length > 0) {
|
|
429
|
+
const entries = layoutMetaEntries.map(l => {
|
|
430
|
+
const metaId = "layoutMeta_" + l.layoutId;
|
|
431
|
+
return `\t"${l.routePrefix}": ${metaId},`;
|
|
432
|
+
});
|
|
433
|
+
metadataMapStr += [
|
|
434
|
+
"",
|
|
435
|
+
"// Layout metadata — cascades to all child pages in the directory",
|
|
436
|
+
"export const layoutMetadataMap: Record<string, any> = {",
|
|
437
|
+
...entries,
|
|
438
|
+
"};",
|
|
439
|
+
].join("\n");
|
|
440
|
+
} else {
|
|
441
|
+
metadataMapStr += "\nexport const layoutMetadataMap: Record<string, any> = {};";
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Layout dynamic generateMetadata map
|
|
445
|
+
const layoutGenMetaEntries = layouts.filter(l => l.hasGenerateMetadata);
|
|
446
|
+
if (layoutGenMetaEntries.length > 0) {
|
|
447
|
+
const entries = layoutGenMetaEntries.map(l => {
|
|
448
|
+
const genMetaId = "layoutGenMeta_" + l.layoutId;
|
|
449
|
+
return `\t"${l.routePrefix}": ${genMetaId},`;
|
|
450
|
+
});
|
|
451
|
+
metadataMapStr += [
|
|
452
|
+
"",
|
|
453
|
+
"// Layout dynamic metadata — cascades to all child pages",
|
|
454
|
+
"export const layoutGenerateMetadataMap: Record<string, Function> = {",
|
|
455
|
+
...entries,
|
|
456
|
+
"};",
|
|
457
|
+
].join("\n");
|
|
458
|
+
} else {
|
|
459
|
+
metadataMapStr += "\nexport const layoutGenerateMetadataMap: Record<string, Function> = {};";
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
204
463
|
const map = [
|
|
205
464
|
"",
|
|
206
465
|
"export interface RouteDef {",
|
|
@@ -208,6 +467,7 @@ function generateRouteFile(
|
|
|
208
467
|
"\tpattern: RegExp;",
|
|
209
468
|
"\tkeys: string[];",
|
|
210
469
|
"\tcomponent: React.ComponentType<any>;",
|
|
470
|
+
"\tloading?: React.ComponentType<any>;",
|
|
211
471
|
"}",
|
|
212
472
|
"",
|
|
213
473
|
"export const routes: RouteDef[] = [",
|
|
@@ -217,10 +477,11 @@ function generateRouteFile(
|
|
|
217
477
|
appImport,
|
|
218
478
|
docImport,
|
|
219
479
|
serverPropsMapStr,
|
|
480
|
+
metadataMapStr,
|
|
220
481
|
"",
|
|
221
482
|
];
|
|
222
483
|
|
|
223
|
-
return [...header, ...imports, ...gspImports, ...map].join("\n");
|
|
484
|
+
return [...header, ...imports, ...gspImports, ...metaImports, ...layoutImports, ...loadingImports, ...map].join("\n");
|
|
224
485
|
}
|
|
225
486
|
|
|
226
487
|
function main() {
|
|
@@ -228,32 +489,79 @@ function main() {
|
|
|
228
489
|
console.log(` Scanning: ${PAGES_DIR}`);
|
|
229
490
|
|
|
230
491
|
const routes = discoverPages();
|
|
492
|
+
const layouts = scanLayouts(PAGES_DIR);
|
|
231
493
|
|
|
232
494
|
if (routes.length === 0) {
|
|
233
495
|
console.error(" ❌ No pages found!");
|
|
234
496
|
process.exit(1);
|
|
235
497
|
}
|
|
236
498
|
|
|
499
|
+
// Print detected layouts
|
|
500
|
+
if (layouts.length > 0) {
|
|
501
|
+
console.log(` Found ${layouts.length} layout(s) with metadata:`);
|
|
502
|
+
for (const l of layouts) {
|
|
503
|
+
const tags: string[] = [];
|
|
504
|
+
if (l.hasMetadata) tags.push("metadata");
|
|
505
|
+
if (l.hasGenerateMetadata) tags.push("generateMetadata");
|
|
506
|
+
const scope = l.routePrefix === "/" ? "(all pages)" : `(${l.routePrefix}/*)` ;
|
|
507
|
+
console.log(` 📐 ${l.importPath}.tsx [${tags.join(", ")}] ${scope}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
237
511
|
console.log(` Found ${routes.length} route(s):`);
|
|
238
512
|
for (const r of routes) {
|
|
239
|
-
const
|
|
240
|
-
|
|
513
|
+
const tags: string[] = [];
|
|
514
|
+
if (r.hasGetServerProps) tags.push("getServerProps");
|
|
515
|
+
if (r.hasMetadata) tags.push("metadata");
|
|
516
|
+
if (r.hasGenerateMetadata) tags.push("generateMetadata");
|
|
517
|
+
if (r.hasLoading) tags.push("loading");
|
|
518
|
+
if (r.isStatic) tags.push("ssg");
|
|
519
|
+
const tagStr = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
|
|
520
|
+
console.log(` ${r.route.padEnd(25)} → ${r.importPath}.tsx${tagStr}`);
|
|
241
521
|
}
|
|
242
522
|
|
|
243
523
|
// Generate SSR routes (node-ssr/ → ../app/pages)
|
|
244
|
-
const ssrContent = generateRouteFile(routes, "../app/pages", true);
|
|
524
|
+
const ssrContent = generateRouteFile(routes, layouts, "../app/pages", true);
|
|
245
525
|
fs.mkdirSync(path.dirname(SSR_OUTPUT), { recursive: true });
|
|
246
526
|
fs.writeFileSync(SSR_OUTPUT, ssrContent, "utf-8");
|
|
247
527
|
console.log(` ✅ Written: ${path.relative(process.cwd(), SSR_OUTPUT)}`);
|
|
248
528
|
|
|
249
529
|
// Generate Client routes (app/client/ → ../pages)
|
|
250
|
-
const clientContent = generateRouteFile(routes, "../pages", false);
|
|
530
|
+
const clientContent = generateRouteFile(routes, layouts, "../pages", false);
|
|
251
531
|
fs.mkdirSync(path.dirname(CLIENT_OUTPUT), { recursive: true });
|
|
252
532
|
fs.writeFileSync(CLIENT_OUTPUT, clientContent, "utf-8");
|
|
253
533
|
console.log(
|
|
254
534
|
` ✅ Written: ${path.relative(process.cwd(), CLIENT_OUTPUT)}`,
|
|
255
535
|
);
|
|
256
536
|
|
|
537
|
+
// Generate API routes (app/api/ → node-ssr/generated-api-routes.ts)
|
|
538
|
+
console.log(" → Scanning API routes...");
|
|
539
|
+
const apiRoutes = generateAPIRoutes();
|
|
540
|
+
if (apiRoutes.length > 0) {
|
|
541
|
+
console.log(` Found ${apiRoutes.length} API route(s):`);
|
|
542
|
+
for (const r of apiRoutes) {
|
|
543
|
+
console.log(` ${r.methods.join("|").padEnd(20)} ${r.route}`);
|
|
544
|
+
}
|
|
545
|
+
console.log(` ✅ Written: node-ssr/generated-api-routes.ts`);
|
|
546
|
+
} else {
|
|
547
|
+
console.log(" No API routes found (app/api/ is empty or missing)");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Write SSG manifest for the build step
|
|
551
|
+
const ssgRoutes = routes.filter(r => r.isStatic);
|
|
552
|
+
if (ssgRoutes.length > 0) {
|
|
553
|
+
const ssgManifest = {
|
|
554
|
+
pages: ssgRoutes.map(r => ({
|
|
555
|
+
route: r.route,
|
|
556
|
+
componentId: r.componentId,
|
|
557
|
+
hasGetServerProps: r.hasGetServerProps,
|
|
558
|
+
}))
|
|
559
|
+
};
|
|
560
|
+
fs.mkdirSync("dist", { recursive: true });
|
|
561
|
+
fs.writeFileSync("dist/ssg-manifest.json", JSON.stringify(ssgManifest, null, 2));
|
|
562
|
+
console.log(` 📄 SSG: ${ssgRoutes.length} page(s) will be pre-rendered at build time`);
|
|
563
|
+
}
|
|
564
|
+
|
|
257
565
|
console.log(" 🎉 Routes generated successfully!");
|
|
258
566
|
}
|
|
259
567
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blumenjs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The React framework powered by Go. Lightning-fast SSR with the DX you deserve.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,9 +49,11 @@
|
|
|
49
49
|
},
|
|
50
50
|
"homepage": "",
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"lucide-react": "^1.14.0"
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
"lucide-react": "^1.14.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
56
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
|
57
59
|
"@babel/core": "^7.29.0",
|