@lucidkiln/core 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0
4
+
5
+ - New `KilnFunctions`, `KilnFilters`, `KilnTests` and `KilnGlobals` interfaces. Add your own to them so your editor can show their types. See [Types from TypeScript](https://kilnlang.com/docs/tooling/editors/#types-from-typescript). ([d55ef63](https://github.com/buildlucid/kiln/commit/d55ef63677df3b34ea247f3b9f25562b201a18b0))
6
+ - `withRender` now keeps the types of your function's arguments and result. ([d55ef63](https://github.com/buildlucid/kiln/commit/d55ef63677df3b34ea247f3b9f25562b201a18b0))
7
+ - Cache tags. A function wrapped in `withRender` can call `render.tagCache("collection:pages")` to tag the `cache` section it runs in. The store's `set` now receives the section's `tags`, so it can clear copies when that data changes. See [Tags](https://kilnlang.com/docs/tags/cache/#tags). ([6a9b26f](https://github.com/buildlucid/kiln/commit/6a9b26f4d8beadc6d04f358e617a11b237a1487e))
8
+ - `memoryCache` has a `clear` method, which removes every copy, or those with any of the tags you pass. ([6a9b26f](https://github.com/buildlucid/kiln/commit/6a9b26f4d8beadc6d04f358e617a11b237a1487e))
9
+ - Copies saved by an earlier version of Kiln are treated as missing, so each section renders once more and is saved again. ([6a9b26f](https://github.com/buildlucid/kiln/commit/6a9b26f4d8beadc6d04f358e617a11b237a1487e))
10
+ - New `@lucidkiln/core/syntax` entry point with Kiln's parser, for editor tools. Its shapes can change in any minor release. ([d55ef63](https://github.com/buildlucid/kiln/commit/d55ef63677df3b34ea247f3b9f25562b201a18b0))
11
+
12
+ ## 1.0.0
13
+
14
+ - The first stable release. Read the [documentation](https://kilnlang.com/docs/) to get started.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Build Lucid Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,38 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/buildlucid/kiln/master/packages/kiln/assets/banner.png" alt="Kiln: fast, safe templates for server-rendered websites" width="100%">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ # Kiln
6
+
7
+ [![Tests](https://github.com/buildlucid/kiln/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/buildlucid/kiln/actions/workflows/test.yml)
8
+
9
+ Kiln is a template language for building websites on the server. The syntax will feel familiar if you know Twig, Jinja or Liquid.
10
+
11
+ **[Read the documentation at kilnlang.com](https://kilnlang.com)**
12
+
13
+ - **Async data.** Templates can call async functions to load what they need. Kiln only waits where a call returns a promise, and runs calls that don't depend on each other at the same time.
14
+ - **Server islands.** Build a page as static HTML and let the parts that need live data load on each visit, or stream them in from a server.
15
+ - **Runs anywhere.** No dependencies and no `eval`. Works on Node 24 and Cloudflare Workers.
16
+ - **Safe by default.** Templates only read what you give them, output is escaped, and limits stop runaway templates.
17
+ - **Layouts and components.** Blocks, includes, components with typed props and slots, and macros.
18
+ - **Clear errors.** Strict mode catches typos, and every error shows the template, line and column.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @lucidkiln/core
24
+ ```
25
+
26
+ Then follow the [quick start](https://kilnlang.com/docs/quick-start/) to render your first page.
27
+
28
+ ## Learn more
29
+
30
+ - [Documentation](https://kilnlang.com/docs/)
31
+ - [Compared with other template languages](https://kilnlang.com/docs/comparison/)
32
+ - [Benchmarks](https://kilnlang.com/docs/benchmarks/)
33
+ - [Editor support](https://kilnlang.com/docs/tooling/editors/)
34
+ - [Changelog](https://github.com/buildlucid/kiln/blob/master/packages/kiln/CHANGELOG.md)
35
+
36
+ ## Licence
37
+
38
+ [MIT](https://github.com/buildlucid/kiln/blob/master/packages/kiln/LICENSE)
@@ -0,0 +1,357 @@
1
+ //#region src/source.d.ts
2
+ /** A range of a template's source, as zero-based offsets. `end` is exclusive. */
3
+ type Span = {
4
+ start: number;
5
+ end: number;
6
+ };
7
+ type Position = {
8
+ /** Zero-based offset into the source. */
9
+ offset: number;
10
+ /** One-based. */
11
+ line: number;
12
+ /** One-based, counted in UTF-16 code units. */
13
+ column: number;
14
+ };
15
+ /**
16
+ * A template's text and resolved name.
17
+ *
18
+ * The line index is built on first use, because only errors and tooling need lines and columns. A byte order mark at the
19
+ * start is not counted in columns or shown in frames, as editors hide it, but offsets still count it.
20
+ */
21
+ declare class Source {
22
+ #private;
23
+ readonly name: string;
24
+ readonly text: string;
25
+ constructor(name: string, text: string);
26
+ /** Converts an offset into a line and column. */
27
+ position(offset: number): Position;
28
+ /**
29
+ * Renders a few lines around a span, with a marker under it.
30
+ *
31
+ * ```text
32
+ * 3 | {% block content %}
33
+ * > 4 | <h1>{{ titel }}</h1>
34
+ * | ^^^^^
35
+ * ```
36
+ */
37
+ frame(span: Span, context?: number): string;
38
+ }
39
+ //#endregion
40
+ //#region src/parser/ast.d.ts
41
+ type EscapeStrategy = "html" | "attr" | "js" | "css" | "url";
42
+ type BinaryOperator = "or" | "and" | "??" | "?:" | "==" | "!=" | "<" | ">" | "<=" | ">=" | "in" | "not in" | "starts with" | "ends with" | ".." | "~" | "+" | "-" | "*" | "/" | "//" | "%" | "**";
43
+ type Spread = {
44
+ type: "Spread";
45
+ value: Expression;
46
+ span: Span;
47
+ };
48
+ type Argument = {
49
+ type: "Positional";
50
+ value: Expression;
51
+ span: Span;
52
+ } | {
53
+ type: "Named";
54
+ name: string;
55
+ value: Expression;
56
+ span: Span;
57
+ } | Spread;
58
+ type HashEntry = {
59
+ type: "Property";
60
+ key: Expression;
61
+ value: Expression;
62
+ span: Span;
63
+ } | Spread;
64
+ type Expression = {
65
+ type: "Literal";
66
+ value: null | boolean | number | string;
67
+ span: Span;
68
+ } | {
69
+ type: "Name";
70
+ name: string;
71
+ span: Span;
72
+ } |
73
+ /** A double-quoted string with `#{}` interpolation. */
74
+ {
75
+ type: "Interpolation";
76
+ parts: Expression[];
77
+ span: Span;
78
+ } | {
79
+ type: "Array";
80
+ elements: (Expression | Spread)[];
81
+ span: Span;
82
+ } | {
83
+ type: "Hash";
84
+ entries: HashEntry[];
85
+ span: Span;
86
+ } | {
87
+ type: "Unary";
88
+ operator: "-" | "+" | "not";
89
+ operand: Expression;
90
+ span: Span;
91
+ } | {
92
+ type: "Binary";
93
+ operator: BinaryOperator;
94
+ left: Expression;
95
+ right: Expression;
96
+ span: Span;
97
+ } | {
98
+ type: "Conditional";
99
+ test: Expression;
100
+ consequent: Expression;
101
+ alternate: Expression | null;
102
+ span: Span;
103
+ } | {
104
+ type: "Member";
105
+ object: Expression;
106
+ property: Expression;
107
+ /** True for `a?.b` and `a?.[b]`. */
108
+ optional: boolean;
109
+ span: Span;
110
+ } | {
111
+ type: "Call";
112
+ callee: Expression;
113
+ args: Argument[];
114
+ span: Span;
115
+ } | {
116
+ type: "Filter";
117
+ name: string;
118
+ nameSpan: Span;
119
+ input: Expression;
120
+ args: Argument[];
121
+ span: Span;
122
+ } | {
123
+ type: "Test";
124
+ name: string;
125
+ nameSpan: Span;
126
+ input: Expression;
127
+ args: Argument[];
128
+ negated: boolean;
129
+ span: Span;
130
+ } | {
131
+ type: "Arrow";
132
+ params: string[];
133
+ body: Expression;
134
+ span: Span;
135
+ } |
136
+ /** Wraps a member and call chain that contains `?.`, so null short-circuits the whole chain. */
137
+ {
138
+ type: "OptionalChain";
139
+ expression: Expression;
140
+ span: Span;
141
+ } |
142
+ /** Stands in for an expression that failed to parse. */
143
+ {
144
+ type: "Invalid";
145
+ span: Span;
146
+ };
147
+ type Param = {
148
+ name: string;
149
+ default: Expression | null;
150
+ span: Span;
151
+ };
152
+ /** Type names a prop can declare. See `TYPE_NAMES`. */
153
+ type TypeName = (typeof TYPE_NAMES)[number];
154
+ /**
155
+ * One member of a prop's type. A full type is a list of members, read as a union.
156
+ *
157
+ * - A built-in type name, such as `string`.
158
+ * - `{ literal }`: one exact string or number, such as `"primary"`.
159
+ * - `{ list }`: `T[]`, an array of the inner type.
160
+ * - `{ shape }`: `{ name: string }`, a hash or object with these keys.
161
+ * - `{ named }`: a type the host registers, such as `Post`.
162
+ */
163
+ type PropType = TypeName | {
164
+ literal: string | number;
165
+ } | {
166
+ list: PropType[];
167
+ } | {
168
+ shape: ShapeKey[];
169
+ } | {
170
+ named: string;
171
+ };
172
+ /** One key of a shape type, such as `url?: string`. */
173
+ type ShapeKey = {
174
+ name: string;
175
+ optional: boolean;
176
+ type: PropType[];
177
+ };
178
+ /** A `props` entry. `type` is null when the prop is untyped. */
179
+ type PropParam = Param & {
180
+ type: PropType[] | null;
181
+ };
182
+ type FilterCall = {
183
+ name: string;
184
+ args: Argument[];
185
+ span: Span;
186
+ };
187
+ /**
188
+ * On a tag that inserts content, when it stands on a line of its own: the line's indentation in the output, which
189
+ * every inserted line gets on top of the indentation around it. See `indent.ts`.
190
+ */
191
+ type Placement = {
192
+ indent?: string;
193
+ };
194
+ type Statement = {
195
+ type: "Text";
196
+ value: string;
197
+ /** Where lines start in `value`, which get the indentation of the content they are inserted into. */
198
+ lines?: number[];
199
+ span: Span;
200
+ } | {
201
+ type: "Output";
202
+ expression: Expression;
203
+ span: Span;
204
+ } | {
205
+ type: "If";
206
+ branches: {
207
+ test: Expression;
208
+ body: Statement[];
209
+ }[];
210
+ alternate: Statement[] | null;
211
+ span: Span;
212
+ } | {
213
+ type: "For";
214
+ keyName: string | null;
215
+ valueName: string;
216
+ iterable: Expression;
217
+ body: Statement[];
218
+ alternate: Statement[] | null;
219
+ span: Span;
220
+ } | {
221
+ type: "Set";
222
+ name: string;
223
+ value: Expression;
224
+ span: Span;
225
+ } | {
226
+ type: "SetBlock";
227
+ name: string;
228
+ body: Statement[];
229
+ span: Span;
230
+ } |
231
+ /** `rest` names the hash that collects undeclared variables, as in `...attributes`, or is null. */
232
+ {
233
+ type: "Props";
234
+ props: PropParam[];
235
+ rest: string | null;
236
+ span: Span;
237
+ } | {
238
+ type: "Extends";
239
+ template: Expression;
240
+ span: Span;
241
+ } | ({
242
+ type: "Block";
243
+ name: string;
244
+ body: Statement[];
245
+ span: Span;
246
+ } & Placement) | ({
247
+ type: "Include";
248
+ template: Expression;
249
+ with: Expression | null;
250
+ ignoreMissing: boolean;
251
+ span: Span;
252
+ } & Placement) | ({
253
+ type: "Component";
254
+ template: Expression;
255
+ with: Expression | null;
256
+ fills: {
257
+ name: string;
258
+ body: Statement[];
259
+ span: Span;
260
+ }[];
261
+ /** Content outside any `fill`, which fills the default slot. */
262
+ body: Statement[];
263
+ span: Span;
264
+ } & Placement) | ({
265
+ type: "Slot";
266
+ name: string;
267
+ body: Statement[];
268
+ span: Span;
269
+ } & Placement) |
270
+ /**
271
+ * A component that can load after the page. `body` is the fallback, shown until a deferred island loads. `closing`
272
+ * is set when `endisland` stands on a line of its own: its indentation, or null when it shares that line with the
273
+ * `island` tag, and whether a line break followed it.
274
+ */
275
+ ({
276
+ type: "Island";
277
+ template: Expression;
278
+ with: Expression | null;
279
+ body: Statement[];
280
+ closing?: {
281
+ indent: string | null;
282
+ newline: boolean;
283
+ };
284
+ span: Span;
285
+ } & Placement) | {
286
+ type: "Macro";
287
+ name: string;
288
+ params: Param[];
289
+ body: Statement[];
290
+ span: Span;
291
+ } | {
292
+ type: "Import";
293
+ template: string;
294
+ alias: string;
295
+ span: Span;
296
+ } | {
297
+ type: "FromImport";
298
+ template: string;
299
+ names: {
300
+ name: string;
301
+ alias: string;
302
+ span: Span;
303
+ }[];
304
+ span: Span;
305
+ } | {
306
+ type: "With";
307
+ value: Expression;
308
+ body: Statement[];
309
+ span: Span;
310
+ } |
311
+ /** A place other templates add content to with `add to`, filled when the render ends. */
312
+ ({
313
+ type: "Outlet";
314
+ name: string;
315
+ span: Span;
316
+ } & Placement) |
317
+ /** Closes the outlets reached so far, so a streamed render can send its output up to here. */
318
+ {
319
+ type: "Flush";
320
+ span: Span;
321
+ } |
322
+ /** Content for the outlet named `outlet`. `once` adds it only the first time the tag runs in a render. */
323
+ {
324
+ type: "Add";
325
+ outlet: string;
326
+ once: boolean;
327
+ body: Statement[];
328
+ span: Span;
329
+ } |
330
+ /** `seconds` is the `for` value, or null. */
331
+ {
332
+ type: "Cache";
333
+ key: Expression;
334
+ seconds: Expression | null;
335
+ body: Statement[];
336
+ span: Span;
337
+ } | {
338
+ type: "Apply";
339
+ filters: FilterCall[];
340
+ body: Statement[];
341
+ span: Span;
342
+ } | {
343
+ type: "Autoescape";
344
+ strategy: EscapeStrategy | false;
345
+ body: Statement[];
346
+ span: Span;
347
+ };
348
+ type TemplateNode = {
349
+ type: "Template";
350
+ body: Statement[];
351
+ span: Span;
352
+ };
353
+ /** The value types a prop can declare, named as in the docs' table of types. */
354
+ declare const TYPE_NAMES: readonly ["null", "boolean", "number", "string", "array", "hash", "set", "date", "markup", "object"];
355
+ //#endregion
356
+ export { HashEntry as a, PropType as c, Statement as d, TemplateNode as f, Span as g, Source as h, Expression as i, ShapeKey as l, Position as m, BinaryOperator as n, Param as o, TypeName as p, EscapeStrategy as r, PropParam as s, Argument as t, Spread as u };
357
+ //# sourceMappingURL=ast-1akLiy-A.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast-1akLiy-A.d.ts","names":[],"sources":["../src/source.ts","../src/parser/ast.ts"],"mappings":";;KAGY;EAAS;EAAe;;KAExB;;EAEX;;EAEA;;EAEA;;;;;;;;cASY;;WAIF;WACA;EAFV,YACU,cACA;;EAIV,SAAS,iBAAiB;;;;;;;;;;EA0B1B,MAAM,MAAM,MAAM;;;;KCrDP;KAEA;KAyBA;EAAW;EAAgB,OAAO;EAAY,MAAM;;KAEpD;EACP;EAAoB,OAAO;EAAY,MAAM;;EAC7C;EAAe;EAAc,OAAO;EAAY,MAAM;IACxD;KAES;EACP;EAAkB,KAAK;EAAY,OAAO;EAAY,MAAM;IAC9D;KAES;EAET;EACA;EACA,MAAM;;EAEJ;EAAc;EAAc,MAAM;;;;EAElC;EAAuB,OAAO;EAAc,MAAM;;EAClD;EAAe,WAAW,aAAa;EAAW,MAAM;;EACxD;EAAc,SAAS;EAAa,MAAM;;EAE5C;EACA;EACA,SAAS;EACT,MAAM;;EAGN;EACA,UAAU;EACV,MAAM;EACN,OAAO;EACP,MAAM;;EAGN;EACA,MAAM;EACN,YAAY;EACZ,WAAW;EACX,MAAM;;EAGN;EACA,QAAQ;EACR,UAAU;;EAEV;EACA,MAAM;;EAEJ;EAAc,QAAQ;EAAY,MAAM;EAAY,MAAM;;EAE5D;EACA;EACA,UAAU;EACV,OAAO;EACP,MAAM;EACN,MAAM;;EAGN;EACA;EACA,UAAU;EACV,OAAO;EACP,MAAM;EACN;EACA,MAAM;;EAEJ;EAAe;EAAkB,MAAM;EAAY,MAAM;;;;EAEzD;EAAuB,YAAY;EAAY,MAAM;;;;EAErD;EAAiB,MAAM;;KAEhB;EAAU;EAAc,SAAS;EAAmB,MAAM;;;KAG1D,mBAAmB;;;;;;;;;;KAWnB,WACT;EACE;;EACA,MAAM;;EACN,OAAO;;EACP;;;KAGO;EAAa;EAAc;EAAmB,MAAM;;;KAGpD,YAAY;EAAU,MAAM;;KAE5B;EACX;EACA,MAAM;EACN,MAAM;;;;;;KAOF;EAAc;;KAEP;EAET;EACA;;EAEA;EACA,MAAM;;EAEJ;EAAgB,YAAY;EAAY,MAAM;;EAEhD;EACA;IAAY,MAAM;IAAY,MAAM;;EACpC,WAAW;EACX,MAAM;;EAGN;EACA;EACA;EACA,UAAU;EACV,MAAM;EACN,WAAW;EACX,MAAM;;EAEJ;EAAa;EAAc,OAAO;EAAY,MAAM;;EACpD;EAAkB;EAAc,MAAM;EAAa,MAAM;;;;EAEzD;EAAe,OAAO;EAAa;EAAqB,MAAM;;EAC9D;EAAiB,UAAU;EAAY,MAAM;;EAC5C;EAAe;EAAc,MAAM;EAAa,MAAM;IAAS;EAElE;EACA,UAAU;EACV,MAAM;EACN;EACA,MAAM;IACF;EAEJ;EACA,UAAU;EACV,MAAM;EACN;IAAS;IAAc,MAAM;IAAa,MAAM;;;EAEhD,MAAM;EACN,MAAM;IACF;EACD;EAAc;EAAc,MAAM;EAAa,MAAM;IAAS;;;;;;;EAOjE;EACA,UAAU;EACV,MAAM;EACN,MAAM;EACN;IAAY;IAAuB;;EACnC,MAAM;IACF;EAEJ;EACA;EACA,QAAQ;EACR,MAAM;EACN,MAAM;;EAEJ;EAAgB;EAAkB;EAAe,MAAM;;EAEzD;EACA;EACA;IAAS;IAAc;IAAe,MAAM;;EAC5C,MAAM;;EAEJ;EAAc,OAAO;EAAY,MAAM;EAAa,MAAM;;;;EAEzD;EAAgB;EAAc,MAAM;IAAS;;;EAE9C;EAAe,MAAM;;;;EAGvB;EACA;EACA;EACA,MAAM;EACN,MAAM;;;;EAIN;EACA,KAAK;EACL,SAAS;EACT,MAAM;EACN,MAAM;;EAEJ;EAAe,SAAS;EAAc,MAAM;EAAa,MAAM;;EAEjE;EACA,UAAU;EACV,MAAM;EACN,MAAM;;KAGG;EAAiB;EAAkB,MAAM;EAAa,MAAM;;;cAoB3D"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/cli.js ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ import { un as KilnSyntaxError } from "./source-BHcxowQp.js";
3
+ import { n as format, t as KilnFormatError } from "./format-CLCViAPn.js";
4
+ import { readFile, readdir, stat, writeFile } from "node:fs/promises";
5
+ import { join, relative, resolve } from "node:path";
6
+ import process from "node:process";
7
+ //#region src/formatter/command.ts
8
+ const USAGE = `Usage: kiln format [--check] [--spaces <width>] <files or folders...>
9
+
10
+ --check List files that need formatting, without changing them. Exits with 1 if any do.
11
+ --spaces <width> Indent with this many spaces instead of tabs.`;
12
+ /**
13
+ * Every `.kiln` file at or under each path, once each, skipping dot folders and node_modules. `skipped` lists files
14
+ * named directly that aren't `.kiln` files. Throws for a path that doesn't exist.
15
+ */
16
+ const findTemplates = async (paths) => {
17
+ const files = [];
18
+ const skipped = [];
19
+ for (const path of paths) {
20
+ if (!(await stat(path)).isDirectory()) {
21
+ (path.endsWith(".kiln") ? files : skipped).push(path);
22
+ continue;
23
+ }
24
+ const entries = await readdir(path, {
25
+ recursive: true,
26
+ withFileTypes: true
27
+ });
28
+ for (const entry of entries) {
29
+ const file = join(entry.parentPath, entry.name);
30
+ const parts = relative(path, file).split(/[\\/]/);
31
+ if (entry.isFile() && entry.name.endsWith(".kiln") && !parts.some((part) => part.startsWith(".") || part === "node_modules")) files.push(file);
32
+ }
33
+ }
34
+ const seen = /* @__PURE__ */ new Set();
35
+ return {
36
+ files: files.filter((file) => {
37
+ const key = resolve(file);
38
+ if (seen.has(key)) return false;
39
+ seen.add(key);
40
+ return true;
41
+ }).sort(),
42
+ skipped
43
+ };
44
+ };
45
+ /** Paths that don't exist. */
46
+ const missingPaths = async (paths) => {
47
+ const missing = [];
48
+ for (const path of paths) try {
49
+ await stat(path);
50
+ } catch {
51
+ missing.push(path);
52
+ }
53
+ return missing;
54
+ };
55
+ /**
56
+ * Runs `kiln format` with the arguments after `format`, and returns the exit code: 0 when done, 1 when files need formatting under `--check` or have errors, and 2 for bad arguments or paths that don't exist.
57
+ */
58
+ const formatCommand = async (args, io) => {
59
+ let check = false;
60
+ let spaces = null;
61
+ const paths = [];
62
+ for (let index = 0; index < args.length; index++) {
63
+ const arg = args[index];
64
+ if (arg === "--check") check = true;
65
+ else if (arg === "--spaces") {
66
+ spaces = Number(args[++index]);
67
+ if (!Number.isInteger(spaces) || spaces < 1) {
68
+ io.stderr("--spaces needs a whole number of at least 1\n");
69
+ return 2;
70
+ }
71
+ } else if (arg.startsWith("-")) {
72
+ io.stderr(`Unknown option ${arg}\n\n${USAGE}\n`);
73
+ return 2;
74
+ } else paths.push(arg);
75
+ }
76
+ if (paths.length === 0) {
77
+ io.stderr(`${USAGE}\n`);
78
+ return 2;
79
+ }
80
+ const missing = await missingPaths(paths);
81
+ if (missing.length > 0) {
82
+ for (const path of missing) io.stderr(`${path}: not found\n`);
83
+ return 2;
84
+ }
85
+ const options = spaces === null ? {} : {
86
+ useTabs: false,
87
+ tabWidth: spaces
88
+ };
89
+ const { files, skipped } = await findTemplates(paths);
90
+ for (const file of skipped) io.stderr(`${file}: skipped, as it isn't a .kiln file\n`);
91
+ let failed = false;
92
+ let unformatted = 0;
93
+ for (const file of files) try {
94
+ const source = await readFile(file, "utf8");
95
+ const formatted = format(source, options);
96
+ if (formatted === source) continue;
97
+ unformatted++;
98
+ io.stdout(`${file}\n`);
99
+ if (!check) await writeFile(file, formatted);
100
+ } catch (error) {
101
+ failed = true;
102
+ if (error instanceof KilnSyntaxError) for (const problem of error.diagnostics) io.stderr(`${file}:${problem.start.line}:${problem.start.column}: ${problem.message}\n`);
103
+ else if (error instanceof KilnFormatError) io.stderr(`${file}: ${error.message}\n`);
104
+ else io.stderr(`${file}: ${error instanceof Error ? error.message : String(error)}\n`);
105
+ }
106
+ if (check && unformatted > 0) io.stderr(`${unformatted} ${unformatted === 1 ? "file needs" : "files need"} formatting. Run kiln format to fix.\n`);
107
+ return failed || check && unformatted > 0 ? 1 : 0;
108
+ };
109
+ //#endregion
110
+ //#region src/cli.ts
111
+ const io = {
112
+ stdout: (text) => process.stdout.write(text),
113
+ stderr: (text) => process.stderr.write(text)
114
+ };
115
+ const [command, ...rest] = process.argv.slice(2);
116
+ if (command === "format") process.exitCode = await formatCommand(rest, io);
117
+ else {
118
+ io.stdout(`${USAGE}\n`);
119
+ process.exitCode = command === void 0 || command === "--help" ? 0 : 2;
120
+ }
121
+ //#endregion
122
+
123
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/formatter/command.ts","../src/cli.ts"],"sourcesContent":["import { readdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { join, relative, resolve } from \"node:path\";\nimport { KilnSyntaxError } from \"../errors.js\";\nimport { format, KilnFormatError } from \"./format.js\";\n\nexport const USAGE = `Usage: kiln format [--check] [--spaces <width>] <files or folders...>\n\n --check List files that need formatting, without changing them. Exits with 1 if any do.\n --spaces <width> Indent with this many spaces instead of tabs.`;\n\n/**\n * Every `.kiln` file at or under each path, once each, skipping dot folders and node_modules. `skipped` lists files\n * named directly that aren't `.kiln` files. Throws for a path that doesn't exist.\n */\nconst findTemplates = async (\n\tpaths: readonly string[],\n): Promise<{ files: string[]; skipped: string[] }> => {\n\tconst files: string[] = [];\n\tconst skipped: string[] = [];\n\tfor (const path of paths) {\n\t\tif (!(await stat(path)).isDirectory()) {\n\t\t\t(path.endsWith(\".kiln\") ? files : skipped).push(path);\n\t\t\tcontinue;\n\t\t}\n\t\tconst entries = await readdir(path, {\n\t\t\trecursive: true,\n\t\t\twithFileTypes: true,\n\t\t});\n\t\tfor (const entry of entries) {\n\t\t\tconst file = join(entry.parentPath, entry.name);\n\t\t\tconst parts = relative(path, file).split(/[\\\\/]/);\n\t\t\tif (\n\t\t\t\tentry.isFile() &&\n\t\t\t\tentry.name.endsWith(\".kiln\") &&\n\t\t\t\t!parts.some((part) => part.startsWith(\".\") || part === \"node_modules\")\n\t\t\t) {\n\t\t\t\tfiles.push(file);\n\t\t\t}\n\t\t}\n\t}\n\t// The same file can be named directly and found in a folder, or named twice.\n\tconst seen = new Set<string>();\n\tconst unique = files.filter((file) => {\n\t\tconst key = resolve(file);\n\t\tif (seen.has(key)) return false;\n\t\tseen.add(key);\n\t\treturn true;\n\t});\n\treturn { files: unique.sort(), skipped };\n};\n\n/** Paths that don't exist. */\nconst missingPaths = async (paths: readonly string[]): Promise<string[]> => {\n\tconst missing: string[] = [];\n\tfor (const path of paths) {\n\t\ttry {\n\t\t\tawait stat(path);\n\t\t} catch {\n\t\t\tmissing.push(path);\n\t\t}\n\t}\n\treturn missing;\n};\n\n/** Where the command writes its messages. */\nexport type CommandOutput = {\n\tstdout: (text: string) => void;\n\tstderr: (text: string) => void;\n};\n\n/**\n * Runs `kiln format` with the arguments after `format`, and returns the exit code: 0 when done, 1 when files need formatting under `--check` or have errors, and 2 for bad arguments or paths that don't exist.\n */\nexport const formatCommand = async (\n\targs: readonly string[],\n\tio: CommandOutput,\n): Promise<number> => {\n\tlet check = false;\n\tlet spaces: number | null = null;\n\tconst paths: string[] = [];\n\tfor (let index = 0; index < args.length; index++) {\n\t\tconst arg = args[index] as string;\n\t\tif (arg === \"--check\") check = true;\n\t\telse if (arg === \"--spaces\") {\n\t\t\tspaces = Number(args[++index]);\n\t\t\tif (!Number.isInteger(spaces) || spaces < 1) {\n\t\t\t\tio.stderr(\"--spaces needs a whole number of at least 1\\n\");\n\t\t\t\treturn 2;\n\t\t\t}\n\t\t} else if (arg.startsWith(\"-\")) {\n\t\t\tio.stderr(`Unknown option ${arg}\\n\\n${USAGE}\\n`);\n\t\t\treturn 2;\n\t\t} else paths.push(arg);\n\t}\n\tif (paths.length === 0) {\n\t\tio.stderr(`${USAGE}\\n`);\n\t\treturn 2;\n\t}\n\n\tconst missing = await missingPaths(paths);\n\tif (missing.length > 0) {\n\t\tfor (const path of missing) io.stderr(`${path}: not found\\n`);\n\t\treturn 2;\n\t}\n\n\tconst options = spaces === null ? {} : { useTabs: false, tabWidth: spaces };\n\tconst { files, skipped } = await findTemplates(paths);\n\tfor (const file of skipped) {\n\t\tio.stderr(`${file}: skipped, as it isn't a .kiln file\\n`);\n\t}\n\tlet failed = false;\n\tlet unformatted = 0;\n\tfor (const file of files) {\n\t\t// Each file's problems are reported, and the rest are still formatted.\n\t\ttry {\n\t\t\tconst source = await readFile(file, \"utf8\");\n\t\t\tconst formatted = format(source, options);\n\t\t\tif (formatted === source) continue;\n\t\t\tunformatted++;\n\t\t\tio.stdout(`${file}\\n`);\n\t\t\tif (!check) await writeFile(file, formatted);\n\t\t} catch (error) {\n\t\t\tfailed = true;\n\t\t\tif (error instanceof KilnSyntaxError) {\n\t\t\t\tfor (const problem of error.diagnostics) {\n\t\t\t\t\tio.stderr(\n\t\t\t\t\t\t`${file}:${problem.start.line}:${problem.start.column}: ${problem.message}\\n`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else if (error instanceof KilnFormatError) {\n\t\t\t\tio.stderr(`${file}: ${error.message}\\n`);\n\t\t\t} else {\n\t\t\t\tio.stderr(\n\t\t\t\t\t`${file}: ${error instanceof Error ? error.message : String(error)}\\n`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\tif (check && unformatted > 0) {\n\t\tio.stderr(\n\t\t\t`${unformatted} ${unformatted === 1 ? \"file needs\" : \"files need\"} formatting. Run kiln format to fix.\\n`,\n\t\t);\n\t}\n\treturn failed || (check && unformatted > 0) ? 1 : 0;\n};\n","#!/usr/bin/env node\nimport process from \"node:process\";\nimport { formatCommand, USAGE } from \"./formatter/command.js\";\n\nconst io = {\n\tstdout: (text: string) => process.stdout.write(text),\n\tstderr: (text: string) => process.stderr.write(text),\n};\nconst [command, ...rest] = process.argv.slice(2);\nif (command === \"format\") process.exitCode = await formatCommand(rest, io);\nelse {\n\tio.stdout(`${USAGE}\\n`);\n\tprocess.exitCode = command === undefined || command === \"--help\" ? 0 : 2;\n}\n"],"mappings":"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"}