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 +21 -0
- package/README.md +214 -0
- package/bin/azox.js +18 -0
- package/core/build.js +218 -0
- package/core/buildError.js +8 -0
- package/core/cli/parseArgs.js +39 -0
- package/core/cli/router.js +76 -0
- package/core/commands/compile.js +35 -0
- package/core/commands/create.js +146 -0
- package/core/commands/dev.js +127 -0
- package/core/commands/doctor.js +68 -0
- package/core/commands/help.js +31 -0
- package/core/commands/version.js +7 -0
- package/core/compiler/compileToJs.js +139 -0
- package/core/compiler/index.js +4 -0
- package/core/compiler/parser.js +317 -0
- package/core/compiler/resolveComponents.js +184 -0
- package/core/dev/liveReload.js +20 -0
- package/core/dev/server.js +179 -0
- package/core/dev/watcher.js +52 -0
- package/core/index.js +7 -0
- package/core/meta.js +15 -0
- package/core/reactivity/signal.js +48 -0
- package/core/renderer/renderToHtml.js +67 -0
- package/core/routes.js +78 -0
- package/package.json +47 -0
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
|
+
[](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, '&')
|
|
216
|
+
.replace(/</g, '<')
|
|
217
|
+
.replace(/>/g, '>');
|
|
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
|
+
}
|