cloesce 0.0.4-unstable.0 → 0.0.4-unstable.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,27 +2,27 @@
2
2
 
3
3
  Cloesce is a full stack compiler for the Cloudflare developer platform, allowing class definitions in high level languages to serve as a metadata basis to create a database schema, backend REST API, frontend API client, and Cloudflare infrastructure (as of v0.0.4, D1 + Workers).
4
4
 
5
- Cloesce is working towards an alpha MVP (v0.1.0), with the general milestones being [here](https://cloesce.pages.dev/schreiber/v0.1.0_milestones/).
5
+ Cloesce is working towards a stable alpha MVP (v0.1.0), with the general milestones being [here](https://cloesce.pages.dev/schreiber/v0.1.0_milestones/).
6
6
 
7
7
  Internal documentation going over design decisions and general thoughts for each milestone can be found [here](https://cloesce.pages.dev/).
8
8
 
9
- # Documentation `v0.0.4`
9
+ # Documentation
10
10
 
11
11
  ## Getting Started
12
12
 
13
13
  `v0.0.4` supports only Typescript-to-Typescript projects. An example project is shown [here](https://github.com/bens-schreiber/cloesce/tree/main/examples).
14
14
 
15
- 1. NPM
15
+ ### 1) NPM
16
16
 
17
- - Create an NPM project and install cloesce
17
+ Create an NPM project and install cloesce
18
18
 
19
19
  ```sh
20
- npm i cloesce@0.0.4-unstable.0
20
+ npm i cloesce@0.0.4-unstable.10
21
21
  ```
22
22
 
23
- 2. TypeScript
23
+ ### 2) TypeScript
24
24
 
25
- - Create a `tsconfig.json` with the following values:
25
+ Create a `tsconfig.json` with the following values:
26
26
 
27
27
  ```json
28
28
  {
@@ -38,9 +38,9 @@ npm i cloesce@0.0.4-unstable.0
38
38
  }
39
39
  ```
40
40
 
41
- 3. Cloesce Config
41
+ ### 3) Cloesce Config
42
42
 
43
- - Create a `cloesce.config.json` with the following keys:
43
+ Create a `cloesce.config.json` with your desired configuration:
44
44
 
45
45
  ```json
46
46
  {
@@ -50,7 +50,7 @@ npm i cloesce@0.0.4-unstable.0
50
50
  }
51
51
  ```
52
52
 
53
- 4. Vite
53
+ ### 4) Vite
54
54
 
55
55
  To prevent CORS issues, a Vite proxy can be used for the frontend:
56
56
 
@@ -70,9 +70,11 @@ export default defineConfig({
70
70
  });
71
71
  ```
72
72
 
73
- 5. Wrangler Config
73
+ Middleware support for CORS is also supported (see Middleware section).
74
74
 
75
- - `v0.0.4` will generate the required areas of your wrangler config. A full config looks like this:
75
+ ### 5) Wrangler Config
76
+
77
+ Cloesce will generate any missing `wrangler.toml` values (or the file if missing). A minimal `wrangler.toml` looks like this:
76
78
 
77
79
  ```toml
78
80
  compatibility_date = "2025-10-02"
@@ -85,12 +87,11 @@ database_id = "..."
85
87
  database_name = "example"
86
88
  ```
87
89
 
88
- ## A Simple Model
90
+ ## Cloesce Models
89
91
 
90
92
  A model is a type which represents:
91
93
 
92
94
  - a database table,
93
- - database views
94
95
  - REST API
95
96
  - Client API
96
97
  - Cloudflare infrastructure (D1 + Workers)
@@ -120,13 +121,18 @@ export class Horse {
120
121
  - `@POST` reveals the method as an API endpoint with the `POST` HTTP Verb.
121
122
  - All Cloesce models need to be under a `.cloesce.ts` file.
122
123
 
123
- To compile this model into a working full stack application, Cloesce must undergo both **compilation** and **migrations**. Compilation is the process of extracting the metadata language that powers Cloesce, ensuring it is a valid program, and then producing code to orchestrate the program across different domains (database, backend, frontend, cloud). Migrations utilize the history of validated metadata to create SQL code, translating the evolution of your models.
124
+ To compile this model into a working full stack application, Cloesce must undergo both **compilation** and **migrations**.
125
+
126
+ Compilation is the process of extracting the metadata language that powers Cloesce, ensuring it is a valid program, and then producing code to orchestrate the program across different domains (database, backend, frontend, cloud).
124
127
 
125
- To compile, run `npx cloesce compile`.
128
+ Migrations utilize the history of validated metadata to create SQL code, translating the evolution of your models.
126
129
 
127
- To create a migration, run `npx cloesce migrate <name>`.
130
+ ### Compiling
128
131
 
129
- After running the above commands, you will have a full project capable of being ran with:
132
+ - `npx cloesce compile`
133
+ - `npx cloesce migrate <name>`.
134
+
135
+ After running the above commands, you will have a full project capable of being ran with Wrangler:
130
136
 
131
137
  ```sh
132
138
  # Apply the generated migrations
@@ -136,17 +142,36 @@ npx wrangler d1 migrations apply <db-name>
136
142
  npx wrangler dev
137
143
  ```
138
144
 
139
- Note the output in the `.generated/` dir. These values should not be committed to git, as they depend on the file system of the machine running it.
145
+ ### Compiled Artifacts
146
+
147
+ #### `.generated/`
148
+
149
+ These values should not be committed to git, as they depend on the file system of the machine running it.
140
150
 
141
151
  - `client.ts` is an importable API with all of your backend types and endpoints
142
152
  - `workers.ts` is the workers entrypoint.
143
153
  - `cidl.json` is the working metadata for the project
144
154
 
145
- Note the output in `migrations`, ex after running `npx cloesce migrate Initial`
155
+ #### `migrations`
156
+
157
+ After running `npx cloesce migrate <name>`, a new migration will be created in the `migrations/` folder. For example, after creating a migration called `Initial`, you will see:
146
158
 
147
159
  - `<date>_Initial.json` contains all model information necessary from SQL
148
160
  - `<date>_Initial.sql` contains the acual SQL migrations. In this early version of Cloesce, it's important to check migrations every time.
149
161
 
162
+ #### Supported Column Types
163
+
164
+ Model columns must directly map to SQLite columns. The supported TypeScript types are:
165
+
166
+ - `number` => `Real` not null
167
+ - `string` => `Text` not null
168
+ - `boolean` and `Boolean` => `Integer` not null
169
+ - `Integer` => `Integer` not null
170
+ - `Date` => `Text` (ISO formatted) not null
171
+ - `N | null` => `N` (nullable)
172
+
173
+ Blob types will be added in v0.0.5 when R2 support is added.
174
+
150
175
  ## Features
151
176
 
152
177
  ### Wrangler Environment
@@ -154,12 +179,10 @@ Note the output in `migrations`, ex after running `npx cloesce migrate Initial`
154
179
  In order to interact with your database, you will need to define a WranglerEnv
155
180
 
156
181
  ```ts
157
- // horse.cloesce.ts
158
-
159
182
  import { WranglerEnv } from "cloesce/backend";
160
183
 
161
184
  @WranglerEnv
162
- export class Env {
185
+ export class MyEnv {
163
186
  db: D1Database; // only one DB is supported for now-- make sure it matches the name in `wrangler.toml`
164
187
 
165
188
  // you can also define values in the toml under [[vars]]
@@ -167,7 +190,7 @@ export class Env {
167
190
  }
168
191
  ```
169
192
 
170
- The wrangler environment is dependency injected into your method calls:
193
+ Your WranglerEnv can then be injected into any model method using the `@Inject` decorator:
171
194
 
172
195
  ```ts
173
196
  @D1
@@ -176,7 +199,7 @@ export class Horse {
176
199
  id: number;
177
200
 
178
201
  @POST
179
- async neigh(@Inject env: WranglerEnv): Promise<string> {
202
+ async neigh(@Inject env: MyEnv): Promise<string> {
180
203
  await env.db.prepare(...);
181
204
 
182
205
  return `i am ${this.name}, this is my horse noise`;
@@ -184,10 +207,9 @@ export class Horse {
184
207
  }
185
208
  ```
186
209
 
187
- ### Foreign Keys, One to One, Data Sources
210
+ ### Foreign Key Column
188
211
 
189
- Complex model relationships are permitted via the `@ForeignKey`, `@OneToOne / @OneToMany @ManyToMany` and `@DataSource` decorators.
190
- Foreign keys are scalar attributes which must reference some other model's primary key:
212
+ Reference another model via a foreign key using the `@ForeignKey` decorator:
191
213
 
192
214
  ```ts
193
215
  @D1
@@ -206,7 +228,9 @@ export class Person {
206
228
  }
207
229
  ```
208
230
 
209
- This representation is true to the underlying SQL table: `Person` has a column `dogId` which is a foreign key to `Dog`. Cloesce allows you to actually join these tables together in your model representation:
231
+ ### One to One
232
+
233
+ Cloesce allows you to relate models via `1:1` relationships using the `@OneToOne` decorator. It requires that a foreign key already exists on the model.
210
234
 
211
235
  ```ts
212
236
  @D1
@@ -228,7 +252,7 @@ export class Person {
228
252
  }
229
253
  ```
230
254
 
231
- In `v0.0.4`, there are no defaults, only very explicit decisons. Because of that, navigation properties won't exist at runtime unless you tell them to. Cloesce does this via a `DataSource`, which describes the foreign key dependencies you wish to include. All scalar properties are included by default and cannot be excluded.
255
+ In `v0.0.4`, there are no defaults, only very explicit decisons. Because of that, navigation properties won't exist at runtime unless you tell them to. Cloesce does this via a `Data Source`, which describes the foreign key dependencies you wish to include. All scalar properties are included by default and cannot be excluded.
232
256
 
233
257
  ```ts
234
258
  @D1
@@ -255,39 +279,9 @@ export class Person {
255
279
  }
256
280
  ```
257
281
 
258
- Data sources are just SQL views and can be invoked in your queries. They are aliased in such a way that its similiar to object properties. The frontend chooses which datasource to use in it's API client. `null` is a valid option, meaning no joins will occur.
259
-
260
- ```ts
261
- @D1
262
- export class Person {
263
- @PrimaryKey
264
- id: number;
265
-
266
- @ForeignKey(Dog)
267
- dogId: number;
268
-
269
- @OneToOne("dogId")
270
- dog: Dog | undefined;
271
-
272
- @DataSource
273
- static readonly default: IncludeTree<Person> = {
274
- dog: {},
275
- };
276
-
277
- @GET
278
- static async get(id: number, @Inject env: WranglerEnv): Promise<Person> {
279
- let records = await env.db
280
- .prepare("SELECT * FROM [Person.default] WHERE [Person.id] = ?") // Person.default is the SQL view generated from the IncludeTree
281
- .bind(id)
282
- .run();
283
-
284
- let persons = Orm.fromSql(Person, records.results, Person.default);
285
- return persons.value[0];
286
- }
287
- }
288
- ```
282
+ Data sources describe how foreign keys should be joined on model hydration (i.e. when invoking any instantiated method). They are composed of an `IncludeTree<T>`, a recursive type composed of the relationships you wish to include. All scalar properties are always included.
289
283
 
290
- Note that the `get` code can be simplified using CRUD methods or ORM primitives.
284
+ Note that `DataSourceOf` is added implicitly to all instantiated methods if no data source parameter is defined.
291
285
 
292
286
  ### One to Many
293
287
 
@@ -351,6 +345,24 @@ export class Course {
351
345
  }
352
346
  ```
353
347
 
348
+ ### DataSourceOf<T>
349
+
350
+ If it is important to determine what data source the frontend called the instantiated method with, the type `DataSourceOf<T>` allows explicit data source parameters:
351
+
352
+ ```ts
353
+ @D1
354
+ class Foo {
355
+ ...
356
+
357
+ @POST
358
+ bar(ds: DataSourceOf<Foo>) {
359
+ // ds = "DataSource1" | "DataSource2" | ... | "none"
360
+ }
361
+ }
362
+ ```
363
+
364
+ Data sources are implicitly added to all instantiated methods if no data source parameter is defined.
365
+
354
366
  ### ORM Methods
355
367
 
356
368
  Cloesce provides a suite of ORM methods for getting, listing, updating and inserting models.
@@ -373,6 +385,44 @@ class Horse {
373
385
 
374
386
  #### List, Get
375
387
 
388
+ Both methods take an optional `IncludeTree<T>` parameter to specify what relationships in the generated CTE.
389
+
390
+ ```ts
391
+ @D1
392
+ export class Person {
393
+ @PrimaryKey
394
+ id: number;
395
+
396
+ @ForeignKey(Dog)
397
+ dogId: number;
398
+
399
+ @OneToOne("dogId")
400
+ dog: Dog | undefined;
401
+
402
+ @DataSource
403
+ static readonly default: IncludeTree<Person> = {
404
+ dog: {},
405
+ };
406
+ }
407
+ ```
408
+
409
+ Running `Orm.listQuery` with the data source `Person.default` would produce the CTE:
410
+
411
+ ```sql
412
+ WITH "Person_view" AS (
413
+ SELECT
414
+ "Person"."id" AS "id",
415
+ "Person"."dogId" AS "dogId",
416
+ "Dog"."id" AS "dog.id"
417
+ FROM
418
+ "Person"
419
+ LEFT JOIN
420
+ "Dog" ON "Person"."dogId" = "Dog"."id"
421
+ ) SELECT * FROM "Person_view"
422
+ ```
423
+
424
+ Example usages:
425
+
376
426
  ```ts
377
427
  @D1
378
428
  class Horse {
@@ -380,23 +430,47 @@ class Horse {
380
430
  @GET
381
431
  static async get(@Inject { db }: Env, id: number): Promise<Horse> {
382
432
  const orm = Orm.fromD1(db);
383
- return (await orm.get(Horse, id, "default")).value;
433
+ return (await orm.get(Horse, id, Horse.default)).value;
384
434
  }
385
435
 
386
436
  @GET
387
437
  static async list(@Inject { db }: Env): Promise<Horse[]> {
388
438
  const orm = Orm.fromD1(db);
389
- return (await orm.list(Horse, "default")).value;
439
+ return (await orm.list(Horse, {})).value;
390
440
  }
391
441
  }
392
442
  ```
393
443
 
444
+ `list` takes an optional `from` parameter to modify the source of the list query. This is useful in filtering / limiting results.
445
+
446
+ ```ts
447
+ await orm.list(
448
+ Horse,
449
+ Horse.default,
450
+ "SELECT * FROM Horse ORDER BY name LIMIT 10"
451
+ );
452
+ ```
453
+
454
+ produces SQL
455
+
456
+ ```sql
457
+ WITH "Horse_view" AS (
458
+ SELECT
459
+ "Horse"."id" AS "id",
460
+ "Horse"."name" AS "name"
461
+ FROM
462
+ (SELECT * FROM Horse ORDER BY name LIMIT 10) as "Horse"
463
+ ) SELECT * FROM "Horse_view"
464
+ ```
465
+
394
466
  ### CRUD Methods
395
467
 
396
- Generic GET, POST, PATCH (and in a future version, DEL) boilerplate methods do not need to be copied around. Cloesce supports CRUD generation, a syntactic sugar that adds the methods to the compiler output.
468
+ Generic `GET, POST, PATCH` (and in a future version, DEL) boilerplate methods do not need to be copied around. Cloesce supports CRUD generation, a syntactic sugar that adds the methods to the compiler output.
469
+
470
+ The `SAVE` method is an `upsert`, meaning it both inserts and updates in the same query.
397
471
 
398
472
  ```ts
399
- @CRUD(["POST", "GET", "LIST"])
473
+ @CRUD(["SAVE", "GET", "LIST"])
400
474
  @D1
401
475
  export class CrudHaver {
402
476
  @PrimaryKey
@@ -414,6 +488,76 @@ static async get(
414
488
  ): Promise<HttpResult<CrudHaver>>
415
489
  ```
416
490
 
491
+ ### Middleware
492
+
493
+ Cloesce supports middleware at the global level (before routing to a model+method), the model level (before validation) and the method level (before hydration). Middleware also exposes read/write access to the dependency injection instance that all models use.
494
+
495
+ Middleware is capable of exiting from the Cloesce Router early with an HTTP Result.
496
+
497
+ An example of all levels of middleware is below. All middleware must be defined in the file `app.cloesce.ts` which exports a `CloesceApp` instance as default.
498
+
499
+ ```ts
500
+ @PlainOldObject
501
+ export class InjectedThing {
502
+ value: string;
503
+ }
504
+
505
+ @WranglerEnv
506
+ export class Env {
507
+ db: D1Database;
508
+ }
509
+
510
+ @D1
511
+ @CRUD(["POST"])
512
+ export class Model {
513
+ @PrimaryKey
514
+ id: number;
515
+
516
+ @GET
517
+ static blockedMethod() {}
518
+
519
+ @GET
520
+ static getInjectedThing(@Inject thing: InjectedThing): InjectedThing {
521
+ return thing;
522
+ }
523
+ }
524
+
525
+ const app: CloesceApp = new CloesceApp();
526
+
527
+ app.onRequest((request: Request, env, ir) => {
528
+ if (request.method === "POST") {
529
+ return { ok: false, status: 401, message: "POST methods aren't allowed." };
530
+ }
531
+ });
532
+
533
+ app.onModel(Model, (request, env, ir) => {
534
+ ir.set(InjectedThing.name, {
535
+ value: "hello world",
536
+ });
537
+ });
538
+
539
+ app.onMethod(Model, "blockedMethod", (request, env, ir) => {
540
+ return { ok: false, status: 401, message: "Blocked method" };
541
+ });
542
+
543
+ app.onResponse(async (request, env, di, response: Response) => {
544
+ // basic CORS, allow all origins
545
+ response.headers.set("Access-Control-Allow-Origin", "*");
546
+ response.headers.set(
547
+ "Access-Control-Allow-Methods",
548
+ "GET, POST, PUT, DELETE, OPTIONS"
549
+ );
550
+ response.headers.set(
551
+ "Access-Control-Allow-Headers",
552
+ "Content-Type, Authorization"
553
+ );
554
+ });
555
+
556
+ export default app;
557
+ ```
558
+
559
+ With this middleware, all POST methods will be blocked, and all methods for the model `Model` will be able to inject `InjectedThing`,and `blockedMethod` will return a 401. Additionally, all responses will have CORS headers.
560
+
417
561
  ### Plain Old Objects
418
562
 
419
563
  Simple non-model objects can be returned and serialized from a model method:
@@ -466,10 +610,11 @@ class Foo {
466
610
  ## Integration Tests
467
611
 
468
612
  - Regression tests: `cargo run --bin test regression`
469
- - Pass fail extractor tests: `cargo run --bin test run-fail`
470
613
 
471
614
  Optionally, pass `--check` if new snapshots should not be created.
472
615
 
616
+ To target a specific fixture, pass `--fixture folder_name`
617
+
473
618
  To update integration snapshots, run:
474
619
 
475
620
  - `cargo run --bin update`
package/dist/cli.js CHANGED
@@ -22,30 +22,28 @@ const cmds = subcommands({
22
22
  },
23
23
  handler: async (args) => {
24
24
  const config = loadCloesceConfig(process.cwd(), args.debug);
25
- if (!config.workersUrl || !config.clientUrl) {
26
- console.error("Error: `workersUrl` and `clientUrl` must be defined in cloesce.config.json");
25
+ if (!config.workersUrl) {
26
+ console.error("Error: `workersUrl`` must be defined in cloesce.config.json");
27
27
  process.exit(1);
28
28
  }
29
29
  // Creates a `cidl.json` file. Exits the process on failure.
30
30
  await extract({ debug: args.debug });
31
31
  const outputDir = config.outputDir ?? ".generated";
32
- const allConfig = {
33
- name: "all",
32
+ const generateConfig = {
33
+ name: "generate",
34
34
  wasmFile: "generator.wasm",
35
35
  args: [
36
36
  "generate",
37
- "all",
38
37
  path.join(outputDir, "cidl.pre.json"),
39
38
  path.join(outputDir, "cidl.json"),
40
39
  "wrangler.toml",
41
40
  path.join(outputDir, "workers.ts"),
42
41
  path.join(outputDir, "client.ts"),
43
- config.clientUrl,
44
42
  config.workersUrl,
45
43
  ],
46
44
  };
47
45
  // Runs a generator command. Exits the process on failure.
48
- await generate(allConfig);
46
+ await generate(generateConfig);
49
47
  },
50
48
  }),
51
49
  extract: command({
@@ -83,6 +81,10 @@ const cmds = subcommands({
83
81
  short: "d",
84
82
  description: "Show debug output",
85
83
  }),
84
+ skipTsCheck: flag({
85
+ long: "skipTsCheck",
86
+ description: "Skip TypeScript compilation checks",
87
+ }),
86
88
  },
87
89
  handler: async (args) => {
88
90
  await extract({ ...args });
@@ -138,20 +140,22 @@ const cmds = subcommands({
138
140
  }),
139
141
  },
140
142
  });
141
- async function extract(opts) {
143
+ async function extract(args) {
142
144
  const root = process.cwd();
143
145
  const projectRoot = process.cwd();
144
- const config = loadCloesceConfig(projectRoot, opts.debug);
145
- const searchPaths = opts.inp ? [opts.inp] : (config.paths ?? [root]);
146
+ const config = loadCloesceConfig(projectRoot, args.debug);
147
+ const searchPaths = args.inp ? [args.inp] : (config.paths ?? [root]);
146
148
  const outputDir = config.outputDir ?? ".generated";
147
- const outPath = opts.out ?? path.join(outputDir, "cidl.pre.json");
148
- const truncate = opts.truncateSourcePaths ?? config.truncateSourcePaths ?? false;
149
- const cloesceProjectName = opts.projectName ??
149
+ const outPath = args.out ?? path.join(outputDir, "cidl.pre.json");
150
+ const truncate = args.truncateSourcePaths ?? config.truncateSourcePaths ?? false;
151
+ const cloesceProjectName = args.projectName ??
150
152
  config.projectName ??
151
153
  readPackageJsonProjectName(projectRoot);
152
154
  const project = new Project({
153
155
  compilerOptions: {
154
156
  strictNullChecks: true,
157
+ experimentalDecorators: true,
158
+ emitDecoratorMetadata: true,
155
159
  },
156
160
  });
157
161
  findCloesceProject(root, searchPaths, project);
@@ -159,16 +163,25 @@ async function extract(opts) {
159
163
  if (fileCount === 0) {
160
164
  new ExtractorError(ExtractorErrorCode.MissingFile);
161
165
  }
162
- if (opts.debug)
166
+ if (args.debug)
163
167
  console.log(`Found ${fileCount} .cloesce.ts files`);
168
+ // Run typescript compiler checks to before extraction
169
+ if (!args.skipTsCheck) {
170
+ const diagnostics = project.getPreEmitDiagnostics();
171
+ if (diagnostics.length > 0) {
172
+ console.error("TypeScript errors detected in provided files:");
173
+ console.error(project.formatDiagnosticsWithColorAndContext(diagnostics));
174
+ process.exit(1);
175
+ }
176
+ }
164
177
  try {
165
- const extractor = new CidlExtractor(cloesceProjectName, "v0.0.3");
178
+ const extractor = new CidlExtractor(cloesceProjectName, "v0.0.4");
166
179
  const result = extractor.extract(project);
167
- if (!result.ok) {
180
+ if (result.isLeft()) {
168
181
  console.error(formatErr(result.value));
169
182
  process.exit(1);
170
183
  }
171
- let ast = result.value;
184
+ let ast = result.unwrap();
172
185
  if (truncate) {
173
186
  ast.wrangler_env.source_path =
174
187
  "./" + path.basename(ast.wrangler_env.source_path);
@@ -189,11 +202,11 @@ async function extract(opts) {
189
202
  const json = JSON.stringify(ast, null, 4);
190
203
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
191
204
  fs.writeFileSync(outPath, json);
192
- console.log(`CIDL generated successfully at ${outPath}`);
205
+ console.log(`CIDL extracted to ${outPath}`);
193
206
  return { outPath, projectName: cloesceProjectName };
194
207
  }
195
208
  catch (err) {
196
- console.error("Critical uncaught error. Submit a ticket to https://github.com/bens-schreiber/cloesce: ", err?.message ?? err);
209
+ console.error("Critical uncaught error in generator. \nSubmit a ticket to https://github.com/bens-schreiber/cloesce\n\n", err?.message ?? "No error message.", "\n", err?.stack ?? "No error stack.");
197
210
  process.exit(1);
198
211
  }
199
212
  }
@@ -293,10 +306,13 @@ function formatErr(e) {
293
306
  const { description, suggestion } = getErrorInfo(e.code);
294
307
  const contextLine = e.context ? `Context: ${e.context}\n` : "";
295
308
  const snippetLine = e.snippet ? `${e.snippet}\n\n` : "";
296
- return `==== CLOESCE ERROR ====
309
+ return `
310
+ ==== CLOESCE ERROR ====
297
311
  Error [${ExtractorErrorCode[e.code]}]: ${description}
298
312
  Phase: TypeScript IDL Extraction
299
- ${contextLine}${snippetLine}Suggested fix: ${suggestion}`;
313
+ ${contextLine}${snippetLine}Suggested fix: ${suggestion}
314
+
315
+ `;
300
316
  }
301
317
  run(cmds, process.argv.slice(2)).catch((err) => {
302
318
  console.error(err);