CLI
Generadores: proyectos, modulos, paginas y migraciones.
Comandos#
soul create <kebab-name>- Proyecto completo desde la plantilla.
soul sync-migrations [--dir]- Copia las migraciones del core. Nunca sobreescribe una existente.
soul g module <kebab-name> [--api]- Modulo de negocio con CRUD, tabla, permisos y rutas.
soul g page <kebab-name> [--public|--static]- Pagina de portal, publica cache-first o estatica.
soul g migration <name>- Migracion vacia, numerada.
soul build [--out public]- Prerenderiza
src/static.ts.
Generar un modulo#
npx soul g module facturas
Hace cinco cosas de una vez:
- Crea
migrations/NNNN_facturas.sqlcon la tabla scopeada por organizacion y su indice. - Inserta
"facturas.read/write/delete"ensrc/config.ts. - Escribe
src/modules/facturas.tscon el CRUD completo. - Inserta el import y el
app.route()ensrc/index.ts. - Imprime el fragmento de
createResourcepara activar el chequeo de IDOR.
Con --api el modulo responde JSON en vez de HTML, y usa GET/POST/DELETE en vez de
POST /:id/delete.
Generar una pagina#
npx soul g page ajustes # portal (tras requireSessionWithOrg)
npx soul g page perfil --public # publica cache-first
npx soul g page landing --static # prerenderizada, 0 requests
Los marcadores#
Los generadores escriben en puntos concretos de tus archivos, señalados por comentarios:
| Marcador | Archivo | Para que |
|---|---|---|
// soul:imports | src/index.ts | Donde se agregan los imports |
// soul:routes | src/index.ts | Donde se agregan los app.route() |
// soul:permissions | src/config.ts | Donde se agregan los permisos |
Por que existe core.ts#
Las instancias compartidas (auth, tenancy, admin, shell,
errorPage) viven en src/core.ts y no en index.ts. Los modulos generados
importan de core; si vivieran en index.ts —que a su vez monta esos modulos— habria un
ciclo de imports.
Ejecutar desde el repo remoto#
npx --package=git+ssh://git@github/jun-studio/soul.git#v4.2.0 soul create mi-saas
De cero a un modulo funcionando#
# 1. El proyecto
npx --package=git+ssh://git@github/jun-studio/soul.git#v4.2.0 soul create facturador
cd facturador && npm install
# 2. La base
npx wrangler d1 create facturador-db # pega el id en wrangler.jsonc
npx soul sync-migrations
npx wrangler d1 migrations apply facturador-db --local
# 3. El negocio
npx soul g module facturas
npx soul g module clientes
npx wrangler d1 migrations apply facturador-db --local
# 4. Una landing a 0 requests
npx soul g page landing --static
# 5. Arrancar
cp .dev.vars.example .dev.vars
npm run dev
Lo que genera g module#
-- migrations/0002_facturas.sql
CREATE TABLE IF NOT EXISTS facturas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id INTEGER NOT NULL REFERENCES orgs(id),
title TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX IF NOT EXISTS idx_facturas_org ON facturas(org_id);
// src/config.ts — insertado antes del marcador
export const PERMISSIONS: PermissionMap = {
"facturas.read": ["owner", "admin", "member"],
"facturas.write": ["owner", "admin", "member"],
"facturas.delete": ["owner", "admin"],
// soul:permissions (no borrar esta linea)
};
// src/index.ts — insertado en los dos marcadores
import facturas from "./modules/facturas";
// soul:imports (no borrar esta linea)
app.route("/facturas", facturas);
// soul:routes (no borrar esta linea)
Activar el chequeo de IDOR#
Esto es lo que imprime el CLI y hay que pegar a mano:
// test/security.spec.ts
const checks = await runSecurityChecks({
fetch: SELF.fetch.bind(SELF),
db: env.DB,
sessionSecret: env.SESSION_SECRET,
protectedPath: "/facturas",
adminPaths: ["/admin/users"],
createResource: async (db, orgId) => {
const r = await db.prepare(
"INSERT INTO facturas (org_id, title) VALUES (?, 'test') RETURNING id",
).bind(orgId).first<{ id: number }>();
return `/facturas/${r!.id}`;
},
});
Modulo API#
npx soul g module webhooks-salientes --api
// Responde JSON. Para que los errores de tenancy tambien sean JSON, el
// proyecto necesita una instancia con mode: "json".
export const apiTenancy = createTenancy({ errorPage, permissions: PERMISSIONS, mode: "json" });
Migracion a mano#
npx soul g migration agregar_estado_a_facturas
-- migrations/0004_agregar_estado_a_facturas.sql
ALTER TABLE facturas ADD COLUMN estado TEXT NOT NULL DEFAULT 'borrador';
CREATE INDEX IF NOT EXISTS idx_facturas_estado ON facturas(org_id, estado);
Aplicar migraciones en produccion#
# Local
npx wrangler d1 migrations apply facturador-db --local
# Produccion (revisa dos veces el nombre de la base)
npx wrangler d1 migrations apply facturador-db --env production --remote
Build y despliegue#
npx soul build # prerenderiza src/static.ts a public/
npm run deploy:production # build + wrangler deploy --env production
{
"scripts": {
"dev": "npm run build && wrangler dev",
"build": "soul build",
"deploy:production": "npm run build && wrangler deploy --env production",
"test": "vitest"
}
}