Architecture
Juntos Architecture
Understanding what Juntos generates and how the pieces connect.
Table of Contents
The dist/ Directory
Running juntos build creates a self-contained JavaScript application:
dist/
├── app/
│ ├── models/
│ │ ├── application_record.js # Base class wrapping ActiveRecord
│ │ ├── article.js # Transpiled model
│ │ ├── comment.js
│ │ └── index.js # Re-exports all models
│ ├── controllers/
│ │ ├── application_controller.js
│ │ ├── articles_controller.js
│ │ └── comments_controller.js
│ ├── views/
│ │ ├── articles/
│ │ │ ├── index.js # Transpiled ERB
│ │ │ ├── show.js
│ │ │ ├── _article.js # Partials
│ │ │ └── *.html.erb # Source (for sourcemaps)
│ │ └── layouts/
│ │ └── application.js # Layout wrapper
│ └── helpers/
├── config/
│ ├── routes.js # Route definitions + dispatch
│ └── paths.js # Path helper functions
├── db/
│ ├── migrate/
│ │ ├── 20241231_create_articles.js
│ │ ├── 20241231_create_comments.js
│ │ └── index.js # Migration registry
│ └── seeds.js # Seed data
├── lib/
│ ├── rails.js # Framework runtime (target-specific)
│ ├── rails_base.js # Shared base classes
│ ├── active_record.mjs # Database adapter
│ └── erb_runtime.mjs # ERB helper functions
├── index.html # Entry point (browser targets)
├── api/[[...path]].js # Entry point (Vercel)
├── src/index.js # Entry point (Cloudflare)
├── vercel.json # Platform config (Vercel)
├── wrangler.toml # Platform config (Cloudflare)
├── package.json
└── tailwind.config.js # If using Tailwind
Standalone JavaScript
The dist/ directory is a complete application. You can:
cd dist
npm install
npm start
No Ruby required. The generated code is idiomatic JavaScript—ES2022 classes, async/await, standard module patterns. You could fork this directory and continue development in pure JavaScript.
Target Differences
Browser
- Entry:
index.htmlloadsconfig/routes.js - Routing: Client-side, updates
#hashor uses History API - Database: IndexedDB (Dexie), SQLite/WASM, or PGlite
- Rendering: Direct DOM manipulation via
innerHTML
Node.js / Bun / Deno
- Entry:
lib/rails.jsexportsApplication.listen() - Routing: HTTP server, parses request path
- Database: better-sqlite3, pg, mysql2
- Rendering: Returns HTML string responses
Vercel Edge
- Entry:
api/[[...path]].jscatch-all route - Routing: Vercel routes requests to the handler
- Database: Neon, Turso, PlanetScale (HTTP-based)
- Rendering: Returns
Responseobjects
Cloudflare Workers
- Entry:
src/index.jsexportsfetchhandler - Routing: Worker receives all requests
- Database: D1 binding, Turso
- Rendering: Returns
Responseobjects
The Runtime
Application
The Application class manages initialization and request handling:
// Browser
Application.start(); // Initialize DB, render initial route
// Node.js
Application.listen(3000); // Start HTTP server
// Vercel
export default Application.handler(); // Export request handler
// Cloudflare
export default Application.worker(); // Export Worker handler
Router
Routes are registered at build time and dispatched at runtime:
// Generated from config/routes.rb
Router.resources('articles', ArticlesController);
Router.resources('comments', CommentsController, { shallow: true });
Router.root('articles#index');
Path helpers are generated as standalone functions:
// config/paths.js
export function article_path(article) {
return `/articles/${article.id || article}`;
}
export function edit_article_path(article) {
return `/articles/${article.id || article}/edit`;
}
ActiveRecord
Models extend ApplicationRecord which wraps the database adapter:
class Article extends ApplicationRecord {
static _tableName = 'articles';
static _associations = { comments: { type: 'hasMany' } };
static _validations = { title: [{ presence: true }] };
// Generated association method
async comments() {
return await Comment.where({ article_id: this.id });
}
}
The adapter is selected at build time based on the database configuration:
| Adapter | File |
|---|---|
| Dexie | active_record_dexie.mjs |
| sql.js | active_record_sqljs.mjs |
| better-sqlite3 | active_record_better_sqlite3.mjs |
| Neon | active_record_neon.mjs |
| D1 | active_record_d1.mjs |
All adapters implement the same interface:
// Static methods
Model.all()
Model.find(id)
Model.where(conditions)
Model.create(attributes)
// Instance methods
record.save()
record.update(attributes)
record.destroy()
Sourcemaps
Each transpiled file includes a sourcemap linking back to the original Ruby:
// article.js
export class Article extends ApplicationRecord { ... }
//# sourceMappingURL=article.js.map
The original .rb files are copied alongside for debugger access. In browser DevTools, you can set breakpoints on Ruby lines and step through Ruby code.
The Build Process
- Load configuration — Read
config/database.yml, determine target - Copy runtime — Copy target-specific
rails.jsand adapter - Transpile models — Apply rails/model filter
- Transpile controllers — Apply rails/controller filter
- Transpile views — Compile ERB to Ruby, apply rails/helpers filter
- Transpile routes — Generate route definitions and path helpers
- Transpile migrations — Generate async migration functions
- Generate entry point — Create index.html or serverless handler
- Setup Tailwind — If detected, configure and build CSS
Continuing in JavaScript
After building, you can take dist/ and develop purely in JavaScript:
- The generated code follows standard patterns
- No Ruby2JS dependencies at runtime
- Add npm packages directly to
dist/package.json - Modify transpiled files as needed
The generated code isn’t obfuscated or minified—it’s meant to be readable and maintainable. This is an intentional escape hatch: Juntos gets you started quickly, but you’re not locked in.