blumenjs 0.2.4 → 0.2.6
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 +22 -0
- package/dist/cli/commands/create.js +22 -0
- package/dist/templates/app/shared/BlumenHead.tsx +157 -0
- package/dist/templates/app/shared/BlumenImage.tsx +209 -0
- package/dist/templates/app/shared/DefaultLoading.tsx +78 -0
- package/dist/templates/app/shared/ErrorBoundary.tsx +174 -0
- package/dist/templates/app/shared/api.ts +99 -0
- package/dist/templates/app/shared/blumenConfig.ts +179 -0
- package/dist/templates/app/shared/i18n.ts +281 -0
- package/dist/templates/app/shared/prefetchCache.ts +142 -0
- package/dist/templates/app/shared/serverAction.ts +84 -0
- package/dist/templates/app/shared/types.ts +132 -0
- package/dist/templates/app/shared/useServerAction.ts +173 -0
- package/dist/templates/app/shared/useWebSocket.ts +237 -0
- package/dist/templates/go.mod +8 -0
- package/dist/templates/go.sum +4 -0
- package/dist/templates/scripts/generate-api-routes.ts +236 -0
- package/package.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blumen API Route Generator
|
|
3
|
+
*
|
|
4
|
+
* Scans app/api/ recursively for route.ts files and generates:
|
|
5
|
+
* node-ssr/generated-api-routes.ts — API route map for the SSR server
|
|
6
|
+
*
|
|
7
|
+
* Convention:
|
|
8
|
+
* - app/api/users/route.ts → /api/users
|
|
9
|
+
* - app/api/users/[id]/route.ts → /api/users/[id]
|
|
10
|
+
* - Only `route.ts` files are scanned (not .tsx — API routes don't render)
|
|
11
|
+
* - Exported function names determine allowed HTTP methods:
|
|
12
|
+
* GET, POST, PUT, DELETE, PATCH
|
|
13
|
+
*
|
|
14
|
+
* Usage: npx tsx scripts/generate-api-routes.ts
|
|
15
|
+
* (Also called automatically by generate-routes.ts)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
import * as ts from "typescript";
|
|
21
|
+
|
|
22
|
+
const API_DIR = path.resolve("app/api");
|
|
23
|
+
const API_OUTPUT = path.resolve("node-ssr/generated-api-routes.ts");
|
|
24
|
+
|
|
25
|
+
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
|
|
26
|
+
|
|
27
|
+
interface APIRouteEntry {
|
|
28
|
+
/** The URL path, e.g. "/api/users" or "/api/users/[id]" */
|
|
29
|
+
route: string;
|
|
30
|
+
/** Safe identifier for imports */
|
|
31
|
+
routeId: string;
|
|
32
|
+
/** Import path relative to the api dir */
|
|
33
|
+
importPath: string;
|
|
34
|
+
/** Regex string for matching the route */
|
|
35
|
+
patternStr: string;
|
|
36
|
+
/** Extracted parameter keys */
|
|
37
|
+
keys: string[];
|
|
38
|
+
/** HTTP methods exported by this route */
|
|
39
|
+
methods: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a file exports a named identifier at the top level.
|
|
44
|
+
* Uses TypeScript AST to avoid false positives from strings/comments.
|
|
45
|
+
*/
|
|
46
|
+
function hasNamedExport(filePath: string, content: string, exportName: string): boolean {
|
|
47
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
48
|
+
let found = false;
|
|
49
|
+
|
|
50
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
51
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
52
|
+
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
53
|
+
if (isExported && node.name?.text === exportName) found = true;
|
|
54
|
+
} else if (ts.isVariableStatement(node)) {
|
|
55
|
+
const isExported = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
56
|
+
if (isExported) {
|
|
57
|
+
for (const decl of node.declarationList.declarations) {
|
|
58
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === exportName) found = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (ts.isExportDeclaration(node)) {
|
|
62
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
63
|
+
for (const element of node.exportClause.elements) {
|
|
64
|
+
if (element.name.text === exportName) found = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scanAPIDir(dir: string, baseDir: string = dir): APIRouteEntry[] {
|
|
74
|
+
if (!fs.existsSync(dir)) return [];
|
|
75
|
+
|
|
76
|
+
let results: APIRouteEntry[] = [];
|
|
77
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
78
|
+
|
|
79
|
+
for (const item of items) {
|
|
80
|
+
const fullPath = path.join(dir, item.name);
|
|
81
|
+
|
|
82
|
+
if (item.isDirectory()) {
|
|
83
|
+
results = results.concat(scanAPIDir(fullPath, baseDir));
|
|
84
|
+
} else if (item.isFile() && item.name === "route.ts") {
|
|
85
|
+
const relDir = path.relative(baseDir, dir).replace(/\\/g, "/");
|
|
86
|
+
const routePath = "/api" + (relDir ? "/" + relDir.toLowerCase() : "");
|
|
87
|
+
|
|
88
|
+
// Generate safe identifier
|
|
89
|
+
const routeId = "api_" + (relDir || "root").replace(/[^a-zA-Z0-9]/g, "_");
|
|
90
|
+
|
|
91
|
+
// Parse bracket syntax for dynamic parameters
|
|
92
|
+
const keys: string[] = [];
|
|
93
|
+
let patternStr = routePath.replace(/\[([^\]]+)\]/g, (_, key) => {
|
|
94
|
+
keys.push(key);
|
|
95
|
+
return "([^/]+)";
|
|
96
|
+
});
|
|
97
|
+
patternStr = `^${patternStr}$`;
|
|
98
|
+
|
|
99
|
+
// Detect exported HTTP method handlers
|
|
100
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
101
|
+
const methods: string[] = [];
|
|
102
|
+
for (const method of HTTP_METHODS) {
|
|
103
|
+
if (hasNamedExport(fullPath, content, method)) {
|
|
104
|
+
methods.push(method);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (methods.length === 0) {
|
|
109
|
+
console.warn(` ⚠️ ${routePath} — no HTTP method exports found, skipping`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const importPath = relDir ? relDir + "/route" : "route";
|
|
114
|
+
|
|
115
|
+
results.push({
|
|
116
|
+
route: routePath,
|
|
117
|
+
routeId,
|
|
118
|
+
importPath,
|
|
119
|
+
patternStr,
|
|
120
|
+
keys,
|
|
121
|
+
methods,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Sort: specific routes before dynamic ones
|
|
127
|
+
results.sort((a, b) => {
|
|
128
|
+
const aIsDynamic = a.route.includes("[");
|
|
129
|
+
const bIsDynamic = b.route.includes("[");
|
|
130
|
+
if (aIsDynamic && !bIsDynamic) return 1;
|
|
131
|
+
if (!aIsDynamic && bIsDynamic) return -1;
|
|
132
|
+
return a.route.localeCompare(b.route);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function generateAPIRouteFile(routes: APIRouteEntry[]): string {
|
|
139
|
+
const header = [
|
|
140
|
+
"// ┌─────────────────────────────────────────────┐",
|
|
141
|
+
"// │ AUTO-GENERATED — DO NOT EDIT │",
|
|
142
|
+
"// │ Run `npm run routes` to regenerate │",
|
|
143
|
+
"// └─────────────────────────────────────────────┘",
|
|
144
|
+
"",
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// Generate imports
|
|
148
|
+
const imports: string[] = [];
|
|
149
|
+
for (const r of routes) {
|
|
150
|
+
const namedImports = r.methods
|
|
151
|
+
.map(m => `${m} as ${m.toLowerCase()}_${r.routeId}`)
|
|
152
|
+
.join(", ");
|
|
153
|
+
imports.push(
|
|
154
|
+
`import { ${namedImports} } from "../app/api/${r.importPath}";`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Generate type and route array
|
|
159
|
+
const body = [
|
|
160
|
+
"",
|
|
161
|
+
"export interface APIRouteDef {",
|
|
162
|
+
"\tpath: string;",
|
|
163
|
+
"\tpattern: RegExp;",
|
|
164
|
+
"\tkeys: string[];",
|
|
165
|
+
"\thandlers: Record<string, Function>;",
|
|
166
|
+
"}",
|
|
167
|
+
"",
|
|
168
|
+
"export const apiRoutes: APIRouteDef[] = [",
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
for (const r of routes) {
|
|
172
|
+
const handlersObj = r.methods
|
|
173
|
+
.map(m => `${m}: ${m.toLowerCase()}_${r.routeId}`)
|
|
174
|
+
.join(", ");
|
|
175
|
+
body.push(`\t{`);
|
|
176
|
+
body.push(`\t\tpath: "${r.route}",`);
|
|
177
|
+
body.push(`\t\tpattern: new RegExp("${r.patternStr.replace(/\\/g, "\\\\")}"),`);
|
|
178
|
+
body.push(`\t\tkeys: ${r.keys.length > 0 ? `["${r.keys.join('", "')}"]` : "[]"},`);
|
|
179
|
+
body.push(`\t\thandlers: { ${handlersObj} }`);
|
|
180
|
+
body.push(`\t},`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
body.push("];");
|
|
184
|
+
body.push("");
|
|
185
|
+
|
|
186
|
+
return [...header, ...imports, ...body].join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function generateAPIRoutes(): APIRouteEntry[] {
|
|
190
|
+
if (!fs.existsSync(API_DIR)) {
|
|
191
|
+
// No app/api/ directory — generate empty file
|
|
192
|
+
const emptyFile = [
|
|
193
|
+
"// No API routes found (app/api/ directory does not exist)",
|
|
194
|
+
"",
|
|
195
|
+
"export interface APIRouteDef {",
|
|
196
|
+
"\tpath: string;",
|
|
197
|
+
"\tpattern: RegExp;",
|
|
198
|
+
"\tkeys: string[];",
|
|
199
|
+
"\thandlers: Record<string, Function>;",
|
|
200
|
+
"}",
|
|
201
|
+
"",
|
|
202
|
+
"export const apiRoutes: APIRouteDef[] = [];",
|
|
203
|
+
"",
|
|
204
|
+
].join("\n");
|
|
205
|
+
fs.mkdirSync(path.dirname(API_OUTPUT), { recursive: true });
|
|
206
|
+
fs.writeFileSync(API_OUTPUT, emptyFile, "utf-8");
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const routes = scanAPIDir(API_DIR);
|
|
211
|
+
|
|
212
|
+
const content = generateAPIRouteFile(routes);
|
|
213
|
+
fs.mkdirSync(path.dirname(API_OUTPUT), { recursive: true });
|
|
214
|
+
fs.writeFileSync(API_OUTPUT, content, "utf-8");
|
|
215
|
+
|
|
216
|
+
return routes;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Allow standalone execution
|
|
220
|
+
if (process.argv[1]?.endsWith("generate-api-routes.ts")) {
|
|
221
|
+
console.log("🌸 Blumen API Route Generator");
|
|
222
|
+
console.log(` Scanning: ${API_DIR}`);
|
|
223
|
+
|
|
224
|
+
const routes = generateAPIRoutes();
|
|
225
|
+
|
|
226
|
+
if (routes.length === 0) {
|
|
227
|
+
console.log(" No API routes found.");
|
|
228
|
+
} else {
|
|
229
|
+
console.log(` Found ${routes.length} API route(s):`);
|
|
230
|
+
for (const r of routes) {
|
|
231
|
+
console.log(` ${r.methods.join("|").padEnd(20)} ${r.route}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(` ✅ Written: ${path.relative(process.cwd(), API_OUTPUT)}`);
|
|
236
|
+
}
|