blumenjs 0.1.0
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/README.md +127 -0
- package/dist/cli/blumen.js +697 -0
- package/dist/cli/commands/build.js +85 -0
- package/dist/cli/commands/create.js +384 -0
- package/dist/cli/commands/dev.js +163 -0
- package/dist/cli/commands/start.js +129 -0
- package/dist/cli/utils.js +85 -0
- package/dist/templates/app/client/entry.tsx +41 -0
- package/dist/templates/app/pages/BlumenStarter.tsx +398 -0
- package/dist/templates/app/pages/NotFound.tsx +22 -0
- package/dist/templates/app/shared/DefaultApp.tsx +5 -0
- package/dist/templates/app/shared/DefaultDocument.tsx +76 -0
- package/dist/templates/app/shared/Link.tsx +73 -0
- package/dist/templates/app/shared/RouterContext.tsx +176 -0
- package/dist/templates/app/shared/router.ts +23 -0
- package/dist/templates/go-server/main.go +175 -0
- package/dist/templates/node-ssr/server.ts +141 -0
- package/dist/templates/scripts/generate-routes.ts +220 -0
- package/package.json +77 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blumen Route Generator
|
|
3
|
+
*
|
|
4
|
+
* Scans app/pages/ recursively for .tsx files and auto-generates:
|
|
5
|
+
* 1. node-ssr/generated-routes.ts — SSR route map
|
|
6
|
+
* 2. app/client/generated-routes.ts — Client hydration route map
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Nested folders (e.g. pages/dashboard/settings.tsx -> /dashboard/settings)
|
|
10
|
+
* - Dynamic routes (e.g. pages/users/[id].tsx -> /users/:id)
|
|
11
|
+
* - Default exports (any component name allowed internally)
|
|
12
|
+
*
|
|
13
|
+
* Convention:
|
|
14
|
+
* - Home.tsx → "/" (index route)
|
|
15
|
+
* - index.tsx → "/" (index route for folders)
|
|
16
|
+
* - NotFound.tsx → skipped (reserved 404)
|
|
17
|
+
* - _app.tsx → skipped (reserved prefix)
|
|
18
|
+
*
|
|
19
|
+
* Usage: npx tsx scripts/generate-routes.ts
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as fs from "fs";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
|
|
25
|
+
const PAGES_DIR = path.resolve("app/pages");
|
|
26
|
+
const SSR_OUTPUT = path.resolve("node-ssr/generated-routes.ts");
|
|
27
|
+
const CLIENT_OUTPUT = path.resolve("app/client/generated-routes.ts");
|
|
28
|
+
|
|
29
|
+
// Reserved filenames that should not become routes
|
|
30
|
+
const RESERVED = new Set(["NotFound", "NotFound.tsx"]);
|
|
31
|
+
|
|
32
|
+
interface RouteEntry {
|
|
33
|
+
/** The URL path, e.g. "/" or "/users/[id]" */
|
|
34
|
+
route: string;
|
|
35
|
+
/** The unique component identifier for imports, e.g. "Page_users_id" */
|
|
36
|
+
componentId: string;
|
|
37
|
+
/** The relative file path without extension, e.g. "users/[id]" */
|
|
38
|
+
importPath: string;
|
|
39
|
+
/** Regex string for matching the route */
|
|
40
|
+
patternStr: string;
|
|
41
|
+
/** Extracted parameter keys */
|
|
42
|
+
keys: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scanDir(dir: string, baseDir: string = dir): RouteEntry[] {
|
|
46
|
+
let results: RouteEntry[] = [];
|
|
47
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const fullPath = path.join(dir, item.name);
|
|
51
|
+
const relPath = path.relative(baseDir, fullPath);
|
|
52
|
+
|
|
53
|
+
if (item.isDirectory()) {
|
|
54
|
+
results = results.concat(scanDir(fullPath, baseDir));
|
|
55
|
+
} else if (item.isFile() && item.name.endsWith(".tsx")) {
|
|
56
|
+
const name = item.name;
|
|
57
|
+
const nameNoExt = name.replace(".tsx", "");
|
|
58
|
+
|
|
59
|
+
// Skip reserved and underscore-prefixed files/folders
|
|
60
|
+
if (name.startsWith("_") || RESERVED.has(name) || relPath.includes("/_")) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Generate the URL route (lowercased for friendly URLs)
|
|
65
|
+
let routePath = "/" + relPath.replace(".tsx", "").replace(/\\/g, "/").toLowerCase();
|
|
66
|
+
|
|
67
|
+
// Handle index files
|
|
68
|
+
if (nameNoExt.toLowerCase() === "home" && routePath === "/home") {
|
|
69
|
+
routePath = "/";
|
|
70
|
+
} else if (routePath.endsWith("/index")) {
|
|
71
|
+
routePath = routePath.replace("/index", "") || "/";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generate a safe identifier for import
|
|
75
|
+
const componentId = "Page_" + relPath.replace(".tsx", "").replace(/[^a-zA-Z0-9]/g, "_");
|
|
76
|
+
|
|
77
|
+
// Parse bracket syntax for dynamic parameters
|
|
78
|
+
const keys: string[] = [];
|
|
79
|
+
let patternStr = routePath.replace(/\[([^\]]+)\]/g, (_, key) => {
|
|
80
|
+
keys.push(key);
|
|
81
|
+
return "([^/]+)";
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (patternStr === "/") {
|
|
85
|
+
patternStr = "^\\/$";
|
|
86
|
+
} else {
|
|
87
|
+
patternStr = `^${patternStr}$`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
results.push({
|
|
91
|
+
route: routePath,
|
|
92
|
+
componentId,
|
|
93
|
+
importPath: relPath.replace(".tsx", "").replace(/\\/g, "/"),
|
|
94
|
+
patternStr,
|
|
95
|
+
keys
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function discoverPages(): RouteEntry[] {
|
|
104
|
+
const routes = scanDir(PAGES_DIR);
|
|
105
|
+
|
|
106
|
+
// Sort so specific routes come before dynamic ones, and "/" comes first
|
|
107
|
+
routes.sort((a, b) => {
|
|
108
|
+
if (a.route === "/") return -1;
|
|
109
|
+
if (b.route === "/") return 1;
|
|
110
|
+
// Push dynamic routes (with brackets) lower down the list
|
|
111
|
+
const aIsDynamic = a.route.includes("[");
|
|
112
|
+
const bIsDynamic = b.route.includes("[");
|
|
113
|
+
if (aIsDynamic && !bIsDynamic) return 1;
|
|
114
|
+
if (!aIsDynamic && bIsDynamic) return -1;
|
|
115
|
+
return a.route.localeCompare(b.route);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return routes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function generateRouteFile(
|
|
122
|
+
routes: RouteEntry[],
|
|
123
|
+
importPathPrefix: string,
|
|
124
|
+
isServer: boolean,
|
|
125
|
+
): string {
|
|
126
|
+
const header = [
|
|
127
|
+
"// ┌─────────────────────────────────────────────┐",
|
|
128
|
+
"// │ AUTO-GENERATED — DO NOT EDIT │",
|
|
129
|
+
"// │ Run `npm run routes` to regenerate │",
|
|
130
|
+
"// └─────────────────────────────────────────────┘",
|
|
131
|
+
"",
|
|
132
|
+
'import React from "react";',
|
|
133
|
+
"",
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Import default exports!
|
|
137
|
+
const imports = routes.map(
|
|
138
|
+
(r) =>
|
|
139
|
+
`import ${r.componentId} from "${importPathPrefix}/${r.importPath}";`,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const hasApp = fs.existsSync(path.join(PAGES_DIR, "_app.tsx"));
|
|
143
|
+
const hasDoc = fs.existsSync(path.join(PAGES_DIR, "_document.tsx"));
|
|
144
|
+
|
|
145
|
+
const appImport = hasApp
|
|
146
|
+
? `import CustomApp from "${importPathPrefix}/_app";\nexport const App = CustomApp;`
|
|
147
|
+
: `import { DefaultApp } from "${importPathPrefix}/../shared/DefaultApp";\nexport const App = DefaultApp;`;
|
|
148
|
+
|
|
149
|
+
let docImport = "";
|
|
150
|
+
if (isServer) {
|
|
151
|
+
docImport = hasDoc
|
|
152
|
+
? `import CustomDocument from "${importPathPrefix}/_document";\nexport const Document = CustomDocument;`
|
|
153
|
+
: `import { DefaultDocument } from "${importPathPrefix}/../shared/DefaultDocument";\nexport const Document = DefaultDocument;`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const routeObjects = routes.map(r => {
|
|
157
|
+
const keysArray = r.keys.length > 0 ? `["${r.keys.join('", "')}"]` : "[]";
|
|
158
|
+
return `\t{
|
|
159
|
+
\t\tpath: "${r.route}",
|
|
160
|
+
\t\tpattern: new RegExp("${r.patternStr.replace(/\\/g, '\\\\')}"),
|
|
161
|
+
\t\tkeys: ${keysArray},
|
|
162
|
+
\t\tcomponent: ${r.componentId}
|
|
163
|
+
\t}`;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const map = [
|
|
167
|
+
"",
|
|
168
|
+
"export interface RouteDef {",
|
|
169
|
+
"\tpath: string;",
|
|
170
|
+
"\tpattern: RegExp;",
|
|
171
|
+
"\tkeys: string[];",
|
|
172
|
+
"\tcomponent: React.ComponentType<any>;",
|
|
173
|
+
"}",
|
|
174
|
+
"",
|
|
175
|
+
"export const routes: RouteDef[] = [",
|
|
176
|
+
routeObjects.join(",\n"),
|
|
177
|
+
"];",
|
|
178
|
+
"",
|
|
179
|
+
appImport,
|
|
180
|
+
docImport,
|
|
181
|
+
"",
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
return [...header, ...imports, ...map].join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function main() {
|
|
188
|
+
console.log("🌸 Blumen Route Generator (Next.js Style)");
|
|
189
|
+
console.log(` Scanning: ${PAGES_DIR}`);
|
|
190
|
+
|
|
191
|
+
const routes = discoverPages();
|
|
192
|
+
|
|
193
|
+
if (routes.length === 0) {
|
|
194
|
+
console.error(" ❌ No pages found!");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(` Found ${routes.length} route(s):`);
|
|
199
|
+
for (const r of routes) {
|
|
200
|
+
console.log(` ${r.route.padEnd(25)} → ${r.importPath}.tsx`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Generate SSR routes (node-ssr/ → ../app/pages)
|
|
204
|
+
const ssrContent = generateRouteFile(routes, "../app/pages", true);
|
|
205
|
+
fs.mkdirSync(path.dirname(SSR_OUTPUT), { recursive: true });
|
|
206
|
+
fs.writeFileSync(SSR_OUTPUT, ssrContent, "utf-8");
|
|
207
|
+
console.log(` ✅ Written: ${path.relative(process.cwd(), SSR_OUTPUT)}`);
|
|
208
|
+
|
|
209
|
+
// Generate Client routes (app/client/ → ../pages)
|
|
210
|
+
const clientContent = generateRouteFile(routes, "../pages", false);
|
|
211
|
+
fs.mkdirSync(path.dirname(CLIENT_OUTPUT), { recursive: true });
|
|
212
|
+
fs.writeFileSync(CLIENT_OUTPUT, clientContent, "utf-8");
|
|
213
|
+
console.log(
|
|
214
|
+
` ✅ Written: ${path.relative(process.cwd(), CLIENT_OUTPUT)}`,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
console.log(" 🎉 Routes generated successfully!");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blumenjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The React framework powered by Go. Lightning-fast SSR with the DX you deserve.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"blumen": "./dist/cli/blumen.js",
|
|
8
|
+
"blumenjs": "./dist/cli/blumen.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/cli/**/*",
|
|
12
|
+
"dist/templates/**/*",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"routes": "tsx scripts/generate-routes.ts",
|
|
18
|
+
"dev": "tsx cli/blumen.ts dev",
|
|
19
|
+
"build": "tsx cli/blumen.ts build",
|
|
20
|
+
"start": "tsx cli/blumen.ts start",
|
|
21
|
+
"create": "tsx cli/blumen.ts create",
|
|
22
|
+
"build:cli": "tsx cli/build-cli.ts",
|
|
23
|
+
"prepublishOnly": "npm run build:cli",
|
|
24
|
+
"dev:legacy": "npm run routes && concurrently \"npm run dev:client\" \"npm run dev:ssr\" \"npm run dev:go\"",
|
|
25
|
+
"dev:client": "webpack serve --mode development",
|
|
26
|
+
"dev:ssr": "NODE_ENV=development tsx watch node-ssr/server.ts",
|
|
27
|
+
"dev:go": "go run go-server/main.go",
|
|
28
|
+
"build:client": "webpack --mode production",
|
|
29
|
+
"build:ssr": "esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --external:react --external:react-dom",
|
|
30
|
+
"clean": "rm -rf dist static/js/bundle.js"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"react",
|
|
34
|
+
"go",
|
|
35
|
+
"ssr",
|
|
36
|
+
"server-side-rendering",
|
|
37
|
+
"framework",
|
|
38
|
+
"blumen",
|
|
39
|
+
"hmr",
|
|
40
|
+
"cli"
|
|
41
|
+
],
|
|
42
|
+
"author": "",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": ""
|
|
47
|
+
},
|
|
48
|
+
"homepage": "",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"lucide-react": "^1.14.0",
|
|
51
|
+
"react": "^18.2.0",
|
|
52
|
+
"react-dom": "^18.2.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@babel/core": "^7.29.0",
|
|
56
|
+
"@babel/preset-env": "^7.29.5",
|
|
57
|
+
"@babel/preset-react": "^7.28.5",
|
|
58
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
59
|
+
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
|
60
|
+
"@types/node": "^20.10.0",
|
|
61
|
+
"@types/react": "^18.2.0",
|
|
62
|
+
"@types/react-dom": "^18.2.0",
|
|
63
|
+
"babel-loader": "^10.1.1",
|
|
64
|
+
"concurrently": "^8.2.2",
|
|
65
|
+
"esbuild": "^0.19.0",
|
|
66
|
+
"react-refresh": "^0.18.0",
|
|
67
|
+
"ts-loader": "^9.5.1",
|
|
68
|
+
"tsx": "^4.6.0",
|
|
69
|
+
"typescript": "^5.3.0",
|
|
70
|
+
"webpack": "^5.89.0",
|
|
71
|
+
"webpack-cli": "^5.1.4",
|
|
72
|
+
"webpack-dev-server": "^5.2.3"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18.0.0"
|
|
76
|
+
}
|
|
77
|
+
}
|