cloesce 0.0.4-unstable.1 → 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.
package/README.md CHANGED
@@ -2,28 +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
- # check https://www.npmjs.com/package/cloesce/v/0.0.4-unstable.0?activeTab=versions for the most recent patch
21
- npm i cloesce@0.0.4-unstable.0
20
+ npm i cloesce@0.0.4-unstable.10
22
21
  ```
23
22
 
24
- 2. TypeScript
23
+ ### 2) TypeScript
25
24
 
26
- - Create a `tsconfig.json` with the following values:
25
+ Create a `tsconfig.json` with the following values:
27
26
 
28
27
  ```json
29
28
  {
@@ -39,9 +38,9 @@ npm i cloesce@0.0.4-unstable.0
39
38
  }
40
39
  ```
41
40
 
42
- 3. Cloesce Config
41
+ ### 3) Cloesce Config
43
42
 
44
- - Create a `cloesce.config.json` with the following keys:
43
+ Create a `cloesce.config.json` with your desired configuration:
45
44
 
46
45
  ```json
47
46
  {
@@ -51,7 +50,7 @@ npm i cloesce@0.0.4-unstable.0
51
50
  }
52
51
  ```
53
52
 
54
- 4. Vite
53
+ ### 4) Vite
55
54
 
56
55
  To prevent CORS issues, a Vite proxy can be used for the frontend:
57
56
 
@@ -71,9 +70,11 @@ export default defineConfig({
71
70
  });
72
71
  ```
73
72
 
74
- 5. Wrangler Config
73
+ Middleware support for CORS is also supported (see Middleware section).
75
74
 
76
- - `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:
77
78
 
78
79
  ```toml
79
80
  compatibility_date = "2025-10-02"
@@ -86,12 +87,11 @@ database_id = "..."
86
87
  database_name = "example"
87
88
  ```
88
89
 
89
- ## A Simple Model
90
+ ## Cloesce Models
90
91
 
91
92
  A model is a type which represents:
92
93
 
93
94
  - a database table,
94
- - database views
95
95
  - REST API
96
96
  - Client API
97
97
  - Cloudflare infrastructure (D1 + Workers)
@@ -121,13 +121,18 @@ export class Horse {
121
121
  - `@POST` reveals the method as an API endpoint with the `POST` HTTP Verb.
122
122
  - All Cloesce models need to be under a `.cloesce.ts` file.
123
123
 
124
- 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).
125
127
 
126
- To compile, run `npx cloesce compile`.
128
+ Migrations utilize the history of validated metadata to create SQL code, translating the evolution of your models.
127
129
 
128
- To create a migration, run `npx cloesce migrate <name>`.
130
+ ### Compiling
129
131
 
130
- 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:
131
136
 
132
137
  ```sh
133
138
  # Apply the generated migrations
@@ -137,17 +142,36 @@ npx wrangler d1 migrations apply <db-name>
137
142
  npx wrangler dev
138
143
  ```
139
144
 
140
- 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.
141
150
 
142
151
  - `client.ts` is an importable API with all of your backend types and endpoints
143
152
  - `workers.ts` is the workers entrypoint.
144
153
  - `cidl.json` is the working metadata for the project
145
154
 
146
- 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:
147
158
 
148
159
  - `<date>_Initial.json` contains all model information necessary from SQL
149
160
  - `<date>_Initial.sql` contains the acual SQL migrations. In this early version of Cloesce, it's important to check migrations every time.
150
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
+
151
175
  ## Features
152
176
 
153
177
  ### Wrangler Environment
@@ -155,12 +179,10 @@ Note the output in `migrations`, ex after running `npx cloesce migrate Initial`
155
179
  In order to interact with your database, you will need to define a WranglerEnv
156
180
 
157
181
  ```ts
158
- // horse.cloesce.ts
159
-
160
182
  import { WranglerEnv } from "cloesce/backend";
161
183
 
162
184
  @WranglerEnv
163
- export class Env {
185
+ export class MyEnv {
164
186
  db: D1Database; // only one DB is supported for now-- make sure it matches the name in `wrangler.toml`
165
187
 
166
188
  // you can also define values in the toml under [[vars]]
@@ -168,7 +190,7 @@ export class Env {
168
190
  }
169
191
  ```
170
192
 
171
- 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:
172
194
 
173
195
  ```ts
174
196
  @D1
@@ -177,7 +199,7 @@ export class Horse {
177
199
  id: number;
178
200
 
179
201
  @POST
180
- async neigh(@Inject env: WranglerEnv): Promise<string> {
202
+ async neigh(@Inject env: MyEnv): Promise<string> {
181
203
  await env.db.prepare(...);
182
204
 
183
205
  return `i am ${this.name}, this is my horse noise`;
@@ -185,10 +207,9 @@ export class Horse {
185
207
  }
186
208
  ```
187
209
 
188
- ### Foreign Keys, One to One, Data Sources
210
+ ### Foreign Key Column
189
211
 
190
- Complex model relationships are permitted via the `@ForeignKey`, `@OneToOne / @OneToMany @ManyToMany` and `@DataSource` decorators.
191
- 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:
192
213
 
193
214
  ```ts
194
215
  @D1
@@ -207,7 +228,9 @@ export class Person {
207
228
  }
208
229
  ```
209
230
 
210
- 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.
211
234
 
212
235
  ```ts
213
236
  @D1
@@ -229,7 +252,7 @@ export class Person {
229
252
  }
230
253
  ```
231
254
 
232
- 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.
233
256
 
234
257
  ```ts
235
258
  @D1
@@ -256,39 +279,9 @@ export class Person {
256
279
  }
257
280
  ```
258
281
 
259
- 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.
260
-
261
- ```ts
262
- @D1
263
- export class Person {
264
- @PrimaryKey
265
- id: number;
266
-
267
- @ForeignKey(Dog)
268
- dogId: number;
269
-
270
- @OneToOne("dogId")
271
- dog: Dog | undefined;
272
-
273
- @DataSource
274
- static readonly default: IncludeTree<Person> = {
275
- dog: {},
276
- };
277
-
278
- @GET
279
- static async get(id: number, @Inject env: WranglerEnv): Promise<Person> {
280
- let records = await env.db
281
- .prepare("SELECT * FROM [Person.default] WHERE [Person.id] = ?") // Person.default is the SQL view generated from the IncludeTree
282
- .bind(id)
283
- .run();
284
-
285
- let persons = Orm.fromSql(Person, records.results, Person.default);
286
- return persons.value[0];
287
- }
288
- }
289
- ```
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.
290
283
 
291
- 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.
292
285
 
293
286
  ### One to Many
294
287
 
@@ -352,6 +345,24 @@ export class Course {
352
345
  }
353
346
  ```
354
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
+
355
366
  ### ORM Methods
356
367
 
357
368
  Cloesce provides a suite of ORM methods for getting, listing, updating and inserting models.
@@ -374,6 +385,44 @@ class Horse {
374
385
 
375
386
  #### List, Get
376
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
+
377
426
  ```ts
378
427
  @D1
379
428
  class Horse {
@@ -381,23 +430,47 @@ class Horse {
381
430
  @GET
382
431
  static async get(@Inject { db }: Env, id: number): Promise<Horse> {
383
432
  const orm = Orm.fromD1(db);
384
- return (await orm.get(Horse, id, "default")).value;
433
+ return (await orm.get(Horse, id, Horse.default)).value;
385
434
  }
386
435
 
387
436
  @GET
388
437
  static async list(@Inject { db }: Env): Promise<Horse[]> {
389
438
  const orm = Orm.fromD1(db);
390
- return (await orm.list(Horse, "default")).value;
439
+ return (await orm.list(Horse, {})).value;
391
440
  }
392
441
  }
393
442
  ```
394
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
+
395
466
  ### CRUD Methods
396
467
 
397
- 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.
398
471
 
399
472
  ```ts
400
- @CRUD(["POST", "GET", "LIST"])
473
+ @CRUD(["SAVE", "GET", "LIST"])
401
474
  @D1
402
475
  export class CrudHaver {
403
476
  @PrimaryKey
@@ -415,6 +488,76 @@ static async get(
415
488
  ): Promise<HttpResult<CrudHaver>>
416
489
  ```
417
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
+
418
561
  ### Plain Old Objects
419
562
 
420
563
  Simple non-model objects can be returned and serialized from a model method:
@@ -467,10 +610,11 @@ class Foo {
467
610
  ## Integration Tests
468
611
 
469
612
  - Regression tests: `cargo run --bin test regression`
470
- - Pass fail extractor tests: `cargo run --bin test run-fail`
471
613
 
472
614
  Optionally, pass `--check` if new snapshots should not be created.
473
615
 
616
+ To target a specific fixture, pass `--fixture folder_name`
617
+
474
618
  To update integration snapshots, run:
475
619
 
476
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);