@chilts/weave 0.7.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,130 @@ While the version is below `1.0.0`, breaking changes may land in minor releases.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.8.0] - 2026-10-22
12
+
13
+ ### Added
14
+
15
+ - **`unless`.** `unless cond` renders its block when `cond` is falsy,
16
+ compiling to the same ternary as `if !(cond)`. It is a guard only: an
17
+ `else` or `else if` after it is a compile error asking for an `if` with the
18
+ branches swapped. Previously `unless` was a compile error pointing at
19
+ `if !(cond)`.
20
+
21
+ - **Class arrays/objects and style objects.** `class=` accepts an array
22
+ (nested, with `null`/`undefined`/`false`/`""` dropped) or an object whose
23
+ truthy keys are kept, also when merged with shorthand classes:
24
+ `li.item(class=({ active: isActive }))` renders `class="item active"`.
25
+ `style=` accepts an object: `div(style=({ marginTop: 0 }))` renders
26
+ `style="margin-top:0"`. Previously these rendered `class="a,b"` or
27
+ `[object Object]`. The `Attrs` type now types `class` and `style` with the
28
+ new exported `ClassValue` and `StyleValue` types, and `mergeClass()` accepts
29
+ any `ClassValue`.
30
+
31
+ - **htmx example and docs.** A new `examples/htmx/` shows a page and the
32
+ fragment endpoints that reuse its mixin, and the README describes using
33
+ Weave for server-rendered apps.
34
+
35
+ - **`for ... else`.** An `else` after a `for` renders when the collection is
36
+ empty.
37
+
38
+ - **Mixin call-site content.** A bare `block` inside a mixin renders the
39
+ content indented under the call (`+card("Hi")` followed by indented lines),
40
+ with indented lines under `block` as its default. The mixin gains a
41
+ `block?: () => unknown` parameter after `attrs`; a call with content passes
42
+ `{}` for `attrs` when it has none. One line of content can also go on the
43
+ call's own line, as for an element: `+btn("save") Save` or
44
+ `+btn("save")(class="wide")= label`. A call can't have both inline and
45
+ indented content (a `ParserError`). Previously text after a mixin call was
46
+ dropped without a word, so the button rendered empty.
47
+
48
+ - **Type errors mapped back to `.weave`.** `mapDiagnostic()` takes a
49
+ diagnostic reported by `tsc` against a compiled `.ts` file and uses the
50
+ `.ts.map` written beside it to translate it to the `.weave` file, line and
51
+ column; `formatDiagnostic()` prints it like a `LexerError`, with a code
52
+ frame. An error inside a TS expression points at the exact token: in
53
+ `h1= user.nmae` it lands on `nmae`, not `h1`. This covers buffered code,
54
+ `#{}` interpolations, attribute values, `if`/`else if` conditions, `for`
55
+ headers, `template`/`mixin` params, and mixin call and `extends` args,
56
+ including every line of a multi-line param list. An error in code weave
57
+ generated around them points at the start of the node.
58
+ `decodeMappings()`, `originalPosition()` and
59
+ `formatCodeFrame()` are exported too. The API takes plain
60
+ `{ file, line, col, message }` objects, so it does not depend on the
61
+ `typescript` package.
62
+
63
+ - **`weave check`.** `weave check views/ lib/views/` compiles the templates,
64
+ type-checks the whole project against its `tsconfig.json` (or
65
+ `--project <path>`), and prints every error — those in a compiled view at
66
+ their `.weave` file, line and column with a code frame, the rest as-is —
67
+ followed by `Found N errors.` It exits 1 on any compile or type error, so it
68
+ can stand in for `tsc --noEmit` in CI. It loads the project's own
69
+ `typescript`, now an optional peer dependency. `--watch` is not supported
70
+ yet.
71
+
72
+ ### Changed
73
+
74
+ - **Layouts declare their own `<Name>Blocks` type.** The compiler now emits
75
+ an exported `<Name>Blocks` interface (one optional `() => unknown` member
76
+ per `block`) above each template with slots, instead of splicing the name
77
+ into the first `import type` and requiring you to write the interface.
78
+ This works with no type import and with several layouts in one file.
79
+ **Breaking:** delete any hand-written `<Name>Blocks` interface, or `tsc`
80
+ reports a duplicate identifier.
81
+
82
+ - `for` loops now compile to the new `each()` runtime helper instead of
83
+ `.map()`, so compiled templates import `each` from `@chilts/weave`.
84
+
85
+ - Text with `#{}` interpolation now compiles to the new `interpolate` tagged
86
+ template (``h("h1", {}, interpolate`Hello ${name}`)``), so compiled
87
+ templates import `interpolate` from `@chilts/weave`. An interpolated `null`
88
+ or `undefined` now renders nothing, the same as `p= value`, where it used
89
+ to print `null` or `undefined`.
90
+
91
+ ### Fixed
92
+
93
+ - `raw()` and mixin output inside `#{}` were escaped: `p Hi #{raw(html)}`
94
+ rendered `Hi &lt;b&gt;...` and `p By #{userLink(author, {})}` showed the
95
+ link's markup as text. Interpolated values now render exactly as element
96
+ children do, so both come out as HTML.
97
+ - A bare `block` inside a mixin compiled to invalid TypeScript
98
+ (`blocks.?.()`) and content indented under a mixin call was silently
99
+ dropped. A bare `block` outside a mixin, or a named block inside one, is now
100
+ a `CodegenError`.
101
+ - `for` over a non-array iterable (`xs.entries()`, `Set`, `Map`, a generator)
102
+ rendered `[object Iterator Helper]` or failed to type-check; any iterable
103
+ now works.
104
+ - The `Attrs` type now accepts `null`, so `a(href=x)` with `x: string | null`
105
+ type-checks; the runtime already omitted `null` attributes.
106
+ - Templates return `SafeHtml`, not a string, despite 0.4.0's note that they
107
+ return strings directly: only doctype-led templates ever did, by accident,
108
+ and 0.5.0 ended that. `SafeHtml` is now the documented contract for templates
109
+ and mixins alike, so nested output is never escaped twice; call `String()` on
110
+ it for the HTML. The README's Express example passed the result straight to
111
+ `res.send()`, which sends it as JSON; it now calls `String()` first.
112
+ - A mixin that never uses `attrs` compiled to an unused `attrs` parameter,
113
+ failing `tsc` under `noUnusedParameters`; it is now named `_attrs`. The
114
+ parameter stays, so call sites may still pass extra attributes.
115
+ - A template with a single root element indented that element's children one
116
+ level too deep in the compiled `.ts`. Cosmetic only; the output is unchanged.
117
+ - Pug syntax that Weave doesn't support no longer mis-parses silently; each
118
+ form is now a `LexerError` that names the Weave alternative. `p!= x` used to
119
+ drop `x`; block expansion (`li: a Home`) and anything else run onto an
120
+ element (`a(href="/")Home`, `br/`) dropped the rest of the line; `p.` emitted
121
+ `class=""` and turned its text into elements; `- const x = 1` became a `-`
122
+ element; and `case`, `when`, `default`, `each`, `while`, `unless`, `append`
123
+ and `prepend` became elements of those names. Tag interpolation
124
+ (`p Hello #[strong world]`) and unescaped interpolation (`p Hi !{html}`)
125
+ rendered their markup characters as text; `!{html}` now points at
126
+ `#{raw(html)}`. Empty `.` / `#` shorthands and tag names that don't start
127
+ with a letter are rejected too.
128
+ - `block` took the rest of its line as the slot name, so Pug's
129
+ `block append scripts` compiled to invalid TypeScript
130
+ (`blocks.append scripts?.()`). A block name must now be a single identifier,
131
+ or it is a `LexerError`; `block append` / `block prepend` (and bare
132
+ `append` / `prepend`) point at adding a second, empty slot to the layout
133
+ (`block pageScripts` after `block scripts`) and filling that instead.
134
+
11
135
  ## [0.7.0] - 2026-10-20
12
136
 
13
137
  ### Added
@@ -168,7 +292,8 @@ Initial release — the Weave compiler and runtime.
168
292
  - Error messages with source code frames.
169
293
  - Source map generation.
170
294
 
171
- [Unreleased]: https://codeberg.org/chilts/weave/compare/v0.7.0...HEAD
295
+ [Unreleased]: https://codeberg.org/chilts/weave/compare/v0.8.0...HEAD
296
+ [0.8.0]: https://codeberg.org/chilts/weave/compare/v0.7.0...v0.8.0
172
297
  [0.7.0]: https://codeberg.org/chilts/weave/compare/v0.6.0...v0.7.0
173
298
  [0.6.0]: https://codeberg.org/chilts/weave/compare/v0.5.0...v0.6.0
174
299
  [0.5.0]: https://codeberg.org/chilts/weave/compare/v0.4.0...v0.5.0
package/ReadMe.md CHANGED
@@ -66,9 +66,40 @@ export function simple({ title, content }: SimpleProps) {
66
66
 
67
67
  Now `tsc` catches type errors, your IDE gives you autocomplete, and calling `simple({ title: "Hello", content: "World" })` returns HTML.
68
68
 
69
+ ### Type-checking templates
70
+
71
+ `tsc` reports errors against the compiled `.ts`, which you never edit.
72
+ `weave check` compiles the templates, type-checks your whole project with your
73
+ `tsconfig.json`, and reports each error in a view at its `.weave` line:
74
+
75
+ ```
76
+ $ npx weave check views/ lib/views/
77
+ weave: views/page.weave:3:5: Property 'nmae' does not exist on type '{ name: string; }'. TS2339
78
+ 3 | h1= user.nmae
79
+ | ^
80
+
81
+ Found 1 error.
82
+ ```
83
+
84
+ It exits 1 on any error, so it can replace `tsc --noEmit` in CI, and the
85
+ `file:line:col:` format works with Emacs `compile`. It uses your project's
86
+ `typescript` package (install it as a dev dependency) and the nearest
87
+ `tsconfig.json`, or the one given with `--project <path>`. Errors point at the
88
+ exact token inside an expression, attribute value, condition, parameter or
89
+ argument, and at the start of the node for code Weave generated around them.
90
+
91
+ To map diagnostics yourself (from another tool or build step), use
92
+ `mapDiagnostic({ file, line, col, message })`, which translates a position in a
93
+ compiled `.ts` file to its `.weave` source via the `.ts.map` beside it, and
94
+ `formatDiagnostic()`, which prints the result with a code frame. Neither
95
+ depends on the `typescript` package.
96
+
69
97
  ## Usage
70
98
 
71
- Templates are just functions that return strings, so they work with any framework:
99
+ Templates are just functions that return HTML, so they work with any framework.
100
+ Every template and mixin returns a `SafeHtml`, so its output isn't escaped
101
+ again when nested in another template. Call `String()` on it to get the HTML
102
+ string:
72
103
 
73
104
  ```typescript
74
105
  import express from 'express'
@@ -77,12 +108,56 @@ import { simple } from './lib/views/simple.js'
77
108
  const app = express()
78
109
 
79
110
  app.get('/', (req, res) => {
80
- res.send(simple({ title: "Hello", content: "World" }))
111
+ res.send(String(simple({ title: "Hello", content: "World" })))
81
112
  })
82
113
 
83
114
  app.listen(3000)
84
115
  ```
85
116
 
117
+ ### Server-rendered apps and htmx
118
+
119
+ Weave is a good fit for apps that render HTML on the server and swap pieces of
120
+ it into the page — [htmx](https://htmx.org), Turbo, Unpoly, or plain `fetch`.
121
+
122
+ - **Every template and mixin is an exported function.** Rendering a fragment
123
+ for a swap is just calling the mixin; there is no partial-rendering API.
124
+ - **The page and its fragments share one definition.** The page loops over
125
+ `+todoItem(todo)`, and the endpoint returns `todoItem(todo)`, so the markup
126
+ can't drift apart.
127
+ - **No runtime engine, zero dependencies.** The runtime is a few small helpers
128
+ with no Node-specific imports, so compiled views run on Node, Deno, Bun and
129
+ Cloudflare Workers alike.
130
+
131
+ A mixin for one row:
132
+
133
+ ```
134
+ mixin todoItem(todo: Todo)
135
+ li.todo(id=`todo-${todo.id}` ...attrs)
136
+ input(
137
+ type="checkbox"
138
+ checked=todo.done
139
+ hx-post=`/todos/${todo.id}/toggle`
140
+ hx-target=`#todo-${todo.id}`
141
+ hx-swap="outerHTML"
142
+ )
143
+ span= todo.text
144
+ ```
145
+
146
+ The page uses it in a loop, and the toggle endpoint returns just that row:
147
+
148
+ ```typescript
149
+ import { todoItem } from './lib/views/components/todo.js'
150
+
151
+ // POST /todos/:id/toggle
152
+ todo.done = !todo.done
153
+ return new Response(String(todoItem(todo)), {
154
+ headers: { 'content-type': 'text/html; charset=utf-8' },
155
+ })
156
+ ```
157
+
158
+ See [`examples/htmx/`](examples/htmx/) for the full page, the mixin, and a
159
+ web-standard `fetch` handler serving both.
160
+
86
161
  ## Syntax
87
162
 
88
163
  Weave's syntax will be familiar if you've used Pug.
@@ -143,6 +218,24 @@ div.pt-4(class=undefined) → <div class="pt-4">
143
218
  div.btn(class=(active ? "active" : undefined)) → <div class="btn"> when active is false
144
219
  ```
145
220
 
221
+ #### Class arrays and objects, style objects
222
+
223
+ `class=` also takes an array or an object, which suits conditional Tailwind classes. Arrays may nest and drop `null`, `undefined`, `false` and `""`; an object keeps each key whose value is truthy. Both merge with shorthand classes:
224
+
225
+ ```
226
+ div(class=(["a", "b"])) → <div class="a b">
227
+ li.item(class=({ active: isActive })) → <li class="item active"> when isActive is true
228
+ a(class=(["link", { "md:flex": wide }])) → <a class="link md:flex"> when wide is true
229
+ ```
230
+
231
+ `style=` takes an object of properties. camelCase keys become kebab-case, `--custom` properties are kept as written, and `null`, `undefined`, `false` and `""` values are dropped:
232
+
233
+ ```
234
+ div(style=({ marginTop: 0, "--gap": gap })) → <div style="margin-top:0;--gap:1rem">
235
+ ```
236
+
237
+ An array or object that yields nothing omits the attribute entirely. Numbers are not given units, so write `"4px"` rather than `4` where CSS needs one.
238
+
146
239
  #### Multi-token values must be wrapped
147
240
 
148
241
  Because a space separates one attribute from the next, a bare (unquoted) value has to be a single token. Wrap anything longer in parentheses or backticks:
@@ -206,14 +299,28 @@ else
206
299
  p Welcome, visitor
207
300
  ```
208
301
 
302
+ `unless cond` is a guard: its block renders when `cond` is falsy, the same as
303
+ `if !(cond)`. It takes no `else` or `else if` — `unless … else` is a double
304
+ negative, so write an `if` with the branches swapped instead:
305
+
306
+ ```
307
+ unless user.verified
308
+ p Please confirm your email address.
309
+ ```
310
+
209
311
  ### Iteration
210
312
 
211
313
  ```
212
314
  for ( const post of posts )
213
315
  h2= post.title
214
316
  p= post.content
317
+ else
318
+ p No posts yet.
215
319
  ```
216
320
 
321
+ The optional `else` renders when the collection is empty. Any iterable works —
322
+ arrays, `Set`, `Map`, generators, or `xs.entries()` — and it is consumed once.
323
+
217
324
  ### Mixins
218
325
 
219
326
  Mixins use positional args. An implicit `attrs` parameter captures any extra attributes passed at the call site.
@@ -233,6 +340,42 @@ Usage:
233
340
  +button("Delete", "secondary")(class="ml-4" data-confirm="true")
234
341
  ```
235
342
 
343
+ #### Call-site content
344
+
345
+ A bare `block` inside a mixin renders whatever is indented under the call,
346
+ so one mixin can wrap different content each time. Lines indented under
347
+ `block` are the default when the call has none, and `if block` tests for it:
348
+
349
+ ```
350
+ mixin card(title: string)
351
+ .card(...attrs)
352
+ h2= title
353
+ block
354
+ p.muted Nothing to show.
355
+ ```
356
+
357
+ ```
358
+ +card("Profile")(class="wide")
359
+ p= user.name
360
+ p= user.email
361
+ +card("Recent activity")
362
+ ```
363
+
364
+ The content is passed as a callback after `attrs`, so the call above compiles
365
+ to `card("Profile", { class: "wide" }, () => ...)`. Named blocks (`block
366
+ content`) are layout slots and are not allowed inside a mixin. See
367
+ [`examples/mixin-block/`](examples/mixin-block/).
368
+
369
+ One line of content can go on the call's own line instead, just as an
370
+ element takes inline text or `= expr`:
371
+
372
+ ```
373
+ +btn("save") Save
374
+ +btn("save")(class="wide")= label
375
+ ```
376
+
377
+ A call takes its content inline or indented, not both.
378
+
236
379
  ### Layouts with extends and blocks
237
380
 
238
381
  A layout defines named blocks as content slots:
@@ -250,6 +393,17 @@ template layout({ title }: LayoutProps)
250
393
  p © 2024
251
394
  ```
252
395
 
396
+ The compiled layout takes its slots as a second `blocks` argument, typed by an
397
+ interface the compiler generates from the `block` names — you don't write it:
398
+
399
+ ```ts
400
+ export interface LayoutBlocks {
401
+ content?: () => unknown
402
+ }
403
+
404
+ export function layout({ title }: LayoutProps, blocks: LayoutBlocks = {}) {
405
+ ```
406
+
253
407
  A page extends the layout, filling in the blocks. Data is passed explicitly — nothing is implicitly inherited:
254
408
 
255
409
  ```
@@ -330,7 +484,7 @@ This renders as: `Hello <strong>user@example.com</strong>.` — with a space bet
330
484
 
331
485
  ### Inline elements
332
486
 
333
- Weave does not support inline element syntax (e.g. Pug's `#[strong bold]` tag interpolation). To mix elements within text, place each element on its own indented line and use `|` for spacing and text fragments.
487
+ Weave does not support inline element syntax (e.g. Pug's `#[strong bold]` tag interpolation), and `#[` in text is a compile error. To mix elements within text, place each element on its own indented line and use `|` for spacing and text fragments. To show a literal `#[` (or `!{`), use a string expression: `p= "Use #[0] for the first item"`.
334
488
 
335
489
  ### Raw blocks
336
490
 
@@ -369,17 +523,65 @@ Standard TypeScript imports pass through to the compiled output:
369
523
  import type { BlogPost } from '../types/models.js'
370
524
  ```
371
525
 
526
+ ## Coming from Pug
527
+
528
+ Most Pug templates read the same in Weave, but some Pug features are left out
529
+ on purpose. Each one below is a compile error that names the Weave way to
530
+ write it, rather than being silently mis-rendered.
531
+
532
+ | Pug | Weave |
533
+ |---|---|
534
+ | `- const x = 1` (unbuffered code) | Templates hold no statements. Compute values in TypeScript before rendering, or use `tag= expr` |
535
+ | `case` / `when` / `default` | `if` / `else if` / `else` |
536
+ | `each x in xs`, `while` | `for ( const x of xs )`; for keys and values, `for ( const [k, v] of Object.entries(obj) )` |
537
+ | `p!= html` | `p= raw(html)` |
538
+ | `p Hi !{html}` | `p Hi #{raw(html)}` |
539
+ | `a(href!=url)` | Attribute values are always escaped |
540
+ | `p.` (block text) | `p!` for raw text, or `\|` lines for escaped text |
541
+ | `li: a Home` (block expansion) | Put the child on its own indented line |
542
+ | `#[strong bold]` (tag interpolation) | Put each element on its own line; see [Inline elements](#inline-elements) |
543
+ | `foo/` (explicit self-closing) | Void elements like `br` and `img` close themselves |
544
+ | `div&attributes(obj)` | `div(...obj)` |
545
+ | `input(disabled)` | `input(disabled=true)`; see [Boolean attributes](#boolean-attributes) |
546
+ | `extends ./layout` | `extends layout({ data })` calls the layout's template function and passes its data |
547
+
548
+ **Filters** (`:markdown-it`, `include:markdown ./post.md`) are not supported
549
+ and are not planned. Render the content in TypeScript and pass it in as a
550
+ `raw()` value.
551
+
552
+ **`block append` / `block prepend`** are not supported. A page replaces a
553
+ layout's block completely. When pages need to add to what the layout already
554
+ renders, give the layout a second, empty slot:
555
+
556
+ ```
557
+ head
558
+ block scripts
559
+ script(src="/app.js")
560
+ block pageScripts
561
+ ```
562
+
563
+ **Dynamic mixin calls** (`+#{name}()`) are not supported. Mixins are ordinary
564
+ functions, so you can choose one at runtime with a buffered expression and
565
+ pass `{}` for its `attrs`: `div= mixins[kind](item, {})`.
566
+
567
+ **Implicit scope.** In Pug, included and extended templates can see their
568
+ caller's variables. In Weave, data only travels explicitly: through template
569
+ params, `extends layout({ ... })` and mixin args.
570
+
372
571
  ## Development
373
572
 
374
573
  ```bash
375
574
  npm run build # compile TypeScript to dist/
376
- npm test # run tests (vitest)
377
- npm run lint # type-check without emitting (tsc --noEmit)
575
+ npm test # lint, then type-check, then run tests (vitest)
576
+ npm run lint # lint with ESLint
577
+ npm run typecheck # type-check without emitting (tsc --noEmit)
378
578
  ```
379
579
 
380
580
  ## Status
381
581
 
382
- Weave is in early design. The `examples/` directory contains paired `.weave` and `.ts` files that define the target syntax and expected compiler output. The compiler is under active development.
582
+ Weave is usable today: the compiler, the `weave` CLI (with watch mode and source maps) and the runtime helpers are published on npm. It is still pre-1.0, so the syntax may change in a minor release — breaking changes are called out in [CHANGELOG.md](CHANGELOG.md).
583
+
584
+ The `examples/` directory pairs each `.weave` template with its compiled `.ts`. Every example is compiled in the test suite and compared byte-for-byte against that file, so they double as a reference for what the compiler emits.
383
585
 
384
586
  ## Editor Support
385
587
 
@@ -0,0 +1,14 @@
1
+ export interface CheckOptions {
2
+ inputPath: string;
3
+ outputPath: string;
4
+ /** tsconfig.json to check against; defaults to the nearest one above the cwd. */
5
+ project?: string;
6
+ }
7
+ /**
8
+ * `weave check`: compile the templates, type-check the whole tsconfig program
9
+ * with the compiled views added, and print every diagnostic — those in a
10
+ * compiled view at its .weave location. Progress goes to stderr and
11
+ * diagnostics to stdout. Returns the exit code: 1 if anything failed.
12
+ */
13
+ export declare function check(options: CheckOptions): number;
14
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { mapDiagnostic, formatDiagnostic } from '../diagnostics/index.js';
5
+ import { compileFiles, discoverFiles, pruneOrphans, singleOutputFile } from './index.js';
6
+ // tsc's "No inputs were found in config file". The compiled views are added
7
+ // as roots, so a tsconfig whose include matches nothing is still fine.
8
+ const NO_INPUTS = 18003;
9
+ /**
10
+ * The project's own typescript, so `check` agrees with the user's `tsc`.
11
+ * Falls back to one weave itself can resolve. Not a dependency of weave: only
12
+ * `check` needs it, and only when it runs.
13
+ */
14
+ function loadTypeScript() {
15
+ for (const base of [path.join(process.cwd(), 'noop.js'), import.meta.url]) {
16
+ try {
17
+ return createRequire(base)('typescript');
18
+ }
19
+ catch {
20
+ // Try the next location.
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ // Relative to the cwd when it is under it, so the path is short and editors
26
+ // resolve it against the directory `weave check` ran in.
27
+ function displayPath(file) {
28
+ const rel = path.relative(process.cwd(), file);
29
+ return rel.startsWith('..') || path.isAbsolute(rel) ? file : rel;
30
+ }
31
+ function readSource(file) {
32
+ try {
33
+ return readFileSync(file, 'utf-8');
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ function compilePairs(inputPath, outputPath) {
40
+ if (inputPath.endsWith('.weave')) {
41
+ return [{ inputFile: inputPath, outputFile: singleOutputFile(inputPath, outputPath) }];
42
+ }
43
+ let files;
44
+ try {
45
+ files = discoverFiles(inputPath);
46
+ }
47
+ catch (err) {
48
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
49
+ return `${inputPath}: no such file or directory`;
50
+ }
51
+ return `${inputPath}: ${err instanceof Error ? err.message : String(err)}`;
52
+ }
53
+ if (files.length === 0)
54
+ return `no .weave files found in ${inputPath}`;
55
+ return files.map(relPath => ({
56
+ inputFile: path.join(inputPath, relPath),
57
+ outputFile: path.join(outputPath, relPath.replace(/\.weave$/, '.ts')),
58
+ }));
59
+ }
60
+ /**
61
+ * `weave check`: compile the templates, type-check the whole tsconfig program
62
+ * with the compiled views added, and print every diagnostic — those in a
63
+ * compiled view at its .weave location. Progress goes to stderr and
64
+ * diagnostics to stdout. Returns the exit code: 1 if anything failed.
65
+ */
66
+ export function check(options) {
67
+ const ts = loadTypeScript();
68
+ if (!ts) {
69
+ console.error('weave: check needs the "typescript" package — npm i -D typescript');
70
+ return 1;
71
+ }
72
+ const project = options.project ?? ts.findConfigFile(process.cwd(), ts.sys.fileExists);
73
+ if (!project) {
74
+ console.error('weave: no tsconfig.json found; pass one with --project');
75
+ return 1;
76
+ }
77
+ if (!ts.sys.fileExists(project)) {
78
+ console.error(`weave: ${project}: no such file`);
79
+ return 1;
80
+ }
81
+ const pairs = compilePairs(options.inputPath, options.outputPath);
82
+ if (typeof pairs === 'string') {
83
+ console.error(`weave: ${pairs}`);
84
+ return 1;
85
+ }
86
+ const log = (msg) => console.error(msg);
87
+ const { written, failed } = compileFiles(pairs, true, log);
88
+ if (!options.inputPath.endsWith('.weave'))
89
+ pruneOrphans(options.inputPath, options.outputPath, log);
90
+ const config = ts.readConfigFile(project, ts.sys.readFile);
91
+ const parsed = config.error
92
+ ? undefined
93
+ : ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(path.resolve(project)), { noEmit: true }, project);
94
+ const rootNames = new Set((parsed?.fileNames ?? []).map(f => path.resolve(f)));
95
+ for (const file of written)
96
+ rootNames.add(path.resolve(file));
97
+ const diagnostics = config.error
98
+ ? [config.error]
99
+ : parsed.errors.filter(d => d.code !== NO_INPUTS);
100
+ if (!config.error) {
101
+ const program = ts.createProgram([...rootNames], parsed.options);
102
+ diagnostics.push(...ts.getPreEmitDiagnostics(program));
103
+ }
104
+ const errorFiles = new Set(failed.map(f => path.resolve(f)));
105
+ let errors = failed.length;
106
+ for (const d of diagnostics) {
107
+ const isError = d.category === ts.DiagnosticCategory.Error;
108
+ const message = (isError ? '' : 'warning: ')
109
+ + ts.flattenDiagnosticMessageText(d.messageText, '\n') + ` TS${d.code}`;
110
+ if (!d.file || d.start === undefined) {
111
+ console.log(`weave: ${message}`);
112
+ if (isError)
113
+ errors++;
114
+ continue;
115
+ }
116
+ const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
117
+ const diag = { file: d.file.fileName, line: line + 1, col: character + 1, message };
118
+ const mapped = mapDiagnostic(diag) ?? diag;
119
+ const source = mapped === diag ? d.file.text : readSource(mapped.file);
120
+ console.log(formatDiagnostic({ ...mapped, file: displayPath(mapped.file) }, source));
121
+ if (isError) {
122
+ errors++;
123
+ errorFiles.add(path.resolve(mapped.file));
124
+ }
125
+ }
126
+ if (errors === 0)
127
+ return 0;
128
+ const noun = errors === 1 ? 'error' : 'errors';
129
+ const where = errorFiles.size > 1 ? ` in ${errorFiles.size} files` : '';
130
+ console.log(`\nFound ${errors} ${noun}${where}.`);
131
+ return 1;
132
+ }
133
+ //# sourceMappingURL=check.js.map
@@ -1,4 +1,6 @@
1
1
  import type { IncludeResolver } from '../codegen/index.js';
2
+ import { formatCodeFrame } from '../diagnostics/index.js';
3
+ import { check } from './check.js';
2
4
  interface CompileArgs {
3
5
  mode: 'compile';
4
6
  inputPath: string;
@@ -11,19 +13,25 @@ interface WatchArgs {
11
13
  outputPath: string;
12
14
  sourcemap: boolean;
13
15
  }
16
+ interface CheckArgs {
17
+ mode: 'check';
18
+ inputPath: string;
19
+ outputPath: string;
20
+ project?: string;
21
+ }
14
22
  interface HelpArgs {
15
23
  mode: 'help';
16
24
  }
17
- type CliArgs = CompileArgs | WatchArgs | HelpArgs;
25
+ type CliArgs = CompileArgs | WatchArgs | CheckArgs | HelpArgs;
18
26
  export declare class UsageError extends Error {
19
27
  constructor(message: string);
20
28
  }
21
29
  export declare function parseArgs(argv: string[]): CliArgs;
22
30
  export declare function usage(): string;
23
31
  export declare function discoverFiles(inputDir: string): string[];
24
- export declare function pruneOrphans(inputDir: string, outputDir: string): void;
32
+ export declare function pruneOrphans(inputDir: string, outputDir: string, log?: (msg: string) => void): void;
25
33
  export declare function formatElapsed(ms: number): string;
26
- export declare function formatCodeFrame(source: string, line: number, col: number): string;
34
+ export { formatCodeFrame, check };
27
35
  export declare function formatError(filePath: string, err: unknown, source?: string): string;
28
36
  /** Resolves `include` paths against the including file and reads their mixins. */
29
37
  export declare function includeResolver(inputFile: string): IncludeResolver;
@@ -42,8 +50,22 @@ export declare function includedFiles(inputFile: string, source: string): string
42
50
  export declare function findIncluders(inputDir: string, relPath: string): string[];
43
51
  export declare function compileFile(inputFile: string, outputFile: string, sourcemap?: boolean): void;
44
52
  export declare function compileDir(inputDir: string, outputDir: string, sourcemap?: boolean): boolean;
53
+ /** Where single-file mode writes: <output> itself if it is a .ts, else a file inside it. */
54
+ export declare function singleOutputFile(inputPath: string, outputPath: string): string;
55
+ export interface CompilePair {
56
+ inputFile: string;
57
+ outputFile: string;
58
+ }
59
+ /**
60
+ * Compile each pair, logging one line per success and printing each error to
61
+ * stderr. Returns the outputs written and the inputs that failed, so a caller
62
+ * can decide what a failure means.
63
+ */
64
+ export declare function compileFiles(pairs: CompilePair[], sourcemap: boolean, log?: (msg: string) => void): {
65
+ written: string[];
66
+ failed: string[];
67
+ };
45
68
  export declare function watchDir(inputDir: string, outputDir: string, sourcemap?: boolean): () => void;
46
69
  export declare function reconcile(inputDir: string, outputDir: string, sourcemap: boolean, pending: Map<string, ReturnType<typeof setTimeout>>): void;
47
70
  export declare function main(argv: string[]): void;
48
- export {};
49
71
  //# sourceMappingURL=index.d.ts.map