Skip to main content

rohas_codegen/
config.rs

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}