azoxjs 0.1.0 → 0.3.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/README.md +188 -5
- package/core/build.js +472 -46
- package/core/cli/router.js +28 -0
- package/core/commands/compile.js +5 -1
- package/core/commands/create.js +44 -10
- package/core/commands/dev.js +42 -18
- package/core/commands/doctor.js +6 -1
- package/core/compiler/compileToJs.js +529 -36
- package/core/compiler/html.js +28 -0
- package/core/compiler/parser.js +219 -9
- package/core/compiler/resolveComponents.js +227 -39
- package/core/compiler/sourceResolver.js +42 -0
- package/core/dev/watcher.js +12 -1
- package/core/nodeResolver.js +15 -0
- package/core/reactivity/signal.js +36 -1
- package/core/renderer/moduleBindings.js +88 -0
- package/core/renderer/renderToHtml.js +94 -18
- package/core/renderer/serverScope.js +95 -0
- package/core/router/navigate.js +211 -0
- package/core/routes.js +44 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Azox Framework
|
|
2
2
|
|
|
3
3
|
[](https://github.com/darilpratomo/azox/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/azoxjs)
|
|
5
|
+
[](https://www.npmjs.com/package/azoxjs)
|
|
4
6
|
|
|
5
7
|
**The Sound of Future Web**
|
|
6
8
|
|
|
@@ -9,7 +11,7 @@ third-party CLI dependencies, no borrowed syntax from React, Vue, or
|
|
|
9
11
|
Next.js. It compiles `.azox` components directly into fine-grained,
|
|
10
12
|
signal-driven DOM updates.
|
|
11
13
|
|
|
12
|
-
> Status: early development (v0.
|
|
14
|
+
> Status: early development (v0.3.0). APIs are unstable and will
|
|
13
15
|
> change without notice until v1.0.
|
|
14
16
|
|
|
15
17
|
## Why Azox
|
|
@@ -63,6 +65,46 @@ pages/blog/first-post.azox → /blog/first-post
|
|
|
63
65
|
Build one page with `azox compile --page=blog/first-post`, or by its
|
|
64
66
|
URL: `azox compile --page=/blog/first-post`.
|
|
65
67
|
|
|
68
|
+
### Dynamic routes
|
|
69
|
+
|
|
70
|
+
A bracketed segment in a filename is a parameter, and the file becomes
|
|
71
|
+
a template that builds one page per entry it declares:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!-- pages/blog/[slug].azox -->
|
|
75
|
+
<script>
|
|
76
|
+
import posts from '../../posts.json' with { type: 'json' };
|
|
77
|
+
|
|
78
|
+
// Which pages to build.
|
|
79
|
+
routes(posts.map((p) => ({ slug: p.slug })));
|
|
80
|
+
|
|
81
|
+
// The parameters of the page being built.
|
|
82
|
+
const { slug } = params();
|
|
83
|
+
const post = posts.find((p) => p.slug === slug);
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<article>
|
|
87
|
+
<h1>{post.title}</h1>
|
|
88
|
+
<p>{post.body}</p>
|
|
89
|
+
</article>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
posts.json with two entries → /blog/hello
|
|
94
|
+
→ /blog/second
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`routes()` takes an array of objects, one per page, each supplying
|
|
98
|
+
every parameter the filename asks for. A filename may hold several
|
|
99
|
+
(`pages/[lang]/[slug].azox`), and `params()` returns them all.
|
|
100
|
+
|
|
101
|
+
Both are build-time declarations: neither reaches the browser. The
|
|
102
|
+
parameters for each page are compiled into its module as a constant.
|
|
103
|
+
|
|
104
|
+
A missing `routes()` call, an entry missing a parameter, a value
|
|
105
|
+
containing a `/`, and two entries producing the same URL are all
|
|
106
|
+
reported as build errors rather than producing a broken site.
|
|
107
|
+
|
|
66
108
|
## Components
|
|
67
109
|
|
|
68
110
|
A component is a `.azox` file that declares what it accepts and
|
|
@@ -120,10 +162,150 @@ Declaring props with `props()` is what lets the compiler reject a
|
|
|
120
162
|
caller that passes something the component never asked for, instead
|
|
121
163
|
of dropping it silently.
|
|
122
164
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
165
|
+
A component may hold its own state. Its `<script>` becomes a scope
|
|
166
|
+
of its own, so two uses of the same component are independent — each
|
|
167
|
+
`<Counter />` below counts separately:
|
|
168
|
+
|
|
169
|
+
```html
|
|
170
|
+
<!-- components/Counter.azox -->
|
|
171
|
+
<script>
|
|
172
|
+
import { signal } from 'azox/reactivity';
|
|
173
|
+
const count = signal(0);
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
<button on:click={() => count.set(count() + 1)}>{count()}</button>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```html
|
|
180
|
+
<main>
|
|
181
|
+
<Counter />
|
|
182
|
+
<Counter />
|
|
183
|
+
</main>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
There is still no component instance at runtime: the compiler wraps
|
|
187
|
+
each use in its own JavaScript scope, which is ordinary scoping
|
|
188
|
+
rather than a framework construct.
|
|
189
|
+
|
|
190
|
+
## Layouts and the document head
|
|
191
|
+
|
|
192
|
+
A component can carry a `<head>` block, so one shared component holds
|
|
193
|
+
the stylesheet, fonts and scripts every page needs:
|
|
194
|
+
|
|
195
|
+
```html
|
|
196
|
+
<!-- components/Shell.azox -->
|
|
197
|
+
<head>
|
|
198
|
+
<link rel="stylesheet" href="/style.css" />
|
|
199
|
+
</head>
|
|
200
|
+
|
|
201
|
+
<div class="shell">
|
|
202
|
+
<header>My site</header>
|
|
203
|
+
<slot />
|
|
204
|
+
</div>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```html
|
|
208
|
+
<!-- pages/index.azox -->
|
|
209
|
+
<head>
|
|
210
|
+
<title>Home — my site</title>
|
|
211
|
+
</head>
|
|
212
|
+
|
|
213
|
+
<script>
|
|
214
|
+
import Shell from '../components/Shell.azox';
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<Shell><main>Just this page's content.</main></Shell>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Blocks are merged with the component's first, so the page has the last
|
|
221
|
+
word. Identical lines are emitted once, and a component used twice
|
|
222
|
+
contributes once. A `<title>` or `<meta name="…">` set by the page
|
|
223
|
+
replaces the component's rather than joining it — a document may hold
|
|
224
|
+
only one of each — so a layout's title is a default, not a conflict.
|
|
225
|
+
|
|
226
|
+
## Loops and conditionals
|
|
227
|
+
|
|
228
|
+
Control flow is expressed as tags, so it nests inside markup like
|
|
229
|
+
anything else.
|
|
230
|
+
|
|
231
|
+
```html
|
|
232
|
+
<ul>
|
|
233
|
+
<each item={todos()} as="todo" index="i">
|
|
234
|
+
<li>{i + 1}. {todo}</li>
|
|
235
|
+
</each>
|
|
236
|
+
</ul>
|
|
237
|
+
|
|
238
|
+
<if cond={user()}>
|
|
239
|
+
<p>Signed in as {user().name}</p>
|
|
240
|
+
<else />
|
|
241
|
+
<a href="/login">Sign in</a>
|
|
242
|
+
</if>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Each block marks its place with a pair of comment nodes, and an
|
|
246
|
+
update replaces only the nodes between them.
|
|
247
|
+
|
|
248
|
+
By default a change to a list rebuilds its rows. Give a row an
|
|
249
|
+
identity with `key` and it survives instead: reordering moves it,
|
|
250
|
+
removing one leaves the rest untouched, and adding one does not
|
|
251
|
+
disturb what is already there.
|
|
252
|
+
|
|
253
|
+
```html
|
|
254
|
+
<each item={tasks()} as="task" key={task.id}>
|
|
255
|
+
<li><TaskRow title={task.title} /></li>
|
|
256
|
+
</each>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Use something stable and unique to the row — a database id, not its
|
|
260
|
+
position, since a position changes when the list does.
|
|
261
|
+
|
|
262
|
+
## Client-side routing
|
|
263
|
+
|
|
264
|
+
By default every link is a full page load, which is the right
|
|
265
|
+
behaviour for a static site. Opt in to client-side navigation with
|
|
266
|
+
`router: true` in your project's `package.json`:
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"router": true
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Internal links are then swapped in place: the new page's HTML is
|
|
275
|
+
fetched, the document body and `<head>` are replaced, and its module
|
|
276
|
+
runs. Scroll position, the back button, and `<a target>` all behave
|
|
277
|
+
as they would with a full load. A link is prefetched when the pointer
|
|
278
|
+
enters it, so the page is usually already in hand by the time it is
|
|
279
|
+
clicked.
|
|
280
|
+
|
|
281
|
+
Anything the router cannot handle — an external origin, a download,
|
|
282
|
+
a modifier-click — falls through to the browser untouched.
|
|
283
|
+
|
|
284
|
+
## Importing data
|
|
285
|
+
|
|
286
|
+
A `<script>` block may import a `.json` file, which is how a page
|
|
287
|
+
reads a constant it should not have written out by hand:
|
|
288
|
+
|
|
289
|
+
```html
|
|
290
|
+
<script>
|
|
291
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
292
|
+
</script>
|
|
293
|
+
|
|
294
|
+
<span>v{pkg.version}</span>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The file is read once during the build. Server rendering evaluates
|
|
298
|
+
against it, and the value is compiled into the module as a constant
|
|
299
|
+
rather than imported — the file sits outside the build directory and
|
|
300
|
+
is never deployed, so an import would 404 in the browser.
|
|
301
|
+
|
|
302
|
+
Only the properties the markup reads are included, so importing
|
|
303
|
+
`package.json` for a version does not ship the rest of the file to
|
|
304
|
+
every visitor.
|
|
305
|
+
|
|
306
|
+
Importing a `.js` module is not supported: it would mean executing
|
|
307
|
+
project code during the build. Use a `.json` file for data, and
|
|
308
|
+
`azox/reactivity` for signals.
|
|
127
309
|
|
|
128
310
|
## Getting Started
|
|
129
311
|
|
|
@@ -179,6 +361,7 @@ azox/
|
|
|
179
361
|
│ ├── dev/ dev server, file watching, live reload
|
|
180
362
|
│ ├── reactivity/ signal() / effect() / computed()
|
|
181
363
|
│ ├── renderer/ server-side HTML rendering
|
|
364
|
+
│ ├── router/ opt-in client-side navigation
|
|
182
365
|
│ ├── build.js the build pipeline, shared by commands
|
|
183
366
|
│ ├── routes.js file layout → urls and output paths
|
|
184
367
|
│ └── meta.js version and identity strings
|