coffeehaml 0.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -0
  3. package/dist/ast.d.ts +107 -0
  4. package/dist/ast.d.ts.map +1 -0
  5. package/dist/ast.js +125 -0
  6. package/dist/ast.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +53 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/compiler.d.ts +10 -0
  12. package/dist/compiler.d.ts.map +1 -0
  13. package/dist/compiler.js +83 -0
  14. package/dist/compiler.js.map +1 -0
  15. package/dist/emitter.d.ts +4 -0
  16. package/dist/emitter.d.ts.map +1 -0
  17. package/dist/emitter.js +464 -0
  18. package/dist/emitter.js.map +1 -0
  19. package/dist/errors.d.ts +3 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +2 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/expressions.d.ts +10 -0
  24. package/dist/expressions.d.ts.map +1 -0
  25. package/dist/expressions.js +70 -0
  26. package/dist/expressions.js.map +1 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/lexer.d.ts +29 -0
  32. package/dist/lexer.d.ts.map +1 -0
  33. package/dist/lexer.js +315 -0
  34. package/dist/lexer.js.map +1 -0
  35. package/dist/parser.d.ts +4 -0
  36. package/dist/parser.d.ts.map +1 -0
  37. package/dist/parser.js +408 -0
  38. package/dist/parser.js.map +1 -0
  39. package/dist/types.d.ts +65 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +12 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/vite-plugin.d.ts +7 -0
  44. package/dist/vite-plugin.d.ts.map +1 -0
  45. package/dist/vite-plugin.js +44 -0
  46. package/dist/vite-plugin.js.map +1 -0
  47. package/docs/README.md +110 -0
  48. package/docs/architecture.md +619 -0
  49. package/docs/ast.md +413 -0
  50. package/docs/examples.md +719 -0
  51. package/docs/grammar.md +432 -0
  52. package/docs/index.html +129 -0
  53. package/package.json +61 -0
package/docs/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # CoffeeHaml
2
+
3
+ > *Haml structure. CoffeeScript semantics. React runtime.*
4
+
5
+ CoffeeHaml is a compiler that transforms an indentation-based, Haml-inspired
6
+ authoring language into JavaScript using the modern React JSX runtime
7
+ (`react/jsx-runtime`).
8
+
9
+ It is **not** a UI framework. React remains the runtime. CoffeeHaml replaces JSX.
10
+
11
+ ---
12
+
13
+ ## Philosophy
14
+
15
+ | Concern | Approach |
16
+ |---------|----------|
17
+ | **Structure** | Haml — significant indentation, `%tag`, `.class`, `#id` |
18
+ | **Semantics** | CoffeeScript — expression-oriented, `->`, `for...in`, implicit return |
19
+ | **Runtime** | React — `jsx()` / `jsxs()` calls, zero CoffeeHaml runtime |
20
+ | **Output** | JavaScript (ES2020+) with `react/jsx-runtime` imports |
21
+
22
+ ## Five-Second Glimpse
23
+
24
+ ```haml
25
+ %SimulatorPanel{source: "gyro", width: "auto", height: 500}
26
+ - for servo in servos
27
+ %ServoGraph{channel: servo.channel}
28
+ %ServoPanel{servo: servo}
29
+
30
+ %Button{onClick: save, disabled: !connected}
31
+ Save
32
+ ```
33
+
34
+ Compiles to:
35
+
36
+ ```js
37
+ import { jsxs, jsx } from "react/jsx-runtime";
38
+ jsxs(SimulatorPanel, { source: "gyro", width: "auto", height: 500 },
39
+ ...servos.map(servo => jsxs(Fragment, {},
40
+ jsx(ServoGraph, { channel: servo.channel }),
41
+ jsx(ServoPanel, { servo })
42
+ )),
43
+ jsx(Button, { onClick: save, disabled: !connected }, "Save")
44
+ );
45
+ ```
46
+
47
+ ## Design Constraints
48
+
49
+ - **Maximum compatibility with Haml** — existing muscle memory transfers
50
+ - **CoffeeScript expression syntax** — not Ruby, not JSX
51
+ - **Token-efficient** — fewer characters than JSX for equivalent output
52
+ - **No invention** — reuse Haml and CoffeeScript conventions
53
+ - **Zero runtime** — the compiler is the only artifact
54
+ - **Excellent source maps** — debug in CoffeeHaml, not generated JS
55
+ - **Incremental compilation** — Vite plugin with HMR support
56
+ - **Emitter-agnostic AST** — future backends (Solid, Vue, Mithril) possible
57
+
58
+ ## Token Efficiency & The Indentation Renaissance
59
+
60
+ CoffeeHaml is designed at the dawn of a shift in how code is generated. Modern
61
+ LLMs are increasingly trained on indentation-based languages — Python, YAML,
62
+ Haml, Sass, Stylus — and their tokenizers have internalized significant
63
+ whitespace as structural signal. Where JSX wastes tokens on `<`, `</`, `>`, `/>`,
64
+ `{`, `}`, and closing tags that echo the opening tag name verbatim, CoffeeHaml
65
+ lets the indentation *be* the structure. No closing tags. No angle brackets.
66
+ No delimiter pairs that must be matched.
67
+
68
+ ### Token count comparison (equivalent output)
69
+
70
+ | Syntax | Tokens |
71
+ |--------|--------|
72
+ | `<div className="box"><h1>Hello</h1><p>World</p></div>` | 17 |
73
+ | `%div.box%h1 Hello%p World` | 8 |
74
+ | **50%+ reduction** in structural tokens | |
75
+
76
+ Every character in CoffeeHaml carries semantic weight. There are no syntactic
77
+ ceremonies — only meaning. This is not merely an aesthetic preference; in an era
78
+ where LLMs both consume and produce code, token efficiency directly impacts
79
+ context window utilization, inference cost, and generation speed.
80
+
81
+ ### The LLM training flywheel
82
+
83
+ Indentation-based languages create a virtuous cycle:
84
+
85
+ 1. **LLMs are trained on them** → tokenizers learn to treat indentation as
86
+ structural, not ornamental
87
+ 2. **LLMs generate them** → more indentation-based code enters the training
88
+ corpus
89
+ 3. **Tooling improves** → better completions, lower error rates, tighter
90
+ feedback loops
91
+
92
+ CoffeeHaml enters this cycle at precisely the right moment. React developers
93
+ have been writing JSX for a decade — the community is ready for a leap in
94
+ expressiveness that removes boilerplate rather than adding it.
95
+
96
+ ### Why not just use Haml with Ruby?
97
+
98
+ Because React is JavaScript. Bridging Ruby semantics to React components
99
+ introduces impedance mismatch. CoffeeScript, by contrast, compiles directly to
100
+ JavaScript and shares its runtime model. `->` becomes `() =>`. `for...in`
101
+ becomes `.map()`. The expressions you write in CoffeeHaml attributes *are*
102
+ CoffeeScript — no foreign runtime, no translation layer, no surprises.
103
+
104
+ ## Status
105
+
106
+ **Design phase.** See:
107
+ - [Grammar](docs/grammar.md)
108
+ - [AST Design](docs/ast.md)
109
+ - [Architecture](docs/architecture.md)
110
+ - [Syntax Examples](docs/examples.md)
@@ -0,0 +1,619 @@
1
+ # CoffeeHaml Architecture
2
+
3
+ ## Compiler Pipeline
4
+
5
+ ```
6
+ CoffeeHaml Source
7
+ │
8
+ ▼
9
+ ┌──────────┐
10
+ │ Lexer │ Tokenizes source into Token stream
11
+ └──────────┘ (INDENT/DEDENT inserted by IndentProcessor)
12
+ │
13
+ ▼
14
+ ┌──────────┐
15
+ │ Parser │ Recursive descent, produces CoffeeHaml AST
16
+ └──────────┘ Delegates expressions to CoffeeScript parser
17
+ │
18
+ ▼
19
+ ┌──────────┐
20
+ │ Resolver │ Resolves imports, validates identifiers
21
+ └──────────┘ (optional: type-checking pass)
22
+ │
23
+ ▼
24
+ ┌──────────┐
25
+ │ Emitter │ Walks AST, emits JavaScript with jsx()/jsxs()
26
+ └──────────┘ Produces final .js/.jsx output + source map
27
+ │
28
+ ▼
29
+ JavaScript + Source Map
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Phase 1: Lexer
35
+
36
+ ### Responsibilities
37
+
38
+ - Scan raw CoffeeHaml source into a stream of tokens
39
+ - Handle significant whitespace (INDENT/DEDENT)
40
+ - Extract raw source for expression blocks (`{...}`, `(...)`, `= expr`)
41
+ - Preserve source locations for every token
42
+
43
+ ### Token Stream
44
+
45
+ ```
46
+ Source: %div.container#main\n %p Hello\n
47
+
48
+ Tokens:
49
+ TAG "%div" @ 1:1
50
+ CLASS ".container" @ 1:4
51
+ ID "#main" @ 1:14
52
+ NEWLINE @ 1:19
53
+ INDENT @ 2:1
54
+ TAG "%p" @ 2:3
55
+ TEXT "Hello" @ 2:6
56
+ NEWLINE @ 2:11
57
+ DEDENT @ EOF
58
+ EOF @ EOF
59
+ ```
60
+
61
+ ### Indentation Processor
62
+
63
+ The lexer contains an internal `IndentStack` that tracks indentation
64
+ levels. On each non-blank line:
65
+
66
+ 1. Compute the line's indent depth (spaces or tabs)
67
+ 2. Compare to the top of the stack
68
+ 3. If deeper → push, emit `INDENT`
69
+ 4. If equal → continue
70
+ 5. If shallower → pop and emit `DEDENT` for each level until match
71
+
72
+ ```
73
+ IndentStack: [0]
74
+ Line " text" (depth 2) → push 2, emit INDENT
75
+ Line " more" (depth 4) → push 4, emit INDENT
76
+ Line " text" (depth 2) → pop 4 (DEDENT), match 2
77
+ Line "" (blank) → skip
78
+ EOF → pop 2 (DEDENT), pop 0 (DEDENT)
79
+ ```
80
+
81
+ ### Expression Extraction
82
+
83
+ When the lexer encounters `{`, it enters a **brace-scanning mode**:
84
+
85
+ 1. Track nesting depth of `{`/`}`
86
+ 2. Track string boundaries (`"`, `'`, `"""`, `'''`, `"` backtick)
87
+ 3. Track comment boundaries (`#` line comments, `###` block comments)
88
+ 4. Track regex boundaries (`///`)
89
+ 5. When depth reaches 0, emit the entire span as one token
90
+
91
+ Similar logic for `(...)` attribute blocks.
92
+
93
+ After `=` or `-`, the lexer scans to end-of-line and emits the
94
+ expression source as a token.
95
+
96
+ ```
97
+ Source: %div{class: "hello}"}
98
+ ↑
99
+ This brace is inside a string → ignored
100
+ ```
101
+
102
+ ### Token Types (Full Enumeration)
103
+
104
+ ```ts
105
+ enum TokenKind {
106
+ // Structural
107
+ TAG, // %div
108
+ CLASS, // .container
109
+ ID, // #main
110
+
111
+ // Attributes
112
+ ATTR_BRACE_OPEN, // { (with extracted content)
113
+ ATTR_BRACE_CLOSE, // }
114
+ ATTR_PAREN_OPEN, // ( (with extracted content)
115
+ ATTR_PAREN_CLOSE, // )
116
+
117
+ // Inline
118
+ TEXT, // literal text
119
+ OUTPUT, // =
120
+ OUTPUT_RAW, // ==
121
+
122
+ // Control
123
+ CONTROL, // -
124
+ COMMENT_HAML, // -#
125
+ COMMENT_HTML, // /
126
+ FILTER, // :css, :javascript, etc.
127
+ DOCTYPE, // !!!
128
+
129
+ // Meta
130
+ NEWLINE,
131
+ INDENT,
132
+ DEDENT,
133
+ EOF,
134
+ }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Phase 2: Parser
140
+
141
+ ### Architecture
142
+
143
+ The parser is a **recursive descent** parser with one token of lookahead.
144
+ It consumes the token stream from the lexer and produces the CoffeeHaml
145
+ AST.
146
+
147
+ For CoffeeScript expressions (attributes, outputs, control conditions),
148
+ the parser delegates to the **CoffeeScript compiler's parser** —
149
+ specifically `CoffeeScript.parse()` — to obtain a CoffeeScript AST for
150
+ the expression source. This avoids re-implementing CoffeeScript's
151
+ expression grammar.
152
+
153
+ ### Parser State
154
+
155
+ ```ts
156
+ class CoffeeHamlParser {
157
+ private tokens: Token[];
158
+ private pos: number;
159
+ private indentStack: number[];
160
+
161
+ constructor(source: string);
162
+
163
+ // Entry point
164
+ parse(): Document;
165
+
166
+ // Node parsers
167
+ private parseNode(): Node;
168
+ private parseElement(): Element;
169
+ private parseImplicitDiv(): ImplicitDiv;
170
+ private parseText(): Text;
171
+ private parseOutput(): Output;
172
+ private parseControlFlow(): ControlFlow;
173
+ private parseComment(): Comment;
174
+ private parseFilter(): Filter;
175
+ private parseDoctype(): Doctype;
176
+ private parseChildren(): Node[];
177
+
178
+ // Helpers
179
+ private peek(): Token;
180
+ private advance(): Token;
181
+ private expect(kind: TokenKind): Token;
182
+ private parseExpression(raw: string): Expression;
183
+ private parseAttributes(raw: string): Attribute[];
184
+ }
185
+ ```
186
+
187
+ ### Parsing Algorithm
188
+
189
+ ```
190
+ parse():
191
+ create Document node
192
+ while not EOF:
193
+ skip blank lines
194
+ node = parseNode()
195
+ append node to document.children
196
+ return document
197
+
198
+ parseNode():
199
+ token = peek()
200
+ switch:
201
+ case "!!!" → parseDoctype()
202
+ case "%" → parseElement()
203
+ case "." → parseImplicitDiv()
204
+ case "#" → parseImplicitDiv()
205
+ case "-#" → parseComment()
206
+ case "/" → parseComment() or parseFilter()? (context)
207
+ case "-" → parseControlFlow()
208
+ case "=" → parseOutput()
209
+ case ":" → parseFilter()
210
+ default → parseText()
211
+
212
+ parseElement():
213
+ advance TAG
214
+ parse tag modifiers (.class, #id) while peek is CLASS or ID
215
+ if peek is ATTR_BRACE_OPEN: parse attribute block
216
+ if peek is TEXT: parse inline text
217
+ if inline text contains '=': parse inline output suffix
218
+ if peek is SELF_CLOSE + NEWLINE: mark self-closing
219
+ advance NEWLINE
220
+ if next token is INDENT:
221
+ children = parseChildren()
222
+ return Element { ... }
223
+
224
+ parseChildren():
225
+ advance INDENT
226
+ nodes = []
227
+ while not DEDENT and not EOF:
228
+ nodes.push(parseNode())
229
+ advance DEDENT
230
+ return nodes
231
+ ```
232
+
233
+ ### Expression Delegation
234
+
235
+ When the parser needs to parse a CoffeeScript expression (e.g., the
236
+ content of `{...}` attributes), it:
237
+
238
+ 1. Extracts the raw source string from the token
239
+ 2. Calls `CoffeeScript.parse(source)` to get a CoffeeScript AST
240
+ 3. Wraps it in our `Expression` node
241
+ 4. For attribute blocks, walks the CoffeeScript object literal AST
242
+ to extract individual `Attribute` nodes
243
+
244
+ ```ts
245
+ parseAttributes(raw: string): Attribute[] {
246
+ // Wrap in braces and parse as CoffeeScript object
247
+ const objAst = CoffeeScript.parse(`{${raw}}`);
248
+ // Walk the object literal AST to extract key-value pairs
249
+ return extractAttributes(objAst);
250
+ }
251
+ ```
252
+
253
+ ### Error Recovery
254
+
255
+ The parser uses a simple **panic mode** recovery:
256
+
257
+ 1. On parse error, skip tokens until NEWLINE or DEDENT
258
+ 2. Record the error in a diagnostic list
259
+ 3. Continue parsing subsequent nodes
260
+
261
+ This enables reporting multiple errors in one pass.
262
+
263
+ ---
264
+
265
+ ## Phase 3: AST Resolver (Optional)
266
+
267
+ Before emission, a resolver pass may:
268
+
269
+ 1. **Validate component references**: Check that capitalized tags resolve
270
+ to identifiers in scope (requires import analysis)
271
+ 2. **Resolve filter processors**: Map filter names to handler functions
272
+ 3. **Optimize static subtrees**: Mark subtrees that are fully static
273
+ (no expressions, no control flow) for potential hoisting
274
+
275
+ This phase is optional for initial implementation but important for
276
+ producing good diagnostics.
277
+
278
+ ---
279
+
280
+ ## Phase 4: Emitter (React JSX Runtime Backend)
281
+
282
+ ### Architecture
283
+
284
+ The emitter walks the CoffeeHaml AST and generates JavaScript source
285
+ code that uses `react/jsx-runtime`.
286
+
287
+ ```ts
288
+ class ReactJSRuntimeEmitter {
289
+ private sourceMap: SourceMapGenerator;
290
+ private indentLevel: number;
291
+
292
+ emit(document: Document): { code: string; map: SourceMap };
293
+
294
+ private emitNode(node: Node): string;
295
+ private emitElement(node: Element): string;
296
+ private emitChildren(children: Node[]): string;
297
+ private emitControlFlow(node: ControlFlow): string;
298
+ // ...
299
+ }
300
+ ```
301
+
302
+ ### Core Emission Rules
303
+
304
+ #### Element → `jsx()` or `jsxs()`
305
+
306
+ An element with **no children** (and no inline text/output) emits `jsx()`:
307
+
308
+ ```ts
309
+ // %br/
310
+ jsx("br", null)
311
+ ```
312
+
313
+ An element with **one child** may also use `jsx()`:
314
+
315
+ ```ts
316
+ // %div Hello
317
+ jsx("div", { children: "Hello" })
318
+ ```
319
+
320
+ An element with **multiple children** uses `jsxs()`:
321
+
322
+ ```ts
323
+ // %div
324
+ // %p A
325
+ // %p B
326
+ jsxs("div", {}, jsx("p", {}, "A"), jsx("p", {}, "B"))
327
+ ```
328
+
329
+ #### Component vs HTML element
330
+
331
+ ```ts
332
+ // %MyComponent{prop: val}
333
+ jsx(MyComponent, { prop: val })
334
+
335
+ // %div{id: "main"}
336
+ jsx("div", { id: "main" })
337
+ ```
338
+
339
+ #### Attributes merging
340
+
341
+ `.class` and `#id` modifiers merge with explicit attributes:
342
+
343
+ ```haml
344
+ %div.container#main{id: "override", "data-x": "y"}
345
+ ```
346
+
347
+ ```js
348
+ jsx("div", { className: "container", id: "override", "data-x": "y" })
349
+ ```
350
+
351
+ If both `#id` and `{id: ...}` are present, the explicit attribute wins.
352
+
353
+ Multiple `.class` modifiers concatenate with spaces.
354
+
355
+ #### Inline text
356
+
357
+ ```haml
358
+ %p Hello, = name
359
+ ```
360
+
361
+ ```js
362
+ jsx("p", {}, "Hello, ", name)
363
+ ```
364
+
365
+ #### Control flow children
366
+
367
+ This is the most complex emission case. Control flow bodies become
368
+ arrays spread into the parent's children:
369
+
370
+ ```haml
371
+ %div
372
+ - for item in items
373
+ %ItemCard{item: item}
374
+ ```
375
+
376
+ ```js
377
+ jsxs("div", {},
378
+ ...items.map(item => jsx(ItemCard, { item }))
379
+ );
380
+ ```
381
+
382
+ The general pattern:
383
+
384
+ ```
385
+ - for PATTERN in EXPR → ...EXPR.map((PATTERN) => BODY)
386
+ - if COND → ...(COND ? [BODY] : [])
387
+ - if COND ... - else → ...(COND ? [BODY] : [ALTERNATE])
388
+ - unless COND → ...(!COND ? [BODY] : [])
389
+ ```
390
+
391
+ **For loop with index**:
392
+
393
+ ```haml
394
+ - for item, idx in items
395
+ %Row{item: item, key: idx}
396
+ ```
397
+
398
+ ```js
399
+ ...items.map((item, idx) => jsx(Row, { item, key: idx }))
400
+ ```
401
+
402
+ **Nested control flow**:
403
+
404
+ ```haml
405
+ %div
406
+ - if user
407
+ %WelcomeBanner{user: user}
408
+ - for post in user.posts
409
+ %PostCard{post: post}
410
+ - else
411
+ %LoginPrompt
412
+ ```
413
+
414
+ ```js
415
+ jsxs("div", {},
416
+ ...(user ? [
417
+ jsx(WelcomeBanner, { user }),
418
+ ...user.posts.map(post => jsx(PostCard, { post }))
419
+ ] : [
420
+ jsx(LoginPrompt, {})
421
+ ])
422
+ );
423
+ ```
424
+
425
+ #### Arbitrary CoffeeScript statements
426
+
427
+ ```
428
+ - console.log("rendering")
429
+ %div Hello
430
+ ```
431
+
432
+ ```js
433
+ console.log("rendering");
434
+ jsx("div", {}, "Hello");
435
+ ```
436
+
437
+ Statements that produce no value are emitted as-is and do not affect
438
+ the parent's child array.
439
+
440
+ #### Output (`=`, `==`)
441
+
442
+ ```haml
443
+ = user.name
444
+ ```
445
+
446
+ ```js
447
+ user.name
448
+ ```
449
+
450
+ Output nodes compile directly to their CoffeeScript expression compiled
451
+ to JavaScript. The parent element wraps them as children.
452
+
453
+ #### Filters
454
+
455
+ ```haml
456
+ :css
457
+ body { margin: 0 }
458
+ ```
459
+
460
+ ```js
461
+ // Compiled at build time; options:
462
+ // 1. Inline as <style> tag (via jsx):
463
+ jsx("style", {}, "body { margin: 0 }")
464
+ // 2. Extract to CSS file (Vite plugin integrates with CSS pipeline)
465
+ // 3. Inline as string for CSS-in-JS libraries
466
+ ```
467
+
468
+ The filter handler is pluggable; the emitter delegates to registered
469
+ filter processors.
470
+
471
+ ---
472
+
473
+ ### Module Wrapper
474
+
475
+ The emitter wraps output in a module that imports from
476
+ `react/jsx-runtime`:
477
+
478
+ ```js
479
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
480
+ export default function CoffeeHamlComponent() {
481
+ // emitted nodes
482
+ }
483
+ ```
484
+
485
+ The component name and export style are configurable (default export,
486
+ named export, arrow function, etc.).
487
+
488
+ ---
489
+
490
+ ## Vite Plugin Integration
491
+
492
+ ```
493
+ Vite config
494
+ │
495
+ ▼
496
+ ┌─────────────────┐
497
+ │ vite-plugin- │ Intercepts .haml / .coffeehaml imports
498
+ │ coffeehaml │
499
+ └─────────────────┘
500
+ │
501
+ ▼
502
+ ┌─────────────────┐
503
+ │ CoffeeHaml │ Compiles source → JS + source map
504
+ │ Compiler │
505
+ └─────────────────┘
506
+ │
507
+ ▼
508
+ ┌─────────────────┐
509
+ │ Vite HMR │ File change → recompile → HMR update
510
+ │ pipeline │
511
+ └─────────────────┘
512
+ ```
513
+
514
+ The Vite plugin:
515
+
516
+ 1. Matches `.haml` and `.coffeehaml` file extensions
517
+ 2. Compiles CoffeeHaml → JavaScript using the compiler
518
+ 3. Returns compiled JS + source map to Vite
519
+ 4. Handles HMR by recompiling on file change
520
+ 5. Optionally processes filter blocks (`:css` → CSS extraction)
521
+
522
+ ---
523
+
524
+ ## Incremental Compilation
525
+
526
+ For incremental builds, the compiler caches:
527
+
528
+ - The parsed AST (keyed by file hash)
529
+ - Resolved import maps
530
+ - Compiled filter output
531
+
532
+ On file change:
533
+ 1. Check if the file's hash changed
534
+ 2. If yes, re-lex and re-parse only that file
535
+ 3. Re-emit only the changed file
536
+ 4. Invalidate dependent files (files that import the changed file)
537
+
538
+ ---
539
+
540
+ ## Source Maps
541
+
542
+ Every emission step records mappings:
543
+
544
+ ```
545
+ CoffeeHaml source position → JavaScript output position
546
+ ```
547
+
548
+ The emitter uses the `loc` fields on AST nodes to create source mappings.
549
+ For CoffeeScript expressions, the CoffeeScript compiler provides its own
550
+ source maps, which are composed with the CoffeeHaml source maps.
551
+
552
+ This yields a **composed source map** chain:
553
+
554
+ ```
555
+ Browser JS position
556
+ → CoffeeHaml JS output position
557
+ → CoffeeHaml source position
558
+ ```
559
+
560
+ For CoffeeScript expressions, there's an intermediate step:
561
+
562
+ ```
563
+ Browser JS position
564
+ → CoffeeHaml JS output position
565
+ → CoffeeScript source position (within attribute/expression)
566
+ → CoffeeHaml source position (the expression as a whole)
567
+ ```
568
+
569
+ ---
570
+
571
+ ## Error Reporting
572
+
573
+ Errors reference CoffeeHaml source locations:
574
+
575
+ ```
576
+ Error: Unknown component "MyCopmonent" (did you mean "MyComponent"?)
577
+ at src/components/Dashboard.coffeehaml:12:3
578
+ 11 |
579
+ 12 | %MyCopmonent{data: items}
580
+ ^
581
+ 13 |
582
+ ```
583
+
584
+ The compiler produces structured diagnostics:
585
+
586
+ ```ts
587
+ interface Diagnostic {
588
+ severity: "error" | "warning" | "info";
589
+ message: string;
590
+ loc: SourceLocation;
591
+ hint?: string; // suggested fix
592
+ code?: string; // error code for documentation
593
+ }
594
+ ```
595
+
596
+ ---
597
+
598
+ ## Implementation Language
599
+
600
+ The compiler is implemented in **TypeScript** for:
601
+
602
+ - Type safety and self-documenting interfaces
603
+ - First-class Node.js/Vite ecosystem integration
604
+ - Access to the CoffeeScript compiler's JS API
605
+ - Ease of contribution from the React community
606
+
607
+ The CoffeeScript dependency is used solely for **expression parsing** —
608
+ the CoffeeHaml compiler itself does not re-implement CoffeeScript.
609
+
610
+ ```json
611
+ {
612
+ "dependencies": {
613
+ "coffeescript": "^2.7.0"
614
+ }
615
+ }
616
+ ```
617
+
618
+ Future: A self-hosting CoffeeHaml compiler written in CoffeeHaml + a
619
+ Node.js backend would be poetically satisfying, but is not a v1 goal.