Cloece — TS → JSON Manifest (v0.0.1)
Goal
Turn a TypeScript input into a single JSON manifest describing:
- Entities → D1 tables + fields
- CRUD Routes → Workers
- Resource list → D1
To do this we essentially have to create a domain specific compiler.
Scope
- Single database (for now). User defines the DB name.
- Decorators supported:
@D1,@PrimaryKey,@GET,@POST - Types supported: float, int, string, boolean, Date
- Out of scope (v0.0.1): relations, indexes, uniques, defaults, auth, hashing, watch mode, migrations.
Decorators (v0.0.1)
@D1(options?) // class: marks an entity
// options: { table?: string } // optional table override
@GET // method: emits a GET route
@POST // method: emits a POST route
Type Mapping
| TS type | IR type |
Notes |
|---|---|---|
| number | int | |
| string | text | |
| boolean | bool | |
| Date | date | store as ISO string |
Nullability: a field is nullable if it has | null attatched to it.
Primary key rule: primary key must explicitly be defined using @PrimaryKey decorator.
JSON Manifest
Note Subject to change. Since all of Cloece’s functionality depends on the manifest, we may tweak it to fit the code generators needs.
{
"version": "0.0.1",
"project_name": "..",
"language": "typescript",
"models": [
{
"Person": {
"attributes": [
{
"name": "id",
"type": 0,
"nullable": false,
"pk": true
},
{
"name": "name",
"type:": 1,
"nullable": false
},
{
"name": "middle_name",
"type:": 1,
"nullable": true
}
],
"methods": [
{
"name": "speak",
"static": true,
"http_verb": "POST",
"parameters": [
{
"name": "phrase",
"type": 1, // string
"nullable": false
},
{
"name": "d1",
"type": 6 // d1 type
}
],
"return": {
"type": 7 // json with http result
}
},
{
"name": "foo",
"static": false,
"http_verb": "GET",
"parameters": [
{
"name": "d1",
"type": 6 // d1 type
}
],
"return": {
"type": 7 // json with http result
}
}
]
}
}
]
}
Example (input → output)
TypeScript Input
@D1
class Person {
@PrimaryKey
id: number;
name: string;
middle_name: string | null;
@GET
async foo(db: D1Db, req: Request) {
const who = new URL(req.url).searchParams.get("name") ?? "world";
return new Response(JSON.stringify({ hello: who }), {
headers: { "content-type": "application/json" },
});
}
@POST
static async speak(db: D1Db, req: Request, phrase: string) {
return new Response(JSON.stringify({ phrase }), {
status: 201,
headers: { "content-type": "application/json" },
});
}
}
The generated JSON manifest for this input is shown above in JSON Manifest.
Validation (This will be handeled by code generator, but good to define here)
- Classes must have
@D1. - Must have
id: number(exact nameid) and explicit declaration of primary key. - Only the five TS types above are allowed; anything else is an error.
- Decorator args must be literal-only.
- For methods, the return type must be a Response.
Naming
- Table name: Use
@D1({ table }). - Field names: use source identifiers as-is.
- Default route path (when omitted):
/<entity>/<methodName>.
CLI (one command)
npm cloece-ts [--project <tsconfig path>] [--include <glob>] [--projectName <name>]
Technical Implementation (Brainstorm)
This is a working plan. Everything here is subject to change as development progresses.
Tech Stack (v0.0.1)
- Runtime: Node.js ≥ 18
- Language: TypeScript
- AST tooling:
ts-morph - File discovery:
globby - CLI:
commander - Schema validation:
zod(Will use for unit testing) - Bundling:
tsup - Publish: npm
1) Startup (CLI)
- Parse flags:
--project,--include,--projectName. - Resolve files via
globbyand the providedtsconfig.
2) Create the AST
- Load the project with ts-morph.
- Add target files and resolve dependencies/diagnostics.
3) Discover Entities
- For each class in
/models with @D1, mark it as an entity. - Determine the entity name and compute the table name (allow
@D1({ table })override).
4) Extract Fields
- For each property, map TypeScript → IR type. I left the 3-5 undefined for now to add more types later:
| TS type | IR type |
|---|---|
| number | int (0) |
| string | text (1) |
| boolean | bool (2) |
| Date | ISO string (3) |
| D1 | string (6) |
| Response | string (7) |
- Determine nullability. User must explicitly define nullability for now.
5) Extract Routes
- For each method with
GET(path)orPOST(path): - Method:
GETorPOST. - Path: provided literal or default
/<entity>/<methodName>. - Handler:
ClassName.method. - Capture the method body and parameter list as a single string (to embed under
routes[].impl.code.source).
6) Accumulate Resources
- D1: single item
{ "name": "default" }.
7) Build Manifest Object
- Assemble
{ version, project, methods, routes, D1 }in memory.
8) Validation
- Validation will be handeled by code generator, but we should keep these in mind.
- User must declare primary key using
@PrimaryKeydecorator. - Only the allowed field types (table above).
- Decorator arguments are literal-only (no identifiers/expressions).
9) Emit
- Pretty-print the manifest to the
/.generated/cidl.json.