@chilts/weave 0.6.0 → 0.7.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 +178 -0
- package/ReadMe.md +53 -2
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +96 -11
- package/dist/codegen/index.d.ts +17 -3
- package/dist/codegen/index.js +194 -48
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/lexer/index.js +220 -91
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +11 -1
- package/package.json +7 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@chilts/weave` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
While the version is below `1.0.0`, breaking changes may land in minor releases.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.7.0] - 2026-10-20
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Escaped dots in class and id shorthand.** `.gap-1\.5` is the single class
|
|
16
|
+
`gap-1.5`, so Tailwind's fractional spacing classes work as shorthand.
|
|
17
|
+
- **Multi-line parameter lists.** The parentheses after `template`, `mixin`,
|
|
18
|
+
`extends`, `for` and a mixin call may now wrap over several lines, staying
|
|
19
|
+
open until they balance, so a long destructured signature can be written
|
|
20
|
+
the way TypeScript is normally wrapped. Continuation lines must be indented
|
|
21
|
+
under the opening line (a closing line such as `}: Props)` may sit level with
|
|
22
|
+
it). A string inside must close on its own line, and `//` comments are
|
|
23
|
+
dropped from the output. The compiled code keeps the wrapped layout, adding
|
|
24
|
+
`attrs`/`blocks` on a line of their own when the `)` was on its own line.
|
|
25
|
+
- **Emacs mode understands context.** `weave-mode` now follows multi-line
|
|
26
|
+
attribute and parameter lists and raw (`tag!`) blocks when highlighting and
|
|
27
|
+
indenting, highlights attribute names, aligns `else` with its `if`, and
|
|
28
|
+
cycles outwards on repeated TAB. An apostrophe in text no longer opens a
|
|
29
|
+
string, `//` in a URL is no longer a comment, and `.`/`#` are only coloured
|
|
30
|
+
as a class or id where they really are one.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- A dotted class such as `.gap-1.5` was silently split into `gap-1` plus a
|
|
35
|
+
stray class `5`. A shorthand class that is a bare number is now a
|
|
36
|
+
`LexerError` that names the split and suggests escaping the dot.
|
|
37
|
+
- `include ./partials` imported a binding named after the file (`partials`)
|
|
38
|
+
instead of the mixins the template calls, so a file defining several
|
|
39
|
+
mixins, or one not named after its file, compiled to code that failed in
|
|
40
|
+
`tsc`. The compiler now reads the included file and imports only the mixins
|
|
41
|
+
called (`import { badge, chip } from './partials.js'`). Calling a mixin that
|
|
42
|
+
no include or local definition provides is a `CodegenError`. For API users,
|
|
43
|
+
`compile()`, `compileWithSourceMap()` and `generate()` take an optional
|
|
44
|
+
`resolveInclude`. Without it, a lone include provides every called mixin
|
|
45
|
+
that isn't defined locally, and several includes are an error.
|
|
46
|
+
- Shorthand classes combined with a non-literal `class=` value leaked the
|
|
47
|
+
value's string form: `div.pt-4(class=undefined)` rendered
|
|
48
|
+
`class="pt-4 undefined"`, as did the usual conditional-class pattern
|
|
49
|
+
`class=(on ? "x" : undefined)` whenever it was off. Non-literal values are
|
|
50
|
+
now merged at render time by a new `mergeClass()` runtime helper, which
|
|
51
|
+
drops `undefined`, `null`, `false` and `""`. An attribute whose value is
|
|
52
|
+
`null` is now omitted instead of rendering as `"null"`.
|
|
53
|
+
- A template literal with a `${...}` interpolation inside a mixin call's
|
|
54
|
+
arguments, or a `template`/`mixin` parameter list, failed with
|
|
55
|
+
"Unterminated parentheses". A string containing a brace inside the
|
|
56
|
+
interpolation is also handled.
|
|
57
|
+
- Error locations and source maps now point at the start of a token rather
|
|
58
|
+
than just past its end — for example, a mixin call is located at its `+`
|
|
59
|
+
instead of its argument list.
|
|
60
|
+
- In watch mode, changing an included file now recompiles the templates that
|
|
61
|
+
include it, so a newly added mixin is picked up without touching each page.
|
|
62
|
+
The start-up reconcile likewise recompiles a template whose include is
|
|
63
|
+
newer than its output.
|
|
64
|
+
- Rebuilding with `--no-sourcemap` now removes a `.ts.map` left over from an
|
|
65
|
+
earlier build instead of leaving a map that no longer matches its `.ts`.
|
|
66
|
+
|
|
67
|
+
## [0.6.0] - 2026-10-19
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- **Multi-line attribute lists.** End a line with the opening `(` to spread an
|
|
72
|
+
element's attributes over several lines — one attribute per line, indented
|
|
73
|
+
under the element, closed by a `)` on a line of its own. Works for mixin call
|
|
74
|
+
attributes too. Anything after the closing `)` is read as trailing content.
|
|
75
|
+
Attribute values follow exactly the same rules as the inline form.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- **Breaking: every attribute must have a value.** The Pug-style valueless
|
|
80
|
+
boolean shorthand has been removed — `input(disabled)` no longer compiles.
|
|
81
|
+
Boolean attributes are now written with an explicit boolean value:
|
|
82
|
+
`disabled=true`, `disabled=false`, or an expression `disabled=isActive`
|
|
83
|
+
(the runtime renders a bare attribute for `true` and omits it for `false`
|
|
84
|
+
or `undefined`). Do not use a quoted `disabled="false"` — in HTML any
|
|
85
|
+
attribute that is present counts as true.
|
|
86
|
+
- A bare (unquoted) attribute value must be a single token. Multi-token
|
|
87
|
+
expressions such as a ternary or string concatenation must be wrapped in
|
|
88
|
+
parentheses (`class=(wide ? "a" : "b")`) or backticks
|
|
89
|
+
(``class=`${wide ? "a" : "b"}` ``), because a space separates one
|
|
90
|
+
attribute from the next.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- The lexer no longer hangs and aborts the process (SIGABRT) on a multi-token
|
|
95
|
+
bare attribute value; it now raises a clear `LexerError` with line and
|
|
96
|
+
column, pointing you to wrap the expression.
|
|
97
|
+
- `main(class=a b)` is rejected with a message offering both readings
|
|
98
|
+
(`class="a b"` as one value, or `class=a b="..."` as two attributes)
|
|
99
|
+
instead of silently parsing as `class="a"` plus a boolean `b`.
|
|
100
|
+
- An attribute list that ran off the end of its line was accepted as if it had
|
|
101
|
+
been closed, so the following lines were lexed as child elements — attribute
|
|
102
|
+
names became tags and their values rendered as visible text, and the stray
|
|
103
|
+
`)` could leak into the generated TypeScript. It is now a `LexerError`
|
|
104
|
+
anchored on the opening `(`.
|
|
105
|
+
- Watch mode reconciles the output tree on start so edits made while the
|
|
106
|
+
watcher was down are not missed.
|
|
107
|
+
- Watch mode on Linux stopped seeing a template after the first time an editor
|
|
108
|
+
saved it atomically (write a temp file, rename it over the original), so
|
|
109
|
+
later edits were never recompiled. Each directory now has its own watcher,
|
|
110
|
+
which survives atomic saves, and subdirectories created while watching are
|
|
111
|
+
picked up — including any templates already written into them.
|
|
112
|
+
- Deleting a `.weave` template now removes its compiled `.ts` from the output
|
|
113
|
+
directory (in one-shot builds and in watch mode). The output directory is
|
|
114
|
+
treated as owned by Weave: any `.ts` with no matching `.weave` is removed.
|
|
115
|
+
- A mixin, or a block's default content, with more than one root element
|
|
116
|
+
referenced `Fragment` without importing it, so the compiled file failed to
|
|
117
|
+
type-check. `Fragment` is now imported whenever it is used.
|
|
118
|
+
|
|
119
|
+
### Removed
|
|
120
|
+
|
|
121
|
+
- Source maps are no longer included in the published npm bundle.
|
|
122
|
+
|
|
123
|
+
## [0.5.0] - 2026-06-11
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
|
|
127
|
+
- Multi-level `extends` and `doctype` now generate type-safe output.
|
|
128
|
+
|
|
129
|
+
## [0.4.0] - 2026-04-05
|
|
130
|
+
|
|
131
|
+
### Changed
|
|
132
|
+
|
|
133
|
+
- Compiled templates return strings directly instead of arrays, simplifying
|
|
134
|
+
the generated output and how templates compose.
|
|
135
|
+
|
|
136
|
+
## [0.3.0] - 2026-04-05
|
|
137
|
+
|
|
138
|
+
### Added
|
|
139
|
+
|
|
140
|
+
- Raw block syntax (`tag!`) for passing literal, unescaped content through to
|
|
141
|
+
the output (inline CSS, JavaScript, etc.).
|
|
142
|
+
- Emacs major mode with syntax highlighting, indentation, and comment support.
|
|
143
|
+
|
|
144
|
+
### Documentation
|
|
145
|
+
|
|
146
|
+
- Added guidance on special characters and the single-pass escaping model to
|
|
147
|
+
the README.
|
|
148
|
+
|
|
149
|
+
## [0.2.0] - 2026-04-05
|
|
150
|
+
|
|
151
|
+
### Added
|
|
152
|
+
|
|
153
|
+
- A bare `|` (pipe) on its own line emits a single space, for adding
|
|
154
|
+
whitespace between sibling elements.
|
|
155
|
+
|
|
156
|
+
## [0.1.0] - 2026-04-05
|
|
157
|
+
|
|
158
|
+
Initial release — the Weave compiler and runtime.
|
|
159
|
+
|
|
160
|
+
### Added
|
|
161
|
+
|
|
162
|
+
- Template syntax compiling `.weave` files to typed TypeScript functions:
|
|
163
|
+
templates, mixins, `extends`/`block` layouts, conditionals, iteration,
|
|
164
|
+
includes, interpolation, buffered expressions, and silent/pass-through
|
|
165
|
+
comments.
|
|
166
|
+
- Runtime helpers: `h`, `doctype`, `Fragment`, `raw`, and single-pass escaping.
|
|
167
|
+
- CLI `compile` command and a `--watch` mode for automatic recompilation.
|
|
168
|
+
- Error messages with source code frames.
|
|
169
|
+
- Source map generation.
|
|
170
|
+
|
|
171
|
+
[Unreleased]: https://codeberg.org/chilts/weave/compare/v0.7.0...HEAD
|
|
172
|
+
[0.7.0]: https://codeberg.org/chilts/weave/compare/v0.6.0...v0.7.0
|
|
173
|
+
[0.6.0]: https://codeberg.org/chilts/weave/compare/v0.5.0...v0.6.0
|
|
174
|
+
[0.5.0]: https://codeberg.org/chilts/weave/compare/v0.4.0...v0.5.0
|
|
175
|
+
[0.4.0]: https://codeberg.org/chilts/weave/compare/v0.3.0...v0.4.0
|
|
176
|
+
[0.3.0]: https://codeberg.org/chilts/weave/compare/v0.2.0...v0.3.0
|
|
177
|
+
[0.2.0]: https://codeberg.org/chilts/weave/compare/v0.1.0...v0.2.0
|
|
178
|
+
[0.1.0]: https://codeberg.org/chilts/weave/src/tag/v0.1.0
|
package/ReadMe.md
CHANGED
|
@@ -12,7 +12,7 @@ Pug is great. Weave takes the same indentation-based syntax and makes it TypeScr
|
|
|
12
12
|
|
|
13
13
|
- **Type-safe templates** — template parameters are real TypeScript types
|
|
14
14
|
- **Type-safe mixins** — positional args with TS type annotations
|
|
15
|
-
- **Tailwind-friendly** — class names with colons (`.md:flex`, `.hover:bg-blue-500`) just work
|
|
15
|
+
- **Tailwind-friendly** — class names with colons (`.md:flex`, `.hover:bg-blue-500`) just work, and dots escape (`.gap-1\.5`)
|
|
16
16
|
- **Single-pass escaping** — values are escaped once at render time, never pre-escaped
|
|
17
17
|
- **No runtime template engine** — compiled output is plain TypeScript
|
|
18
18
|
|
|
@@ -97,6 +97,14 @@ p.intro This is a paragraph with a class
|
|
|
97
97
|
a(href="/about" class="nav-link") About
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
A `.` always starts a new class, so a class name that contains a dot — like Tailwind's `gap-1.5` — must escape it with a backslash:
|
|
101
|
+
|
|
102
|
+
```pug
|
|
103
|
+
.flex.gap-1\.5.px-2\.5
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
An unescaped `.gap-1.5` would split into `gap-1` and a stray `5`, so a class that is a bare number is rejected with an error pointing at the fix.
|
|
107
|
+
|
|
100
108
|
### Attributes
|
|
101
109
|
|
|
102
110
|
Attributes live inside parentheses and are separated by spaces. **Every attribute must have a value** — there is no valueless shorthand:
|
|
@@ -115,7 +123,7 @@ button(class=`btn-${variant}`)= label
|
|
|
115
123
|
|
|
116
124
|
#### Boolean attributes
|
|
117
125
|
|
|
118
|
-
Write the boolean explicitly. The runtime renders `true` as a bare attribute and omits the attribute entirely when the value is `false` or `
|
|
126
|
+
Write the boolean explicitly. The runtime renders `true` as a bare attribute and omits the attribute entirely when the value is `false`, `undefined` or `null`:
|
|
119
127
|
|
|
120
128
|
```
|
|
121
129
|
input(type="checkbox" checked=true) → <input type="checkbox" checked>
|
|
@@ -125,6 +133,16 @@ input(type="checkbox" checked=isActive) → omitted when isActive is false
|
|
|
125
133
|
|
|
126
134
|
Avoid a quoted `checked="false"` — in HTML any attribute that is present counts as true, so that renders as checked.
|
|
127
135
|
|
|
136
|
+
#### Combining shorthand classes with `class=`
|
|
137
|
+
|
|
138
|
+
Shorthand classes and a `class=` value are merged into one attribute. The shorthand classes are always kept; a value of `undefined`, `null`, `false` or `""` just adds nothing:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
div.pt-4(class="mt-2") → <div class="pt-4 mt-2">
|
|
142
|
+
div.pt-4(class=undefined) → <div class="pt-4">
|
|
143
|
+
div.btn(class=(active ? "active" : undefined)) → <div class="btn"> when active is false
|
|
144
|
+
```
|
|
145
|
+
|
|
128
146
|
#### Multi-token values must be wrapped
|
|
129
147
|
|
|
130
148
|
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:
|
|
@@ -244,6 +262,32 @@ template home({ title, welcomeMessage }: HomeProps)
|
|
|
244
262
|
|
|
245
263
|
Blocks can have default content in the layout. If a page doesn't override a block, the default renders.
|
|
246
264
|
|
|
265
|
+
### Long parameter lists
|
|
266
|
+
|
|
267
|
+
The parentheses after `template`, `mixin`, `extends`, `for` and a mixin call hold
|
|
268
|
+
TypeScript, which Weave copies through untouched. They can wrap over several lines
|
|
269
|
+
however TypeScript usually wraps: the group simply stays open until its parentheses
|
|
270
|
+
balance.
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
template dashboard({
|
|
274
|
+
title,
|
|
275
|
+
user,
|
|
276
|
+
}: DashboardProps)
|
|
277
|
+
h1= title
|
|
278
|
+
|
|
279
|
+
+card(
|
|
280
|
+
post.title,
|
|
281
|
+
post.summary,
|
|
282
|
+
)(class="mt-4")
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Unlike a long attribute list there is no required shape, but continuation lines must
|
|
286
|
+
be indented under the line that opens the group. The closing line may sit level with
|
|
287
|
+
it if it starts with a closing bracket, as `}: DashboardProps)` does. So a forgotten
|
|
288
|
+
`)` is reported where the indentation drops back. A string must close on the line it
|
|
289
|
+
starts on, and `//` comments are allowed but left out of the compiled output.
|
|
290
|
+
|
|
247
291
|
### Includes
|
|
248
292
|
|
|
249
293
|
Include another `.weave` file to use its mixins:
|
|
@@ -258,6 +302,12 @@ template actionsPage({ title }: ActionsPageProps)
|
|
|
258
302
|
+button("Save", "primary")
|
|
259
303
|
```
|
|
260
304
|
|
|
305
|
+
An included file can define any number of mixins. Weave reads it and imports
|
|
306
|
+
only the ones the template calls, so `include ./partials` followed by
|
|
307
|
+
`+badge(...)` and `+chip(...)` compiles to
|
|
308
|
+
`import { badge, chip } from './partials.js'`. Calling a mixin that is neither
|
|
309
|
+
defined in the file nor provided by an include is a compile error.
|
|
310
|
+
|
|
261
311
|
### Text content
|
|
262
312
|
|
|
263
313
|
Use `|` (pipe) to add literal text as a child of an element:
|
|
@@ -341,6 +391,7 @@ See [CHANGELOG.md](CHANGELOG.md) for the full details of each release.
|
|
|
341
391
|
|
|
342
392
|
| Version | Date | Highlights |
|
|
343
393
|
|---------|------------|------------|
|
|
394
|
+
| 0.7.0 | 2026-10-20 | Multi-line parameter lists; escaped dots in class shorthand; `include` imports the mixins it provides; context-aware Emacs mode |
|
|
344
395
|
| 0.6.0 | 2026-10-19 | Multi-line attribute lists; every attribute requires a value (breaking); watch-mode fixes |
|
|
345
396
|
| 0.5.0 | 2026-06-11 | Type-safe multi-level `extends` and `doctype` |
|
|
346
397
|
| 0.4.0 | 2026-04-05 | Templates return strings directly |
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IncludeResolver } from '../codegen/index.js';
|
|
1
2
|
interface CompileArgs {
|
|
2
3
|
mode: 'compile';
|
|
3
4
|
inputPath: string;
|
|
@@ -24,6 +25,21 @@ export declare function pruneOrphans(inputDir: string, outputDir: string): void;
|
|
|
24
25
|
export declare function formatElapsed(ms: number): string;
|
|
25
26
|
export declare function formatCodeFrame(source: string, line: number, col: number): string;
|
|
26
27
|
export declare function formatError(filePath: string, err: unknown, source?: string): string;
|
|
28
|
+
/** Resolves `include` paths against the including file and reads their mixins. */
|
|
29
|
+
export declare function includeResolver(inputFile: string): IncludeResolver;
|
|
30
|
+
/**
|
|
31
|
+
* Absolute paths of the .weave files a template includes. A template that does
|
|
32
|
+
* not lex or parse includes nothing as far as the watcher is concerned — its
|
|
33
|
+
* own compile reports the error.
|
|
34
|
+
*/
|
|
35
|
+
export declare function includedFiles(inputFile: string, source: string): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Templates under inputDir (relative paths) that include relPath. An includer's
|
|
38
|
+
* import line is built from the mixins its includes define, so it must be
|
|
39
|
+
* recompiled when one of them changes. Includes do not re-export, so only
|
|
40
|
+
* direct includers are affected.
|
|
41
|
+
*/
|
|
42
|
+
export declare function findIncluders(inputDir: string, relPath: string): string[];
|
|
27
43
|
export declare function compileFile(inputFile: string, outputFile: string, sourcemap?: boolean): void;
|
|
28
44
|
export declare function compileDir(inputDir: string, outputDir: string, sourcemap?: boolean): boolean;
|
|
29
45
|
export declare function watchDir(inputDir: string, outputDir: string, sourcemap?: boolean): () => void;
|
package/dist/cli/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync, rmdirSync, watch, } from 'node:fs';
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync, rmdirSync, rmSync, watch, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { compile, compileWithSourceMap } from '../codegen/index.js';
|
|
4
|
-
import { LexerError } from '../lexer/index.js';
|
|
5
|
-
import { ParserError } from '../parser/index.js';
|
|
3
|
+
import { compile, compileWithSourceMap, definedMixins } from '../codegen/index.js';
|
|
4
|
+
import { lex, LexerError } from '../lexer/index.js';
|
|
5
|
+
import { parse, ParserError } from '../parser/index.js';
|
|
6
6
|
import { CodegenError } from '../codegen/index.js';
|
|
7
7
|
export class UsageError extends Error {
|
|
8
8
|
constructor(message) {
|
|
@@ -173,6 +173,69 @@ export function formatError(filePath, err, source) {
|
|
|
173
173
|
}
|
|
174
174
|
return result;
|
|
175
175
|
}
|
|
176
|
+
/** Resolves `include` paths against the including file and reads their mixins. */
|
|
177
|
+
export function includeResolver(inputFile) {
|
|
178
|
+
return includePath => {
|
|
179
|
+
const file = path.resolve(path.dirname(inputFile), includePath + '.weave');
|
|
180
|
+
let source;
|
|
181
|
+
try {
|
|
182
|
+
source = readFileSync(file, 'utf-8');
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
186
|
+
throw new Error(`${path.relative(process.cwd(), file)}: no such file`);
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
return definedMixins(parse(lex(source)));
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Absolute paths of the .weave files a template includes. A template that does
|
|
195
|
+
* not lex or parse includes nothing as far as the watcher is concerned — its
|
|
196
|
+
* own compile reports the error.
|
|
197
|
+
*/
|
|
198
|
+
export function includedFiles(inputFile, source) {
|
|
199
|
+
if (!/^include\s/m.test(source))
|
|
200
|
+
return [];
|
|
201
|
+
let doc;
|
|
202
|
+
try {
|
|
203
|
+
doc = parse(lex(source));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
return doc.children.flatMap(c => c.type === 'Include' ? [path.resolve(path.dirname(inputFile), c.path + '.weave')] : []);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Templates under inputDir (relative paths) that include relPath. An includer's
|
|
212
|
+
* import line is built from the mixins its includes define, so it must be
|
|
213
|
+
* recompiled when one of them changes. Includes do not re-export, so only
|
|
214
|
+
* direct includers are affected.
|
|
215
|
+
*/
|
|
216
|
+
export function findIncluders(inputDir, relPath) {
|
|
217
|
+
const target = path.resolve(inputDir, relPath);
|
|
218
|
+
let files;
|
|
219
|
+
try {
|
|
220
|
+
files = discoverFiles(inputDir);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
return files.filter(file => {
|
|
226
|
+
if (file === relPath)
|
|
227
|
+
return false;
|
|
228
|
+
const inFile = path.join(inputDir, file);
|
|
229
|
+
let source;
|
|
230
|
+
try {
|
|
231
|
+
source = readFileSync(inFile, 'utf-8');
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return includedFiles(inFile, source).includes(target);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
176
239
|
function writeCompiled(inputFile, outputFile, source, sourcemap) {
|
|
177
240
|
const start = performance.now();
|
|
178
241
|
if (sourcemap) {
|
|
@@ -181,15 +244,19 @@ function writeCompiled(inputFile, outputFile, source, sourcemap) {
|
|
|
181
244
|
const { code, sourceMap } = compileWithSourceMap(source, {
|
|
182
245
|
file: basename,
|
|
183
246
|
sourceFile: sourceRelPath,
|
|
247
|
+
resolveInclude: includeResolver(inputFile),
|
|
184
248
|
});
|
|
185
249
|
mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
186
250
|
writeFileSync(outputFile, code + `//# sourceMappingURL=${basename}.map\n`);
|
|
187
251
|
writeFileSync(outputFile + '.map', JSON.stringify(sourceMap));
|
|
188
252
|
}
|
|
189
253
|
else {
|
|
190
|
-
const result = compile(source);
|
|
254
|
+
const result = compile(source, { resolveInclude: includeResolver(inputFile) });
|
|
191
255
|
mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
192
256
|
writeFileSync(outputFile, result);
|
|
257
|
+
// A map from an earlier sourcemap build no longer matches this .ts,
|
|
258
|
+
// and pruning keeps it because the .weave still exists.
|
|
259
|
+
rmSync(outputFile + '.map', { force: true });
|
|
193
260
|
}
|
|
194
261
|
return performance.now() - start;
|
|
195
262
|
}
|
|
@@ -259,6 +326,13 @@ export function watchDir(inputDir, outputDir, sourcemap = true) {
|
|
|
259
326
|
pending.set(relPath, setTimeout(() => {
|
|
260
327
|
pending.delete(relPath);
|
|
261
328
|
compileWatchedFile(inputDir, outputDir, relPath, sourcemap);
|
|
329
|
+
// Compiled directly rather than scheduled, so an include cycle
|
|
330
|
+
// cannot keep re-triggering itself. One with its own pending
|
|
331
|
+
// timer will be compiled by that.
|
|
332
|
+
for (const includer of findIncluders(inputDir, relPath)) {
|
|
333
|
+
if (!pending.has(includer))
|
|
334
|
+
compileWatchedFile(inputDir, outputDir, includer, sourcemap);
|
|
335
|
+
}
|
|
262
336
|
}, 50));
|
|
263
337
|
};
|
|
264
338
|
const isDir = (relPath) => {
|
|
@@ -355,9 +429,9 @@ export function watchDir(inputDir, outputDir, sourcemap = true) {
|
|
|
355
429
|
};
|
|
356
430
|
}
|
|
357
431
|
// Walk the input dir and recompile any .weave whose .ts output is missing or
|
|
358
|
-
// older than the source. Files with a pending debounce
|
|
359
|
-
// never race an edit the watcher is already handling —
|
|
360
|
-
// changes whose fs.watch event was dropped.
|
|
432
|
+
// older than the source or any file it includes. Files with a pending debounce
|
|
433
|
+
// timer are skipped so we never race an edit the watcher is already handling —
|
|
434
|
+
// this only catches changes whose fs.watch event was dropped.
|
|
361
435
|
export function reconcile(inputDir, outputDir, sourcemap, pending) {
|
|
362
436
|
let files;
|
|
363
437
|
try {
|
|
@@ -386,9 +460,6 @@ export function reconcile(inputDir, outputDir, sourcemap, pending) {
|
|
|
386
460
|
catch {
|
|
387
461
|
outMtime = undefined;
|
|
388
462
|
}
|
|
389
|
-
// Up to date — the watcher already handled this file.
|
|
390
|
-
if (outMtime !== undefined && outMtime >= sourceMtime)
|
|
391
|
-
continue;
|
|
392
463
|
let source;
|
|
393
464
|
try {
|
|
394
465
|
source = readFileSync(inFile, 'utf-8');
|
|
@@ -396,6 +467,20 @@ export function reconcile(inputDir, outputDir, sourcemap, pending) {
|
|
|
396
467
|
catch {
|
|
397
468
|
continue;
|
|
398
469
|
}
|
|
470
|
+
// The output also depends on the mixins each include defines, so an
|
|
471
|
+
// include edited since the last compile makes it stale too.
|
|
472
|
+
let newestMtime = sourceMtime;
|
|
473
|
+
for (const included of includedFiles(inFile, source)) {
|
|
474
|
+
try {
|
|
475
|
+
newestMtime = Math.max(newestMtime, statSync(included).mtimeMs);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// A missing include is reported when the template compiles.
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Up to date — the watcher already handled this file.
|
|
482
|
+
if (outMtime !== undefined && outMtime >= newestMtime)
|
|
483
|
+
continue;
|
|
399
484
|
try {
|
|
400
485
|
const elapsed = writeCompiled(inFile, outFile, source, sourcemap);
|
|
401
486
|
console.log(`${outFile} (reconciled, ${formatElapsed(elapsed)})`);
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -5,22 +5,36 @@ export interface GenerateResult {
|
|
|
5
5
|
code: string;
|
|
6
6
|
mappings: Mapping[];
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Lists the mixins defined by the `.weave` file an `include` path names.
|
|
10
|
+
* The path is exactly as written after `include`; throw if it cannot be read.
|
|
11
|
+
*/
|
|
12
|
+
export type IncludeResolver = (includePath: string) => string[];
|
|
13
|
+
export interface GenerateOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Without a resolver, a lone `include` is assumed to provide every called
|
|
16
|
+
* mixin not defined in this file, and more than one `include` is an error.
|
|
17
|
+
*/
|
|
18
|
+
resolveInclude?: IncludeResolver;
|
|
19
|
+
}
|
|
8
20
|
export declare class CodegenError extends Error {
|
|
9
21
|
readonly line: number;
|
|
10
22
|
readonly col: number;
|
|
11
23
|
constructor(message: string, line: number, col: number);
|
|
12
24
|
}
|
|
25
|
+
/** Names of the mixins a document defines at top level. */
|
|
26
|
+
export declare function definedMixins(doc: DocumentNode): string[];
|
|
13
27
|
/** Generate a TypeScript expression from a single AST node. */
|
|
14
28
|
export declare function generateExpr(node: AstNode): string;
|
|
15
29
|
/** Generate TypeScript source from a DocumentNode. */
|
|
16
|
-
export declare function generate(doc: DocumentNode): GenerateResult;
|
|
30
|
+
export declare function generate(doc: DocumentNode, options?: GenerateOptions): GenerateResult;
|
|
17
31
|
/** Full pipeline: source string → TypeScript source. */
|
|
18
|
-
export declare function compile(source: string): string;
|
|
32
|
+
export declare function compile(source: string, options?: GenerateOptions): string;
|
|
19
33
|
/** Full pipeline with source map generation. */
|
|
20
34
|
export declare function compileWithSourceMap(source: string, opts: {
|
|
21
35
|
file: string;
|
|
22
36
|
sourceFile: string;
|
|
23
|
-
}): {
|
|
37
|
+
} & GenerateOptions): {
|
|
24
38
|
code: string;
|
|
25
39
|
sourceMap: SourceMap;
|
|
26
40
|
};
|