@rainfw/core 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/LICENSE +21 -0
- package/cli/commands/db.js +399 -0
- package/cli/index.js +206 -0
- package/cli/templates/drizzle-config.js +15 -0
- package/cli/templates/schema.js +16 -0
- package/cli/utils/process.js +23 -0
- package/cli/utils/sanitize.d.ts +1 -0
- package/cli/utils/sanitize.js +8 -0
- package/cli/utils/toml-parser.d.ts +10 -0
- package/cli/utils/toml-parser.js +58 -0
- package/dist/bindings.d.ts +3 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +17 -0
- package/dist/bindings.js.map +1 -0
- package/dist/context.d.ts +53 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +246 -0
- package/dist/context.js.map +1 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +68 -0
- package/dist/cookie.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx/createElement.d.ts +4 -0
- package/dist/jsx/createElement.d.ts.map +1 -0
- package/dist/jsx/createElement.js +19 -0
- package/dist/jsx/createElement.js.map +1 -0
- package/dist/jsx/escape.d.ts +2 -0
- package/dist/jsx/escape.d.ts.map +1 -0
- package/dist/jsx/escape.js +12 -0
- package/dist/jsx/escape.js.map +1 -0
- package/dist/jsx/index.d.ts +6 -0
- package/dist/jsx/index.d.ts.map +1 -0
- package/dist/jsx/index.js +5 -0
- package/dist/jsx/index.js.map +1 -0
- package/dist/jsx/jsx-namespace.d.ts +23 -0
- package/dist/jsx/jsx-namespace.d.ts.map +1 -0
- package/dist/jsx/jsx-namespace.js +2 -0
- package/dist/jsx/jsx-namespace.js.map +1 -0
- package/dist/jsx/jsx-runtime.d.ts +6 -0
- package/dist/jsx/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime.js +20 -0
- package/dist/jsx/jsx-runtime.js.map +1 -0
- package/dist/jsx/render.d.ts +4 -0
- package/dist/jsx/render.d.ts.map +1 -0
- package/dist/jsx/render.js +134 -0
- package/dist/jsx/render.js.map +1 -0
- package/dist/jsx/types.d.ts +11 -0
- package/dist/jsx/types.d.ts.map +1 -0
- package/dist/jsx/types.js +2 -0
- package/dist/jsx/types.js.map +1 -0
- package/dist/jsx-runtime.d.ts +23 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/middleware/cors.d.ts +12 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +75 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/session.d.ts +24 -0
- package/dist/middleware/session.d.ts.map +1 -0
- package/dist/middleware/session.js +124 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/package.json +1 -0
- package/dist/renderers/react.d.ts +9 -0
- package/dist/renderers/react.d.ts.map +1 -0
- package/dist/renderers/react.js +40 -0
- package/dist/renderers/react.js.map +1 -0
- package/dist/router.d.ts +36 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +363 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/regexp.d.ts +2 -0
- package/dist/utils/regexp.d.ts.map +1 -0
- package/dist/utils/regexp.js +4 -0
- package/dist/utils/regexp.js.map +1 -0
- package/dist/utils/url.d.ts +2 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +9 -0
- package/dist/utils/url.js.map +1 -0
- package/package.json +81 -0
- package/scripts/dev.js +58 -0
- package/scripts/generate.js +660 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { execSync } = require("node:child_process");
|
|
4
|
+
const ts = require("typescript");
|
|
5
|
+
|
|
6
|
+
const PROJECT_ROOT = process.cwd();
|
|
7
|
+
|
|
8
|
+
function unwrapExpression(node) {
|
|
9
|
+
if (ts.isSatisfiesExpression(node)) return node.expression;
|
|
10
|
+
if (ts.isAsExpression(node)) return node.expression;
|
|
11
|
+
if (ts.isParenthesizedExpression(node)) return node.expression;
|
|
12
|
+
return node;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractStringProps(obj, sourceFile, keys) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const prop of obj.properties) {
|
|
18
|
+
if (
|
|
19
|
+
!(ts.isPropertyAssignment(prop) && ts.isStringLiteral(prop.initializer))
|
|
20
|
+
)
|
|
21
|
+
continue;
|
|
22
|
+
const name = prop.name.getText(sourceFile);
|
|
23
|
+
if (keys.includes(name)) result[name] = prop.initializer.text;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadBuildConfig() {
|
|
29
|
+
const configPath = path.join(PROJECT_ROOT, "rain.config.ts");
|
|
30
|
+
const defaults = { routesDir: "src/routes", outDir: ".rainjs" };
|
|
31
|
+
if (!fs.existsSync(configPath)) return defaults;
|
|
32
|
+
|
|
33
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
34
|
+
const sourceFile = ts.createSourceFile(
|
|
35
|
+
"rain.config.ts",
|
|
36
|
+
content,
|
|
37
|
+
ts.ScriptTarget.Latest,
|
|
38
|
+
true,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const config = { ...defaults };
|
|
42
|
+
|
|
43
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
44
|
+
if (!ts.isExportAssignment(node)) return;
|
|
45
|
+
const obj = unwrapExpression(node.expression);
|
|
46
|
+
if (!ts.isObjectLiteralExpression(obj)) return;
|
|
47
|
+
Object.assign(
|
|
48
|
+
config,
|
|
49
|
+
extractStringProps(obj, sourceFile, ["routesDir", "outDir"]),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const BUILD_CONFIG = loadBuildConfig();
|
|
57
|
+
const ROUTES_DIR = path.join(PROJECT_ROOT, BUILD_CONFIG.routesDir);
|
|
58
|
+
const ENTRY_FILE = path.join(PROJECT_ROOT, BUILD_CONFIG.outDir, "entry.ts");
|
|
59
|
+
const CONFIG_FILE = path.join(PROJECT_ROOT, "rain.config.ts");
|
|
60
|
+
|
|
61
|
+
function relativeImportPath(targetPath) {
|
|
62
|
+
const entryDir = path.dirname(ENTRY_FILE);
|
|
63
|
+
let rel = path.relative(entryDir, targetPath).replace(/\\/g, "/");
|
|
64
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
65
|
+
return rel;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const HTTP_METHODS = [
|
|
69
|
+
"GET",
|
|
70
|
+
"POST",
|
|
71
|
+
"PUT",
|
|
72
|
+
"DELETE",
|
|
73
|
+
"PATCH",
|
|
74
|
+
"HEAD",
|
|
75
|
+
"OPTIONS",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function getRouteFiles(dir, base = "") {
|
|
79
|
+
if (!fs.existsSync(dir)) return [];
|
|
80
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
81
|
+
const files = [];
|
|
82
|
+
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const fullPath = path.join(dir, entry.name);
|
|
85
|
+
const relativePath = path.join(base, entry.name);
|
|
86
|
+
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
files.push(...getRouteFiles(fullPath, relativePath));
|
|
89
|
+
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
90
|
+
files.push(relativePath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getMiddlewareFiles(dir, base = "") {
|
|
98
|
+
if (!fs.existsSync(dir)) return [];
|
|
99
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
100
|
+
const files = [];
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const fullPath = path.join(dir, entry.name);
|
|
104
|
+
const relativePath = path.join(base, entry.name);
|
|
105
|
+
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
files.push(...getMiddlewareFiles(fullPath, relativePath));
|
|
108
|
+
} else if (entry.name === "_middleware.ts") {
|
|
109
|
+
files.push(relativePath);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return files;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getPageFiles(dir, base = "") {
|
|
117
|
+
if (!fs.existsSync(dir)) return [];
|
|
118
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
119
|
+
const files = [];
|
|
120
|
+
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const fullPath = path.join(dir, entry.name);
|
|
123
|
+
const relativePath = path.join(base, entry.name);
|
|
124
|
+
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
files.push(...getPageFiles(fullPath, relativePath));
|
|
127
|
+
} else if (entry.name === "page.ts" || entry.name === "page.tsx") {
|
|
128
|
+
files.push(relativePath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return files;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getLayoutFiles(dir, base = "") {
|
|
136
|
+
if (!fs.existsSync(dir)) return [];
|
|
137
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
138
|
+
const files = [];
|
|
139
|
+
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const fullPath = path.join(dir, entry.name);
|
|
142
|
+
const relativePath = path.join(base, entry.name);
|
|
143
|
+
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
files.push(...getLayoutFiles(fullPath, relativePath));
|
|
146
|
+
} else if (entry.name === "layout.ts" || entry.name === "layout.tsx") {
|
|
147
|
+
files.push(relativePath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return files;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function middlewarePathToDir(filePath) {
|
|
155
|
+
return filePath
|
|
156
|
+
.replace(/\\/g, "/")
|
|
157
|
+
.replace(/_middleware\.ts$/, "")
|
|
158
|
+
.replace(/\/$/, "");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function routePathToDir(filePath) {
|
|
162
|
+
return filePath
|
|
163
|
+
.replace(/\\/g, "/")
|
|
164
|
+
.replace(/route\.tsx?$/, "")
|
|
165
|
+
.replace(/\/$/, "");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function middlewareImportName(filePath) {
|
|
169
|
+
const base = filePath
|
|
170
|
+
.replace(/_middleware\.ts$/, "")
|
|
171
|
+
.replace(/\\/g, "_")
|
|
172
|
+
.replace(/\//g, "_")
|
|
173
|
+
.replace(/\[/g, "$")
|
|
174
|
+
.replace(/\]/g, "")
|
|
175
|
+
.replace(/-/g, "_")
|
|
176
|
+
.replace(/_+$/, "");
|
|
177
|
+
return `mw_${base || "root"}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function pageFilePathToDir(filePath) {
|
|
181
|
+
return filePath
|
|
182
|
+
.replace(/\\/g, "/")
|
|
183
|
+
.replace(/page\.tsx?$/, "")
|
|
184
|
+
.replace(/\/$/, "");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function layoutPathToDir(filePath) {
|
|
188
|
+
return filePath
|
|
189
|
+
.replace(/\\/g, "/")
|
|
190
|
+
.replace(/layout\.tsx?$/, "")
|
|
191
|
+
.replace(/\/$/, "");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function pageFilePathToUrlPath(filePath) {
|
|
195
|
+
let urlPath = filePath.replace(/\.tsx?$/, "");
|
|
196
|
+
urlPath = urlPath.replace(/\\/g, "/");
|
|
197
|
+
urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
|
|
198
|
+
urlPath = urlPath.replace(/\/page$/, "");
|
|
199
|
+
if (urlPath === "page") urlPath = "";
|
|
200
|
+
return `/${urlPath}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function pageFilePathToImportName(filePath) {
|
|
204
|
+
return (
|
|
205
|
+
"page_" +
|
|
206
|
+
filePath
|
|
207
|
+
.replace(/\.tsx?$/, "")
|
|
208
|
+
.replace(/\\/g, "_")
|
|
209
|
+
.replace(/\//g, "_")
|
|
210
|
+
.replace(/\[/g, "$")
|
|
211
|
+
.replace(/\]/g, "")
|
|
212
|
+
.replace(/-/g, "_")
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function layoutImportName(filePath) {
|
|
217
|
+
const base = filePath
|
|
218
|
+
.replace(/layout\.tsx?$/, "")
|
|
219
|
+
.replace(/\\/g, "_")
|
|
220
|
+
.replace(/\//g, "_")
|
|
221
|
+
.replace(/\[/g, "$")
|
|
222
|
+
.replace(/\]/g, "")
|
|
223
|
+
.replace(/-/g, "_")
|
|
224
|
+
.replace(/_+$/, "");
|
|
225
|
+
return `layout_${base || "root"}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getLayoutsForPage(pageFile, layoutFiles) {
|
|
229
|
+
const pageDir = pageFilePathToDir(pageFile);
|
|
230
|
+
const applicable = [];
|
|
231
|
+
|
|
232
|
+
for (const lf of layoutFiles) {
|
|
233
|
+
const lfDir = layoutPathToDir(lf);
|
|
234
|
+
if (lfDir === "" || pageDir === lfDir || pageDir.startsWith(`${lfDir}/`)) {
|
|
235
|
+
applicable.push(lf);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return applicable;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function detectDefaultExportFromContent(content) {
|
|
243
|
+
const sourceFile = ts.createSourceFile(
|
|
244
|
+
"page.tsx",
|
|
245
|
+
content,
|
|
246
|
+
ts.ScriptTarget.Latest,
|
|
247
|
+
true,
|
|
248
|
+
);
|
|
249
|
+
let found = false;
|
|
250
|
+
|
|
251
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
252
|
+
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
253
|
+
found = true;
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
(ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) &&
|
|
257
|
+
hasExportKeyword(node)
|
|
258
|
+
) {
|
|
259
|
+
const modifiers = ts.canHaveModifiers(node)
|
|
260
|
+
? ts.getModifiers(node)
|
|
261
|
+
: undefined;
|
|
262
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
263
|
+
found = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return found;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function detectDefaultExport(filePath) {
|
|
272
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
273
|
+
return detectDefaultExportFromContent(content);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function validateNoPageRouteColocation(routeFiles, pageFiles) {
|
|
277
|
+
const routeDirs = new Set(routeFiles.map((f) => routePathToDir(f)));
|
|
278
|
+
const errors = [];
|
|
279
|
+
|
|
280
|
+
for (const pageFile of pageFiles) {
|
|
281
|
+
const pageDir = pageFilePathToDir(pageFile);
|
|
282
|
+
if (routeDirs.has(pageDir)) {
|
|
283
|
+
const relDir = pageDir || "(root)";
|
|
284
|
+
errors.push(
|
|
285
|
+
`[Rain] Error: page and route files cannot coexist in the same directory: ${relDir}\n` +
|
|
286
|
+
" → Use page.tsx for pages (returns JSX) or route.ts for API endpoints (returns Response), not both.\n" +
|
|
287
|
+
" → Move one of them to a different directory.",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return errors;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getMiddlewaresForRoute(routeFile, middlewareFiles) {
|
|
296
|
+
const isPage = /page\.tsx?$/.test(routeFile);
|
|
297
|
+
const routeDir = isPage
|
|
298
|
+
? pageFilePathToDir(routeFile)
|
|
299
|
+
: routePathToDir(routeFile);
|
|
300
|
+
const applicable = [];
|
|
301
|
+
|
|
302
|
+
for (const mwFile of middlewareFiles) {
|
|
303
|
+
const mwDir = middlewarePathToDir(mwFile);
|
|
304
|
+
if (
|
|
305
|
+
mwDir === "" ||
|
|
306
|
+
routeDir === mwDir ||
|
|
307
|
+
routeDir.startsWith(`${mwDir}/`)
|
|
308
|
+
) {
|
|
309
|
+
applicable.push(mwFile);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return applicable;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function filePathToUrlPath(filePath) {
|
|
317
|
+
let urlPath = filePath.replace(/\.tsx?$/, "");
|
|
318
|
+
urlPath = urlPath.replace(/\\/g, "/");
|
|
319
|
+
urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
|
|
320
|
+
urlPath = urlPath.replace(/\/route$/, "");
|
|
321
|
+
if (urlPath === "route") urlPath = "";
|
|
322
|
+
return `/${urlPath}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function filePathToImportName(filePath) {
|
|
326
|
+
return (
|
|
327
|
+
"route_" +
|
|
328
|
+
filePath
|
|
329
|
+
.replace(/\.tsx?$/, "")
|
|
330
|
+
.replace(/\\/g, "_")
|
|
331
|
+
.replace(/\//g, "_")
|
|
332
|
+
.replace(/\[/g, "$")
|
|
333
|
+
.replace(/\]/g, "")
|
|
334
|
+
.replace(/-/g, "_")
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function hasExportKeyword(node) {
|
|
339
|
+
const modifiers = ts.canHaveModifiers(node)
|
|
340
|
+
? ts.getModifiers(node)
|
|
341
|
+
: undefined;
|
|
342
|
+
return (
|
|
343
|
+
modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function collectFromExportedDeclaration(node, targets) {
|
|
348
|
+
if (ts.isVariableStatement(node)) {
|
|
349
|
+
return node.declarationList.declarations
|
|
350
|
+
.filter((d) => ts.isIdentifier(d.name) && targets.has(d.name.text))
|
|
351
|
+
.map((d) => d.name.text);
|
|
352
|
+
}
|
|
353
|
+
if (
|
|
354
|
+
ts.isFunctionDeclaration(node) &&
|
|
355
|
+
node.name &&
|
|
356
|
+
targets.has(node.name.text)
|
|
357
|
+
) {
|
|
358
|
+
return [node.name.text];
|
|
359
|
+
}
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function collectFromNamedExports(node, targets) {
|
|
364
|
+
if (
|
|
365
|
+
!(
|
|
366
|
+
ts.isExportDeclaration(node) &&
|
|
367
|
+
node.exportClause &&
|
|
368
|
+
ts.isNamedExports(node.exportClause)
|
|
369
|
+
)
|
|
370
|
+
) {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
return node.exportClause.elements
|
|
374
|
+
.filter((el) => targets.has(el.name.text))
|
|
375
|
+
.map((el) => el.name.text);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function detectExportedNamesFromContent(content, targetNames) {
|
|
379
|
+
const sourceFile = ts.createSourceFile(
|
|
380
|
+
"route.ts",
|
|
381
|
+
content,
|
|
382
|
+
ts.ScriptTarget.Latest,
|
|
383
|
+
true,
|
|
384
|
+
);
|
|
385
|
+
const targets = new Set(targetNames);
|
|
386
|
+
const found = [];
|
|
387
|
+
|
|
388
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
389
|
+
if (hasExportKeyword(node)) {
|
|
390
|
+
found.push(...collectFromExportedDeclaration(node, targets));
|
|
391
|
+
}
|
|
392
|
+
found.push(...collectFromNamedExports(node, targets));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return found;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function detectExportedMethodsFromContent(content) {
|
|
399
|
+
return detectExportedNamesFromContent(content, HTTP_METHODS);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function detectExportedMethods(filePath) {
|
|
403
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
404
|
+
return detectExportedMethodsFromContent(content);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function detectMiddlewareExportFromContent(content) {
|
|
408
|
+
return detectExportedNamesFromContent(content, ["onRequest"]).length > 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function detectMiddlewareExport(filePath) {
|
|
412
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
413
|
+
return detectMiddlewareExportFromContent(content);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function processMiddlewares(middlewareFiles, imports) {
|
|
417
|
+
for (const mwFile of middlewareFiles) {
|
|
418
|
+
const fullMwPath = path.join(ROUTES_DIR, mwFile);
|
|
419
|
+
const relMwPath = `${BUILD_CONFIG.routesDir}/${mwFile.replace(/\\/g, "/")}`;
|
|
420
|
+
if (!detectMiddlewareExport(fullMwPath)) {
|
|
421
|
+
console.error(
|
|
422
|
+
`[Rain] No "onRequest" export found in ${relMwPath}. ` +
|
|
423
|
+
'Middleware files must export "onRequest". Example:\n' +
|
|
424
|
+
" export const onRequest: Middleware = async (ctx, next) => { ... }",
|
|
425
|
+
);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const name = middlewareImportName(mwFile);
|
|
429
|
+
const importPath = relativeImportPath(
|
|
430
|
+
path.join(ROUTES_DIR, mwFile.replace(/\.tsx?$/, "")),
|
|
431
|
+
);
|
|
432
|
+
imports.push(`import { onRequest as ${name} } from "${importPath}";`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function processRoutes(files, middlewareFiles, imports, registrations) {
|
|
437
|
+
for (const file of files) {
|
|
438
|
+
const importName = filePathToImportName(file);
|
|
439
|
+
const urlPath = filePathToUrlPath(file);
|
|
440
|
+
const importPath = relativeImportPath(
|
|
441
|
+
path.join(ROUTES_DIR, file.replace(/\.tsx?$/, "")),
|
|
442
|
+
);
|
|
443
|
+
const fullPath = path.join(ROUTES_DIR, file);
|
|
444
|
+
|
|
445
|
+
const methods = detectExportedMethods(fullPath);
|
|
446
|
+
if (methods.length === 0) {
|
|
447
|
+
const relPath = `${BUILD_CONFIG.routesDir}/${file.replace(/\\/g, "/")}`;
|
|
448
|
+
console.error(
|
|
449
|
+
`[Rain] No exported handlers found in ${relPath}. ` +
|
|
450
|
+
"Add an exported handler, e.g.: export const GET: Handler = (ctx) => { ... }",
|
|
451
|
+
);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const importSpecifiers = methods
|
|
456
|
+
.map((m) => `${m} as ${importName}_${m}`)
|
|
457
|
+
.join(", ");
|
|
458
|
+
imports.push(`import { ${importSpecifiers} } from "${importPath}";`);
|
|
459
|
+
|
|
460
|
+
const applicableMw = getMiddlewaresForRoute(file, middlewareFiles);
|
|
461
|
+
const mwNames = applicableMw.map((mf) => middlewareImportName(mf));
|
|
462
|
+
const mwArrayStr = mwNames.length > 0 ? `, [${mwNames.join(", ")}]` : "";
|
|
463
|
+
|
|
464
|
+
for (const method of methods) {
|
|
465
|
+
const methodLower = method.toLowerCase();
|
|
466
|
+
registrations.push(
|
|
467
|
+
`app.${methodLower}("${urlPath}", ${importName}_${method}${mwArrayStr});`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function processLayouts(layoutFiles, imports) {
|
|
474
|
+
for (const lf of layoutFiles) {
|
|
475
|
+
const fullLfPath = path.join(ROUTES_DIR, lf);
|
|
476
|
+
const relLfPath = `${BUILD_CONFIG.routesDir}/${lf.replace(/\\/g, "/")}`;
|
|
477
|
+
if (!detectDefaultExport(fullLfPath)) {
|
|
478
|
+
console.error(
|
|
479
|
+
`[Rain] No default export found in ${relLfPath}. ` +
|
|
480
|
+
"Layout files must use default export. Example:\n" +
|
|
481
|
+
" export default function RootLayout(ctx, children) { ... }",
|
|
482
|
+
);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const name = layoutImportName(lf);
|
|
486
|
+
const importPath = relativeImportPath(
|
|
487
|
+
path.join(ROUTES_DIR, lf.replace(/\.tsx?$/, "")),
|
|
488
|
+
);
|
|
489
|
+
imports.push(`import ${name} from "${importPath}";`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function processPages(
|
|
494
|
+
pageFiles,
|
|
495
|
+
layoutFiles,
|
|
496
|
+
middlewareFiles,
|
|
497
|
+
hasRootLayout,
|
|
498
|
+
imports,
|
|
499
|
+
registrations,
|
|
500
|
+
) {
|
|
501
|
+
for (const pageFile of pageFiles) {
|
|
502
|
+
const fullPagePath = path.join(ROUTES_DIR, pageFile);
|
|
503
|
+
const relPagePath = `${BUILD_CONFIG.routesDir}/${pageFile.replace(/\\/g, "/")}`;
|
|
504
|
+
if (!detectDefaultExport(fullPagePath)) {
|
|
505
|
+
console.error(
|
|
506
|
+
`[Rain] No default export found in ${relPagePath}. ` +
|
|
507
|
+
"Page files must use default export. Example:\n" +
|
|
508
|
+
" export default function Page(ctx) { return <h1>Hello</h1>; }",
|
|
509
|
+
);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const importName = pageFilePathToImportName(pageFile);
|
|
513
|
+
const urlPath = pageFilePathToUrlPath(pageFile);
|
|
514
|
+
const importPath = relativeImportPath(
|
|
515
|
+
path.join(ROUTES_DIR, pageFile.replace(/\.tsx?$/, "")),
|
|
516
|
+
);
|
|
517
|
+
imports.push(`import ${importName} from "${importPath}";`);
|
|
518
|
+
|
|
519
|
+
const applicableLayouts = getLayoutsForPage(pageFile, layoutFiles);
|
|
520
|
+
const layoutNames = applicableLayouts.map((f) => layoutImportName(f));
|
|
521
|
+
const layoutArrayStr =
|
|
522
|
+
layoutNames.length > 0 ? `[${layoutNames.join(", ")}]` : "[]";
|
|
523
|
+
|
|
524
|
+
const applicableMw = getMiddlewaresForRoute(pageFile, middlewareFiles);
|
|
525
|
+
const mwNames = applicableMw.map((mf) => middlewareImportName(mf));
|
|
526
|
+
const mwArrayStr = mwNames.length > 0 ? `[${mwNames.join(", ")}]` : "[]";
|
|
527
|
+
|
|
528
|
+
const doctype = hasRootLayout ? "true" : "false";
|
|
529
|
+
|
|
530
|
+
registrations.push(
|
|
531
|
+
`app.page("${urlPath}", ${importName}, ${layoutArrayStr}, ${mwArrayStr}, ${doctype});`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function generate() {
|
|
537
|
+
if (!fs.existsSync(ROUTES_DIR)) {
|
|
538
|
+
console.error(
|
|
539
|
+
"[Rain] Error: src/routes/ directory not found.\n" +
|
|
540
|
+
` → Current directory: ${PROJECT_ROOT}\n` +
|
|
541
|
+
" → Ensure you are running this command from a Rain.js project root.\n" +
|
|
542
|
+
" → Or create the directory: mkdir -p src/routes",
|
|
543
|
+
);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
execSync("npx wrangler types", {
|
|
549
|
+
cwd: PROJECT_ROOT,
|
|
550
|
+
stdio: "pipe",
|
|
551
|
+
timeout: 30_000,
|
|
552
|
+
});
|
|
553
|
+
} catch (_wranglerTypesOptional) {
|
|
554
|
+
console.warn(
|
|
555
|
+
"[Rain] Warning: wrangler types failed.\n" +
|
|
556
|
+
' → This is optional but recommended. Run "npx wrangler types" manually to debug.',
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const dir = path.dirname(ENTRY_FILE);
|
|
561
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
562
|
+
|
|
563
|
+
const files = getRouteFiles(ROUTES_DIR);
|
|
564
|
+
const middlewareFiles = getMiddlewareFiles(ROUTES_DIR);
|
|
565
|
+
const pageFiles = getPageFiles(ROUTES_DIR);
|
|
566
|
+
const layoutFiles = getLayoutFiles(ROUTES_DIR);
|
|
567
|
+
|
|
568
|
+
const depthSortComparator = (a, b) =>
|
|
569
|
+
a.split(/[\\/]/).length - b.split(/[\\/]/).length;
|
|
570
|
+
layoutFiles.sort(depthSortComparator);
|
|
571
|
+
middlewareFiles.sort(depthSortComparator);
|
|
572
|
+
const imports = [];
|
|
573
|
+
const registrations = [];
|
|
574
|
+
|
|
575
|
+
const colocationErrors = validateNoPageRouteColocation(files, pageFiles);
|
|
576
|
+
for (const err of colocationErrors) {
|
|
577
|
+
console.error(err);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const hasRootLayout = layoutFiles.some((f) => layoutPathToDir(f) === "");
|
|
582
|
+
|
|
583
|
+
processMiddlewares(middlewareFiles, imports);
|
|
584
|
+
processRoutes(files, middlewareFiles, imports, registrations);
|
|
585
|
+
processLayouts(layoutFiles, imports);
|
|
586
|
+
processPages(
|
|
587
|
+
pageFiles,
|
|
588
|
+
layoutFiles,
|
|
589
|
+
middlewareFiles,
|
|
590
|
+
hasRootLayout,
|
|
591
|
+
imports,
|
|
592
|
+
registrations,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const hasConfig = fs.existsSync(CONFIG_FILE);
|
|
596
|
+
const frameworkPath = relativeImportPath(
|
|
597
|
+
path.join(PROJECT_ROOT, "src", "framework"),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const headerImports = [`import { Rain } from "${frameworkPath}";`];
|
|
601
|
+
if (hasConfig) {
|
|
602
|
+
const configPath = relativeImportPath(
|
|
603
|
+
path.join(PROJECT_ROOT, "rain.config"),
|
|
604
|
+
);
|
|
605
|
+
headerImports.push(`import config from "${configPath}";`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const appInit = hasConfig
|
|
609
|
+
? "const app = new Rain(config);"
|
|
610
|
+
: "const app = new Rain();";
|
|
611
|
+
|
|
612
|
+
const content = [
|
|
613
|
+
...headerImports,
|
|
614
|
+
...imports,
|
|
615
|
+
"",
|
|
616
|
+
appInit,
|
|
617
|
+
"",
|
|
618
|
+
...registrations,
|
|
619
|
+
"",
|
|
620
|
+
"export default app;",
|
|
621
|
+
"",
|
|
622
|
+
].join("\n");
|
|
623
|
+
|
|
624
|
+
fs.writeFileSync(ENTRY_FILE, content);
|
|
625
|
+
const total = files.length + pageFiles.length;
|
|
626
|
+
console.log(
|
|
627
|
+
`[gen] ${total} route(s) (${files.length} api, ${pageFiles.length} page, ${layoutFiles.length} layout) -> .rainjs/entry.ts`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
module.exports = {
|
|
632
|
+
generate,
|
|
633
|
+
loadBuildConfig,
|
|
634
|
+
getRouteFiles,
|
|
635
|
+
getMiddlewareFiles,
|
|
636
|
+
getPageFiles,
|
|
637
|
+
getLayoutFiles,
|
|
638
|
+
getMiddlewaresForRoute,
|
|
639
|
+
getLayoutsForPage,
|
|
640
|
+
filePathToUrlPath,
|
|
641
|
+
filePathToImportName,
|
|
642
|
+
pageFilePathToUrlPath,
|
|
643
|
+
pageFilePathToImportName,
|
|
644
|
+
middlewareImportName,
|
|
645
|
+
layoutImportName,
|
|
646
|
+
detectExportedMethods,
|
|
647
|
+
detectExportedMethodsFromContent,
|
|
648
|
+
detectMiddlewareExport,
|
|
649
|
+
detectMiddlewareExportFromContent,
|
|
650
|
+
detectDefaultExport,
|
|
651
|
+
detectDefaultExportFromContent,
|
|
652
|
+
validateNoPageRouteColocation,
|
|
653
|
+
ROUTES_DIR,
|
|
654
|
+
ENTRY_FILE,
|
|
655
|
+
HTTP_METHODS,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
if (require.main === module) {
|
|
659
|
+
generate();
|
|
660
|
+
}
|