@lucidkiln/core 0.0.0-stage → 1.0.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,5 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ - 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)
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 { mn as KilnSyntaxError } from "./source-BQxJGo5Y.js";
3
+ import { n as format, t as KilnFormatError } from "./format-CJoqhehX.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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"}