1use crate::error::Result;
2use rohas_parser::Schema;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6pub fn generate_package_json(_schema: &Schema, output_dir: &Path) -> Result<()> {
7 let project_root = get_project_root(output_dir);
8 let project_name = extract_project_name(&project_root);
9
10 let content = format!(
11 r#"{{
12 "name": "{}",
13 "version": "0.1.0",
14 "description": "Rohas event-driven application",
15 "main": ".rohas/index.js",
16 "type": "module",
17 "scripts": {{
18 "dev": "rohas dev",
19 "build": "npm run compile",
20 "compile": "rspack build",
21 "compile:watch": "rspack build --watch",
22 "start": "node .rohas/index.js",
23 "codegen": "rohas codegen",
24 "validate": "rohas validate"
25 }},
26 "dependencies": {{
27 "typescript": "^5.3.3",
28 "zod": "^3.22.4"
29 }},
30 "devDependencies": {{
31 "@types/node": "^20.10.0",
32 "@rspack/cli": "^1.1.7",
33 "@rspack/core": "^1.1.7"
34 }},
35 "engines": {{
36 "node": ">=18.0.0"
37 }}
38}}
39"#,
40 project_name
41 );
42
43 fs::write(project_root.join("package.json"), content)?;
44 Ok(())
45}
46
47pub fn generate_tsconfig_json(_schema: &Schema, output_dir: &Path) -> Result<()> {
48 let project_root = get_project_root(output_dir);
49 let content = r#"{
50 "compilerOptions": {
51 "target": "ES2022",
52 "module": "ESNext",
53 "moduleResolution": "node",
54 "lib": ["ES2022"],
55 "outDir": "./dist",
56 "rootDir": "./src",
57 "strict": true,
58 "esModuleInterop": true,
59 "skipLibCheck": true,
60 "forceConsistentCasingInFileNames": true,
61 "resolveJsonModule": true,
62 "declaration": true,
63 "declarationMap": true,
64 "sourceMap": true,
65 "noUnusedLocals": true,
66 "noUnusedParameters": true,
67 "noImplicitReturns": true,
68 "noFallthroughCasesInSwitch": true,
69 "baseUrl": ".",
70 "paths": {
71 "@generated/*": ["src/generated/*"],
72 "@handlers/*": ["src/handlers/*"],
73 "@/*": ["src/*"]
74 }
75 },
76 "include": [
77 "src/**/*"
78 ],
79 "exclude": [
80 "node_modules",
81 "dist"
82 ]
83}
84"#;
85
86 fs::write(project_root.join("tsconfig.json"), content)?;
87 Ok(())
88}
89
90pub fn generate_requirements_txt(_schema: &Schema, output_dir: &Path) -> Result<()> {
91 let project_root = get_project_root(output_dir);
92 let content = r#"# Python dependencies for Rohas project
93# Add your project-specific dependencies here
94
95# Common dependencies
96pydantic>=2.0.0
97typing-extensions>=4.0.0
98"#;
99
100 fs::write(project_root.join("requirements.txt"), content)?;
101 Ok(())
102}
103
104pub fn generate_pyproject_toml(_schema: &Schema, output_dir: &Path) -> Result<()> {
105 let project_root = get_project_root(output_dir);
106 let project_name = extract_project_name(&project_root);
107
108 let content = format!(
109 r#"[project]
110name = "{}"
111version = "0.1.0"
112description = "Rohas event-driven application"
113requires-python = ">=3.9"
114dependencies = [
115 "pydantic>=2.0.0",
116 "typing-extensions>=4.0.0",
117]
118
119[project.optional-dependencies]
120dev = [
121 "pytest>=7.0.0",
122 "black>=23.0.0",
123 "mypy>=1.0.0",
124 "ruff>=0.1.0",
125]
126
127[tool.black]
128line-length = 100
129target-version = ['py39', 'py310', 'py311']
130
131[tool.mypy]
132python_version = "3.9"
133strict = true
134warn_return_any = true
135warn_unused_configs = true
136
137[tool.ruff]
138line-length = 100
139target-version = "py39"
140"#,
141 project_name
142 );
143
144 fs::write(project_root.join("pyproject.toml"), content)?;
145 Ok(())
146}
147
148pub fn generate_cargo_toml(_schema: &Schema, output_dir: &Path) -> Result<()> {
149 let project_root = get_project_root(output_dir);
150 let project_name = extract_project_name(&project_root);
151
152 let lib_name = project_name.replace('-', "_");
153
154 let content = format!(
155 r#"[package]
156name = "{}"
157version = "0.1.0"
158edition = "2021"
159
160[workspace]
161
162[lib]
163name = "{}"
164path = "src/lib.rs"
165
166[dependencies]
167rohas-runtime = {{ path = "../../crates/rohas-runtime" }}
168serde = {{ version = "1.0", features = ["derive"] }}
169serde_json = "1.0"
170tokio = {{ version = "1.0", features = ["full"] }}
171chrono = {{ version = "0.4", features = ["serde"] }}
172tracing = "0.1"
173
174[dev-dependencies]
175tokio-test = "0.4"
176"#,
177 project_name,
178 lib_name
179 );
180
181 fs::write(project_root.join("Cargo.toml"), content)?;
182 Ok(())
183}
184
185pub fn generate_gitignore(_schema: &Schema, output_dir: &Path) -> Result<()> {
186 let project_root = get_project_root(output_dir);
187 let content = r#"# Dependencies
188node_modules/
189__pycache__/
190*.pyc
191*.pyo
192*.pyd
193.Python
194env/
195venv/
196ENV/
197.venv/
198
199# Build outputs
200dist/
201build/
202*.egg-info/
203.tsbuildinfo
204
205# IDE
206.vscode/
207.idea/
208*.swp
209*.swo
210*~
211.DS_Store
212
213# Logs
214*.log
215logs/
216npm-debug.log*
217yarn-debug.log*
218yarn-error.log*
219
220# Environment variables
221.env
222.env.local
223.env.*.local
224
225# OS
226.DS_Store
227Thumbs.db
228
229# Testing
230coverage/
231.coverage
232.pytest_cache/
233*.cover
234.hypothesis/
235
236# Rohas compiled output
237.rohas/
238src/generated/
239"#;
240
241 fs::write(project_root.join(".gitignore"), content)?;
242 Ok(())
243}
244
245pub fn generate_editorconfig(_schema: &Schema, output_dir: &Path) -> Result<()> {
246 let project_root = get_project_root(output_dir);
247 let content = r#"# EditorConfig is awesome: https://EditorConfig.org
248
249root = true
250
251[*]
252charset = utf-8
253end_of_line = lf
254insert_final_newline = true
255trim_trailing_whitespace = true
256
257[*.{ts,tsx,js,jsx,json}]
258indent_style = space
259indent_size = 2
260
261[*.{py}]
262indent_style = space
263indent_size = 4
264
265[*.{yml,yaml}]
266indent_style = space
267indent_size = 2
268
269[*.md]
270trim_trailing_whitespace = false
271"#;
272
273 fs::write(project_root.join(".editorconfig"), content)?;
274 Ok(())
275}
276
277pub fn generate_readme(schema: &Schema, output_dir: &Path) -> Result<()> {
278 let project_root = get_project_root(output_dir);
279 let project_name = extract_project_name(&project_root);
280 let has_apis = !schema.apis.is_empty();
281 let has_events = !schema.events.is_empty();
282 let has_crons = !schema.crons.is_empty();
283
284 let mut api_list = String::new();
285 for api in &schema.apis {
286 api_list.push_str(&format!("- `{} {}` - {}\n", api.method, api.path, api.name));
287 }
288
289 let mut event_list = String::new();
290 for event in &schema.events {
291 event_list.push_str(&format!(
292 "- `{}` - Payload: {}\n",
293 event.name, event.payload
294 ));
295 }
296
297 let mut cron_list = String::new();
298 for cron in &schema.crons {
299 cron_list.push_str(&format!(
300 "- `{}` - Schedule: {}\n",
301 cron.name, cron.schedule
302 ));
303 }
304
305 let content = format!(
306 r#"# {}
307
308Rohas event-driven application
309
310## Project Structure
311
312```
313├── schema/ # Schema definitions (.ro files)
314│ ├── api/ # API endpoint schemas
315│ ├── events/ # Event schemas
316│ ├── models/ # Data model schemas
317│ └── cron/ # Cron job schemas
318├── src/
319│ ├── generated/ # Auto-generated types (DO NOT EDIT)
320│ └── handlers/ # Your handler implementations
321│ ├── api/ # API handlers
322│ ├── events/ # Event handlers
323│ └── cron/ # Cron job handlers
324└── config/ # Configuration files
325```
326
327## Getting Started
328
329### Installation
330
331```bash
332# Install dependencies (TypeScript)
333npm install
334
335# Or for Python
336pip install -r requirements.txt
337```
338
339### Development
340
341```bash
342# Generate code from schema
343rohas codegen
344
345# Start development server
346rohas dev
347
348# Validate schema
349rohas validate
350```
351
352## Schema Overview
353
354{}{}{}
355
356## Handler Naming Convention
357
358Handler files must be named exactly as the API/Event/Cron name in the schema:
359
360- API `Health` → `src/handlers/api/Health.ts`
361- Event `UserCreated` → Handler defined in event schema
362- Cron `DailyCleanup` → `src/handlers/cron/DailyCleanup.ts`
363
364## Generated Code
365
366The `src/generated/` directory contains auto-generated TypeScript types and interfaces.
367**DO NOT EDIT** these files manually - they will be regenerated when you run `rohas codegen`.
368
369## Adding New Features
370
3711. Define your schema in `schema/` directory
3722. Run `rohas codegen` to generate types and handler stubs
3733. Implement your handler logic in `src/handlers/`
3744. Test with `rohas dev`
375
376## Configuration
377
378See `config/rohas.toml` for project configuration.
379
380## License
381
382MIT
383"#,
384 project_name,
385 if has_apis {
386 format!("\n### APIs\n\n{}", api_list)
387 } else {
388 String::new()
389 },
390 if has_events {
391 format!("\n### Events\n\n{}", event_list)
392 } else {
393 String::new()
394 },
395 if has_crons {
396 format!("\n### Cron Jobs\n\n{}", cron_list)
397 } else {
398 String::new()
399 },
400 );
401
402 let readme_path = project_root.join("README.md");
403 if !readme_path.exists() {
404 fs::write(readme_path, content)?;
405 }
406
407 Ok(())
408}
409
410pub fn generate_nvmrc(_schema: &Schema, output_dir: &Path) -> Result<()> {
411 let project_root = get_project_root(output_dir);
412 let content = "18.0.0\n";
413 fs::write(project_root.join(".nvmrc"), content)?;
414 Ok(())
415}
416
417pub fn generate_prettierrc(_schema: &Schema, output_dir: &Path) -> Result<()> {
418 let project_root = get_project_root(output_dir);
419 let content = r#"{
420 "semi": true,
421 "trailingComma": "es5",
422 "singleQuote": true,
423 "printWidth": 100,
424 "tabWidth": 2,
425 "useTabs": false,
426 "arrowParens": "always"
427}
428"#;
429
430 fs::write(project_root.join(".prettierrc"), content)?;
431 Ok(())
432}
433
434pub fn generate_prettierignore(_schema: &Schema, output_dir: &Path) -> Result<()> {
435 let project_root = get_project_root(output_dir);
436 let content = r#"node_modules/
437dist/
438build/
439coverage/
440*.min.js
441src/generated/
442.rohas/
443"#;
444
445 fs::write(project_root.join(".prettierignore"), content)?;
446 Ok(())
447}
448
449pub fn generate_rspack_config(_schema: &Schema, output_dir: &Path) -> Result<()> {
450 let project_root = get_project_root(output_dir);
451 let content = r#"const path = require('path');
452const fs = require('fs');
453
454// Find all TypeScript handler files
455function findHandlers(dir, basePath = '') {
456 const entries = {};
457 const items = fs.readdirSync(dir, { withFileTypes: true });
458
459 for (const item of items) {
460 const fullPath = path.join(dir, item.name);
461 const relativePath = path.join(basePath, item.name);
462
463 if (item.isDirectory() && item.name !== 'generated') {
464 Object.assign(entries, findHandlers(fullPath, relativePath));
465 } else if (item.isFile() && (item.name.endsWith('.ts') || item.name.endsWith('.tsx'))) {
466 const entryName = path.join(basePath, item.name.replace(/\.tsx?$/, ''));
467 entries[entryName] = fullPath;
468 }
469 }
470
471 return entries;
472}
473
474const srcDir = path.join(__dirname, 'src');
475const handlers = findHandlers(srcDir);
476
477/** @type {import('@rspack/cli').Configuration} */
478module.exports = {
479 mode: 'development',
480 entry: handlers,
481 output: {
482 path: path.resolve(__dirname, '.rohas'),
483 filename: '[name].js',
484 clean: false,
485 library: {
486 type: 'commonjs2',
487 },
488 },
489 target: 'node',
490 resolve: {
491 extensions: ['.ts', '.tsx', '.js', '.jsx'],
492 alias: {
493 '@generated': path.resolve(__dirname, 'src/generated'),
494 '@handlers': path.resolve(__dirname, 'src/handlers'),
495 '@': path.resolve(__dirname, 'src'),
496 },
497 },
498 module: {
499 rules: [
500 {
501 test: /\.tsx?$/,
502 use: {
503 loader: 'builtin:swc-loader',
504 options: {
505 jsc: {
506 parser: {
507 syntax: 'typescript',
508 tsx: false,
509 decorators: true,
510 dynamicImport: true,
511 },
512 target: 'es2022',
513 loose: false,
514 externalHelpers: false,
515 keepClassNames: true,
516 },
517 module: {
518 type: 'commonjs',
519 },
520 },
521 },
522 type: 'javascript/auto',
523 },
524 ],
525 },
526 externals: [
527 // Don't bundle node_modules, treat them as externals
528 function ({ request }, callback) {
529 // If it's a node module (starts with a letter/@ and not a relative path)
530 if (/^[a-z@]/i.test(request)) {
531 return callback(null, 'commonjs ' + request);
532 }
533 callback();
534 },
535 ],
536 devtool: 'source-map',
537 optimization: {
538 minimize: false,
539 },
540 stats: {
541 preset: 'normal',
542 colors: true,
543 },
544};
545"#;
546
547 fs::write(project_root.join("rspack.config.cjs"), content)?;
548 Ok(())
549}
550
551fn get_project_root(output_dir: &Path) -> PathBuf {
552 if output_dir.file_name().and_then(|s| s.to_str()) == Some("src") {
553 output_dir.parent().unwrap_or(output_dir).to_path_buf()
554 } else {
555 output_dir.to_path_buf()
556 }
557}
558
559fn extract_project_name(project_root: &Path) -> String {
560 project_root
561 .file_name()
562 .and_then(|s| s.to_str())
563 .unwrap_or("rohas-app")
564 .to_string()
565}