@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 +126 -1
- package/ReadMe.md +208 -6
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.js +133 -0
- package/dist/cli/index.d.ts +26 -4
- package/dist/cli/index.js +95 -26
- package/dist/codegen/index.js +269 -217
- package/dist/codegen/sourcemap.d.ts +49 -1
- package/dist/codegen/sourcemap.js +154 -2
- package/dist/diagnostics/index.d.ts +25 -0
- package/dist/diagnostics/index.js +68 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/lexer/index.d.ts +5 -1
- package/dist/lexer/index.js +129 -10
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.js +74 -9
- package/dist/parser/types.d.ts +13 -0
- package/dist/runtime/index.d.ts +40 -2
- package/dist/runtime/index.js +59 -4
- package/package.json +17 -3
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 <b>...` 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.
|
|
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
|
|
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 #
|
|
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
|
|
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
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -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
|
|
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
|