azoxjs 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daril Pratomo
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 ADDED
@@ -0,0 +1,214 @@
1
+ # Azox Framework
2
+
3
+ [![CI](https://github.com/darilpratomo/azox/actions/workflows/ci.yml/badge.svg)](https://github.com/darilpratomo/azox/actions/workflows/ci.yml)
4
+
5
+ **The Sound of Future Web**
6
+
7
+ Azox is a web framework built from scratch — no Virtual DOM, no
8
+ third-party CLI dependencies, no borrowed syntax from React, Vue, or
9
+ Next.js. It compiles `.azox` components directly into fine-grained,
10
+ signal-driven DOM updates.
11
+
12
+ > Status: early development (v0.1.0). APIs are unstable and will
13
+ > change without notice until v1.0.
14
+
15
+ ## Why Azox
16
+
17
+ Most frameworks solve reactivity with a Virtual DOM (React) or a
18
+ build-time compiler that inlines everything (Svelte). Azox takes a
19
+ third path: **fine-grained signals**. Every dynamic binding in your
20
+ template compiles to its own tiny `effect()` that writes directly to
21
+ the one DOM node it owns. No tree diffing, no wasted re-renders.
22
+
23
+ ```html
24
+ <script>
25
+ import { signal } from 'azox/reactivity';
26
+ const count = signal(0);
27
+ </script>
28
+
29
+ <button on:click={() => count.set(count() + 1)}>
30
+ Clicks: {count()}
31
+ </button>
32
+ ```
33
+
34
+ ## Routing
35
+
36
+ The file layout is the routing table. A page becomes a directory with
37
+ an `index.html`, so URLs carry no extension and work on any static
38
+ host without rewrite rules.
39
+
40
+ ```
41
+ pages/index.azox → /
42
+ pages/about.azox → /about
43
+ pages/blog/index.azox → /blog
44
+ pages/blog/first-post.azox → /blog/first-post
45
+ ```
46
+
47
+ ```
48
+ .azox/build/
49
+ ├── index.html
50
+ ├── page.client.js
51
+ ├── azox-runtime.js one runtime, shared by every page
52
+ ├── about/
53
+ │ ├── index.html
54
+ │ └── page.client.js
55
+ └── blog/
56
+ ├── index.html
57
+ ├── page.client.js
58
+ └── first-post/
59
+ ├── index.html
60
+ └── page.client.js
61
+ ```
62
+
63
+ Build one page with `azox compile --page=blog/first-post`, or by its
64
+ URL: `azox compile --page=/blog/first-post`.
65
+
66
+ ## Components
67
+
68
+ A component is a `.azox` file that declares what it accepts and
69
+ renders markup. Import it, then use it as a capitalised tag:
70
+
71
+ ```html
72
+ <!-- components/Card.azox -->
73
+ <script>
74
+ const { title, body } = props();
75
+ </script>
76
+
77
+ <article class="card">
78
+ <h2>{title}</h2>
79
+ <p>{body}</p>
80
+ </article>
81
+ ```
82
+
83
+ ```html
84
+ <!-- pages/index.azox -->
85
+ <script>
86
+ import Card from '../components/Card.azox';
87
+ import { signal } from 'azox/reactivity';
88
+
89
+ const count = signal(0);
90
+ </script>
91
+
92
+ <main>
93
+ <Card title="Live" body={count()} />
94
+ <button on:click={() => count.set(count() + 1)}>Add one</button>
95
+ </main>
96
+ ```
97
+
98
+ Components are resolved at build time: the markup is inlined into the
99
+ caller, so there is no component instance and no per-component
100
+ overhead at runtime. A prop passed as an expression stays reactive
101
+ across the boundary — clicking the button above updates the text
102
+ inside the card and nothing else. A prop passed as a plain string
103
+ compiles to static text with no effect attached.
104
+
105
+ `<slot />` renders whatever the caller nested inside the tag:
106
+
107
+ ```html
108
+ <!-- components/Layout.azox -->
109
+ <script>
110
+ const { heading } = props();
111
+ </script>
112
+
113
+ <section>
114
+ <header>{heading}</header>
115
+ <slot />
116
+ </section>
117
+ ```
118
+
119
+ Declaring props with `props()` is what lets the compiler reject a
120
+ caller that passes something the component never asked for, instead
121
+ of dropping it silently.
122
+
123
+ In this version components are presentational: they take props and
124
+ render markup, and state lives in the page that uses them. A
125
+ component that declares its own logic is rejected with an explicit
126
+ error rather than quietly sharing the caller's scope.
127
+
128
+ ## Getting Started
129
+
130
+ The package on npm is `azoxjs`; the CLI it installs is `azox`.
131
+
132
+ ```bash
133
+ npx azoxjs create my-app
134
+ cd my-app
135
+ npm install
136
+ npm run dev
137
+ ```
138
+
139
+ `azox dev` serves the project at `http://localhost:4321`, rebuilds on
140
+ every save, and reloads the browser. When a page fails to compile it
141
+ serves the error instead of stale output, then recovers on its own
142
+ once the page is fixed.
143
+
144
+ For a production build:
145
+
146
+ ```bash
147
+ npm run build
148
+ ```
149
+
150
+ The build lands in `.azox/build/` as a self-contained static bundle —
151
+ an `index.html` per route, a compiled hydration module beside it, and
152
+ one shared copy of the runtime. No dev machinery is included. Serve
153
+ that directory with any static host and it works with no install
154
+ step and no rewrite configuration.
155
+
156
+ ## CLI
157
+
158
+ ```
159
+ azox create <name> Scaffold a new Azox project
160
+ azox dev Serve the project, rebuilding on every change
161
+ azox compile Build every page (--page=<name|url> for one)
162
+ azox doctor Check that the toolchain and project are healthy
163
+ azox -v Print the version
164
+ azox help Show all commands
165
+ ```
166
+
167
+ `azox dev` takes `--port=<n>` and `--host=<addr>`. If the port is
168
+ busy it steps forward to the next free one rather than failing.
169
+
170
+ ## Project Structure
171
+
172
+ ```
173
+ azox/
174
+ ├── bin/azox.js CLI entry point
175
+ ├── core/
176
+ │ ├── cli/ argument parsing + command routing
177
+ │ ├── commands/ built-in CLI commands
178
+ │ ├── compiler/ .azox parser, component resolver, compiler
179
+ │ ├── dev/ dev server, file watching, live reload
180
+ │ ├── reactivity/ signal() / effect() / computed()
181
+ │ ├── renderer/ server-side HTML rendering
182
+ │ ├── build.js the build pipeline, shared by commands
183
+ │ ├── routes.js file layout → urls and output paths
184
+ │ └── meta.js version and identity strings
185
+ ├── pages/ example .azox pages
186
+ └── components/ example .azox components
187
+ ```
188
+
189
+ ## Tests
190
+
191
+ ```bash
192
+ npm test
193
+ ```
194
+
195
+ The suite runs on Node's built-in test runner — no test framework
196
+ dependency. It covers the reactivity primitives, the parser, the
197
+ compiler's emitted code, HTML escaping in the server renderer, CLI
198
+ argument parsing, and an end-to-end pass that compiles a fixture
199
+ project and executes the output to confirm the page really is
200
+ reactive.
201
+
202
+ ## Design Principles
203
+
204
+ 1. **Zero dependencies at the core.** No Commander, no Yargs, no
205
+ Virtual DOM library. The CLI and compiler are written in plain
206
+ JavaScript.
207
+ 2. **Fine-grained reactivity, not a Virtual DOM.** Updates target
208
+ exact DOM nodes, not a re-rendered tree.
209
+ 3. **Its own syntax.** `.azox` files are not JSX. They're closer to
210
+ plain HTML with `{expr}` interpolation and `on:event` bindings.
211
+
212
+ ## License
213
+
214
+ MIT — see [LICENSE](./LICENSE).
package/bin/azox.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Azox CLI entry point. Kept thin on purpose — it just reads argv
4
+ // and delegates to the engine in core/.
5
+
6
+ import { parseArgs } from '../core/cli/parseArgs.js';
7
+ import { runCommand } from '../core/cli/router.js';
8
+
9
+ const { command, positionals, flags } = parseArgs(process.argv.slice(2));
10
+
11
+ try {
12
+ await runCommand(command, { positionals, flags });
13
+ } catch (error) {
14
+ // Anything reaching here is a bug rather than user error, so keep
15
+ // the stack: it's what makes the report actionable.
16
+ console.error(`Azox: ${error.stack ?? error.message}`);
17
+ process.exitCode = 1;
18
+ }
package/core/build.js ADDED
@@ -0,0 +1,218 @@
1
+ // The build pipeline, independent of any CLI command: parse a page,
2
+ // server-render it, compile the hydration module, and write the
3
+ // result. `azox compile` runs it once; `azox dev` runs it on every
4
+ // change, so it returns data rather than printing.
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync } from 'node:fs';
7
+ import { resolve, basename } from 'node:path';
8
+
9
+ import { parseAzox } from './compiler/parser.js';
10
+ import { resolveComponents } from './compiler/resolveComponents.js';
11
+ import { compileToModule } from './compiler/compileToJs.js';
12
+ import { renderToHtml } from './renderer/renderToHtml.js';
13
+ import { collectRoutes, findRoute } from './routes.js';
14
+ import { ROOT_DIR } from './meta.js';
15
+ import { BuildError } from './buildError.js';
16
+
17
+ // Browsers can't resolve bare specifiers like "azox/reactivity", so
18
+ // the runtime is copied into the build and imports are rewritten to
19
+ // point at it. That also makes the output self-contained: any static
20
+ // host can serve it with no install step.
21
+ const RUNTIME_FILENAME = 'azox-runtime.js';
22
+
23
+ export const PAGES_DIR = 'pages';
24
+ export const BUILD_DIR = '.azox/build';
25
+
26
+ export { BuildError };
27
+
28
+ export function listRoutes(projectDir) {
29
+ return collectRoutes(resolve(projectDir, PAGES_DIR));
30
+ }
31
+
32
+ // transformHtml lets the dev server inject its live-reload snippet
33
+ // without that ever reaching a production build.
34
+ export function buildRoute(projectDir, route, { transformHtml } = {}) {
35
+ const { sourcePath, name } = route;
36
+ const parsed = parseAzox(readFileSync(sourcePath, 'utf8'));
37
+
38
+ // Components are inlined here, before either output is produced, so
39
+ // the compiler and the renderer both see plain markup.
40
+ const ast = resolveComponents(parsed, sourcePath);
41
+
42
+ // SSR pass: render initial markup without touching browser DOM APIs.
43
+ let html;
44
+ try {
45
+ html = renderToHtml(ast, buildServerScope(ast.script));
46
+ } catch (error) {
47
+ if (!(error instanceof BuildError)) throw error;
48
+ throw new BuildError(`in ${PAGES_DIR}/${name}.azox: ${error.message}`);
49
+ }
50
+
51
+ const buildRoot = resolve(projectDir, BUILD_DIR);
52
+ const outDir = resolve(buildRoot, route.outputDir);
53
+ mkdirSync(outDir, { recursive: true });
54
+
55
+ const clientPath = resolve(outDir, 'page.client.js');
56
+
57
+ // Client pass: the same AST becomes a hydration module that wires
58
+ // signals straight to DOM nodes once it runs in the browser.
59
+ // The runtime lives at the build root, so a nested page reaches it
60
+ // through its own prefix ("../", "../../", …).
61
+ const runtimeSpecifier = `${route.assetPrefix}${RUNTIME_FILENAME}`;
62
+
63
+ const clientModule = rewriteRuntimeImports(
64
+ compileToModule(ast, {
65
+ sourcePath,
66
+ outPath: clientPath,
67
+ runtimeSpecifier,
68
+ }),
69
+ runtimeSpecifier
70
+ );
71
+
72
+ assertValidJavaScript(clientModule, name);
73
+ writeFileSync(clientPath, clientModule, 'utf8');
74
+
75
+ // One runtime at the build root, shared by every page.
76
+ const runtimePath = resolve(buildRoot, RUNTIME_FILENAME);
77
+ copyFileSync(resolve(ROOT_DIR, 'core/reactivity/signal.js'), runtimePath);
78
+
79
+ let document = wrapDocument(html, projectTitle(projectDir));
80
+ if (transformHtml) document = transformHtml(document);
81
+
82
+ const htmlPath = resolve(buildRoot, route.htmlPath);
83
+ writeFileSync(htmlPath, document, 'utf8');
84
+
85
+ return { ...route, htmlPath, clientPath, runtimePath };
86
+ }
87
+
88
+ export function buildAll(projectDir, options) {
89
+ const routes = listRoutes(projectDir);
90
+
91
+ if (!routes.length) {
92
+ throw new BuildError(
93
+ `no .azox pages found in ${PAGES_DIR}/ — run "azox create <name>" to start one`
94
+ );
95
+ }
96
+
97
+ return routes.map((route) => buildRoute(projectDir, route, options));
98
+ }
99
+
100
+ // Builds a single page by route name or URL.
101
+ export function buildPage(projectDir, pageName, options) {
102
+ const route = findRoute(listRoutes(projectDir), pageName);
103
+
104
+ if (!route) {
105
+ throw new BuildError(`page "${PAGES_DIR}/${pageName}.azox" not found in ${projectDir}`);
106
+ }
107
+
108
+ return buildRoute(projectDir, route, options);
109
+ }
110
+
111
+ // Rewrites the bare specifier a page's own <script> uses to the same
112
+ // path the compiler emitted for the runtime import.
113
+ function rewriteRuntimeImports(code, runtimeSpecifier) {
114
+ return code.replace(/(['"])azox(?:\/reactivity)?\1/g, `'${runtimeSpecifier}'`);
115
+ }
116
+
117
+ // A compiler must never write output it knows is broken. Parsing the
118
+ // emitted module catches a malformed expression here, with the page
119
+ // named, instead of leaving the user to find a syntax error in the
120
+ // browser console.
121
+ function assertValidJavaScript(code, pageName) {
122
+ try {
123
+ new Function(`return (async () => { ${stripModuleSyntax(code)} })`);
124
+ } catch (error) {
125
+ throw new BuildError(
126
+ `compiling ${PAGES_DIR}/${pageName}.azox produced invalid JavaScript ` +
127
+ `(${error.message}). This is an Azox bug — please report the page that caused it.`
128
+ );
129
+ }
130
+ }
131
+
132
+ // `new Function` cannot hold import/export statements, so they are
133
+ // removed before the syntax check. What remains is the generated
134
+ // body, which is where a malformed expression would land.
135
+ function stripModuleSyntax(code) {
136
+ return code
137
+ .replace(/^\s*import\s[^;]+;?\s*$/gm, '')
138
+ .replace(/^\s*export\s+(?=function|const|let|class)/gm, '');
139
+ }
140
+
141
+ // Page title comes from the project's package.json name, falling back
142
+ // to the directory name.
143
+ function projectTitle(projectDir) {
144
+ const pkgPath = resolve(projectDir, 'package.json');
145
+
146
+ if (existsSync(pkgPath)) {
147
+ try {
148
+ const { name } = JSON.parse(readFileSync(pkgPath, 'utf8'));
149
+ if (name) return name;
150
+ } catch {
151
+ // A malformed package.json shouldn't block a build.
152
+ }
153
+ }
154
+
155
+ return basename(projectDir);
156
+ }
157
+
158
+ // Runs the page's <script> block in a server-side scope so SSR can
159
+ // evaluate the expressions the template references. The script is
160
+ // trusted project source, not user input — the same assumption any
161
+ // template engine's SSR step makes.
162
+ function buildServerScope(script) {
163
+ // Strip imports: the server supplies its own `signal` stub rather
164
+ // than loading the real reactive runtime.
165
+ const body = script.replace(/^\s*import\s.+?;\s*$/gm, '');
166
+
167
+ try {
168
+ const fn = new Function('signal', `${body}\nreturn { ${declaredNames(body).join(', ')} };`);
169
+ return fn(serverSignal);
170
+ } catch (error) {
171
+ throw new BuildError(`failed to evaluate the page's <script> block: ${error.message}`);
172
+ }
173
+ }
174
+
175
+ // SSR needs only the current value, not reactivity, so `signal()` on
176
+ // the server is a plain boxed value.
177
+ function serverSignal(initial) {
178
+ let value = initial;
179
+ const read = () => value;
180
+ read.set = (next) => {
181
+ value = typeof next === 'function' ? next(value) : next;
182
+ };
183
+ read.peek = () => value;
184
+ return read;
185
+ }
186
+
187
+ function declaredNames(script) {
188
+ const names = [];
189
+ const regex = /const\s+(\w+)\s*=/g;
190
+ let match;
191
+ while ((match = regex.exec(script))) names.push(match[1]);
192
+ return names;
193
+ }
194
+
195
+ // The client module sits next to the page's index.html, so the src is
196
+ // the same for every route regardless of how deep it is.
197
+ function wrapDocument(bodyHtml, title) {
198
+ return `<!doctype html>
199
+ <html lang="en">
200
+ <head>
201
+ <meta charset="utf-8" />
202
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
203
+ <title>${escapeHtml(title)}</title>
204
+ </head>
205
+ <body>
206
+ <div data-azox-root>${bodyHtml}</div>
207
+ <script type="module" src="./page.client.js"></script>
208
+ </body>
209
+ </html>
210
+ `;
211
+ }
212
+
213
+ function escapeHtml(str) {
214
+ return String(str)
215
+ .replace(/&/g, '&amp;')
216
+ .replace(/</g, '&lt;')
217
+ .replace(/>/g, '&gt;');
218
+ }
@@ -0,0 +1,8 @@
1
+ // Lives on its own so both the build pipeline and the compiler can
2
+ // use it without importing each other.
3
+ //
4
+ // A BuildError means the user's project is wrong — a missing page, a
5
+ // bad component reference — and the CLI prints its message plainly.
6
+ // Anything else that escapes is a bug in Azox and keeps its stack.
7
+
8
+ export class BuildError extends Error {}
@@ -0,0 +1,39 @@
1
+ // Minimal terminal argument parser. No Commander, no Yargs —
2
+ // keeping the CLI dependency-free is part of the point.
3
+ //
4
+ // Usage: azox <command> [--flag] [-f] [--key=value] [positional...]
5
+ //
6
+ // Short flags are single-dash, single-letter (-v). Grouping like -abc
7
+ // is deliberately not supported: it buys little for a framework CLI
8
+ // and makes error messages harder to read.
9
+ //
10
+ // Returns:
11
+ // { command, positionals, flags }
12
+
13
+ const isFlag = (arg) => arg.startsWith('-');
14
+
15
+ export function parseArgs(argv) {
16
+ const command = argv.length > 0 && !isFlag(argv[0]) ? argv[0] : null;
17
+ const rest = command ? argv.slice(1) : argv;
18
+
19
+ const positionals = [];
20
+ const flags = {};
21
+
22
+ for (const arg of rest) {
23
+ if (!isFlag(arg)) {
24
+ positionals.push(arg);
25
+ continue;
26
+ }
27
+
28
+ const raw = arg.startsWith('--') ? arg.slice(2) : arg.slice(1);
29
+ const eqIndex = raw.indexOf('=');
30
+
31
+ if (eqIndex === -1) {
32
+ flags[raw] = true; // e.g. --watch, -v
33
+ } else {
34
+ flags[raw.slice(0, eqIndex)] = raw.slice(eqIndex + 1); // e.g. --port=3000
35
+ }
36
+ }
37
+
38
+ return { command, positionals, flags };
39
+ }
@@ -0,0 +1,76 @@
1
+ // The command registry is the single source of truth: each entry
2
+ // carries its handler, its one-line description, and example usage.
3
+ // `azox help` renders itself from this, so adding a command here is
4
+ // enough to make it discoverable.
5
+
6
+ import { doctorCommand } from '../commands/doctor.js';
7
+ import { compileCommand } from '../commands/compile.js';
8
+ import { devCommand } from '../commands/dev.js';
9
+ import { versionCommand } from '../commands/version.js';
10
+ import { createCommand } from '../commands/create.js';
11
+ import { helpCommand } from '../commands/help.js';
12
+
13
+ export const registry = {
14
+ create: {
15
+ run: createCommand,
16
+ describe: 'Scaffold a new Azox project',
17
+ examples: ['azox create my-app'],
18
+ },
19
+ dev: {
20
+ run: devCommand,
21
+ describe: 'Serve the project and rebuild on every change',
22
+ examples: ['azox dev', 'azox dev --port=5000'],
23
+ },
24
+ compile: {
25
+ run: compileCommand,
26
+ describe: 'Compile pages to HTML + hydration modules',
27
+ examples: ['azox compile', 'azox compile --page=about'],
28
+ },
29
+ doctor: {
30
+ run: doctorCommand,
31
+ describe: 'Check that the toolchain and project are healthy',
32
+ examples: ['azox doctor'],
33
+ },
34
+ version: {
35
+ run: versionCommand,
36
+ describe: 'Print the Azox version',
37
+ examples: ['azox -v'],
38
+ },
39
+ help: {
40
+ run: helpCommand,
41
+ describe: 'Show this help',
42
+ examples: ['azox help'],
43
+ },
44
+ };
45
+
46
+ // Flags that stand in for a command, so `azox -v` works like
47
+ // `azox version` without making every command parse them.
48
+ const FLAG_ALIASES = {
49
+ v: 'version',
50
+ version: 'version',
51
+ h: 'help',
52
+ help: 'help',
53
+ };
54
+
55
+ // Returns whatever the handler returns, so an async command (dev)
56
+ // can be awaited by the entry point.
57
+ export function runCommand(command, context) {
58
+ const resolved = command ?? aliasFor(context.flags) ?? 'help';
59
+ const entry = registry[resolved];
60
+
61
+ if (!entry) {
62
+ console.error(`Azox: unknown command "${resolved}".`);
63
+ console.error(`Run "azox help" to see available commands.`);
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+
68
+ return entry.run({ ...context, registry });
69
+ }
70
+
71
+ function aliasFor(flags) {
72
+ for (const [flag, command] of Object.entries(FLAG_ALIASES)) {
73
+ if (flags[flag]) return command;
74
+ }
75
+ return null;
76
+ }
@@ -0,0 +1,35 @@
1
+ // `azox compile` — builds pages into static HTML plus hydration
2
+ // modules.
3
+ //
4
+ // azox compile build every page in pages/
5
+ // azox compile --page=about build one page
6
+
7
+ import { relative } from 'node:path';
8
+
9
+ import { buildAll, buildPage, BuildError, BUILD_DIR } from '../build.js';
10
+ import { BANNER } from '../meta.js';
11
+
12
+ export function compileCommand({ flags }) {
13
+ // Rooted at the user's project, not the framework checkout, so
14
+ // `azox compile` inside a scaffolded app builds that app.
15
+ const projectDir = process.cwd();
16
+
17
+ try {
18
+ const results = flags.page ? [buildPage(projectDir, flags.page)] : buildAll(projectDir);
19
+
20
+ console.log(BANNER);
21
+ console.log('');
22
+ console.log(`Built ${results.length} page${results.length === 1 ? '' : 's'} to ${BUILD_DIR}/`);
23
+ console.log('');
24
+
25
+ const width = Math.max(...results.map((result) => result.url.length));
26
+ for (const result of results) {
27
+ console.log(` ${result.url.padEnd(width)} ${relative(projectDir, result.htmlPath)}`);
28
+ }
29
+ } catch (error) {
30
+ if (!(error instanceof BuildError)) throw error;
31
+
32
+ console.error(`Azox: ${error.message}`);
33
+ process.exitCode = 1;
34
+ }
35
+ }