blumenjs 0.1.7 → 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 +270 -15
- package/dist/cli/commands/build.js +25 -6
- 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/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 +81 -18
- package/dist/templates/app/shared/router.ts +2 -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 +394 -39
- 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 +364 -8
- package/dist/templates/scripts/generate-routes.ts +355 -7
- package/package.json +12 -6
|
@@ -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]" */
|
|
@@ -40,6 +43,75 @@ interface RouteEntry {
|
|
|
40
43
|
patternStr: string;
|
|
41
44
|
/** Extracted parameter keys */
|
|
42
45
|
keys: string[];
|
|
46
|
+
/** Whether this page exports a getServerProps function */
|
|
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;
|
|
43
115
|
}
|
|
44
116
|
|
|
45
117
|
function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
|
|
@@ -87,16 +159,83 @@ function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
|
|
|
87
159
|
patternStr = `^${patternStr}$`;
|
|
88
160
|
}
|
|
89
161
|
|
|
162
|
+
// Check if the page exports getServerProps, metadata, or generateMetadata
|
|
163
|
+
const fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
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;
|
|
177
|
+
|
|
90
178
|
results.push({
|
|
91
179
|
route: routePath,
|
|
92
180
|
componentId,
|
|
93
181
|
importPath: relPath.replace(".tsx", "").replace(/\\/g, "/"),
|
|
94
182
|
patternStr,
|
|
95
|
-
keys
|
|
183
|
+
keys,
|
|
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,
|
|
96
225
|
});
|
|
97
226
|
}
|
|
98
227
|
}
|
|
99
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
|
+
|
|
100
239
|
return results;
|
|
101
240
|
}
|
|
102
241
|
|
|
@@ -120,6 +259,7 @@ function discoverPages(): RouteEntry[] {
|
|
|
120
259
|
|
|
121
260
|
function generateRouteFile(
|
|
122
261
|
routes: RouteEntry[],
|
|
262
|
+
layouts: LayoutEntry[],
|
|
123
263
|
importPathPrefix: string,
|
|
124
264
|
isServer: boolean,
|
|
125
265
|
): string {
|
|
@@ -139,6 +279,56 @@ function generateRouteFile(
|
|
|
139
279
|
`import ${r.componentId} from "${importPathPrefix}/${r.importPath}";`,
|
|
140
280
|
);
|
|
141
281
|
|
|
282
|
+
// For SSR: import getServerProps from pages that export it
|
|
283
|
+
const gspImports: string[] = [];
|
|
284
|
+
const gspRoutes = routes.filter(r => r.hasGetServerProps);
|
|
285
|
+
if (isServer && gspRoutes.length > 0) {
|
|
286
|
+
for (const r of gspRoutes) {
|
|
287
|
+
const gspId = "gsp_" + r.componentId.replace("Page_", "");
|
|
288
|
+
gspImports.push(
|
|
289
|
+
`import { getServerProps as ${gspId} } from "${importPathPrefix}/${r.importPath}";`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
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
|
+
|
|
142
332
|
const hasApp = fs.existsSync(path.join(PAGES_DIR, "_app.tsx"));
|
|
143
333
|
const hasDoc = fs.existsSync(path.join(PAGES_DIR, "_document.tsx"));
|
|
144
334
|
|
|
@@ -153,16 +343,123 @@ function generateRouteFile(
|
|
|
153
343
|
: `import { DefaultDocument } from "${importPathPrefix}/../shared/DefaultDocument";\nexport const Document = DefaultDocument;`;
|
|
154
344
|
}
|
|
155
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
|
+
|
|
156
358
|
const routeObjects = routes.map(r => {
|
|
157
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}` : "";
|
|
158
362
|
return `\t{
|
|
159
363
|
\t\tpath: "${r.route}",
|
|
160
364
|
\t\tpattern: new RegExp("${r.patternStr.replace(/\\/g, '\\\\')}"),
|
|
161
365
|
\t\tkeys: ${keysArray},
|
|
162
|
-
\t\tcomponent: ${r.componentId}
|
|
366
|
+
\t\tcomponent: ${r.componentId}${loadingLine}
|
|
163
367
|
\t}`;
|
|
164
368
|
});
|
|
165
369
|
|
|
370
|
+
// Generate serverPropsMap for SSR routes
|
|
371
|
+
let serverPropsMapStr = "";
|
|
372
|
+
if (isServer && gspRoutes.length > 0) {
|
|
373
|
+
const entries = gspRoutes.map(r => {
|
|
374
|
+
const gspId = "gsp_" + r.componentId.replace("Page_", "");
|
|
375
|
+
return `\t"${r.route}": ${gspId},`;
|
|
376
|
+
});
|
|
377
|
+
serverPropsMapStr = [
|
|
378
|
+
"",
|
|
379
|
+
"// Map of routes that export getServerProps",
|
|
380
|
+
"// Used by the SSR server to run data fetching before rendering",
|
|
381
|
+
"export const serverPropsMap: Record<string, Function> = {",
|
|
382
|
+
...entries,
|
|
383
|
+
"};",
|
|
384
|
+
].join("\n");
|
|
385
|
+
} else if (isServer) {
|
|
386
|
+
serverPropsMapStr = "\nexport const serverPropsMap: Record<string, Function> = {};";
|
|
387
|
+
}
|
|
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
|
+
|
|
166
463
|
const map = [
|
|
167
464
|
"",
|
|
168
465
|
"export interface RouteDef {",
|
|
@@ -170,6 +467,7 @@ function generateRouteFile(
|
|
|
170
467
|
"\tpattern: RegExp;",
|
|
171
468
|
"\tkeys: string[];",
|
|
172
469
|
"\tcomponent: React.ComponentType<any>;",
|
|
470
|
+
"\tloading?: React.ComponentType<any>;",
|
|
173
471
|
"}",
|
|
174
472
|
"",
|
|
175
473
|
"export const routes: RouteDef[] = [",
|
|
@@ -178,10 +476,12 @@ function generateRouteFile(
|
|
|
178
476
|
"",
|
|
179
477
|
appImport,
|
|
180
478
|
docImport,
|
|
479
|
+
serverPropsMapStr,
|
|
480
|
+
metadataMapStr,
|
|
181
481
|
"",
|
|
182
482
|
];
|
|
183
483
|
|
|
184
|
-
return [...header, ...imports, ...map].join("\n");
|
|
484
|
+
return [...header, ...imports, ...gspImports, ...metaImports, ...layoutImports, ...loadingImports, ...map].join("\n");
|
|
185
485
|
}
|
|
186
486
|
|
|
187
487
|
function main() {
|
|
@@ -189,31 +489,79 @@ function main() {
|
|
|
189
489
|
console.log(` Scanning: ${PAGES_DIR}`);
|
|
190
490
|
|
|
191
491
|
const routes = discoverPages();
|
|
492
|
+
const layouts = scanLayouts(PAGES_DIR);
|
|
192
493
|
|
|
193
494
|
if (routes.length === 0) {
|
|
194
495
|
console.error(" ❌ No pages found!");
|
|
195
496
|
process.exit(1);
|
|
196
497
|
}
|
|
197
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
|
+
|
|
198
511
|
console.log(` Found ${routes.length} route(s):`);
|
|
199
512
|
for (const r of routes) {
|
|
200
|
-
|
|
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}`);
|
|
201
521
|
}
|
|
202
522
|
|
|
203
523
|
// Generate SSR routes (node-ssr/ → ../app/pages)
|
|
204
|
-
const ssrContent = generateRouteFile(routes, "../app/pages", true);
|
|
524
|
+
const ssrContent = generateRouteFile(routes, layouts, "../app/pages", true);
|
|
205
525
|
fs.mkdirSync(path.dirname(SSR_OUTPUT), { recursive: true });
|
|
206
526
|
fs.writeFileSync(SSR_OUTPUT, ssrContent, "utf-8");
|
|
207
527
|
console.log(` ✅ Written: ${path.relative(process.cwd(), SSR_OUTPUT)}`);
|
|
208
528
|
|
|
209
529
|
// Generate Client routes (app/client/ → ../pages)
|
|
210
|
-
const clientContent = generateRouteFile(routes, "../pages", false);
|
|
530
|
+
const clientContent = generateRouteFile(routes, layouts, "../pages", false);
|
|
211
531
|
fs.mkdirSync(path.dirname(CLIENT_OUTPUT), { recursive: true });
|
|
212
532
|
fs.writeFileSync(CLIENT_OUTPUT, clientContent, "utf-8");
|
|
213
533
|
console.log(
|
|
214
534
|
` ✅ Written: ${path.relative(process.cwd(), CLIENT_OUTPUT)}`,
|
|
215
535
|
);
|
|
216
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
|
+
|
|
217
565
|
console.log(" 🎉 Routes generated successfully!");
|
|
218
566
|
}
|
|
219
567
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blumenjs",
|
|
3
|
-
"version": "0.1
|
|
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": {
|
|
@@ -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": [
|
|
@@ -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",
|
|
@@ -64,12 +66,16 @@
|
|
|
64
66
|
"@types/react-dom": "^18.2.0",
|
|
65
67
|
"babel-loader": "^10.1.1",
|
|
66
68
|
"concurrently": "^8.2.2",
|
|
69
|
+
"css-loader": "^7.1.4",
|
|
67
70
|
"esbuild": "^0.19.0",
|
|
71
|
+
"mini-css-extract-plugin": "^2.10.2",
|
|
68
72
|
"react-refresh": "^0.18.0",
|
|
73
|
+
"style-loader": "^4.0.0",
|
|
69
74
|
"ts-loader": "^9.5.1",
|
|
70
75
|
"tsx": "^4.6.0",
|
|
71
76
|
"typescript": "^5.3.0",
|
|
72
77
|
"webpack": "^5.89.0",
|
|
78
|
+
"webpack-bundle-analyzer": "^5.3.0",
|
|
73
79
|
"webpack-cli": "^5.1.4",
|
|
74
80
|
"webpack-dev-server": "^5.2.3"
|
|
75
81
|
},
|