create-acrylblends 0.0.0-stage → 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/bin/create-acrylblends.js +4 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/docs/framework/LICENSE +23 -0
- package/docs/framework/README.md +63 -0
- package/docs/framework/UPSTREAM.md +3 -0
- package/docs/framework/agents/index.md +24 -0
- package/docs/framework/api/acryl.md +259 -0
- package/docs/framework/api/context.md +527 -0
- package/docs/framework/api/exception.md +80 -0
- package/docs/framework/api/index.md +12 -0
- package/docs/framework/api/presets.md +65 -0
- package/docs/framework/api/request.md +396 -0
- package/docs/framework/api/routing.md +303 -0
- package/docs/framework/api/summary.md +140 -0
- package/docs/framework/concepts/benchmarks.md +26 -0
- package/docs/framework/concepts/developer-experience.md +5 -0
- package/docs/framework/concepts/middleware.md +21 -0
- package/docs/framework/concepts/motivation.md +14 -0
- package/docs/framework/concepts/routers.md +63 -0
- package/docs/framework/concepts/stacks.md +235 -0
- package/docs/framework/concepts/web-standard.md +37 -0
- package/docs/framework/cordis/acryl-in-a-cordis-app.md +146 -0
- package/docs/framework/cordis/concepts.md +105 -0
- package/docs/framework/cordis/middleware.md +8 -0
- package/docs/framework/cordis/migration-from-0-3.md +111 -0
- package/docs/framework/cordis/migration-from-0-4.md +14 -0
- package/docs/framework/cordis/migration-from-dsh-webserver.md +103 -0
- package/docs/framework/cordis/mounting.md +85 -0
- package/docs/framework/cordis/node-server.md +75 -0
- package/docs/framework/cordis/plugin-lifecycle.md +111 -0
- package/docs/framework/cordis/surfaces.md +71 -0
- package/docs/framework/cordis/transport-and-cancellation.md +57 -0
- package/docs/framework/cordis/validation.md +135 -0
- package/docs/framework/getting-started/basic.md +196 -0
- package/docs/framework/getting-started/bun.md +221 -0
- package/docs/framework/getting-started/nodejs.md +344 -0
- package/docs/framework/getting-started/other-runtimes.md +23 -0
- package/docs/framework/guides/best-practices.md +231 -0
- package/docs/framework/guides/create-acrylblends.md +79 -0
- package/docs/framework/guides/examples.md +3 -0
- package/docs/framework/guides/faq.md +19 -0
- package/docs/framework/guides/helpers.md +33 -0
- package/docs/framework/guides/jsx-dom.md +299 -0
- package/docs/framework/guides/jsx.md +440 -0
- package/docs/framework/guides/middleware.md +253 -0
- package/docs/framework/guides/rpc.md +886 -0
- package/docs/framework/guides/testing.md +133 -0
- package/docs/framework/guides/validation.md +383 -0
- package/docs/framework/helpers/accepts.md +60 -0
- package/docs/framework/helpers/adapter.md +87 -0
- package/docs/framework/helpers/conninfo.md +112 -0
- package/docs/framework/helpers/cookie.md +232 -0
- package/docs/framework/helpers/css.md +285 -0
- package/docs/framework/helpers/dev.md +67 -0
- package/docs/framework/helpers/factory.md +180 -0
- package/docs/framework/helpers/html.md +123 -0
- package/docs/framework/helpers/jwt.md +184 -0
- package/docs/framework/helpers/proxy.md +118 -0
- package/docs/framework/helpers/route.md +143 -0
- package/docs/framework/helpers/ssg.md +393 -0
- package/docs/framework/helpers/streaming.md +119 -0
- package/docs/framework/helpers/testing.md +101 -0
- package/docs/framework/helpers/websocket.md +215 -0
- package/docs/framework/middleware/builtin/basic-auth.md +165 -0
- package/docs/framework/middleware/builtin/bearer-auth.md +138 -0
- package/docs/framework/middleware/builtin/body-limit.md +71 -0
- package/docs/framework/middleware/builtin/cache.md +122 -0
- package/docs/framework/middleware/builtin/combine.md +94 -0
- package/docs/framework/middleware/builtin/compress.md +59 -0
- package/docs/framework/middleware/builtin/context-storage.md +78 -0
- package/docs/framework/middleware/builtin/cors.md +135 -0
- package/docs/framework/middleware/builtin/csrf.md +105 -0
- package/docs/framework/middleware/builtin/etag.md +54 -0
- package/docs/framework/middleware/builtin/ip-restriction.md +69 -0
- package/docs/framework/middleware/builtin/jsx-renderer.md +286 -0
- package/docs/framework/middleware/builtin/jwk.md +192 -0
- package/docs/framework/middleware/builtin/jwt.md +145 -0
- package/docs/framework/middleware/builtin/language.md +232 -0
- package/docs/framework/middleware/builtin/logger.md +74 -0
- package/docs/framework/middleware/builtin/loopback-only.md +31 -0
- package/docs/framework/middleware/builtin/method-not-allowed.md +48 -0
- package/docs/framework/middleware/builtin/method-override.md +80 -0
- package/docs/framework/middleware/builtin/mount.md +106 -0
- package/docs/framework/middleware/builtin/pretty-json.md +53 -0
- package/docs/framework/middleware/builtin/request-id.md +94 -0
- package/docs/framework/middleware/builtin/secure-headers.md +247 -0
- package/docs/framework/middleware/builtin/timeout.md +98 -0
- package/docs/framework/middleware/builtin/timing.md +101 -0
- package/docs/framework/middleware/builtin/trailing-slash.md +74 -0
- package/docs/framework/middleware/third-party.md +4 -0
- package/docs/framework/upstream.json +9 -0
- package/package.json +47 -3
- package/templates/bun/gitignore +1 -0
- package/templates/bun/package.json +19 -0
- package/templates/bun/src/index.ts +7 -0
- package/templates/bun/test/app.test.ts +8 -0
- package/templates/bun/tsconfig.json +15 -0
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +22 -0
- package/templates/nodejs/src/app.ts +7 -0
- package/templates/nodejs/src/index.ts +9 -0
- package/templates/nodejs/test/app.test.ts +9 -0
- package/templates/nodejs/tsconfig.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Best Practices
|
|
2
|
+
|
|
3
|
+
Acryl is very flexible. You can write your app as you like.
|
|
4
|
+
However, there are best practices that are better to follow.
|
|
5
|
+
|
|
6
|
+
## Don't make "Controllers" when possible
|
|
7
|
+
|
|
8
|
+
When possible, you should not create "Ruby on Rails-like Controllers".
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
// 🙁
|
|
12
|
+
// A RoR-like Controller
|
|
13
|
+
const booksList = (c: Context) => {
|
|
14
|
+
return c.json('list books')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
app.get('/books', booksList)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The issue is related to types. For example, the path parameter cannot be inferred in the Controller without writing complex generics.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// 🙁
|
|
24
|
+
// A RoR-like Controller
|
|
25
|
+
const bookPermalink = (c: Context) => {
|
|
26
|
+
const id = c.req.param('id') // Can't infer the path param
|
|
27
|
+
return c.json(`get ${id}`)
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Therefore, you don't need to create RoR-like controllers and should write handlers directly after path definitions.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// 😃
|
|
35
|
+
app.get('/books/:id', (c) => {
|
|
36
|
+
const id = c.req.param('id') // Can infer the path param
|
|
37
|
+
return c.json(`get ${id}`)
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## `factory.createHandlers()` in `acrylblends/factory`
|
|
42
|
+
|
|
43
|
+
If you still want to create a RoR-like Controller, use `factory.createHandlers()` in [`acrylblends/factory`](../helpers/factory.md). If you use this, type inference will work correctly.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createFactory } from 'acrylblends/factory'
|
|
47
|
+
import { logger } from 'acrylblends/logger'
|
|
48
|
+
|
|
49
|
+
declare module 'acrylblends' {
|
|
50
|
+
interface ContextVariableMap {
|
|
51
|
+
foo: string
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ...
|
|
56
|
+
|
|
57
|
+
// 😃
|
|
58
|
+
const factory = createFactory()
|
|
59
|
+
|
|
60
|
+
const middleware = factory.createMiddleware(async (c, next) => {
|
|
61
|
+
c.set('foo', 'bar')
|
|
62
|
+
await next()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const handlers = factory.createHandlers(logger(), middleware, (c) => {
|
|
66
|
+
return c.json(c.var.foo)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
app.get('/api', ...handlers)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Building a larger application
|
|
73
|
+
|
|
74
|
+
Use `app.route()` to build a larger application without creating "Ruby on Rails-like Controllers".
|
|
75
|
+
|
|
76
|
+
If your application has `/authors` and `/books` endpoints and you wish to separate files from `index.ts`, create `authors.ts` and `books.ts`.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// authors.ts
|
|
80
|
+
import { Acryl } from 'acrylblends'
|
|
81
|
+
|
|
82
|
+
const app = new Acryl()
|
|
83
|
+
|
|
84
|
+
app.get('/', (c) => c.json('list authors'))
|
|
85
|
+
app.post('/', (c) => c.json('create an author', 201))
|
|
86
|
+
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
|
|
87
|
+
|
|
88
|
+
export default app
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// books.ts
|
|
93
|
+
import { Acryl } from 'acrylblends'
|
|
94
|
+
|
|
95
|
+
const app = new Acryl()
|
|
96
|
+
|
|
97
|
+
app.get('/', (c) => c.json('list books'))
|
|
98
|
+
app.post('/', (c) => c.json('create a book', 201))
|
|
99
|
+
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
|
|
100
|
+
|
|
101
|
+
export default app
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then, import them and mount on the paths `/authors` and `/books` with `app.route()`.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// index.ts
|
|
108
|
+
import { Acryl } from 'acrylblends'
|
|
109
|
+
import authors from './authors'
|
|
110
|
+
import books from './books'
|
|
111
|
+
|
|
112
|
+
const app = new Acryl()
|
|
113
|
+
|
|
114
|
+
// 😃
|
|
115
|
+
app.route('/authors', authors)
|
|
116
|
+
app.route('/books', books)
|
|
117
|
+
|
|
118
|
+
export default app
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### If you want to use RPC features
|
|
122
|
+
|
|
123
|
+
The code above works well for normal use cases.
|
|
124
|
+
However, if you want to use the `RPC` feature, you can get the correct type by chaining as follows.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// authors.ts
|
|
128
|
+
import { Acryl } from 'acrylblends'
|
|
129
|
+
|
|
130
|
+
const app = new Acryl()
|
|
131
|
+
.get('/', (c) => c.json('list authors'))
|
|
132
|
+
.post('/', (c) => c.json('create an author', 201))
|
|
133
|
+
.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
|
|
134
|
+
|
|
135
|
+
export default app
|
|
136
|
+
export type AppType = typeof app
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If you pass the type of the `app` to `hc`, it will get the correct type.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import type { AppType } from './authors'
|
|
143
|
+
import { hc } from 'acrylblends/client'
|
|
144
|
+
|
|
145
|
+
// 😃
|
|
146
|
+
const client = hc<AppType>('http://localhost') // Typed correctly
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For more detailed information, please see [the RPC page](rpc.md#using-rpc-with-larger-applications).
|
|
150
|
+
|
|
151
|
+
## HEAD Request Best Practices
|
|
152
|
+
|
|
153
|
+
### Understanding Acryl's HEAD Handling
|
|
154
|
+
|
|
155
|
+
Acryl automatically handles HEAD requests by converting them to GET requests and stripping the response body. This behavior is built into the framework's dispatch layer and happens before route matching occurs.
|
|
156
|
+
|
|
157
|
+
### ✅ Do: Use GET Routes for HEAD Requests
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// GOOD: This GET route automatically handles HEAD requests
|
|
161
|
+
app.get('/api/users', async (c) => {
|
|
162
|
+
const users = await getUsers()
|
|
163
|
+
c.header('X-Total-Count', users.length.toString())
|
|
164
|
+
return c.json(users)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// HEAD /api/users will return:
|
|
168
|
+
// - Same headers as GET (including X-Total-Count)
|
|
169
|
+
// - Status 200
|
|
170
|
+
// - No body (null)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### ✅ Do: Use Middleware for HEAD-Specific Logic
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// GOOD: Use middleware when HEAD needs different behavior
|
|
177
|
+
app.use('/api/resource', async (c, next) => {
|
|
178
|
+
await next()
|
|
179
|
+
|
|
180
|
+
// Add HEAD-specific headers after the handler
|
|
181
|
+
if (c.req.method === 'HEAD') {
|
|
182
|
+
c.header('X-HEAD-Processed', 'true')
|
|
183
|
+
// Don't compute expensive body content for HEAD
|
|
184
|
+
c.res = new Response(null, c.res)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### ❌ Don't: Try to Create Dedicated HEAD Handlers
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// BAD: This won't work as expected
|
|
193
|
+
app.head('/api/users', (c) => {
|
|
194
|
+
// This handler will NEVER be called
|
|
195
|
+
c.header('X-Custom', 'value')
|
|
196
|
+
return c.text('ignored')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// BAD: Using on() also won't work
|
|
200
|
+
app.on('HEAD', '/api/users', (c) => {
|
|
201
|
+
// Still converted to GET before route matching
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Performance Considerations
|
|
206
|
+
|
|
207
|
+
- **Avoid expensive operations in GET handlers if you expect many HEAD requests**: Use middleware to detect HEAD and skip body generation
|
|
208
|
+
- **Cache headers work identically**: HEAD responses respect the same caching rules as GET
|
|
209
|
+
- **Middleware compatibility**: Most middleware works with HEAD, but body-processing middleware (like compression) automatically skips HEAD requests
|
|
210
|
+
|
|
211
|
+
### Testing HEAD Requests
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
// Always test both GET and HEAD responses
|
|
215
|
+
it('handles HEAD requests correctly', async () => {
|
|
216
|
+
const getRes = await app.request('/api/users')
|
|
217
|
+
const headRes = await app.request('/api/users', { method: 'HEAD' })
|
|
218
|
+
|
|
219
|
+
expect(headRes.status).toBe(getRes.status)
|
|
220
|
+
expect(headRes.headers.get('X-Total-Count')).toBe(
|
|
221
|
+
getRes.headers.get('X-Total-Count')
|
|
222
|
+
)
|
|
223
|
+
expect(headRes.body).toBe(null)
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Notes
|
|
228
|
+
|
|
229
|
+
- The automatic HEAD conversion ensures consistent headers between GET and HEAD responses
|
|
230
|
+
- This behavior is consistent across all Acryl runtimes (Node.js, Bun)
|
|
231
|
+
- If you need completely different logic for HEAD vs GET, consider using different endpoints rather than trying to override the framework's HEAD handling
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# create-acrylblends
|
|
2
|
+
|
|
3
|
+
`create-acrylblends` is the project initializer that runs when you run `npm create acrylblends@latest`, `npx create-acrylblends@latest`, `pnpm create acrylblends@latest`,
|
|
4
|
+
`yarn create acrylblends` or `bun create acrylblends@latest`. It creates a folder with a working Acryl application for the runtime you choose.
|
|
5
|
+
|
|
6
|
+
## Templates
|
|
7
|
+
|
|
8
|
+
| Template | What you get |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `nodejs` | an Acryl application served with `serve()` from `acrylblends/node-server`; run with Node.js 22.19 or newer |
|
|
11
|
+
| `bun` (experimental) | an Acryl application with `export default app`, run with `bun` |
|
|
12
|
+
| `acryl-app` | a Cordis app built from plugins, with an agent that can extend it while it runs (the same app `npx acrylblends init --template agent` creates) |
|
|
13
|
+
|
|
14
|
+
Every template includes `AGENTS.md` and `CLAUDE.md` (which imports `AGENTS.md`) and a copy of these docs in `docs/framework/`, so a coding agent working in the folder reads the
|
|
15
|
+
API from the folder.
|
|
16
|
+
|
|
17
|
+
## Passing arguments
|
|
18
|
+
|
|
19
|
+
When you use `npm create` (or `npx`), arguments meant for the initializer must be placed **after** `--`. Anything after `--` is forwarded to the initializer.
|
|
20
|
+
|
|
21
|
+
**npm**
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm create acrylblends@latest my-app -- --template nodejs
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**yarn**
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
yarn create acrylblends my-app --template nodejs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**pnpm**
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pnpm create acrylblends@latest my-app --template nodejs
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**bun**
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
bun create acrylblends@latest my-app --template nodejs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Arguments
|
|
46
|
+
|
|
47
|
+
| Argument | Description | Example |
|
|
48
|
+
| :-- | :-- | :-- |
|
|
49
|
+
| `[directory]` | The folder to create. It must not exist or must be empty. Asked for when it is missing. | `my-app` |
|
|
50
|
+
| `--template <template>` | One of `nodejs`, `bun` (experimental), `acryl-app`. Skips the template question. | `--template bun` |
|
|
51
|
+
| `--install` | Install the dependencies with the package manager after creating the project. | `--install` |
|
|
52
|
+
| `--pm <packageManager>` | The package manager for `--install`: `npm`, `pnpm`, `yarn` or `bun`. Defaults to the one that started the initializer. | `--pm pnpm` |
|
|
53
|
+
| `--yes`, `-y` | Ask nothing: use `nodejs` unless `--template` is given, and the directory `acryl-app`. | `-y` |
|
|
54
|
+
| `--help` | Print the arguments. | `--help` |
|
|
55
|
+
|
|
56
|
+
The templates are inside the package, so creating a project needs no network except for `--install`.
|
|
57
|
+
|
|
58
|
+
## Example flows
|
|
59
|
+
|
|
60
|
+
### Interactive
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm create acrylblends@latest my-app
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This asks which template you want.
|
|
67
|
+
|
|
68
|
+
### Non-interactive
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm create acrylblends@latest my-app -- --template nodejs --pm npm --install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This creates `my-app` from the `nodejs` template and installs its dependencies with npm.
|
|
75
|
+
|
|
76
|
+
## Troubleshooting
|
|
77
|
+
|
|
78
|
+
- If an argument is not recognized, make sure you forward it with `--` when you use `npm create` or `npx`.
|
|
79
|
+
- Run `npm create acrylblends@latest -- --help` to see the arguments of the version you have.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Frequently Asked Questions
|
|
2
|
+
|
|
3
|
+
## Why does `/orders/` return 404 when `/orders` works?
|
|
4
|
+
|
|
5
|
+
Trailing slashes are strict by default: `/orders` and `/orders/` are different paths. Add `trimTrailingSlash()` to redirect, or set the router to ignore the slash. See
|
|
6
|
+
[Mounting and trailing slashes](../cordis/mounting.md#trailing-slashes).
|
|
7
|
+
|
|
8
|
+
## Why does my validator see `{}` for a JSON body?
|
|
9
|
+
|
|
10
|
+
A `json` validation target reads the body only when the request has a JSON `Content-Type`; without it the validator sees `{}`. See
|
|
11
|
+
[Validation with Standard Schema](../cordis/validation.md).
|
|
12
|
+
|
|
13
|
+
## Which runtimes are verified?
|
|
14
|
+
|
|
15
|
+
Node.js. Bun is experimental. The others are listed, as not yet verified, on [Other runtimes](../getting-started/other-runtimes.md).
|
|
16
|
+
|
|
17
|
+
## Where are the parts marked "not yet available"?
|
|
18
|
+
|
|
19
|
+
Their pages are part of the documentation so that the structure is complete; each starts with a note saying so. They are enabled as they are ported.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Helpers
|
|
2
|
+
|
|
3
|
+
Helpers are available to assist in developing your application. Unlike middleware, they don't act as handlers, but rather provide useful functions.
|
|
4
|
+
|
|
5
|
+
For instance, here's how to use the [Cookie helper](../helpers/cookie.md):
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { getCookie, setCookie } from 'acrylblends/cookie'
|
|
9
|
+
|
|
10
|
+
const app = new Acryl()
|
|
11
|
+
|
|
12
|
+
app.get('/cookie', (c) => {
|
|
13
|
+
const yummyCookie = getCookie(c, 'yummy_cookie')
|
|
14
|
+
// ...
|
|
15
|
+
setCookie(c, 'delicious_cookie', 'macha')
|
|
16
|
+
//
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Available Helpers
|
|
21
|
+
|
|
22
|
+
- [Accepts](../helpers/accepts.md)
|
|
23
|
+
- [Adapter](../helpers/adapter.md)
|
|
24
|
+
- [Cookie](../helpers/cookie.md)
|
|
25
|
+
- [css](../helpers/css.md)
|
|
26
|
+
- [Dev](../helpers/dev.md)
|
|
27
|
+
- [Factory](../helpers/factory.md)
|
|
28
|
+
- [html](../helpers/html.md)
|
|
29
|
+
- [JWT](../helpers/jwt.md)
|
|
30
|
+
- [SSG](../helpers/ssg.md)
|
|
31
|
+
- [Streaming](../helpers/streaming.md)
|
|
32
|
+
- [Testing](../helpers/testing.md)
|
|
33
|
+
- [WebSocket](../helpers/websocket.md)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Client Components
|
|
2
|
+
|
|
3
|
+
> **Not yet available.** The client-side JSX package (`acrylblends/jsx/dom`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
`acrylblends/jsx` supports not only server side but also client side. This means that it is possible to create an interactive UI that runs in the browser. We call it Client Components or `acrylblends/jsx/dom`.
|
|
7
|
+
|
|
8
|
+
It is fast and very small. The counter program in `acrylblends/jsx/dom` is only 2.8KB with Brotli compression, but 47.8KB for React.
|
|
9
|
+
|
|
10
|
+
This section introduces Client Components-specific features.
|
|
11
|
+
|
|
12
|
+
## Counter example
|
|
13
|
+
|
|
14
|
+
Here is an example of a simple counter, the same code works as in React.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { useState } from 'acrylblends/jsx'
|
|
18
|
+
import { render } from 'acrylblends/jsx/dom'
|
|
19
|
+
|
|
20
|
+
function Counter() {
|
|
21
|
+
const [count, setCount] = useState(0)
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<p>Count: {count}</p>
|
|
25
|
+
<button onClick={() => setCount(count + 1)}>Increment</button>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function App() {
|
|
31
|
+
return (
|
|
32
|
+
<html>
|
|
33
|
+
<body>
|
|
34
|
+
<Counter />
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const root = document.getElementById('root')
|
|
41
|
+
render(<App />, root)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## `render()`
|
|
45
|
+
|
|
46
|
+
You can use `render()` to insert JSX components within a specified HTML element.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
render(<Component />, container)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
You can see full example code here: Counter example.
|
|
53
|
+
|
|
54
|
+
## Hooks compatible with React
|
|
55
|
+
|
|
56
|
+
acrylblends/jsx/dom has Hooks that are compatible or partially compatible with React. You can learn about these APIs by looking at [the React documentation](https://react.dev/reference/react/hooks).
|
|
57
|
+
|
|
58
|
+
- `useState()`
|
|
59
|
+
- `useEffect()`
|
|
60
|
+
- `useRef()`
|
|
61
|
+
- `useCallback()`
|
|
62
|
+
- `use()`
|
|
63
|
+
- `startTransition()`
|
|
64
|
+
- `useTransition()`
|
|
65
|
+
- `useDeferredValue()`
|
|
66
|
+
- `useMemo()`
|
|
67
|
+
- `useLayoutEffect()`
|
|
68
|
+
- `useReducer()`
|
|
69
|
+
- `useDebugValue()`
|
|
70
|
+
- `createElement()`
|
|
71
|
+
- `memo()`
|
|
72
|
+
- `isValidElement()`
|
|
73
|
+
- `useId()`
|
|
74
|
+
- `createRef()`
|
|
75
|
+
- `forwardRef()`
|
|
76
|
+
- `useImperativeHandle()`
|
|
77
|
+
- `useSyncExternalStore()`
|
|
78
|
+
- `useInsertionEffect()`
|
|
79
|
+
- `useFormStatus()`
|
|
80
|
+
- `useActionState()`
|
|
81
|
+
- `useOptimistic()`
|
|
82
|
+
|
|
83
|
+
## `startViewTransition()` family
|
|
84
|
+
|
|
85
|
+
The `startViewTransition()` family contains original hooks and functions to handle [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) easily. The examples below demonstrate how to use them.
|
|
86
|
+
|
|
87
|
+
### 1. A very simple example
|
|
88
|
+
|
|
89
|
+
You can write a transition using the `document.startViewTransition` shortly with the `startViewTransition()`.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useState, startViewTransition } from 'acrylblends/jsx'
|
|
93
|
+
import { css, Style } from 'acrylblends/css'
|
|
94
|
+
|
|
95
|
+
export default function App() {
|
|
96
|
+
const [showLargeImage, setShowLargeImage] = useState(false)
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<Style />
|
|
100
|
+
<button
|
|
101
|
+
onClick={() =>
|
|
102
|
+
startViewTransition(() =>
|
|
103
|
+
setShowLargeImage((state) => !state)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
Click!
|
|
108
|
+
</button>
|
|
109
|
+
<div>
|
|
110
|
+
{!showLargeImage ? (
|
|
111
|
+
<img src=' />
|
|
112
|
+
) : (
|
|
113
|
+
<div
|
|
114
|
+
class={css`
|
|
115
|
+
background: url(');
|
|
116
|
+
background-size: contain;
|
|
117
|
+
background-repeat: no-repeat;
|
|
118
|
+
background-position: center;
|
|
119
|
+
width: 600px;
|
|
120
|
+
height: 600px;
|
|
121
|
+
`}
|
|
122
|
+
></div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 2. Using `viewTransition()` with `keyframes()`
|
|
131
|
+
|
|
132
|
+
The `viewTransition()` function allows you to get the unique `view-transition-name`.
|
|
133
|
+
|
|
134
|
+
You can use it with the `keyframes()`, The `::view-transition-old()` is converted to `::view-transition-old(${uniqueName))`.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { useState, startViewTransition } from 'acrylblends/jsx'
|
|
138
|
+
import { viewTransition } from 'acrylblends/jsx/dom/css'
|
|
139
|
+
import { css, keyframes, Style } from 'acrylblends/css'
|
|
140
|
+
|
|
141
|
+
const rotate = keyframes`
|
|
142
|
+
from {
|
|
143
|
+
rotate: 0deg;
|
|
144
|
+
}
|
|
145
|
+
to {
|
|
146
|
+
rotate: 360deg;
|
|
147
|
+
}
|
|
148
|
+
`
|
|
149
|
+
|
|
150
|
+
export default function App() {
|
|
151
|
+
const [showLargeImage, setShowLargeImage] = useState(false)
|
|
152
|
+
const [transitionNameClass] = useState(() =>
|
|
153
|
+
viewTransition(css`
|
|
154
|
+
::view-transition-old() {
|
|
155
|
+
animation-name: ${rotate};
|
|
156
|
+
}
|
|
157
|
+
::view-transition-new() {
|
|
158
|
+
animation-name: ${rotate};
|
|
159
|
+
}
|
|
160
|
+
`)
|
|
161
|
+
)
|
|
162
|
+
return (
|
|
163
|
+
<>
|
|
164
|
+
<Style />
|
|
165
|
+
<button
|
|
166
|
+
onClick={() =>
|
|
167
|
+
startViewTransition(() =>
|
|
168
|
+
setShowLargeImage((state) => !state)
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
Click!
|
|
173
|
+
</button>
|
|
174
|
+
<div>
|
|
175
|
+
{!showLargeImage ? (
|
|
176
|
+
<img src=' />
|
|
177
|
+
) : (
|
|
178
|
+
<div
|
|
179
|
+
class={css`
|
|
180
|
+
${transitionNameClass}
|
|
181
|
+
background: url(');
|
|
182
|
+
background-size: contain;
|
|
183
|
+
background-repeat: no-repeat;
|
|
184
|
+
background-position: center;
|
|
185
|
+
width: 600px;
|
|
186
|
+
height: 600px;
|
|
187
|
+
`}
|
|
188
|
+
></div>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 3. Using `useViewTransition`
|
|
197
|
+
|
|
198
|
+
If you want to change the style only during the animation. You can use `useViewTransition()`. This hook returns the `[boolean, (callback: () => void) => void]`, and they are the `isUpdating` flag and the `startViewTransition()` function.
|
|
199
|
+
|
|
200
|
+
When this hook is used, the Component is evaluated at the following two times.
|
|
201
|
+
|
|
202
|
+
- Inside the callback of a call to `startViewTransition()`.
|
|
203
|
+
- When [the `finish` promise becomes fulfilled](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/finished)
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import { useState, useViewTransition } from 'acrylblends/jsx'
|
|
207
|
+
import { viewTransition } from 'acrylblends/jsx/dom/css'
|
|
208
|
+
import { css, keyframes, Style } from 'acrylblends/css'
|
|
209
|
+
|
|
210
|
+
const rotate = keyframes`
|
|
211
|
+
from {
|
|
212
|
+
rotate: 0deg;
|
|
213
|
+
}
|
|
214
|
+
to {
|
|
215
|
+
rotate: 360deg;
|
|
216
|
+
}
|
|
217
|
+
`
|
|
218
|
+
|
|
219
|
+
export default function App() {
|
|
220
|
+
const [isUpdating, startViewTransition] = useViewTransition()
|
|
221
|
+
const [showLargeImage, setShowLargeImage] = useState(false)
|
|
222
|
+
const [transitionNameClass] = useState(() =>
|
|
223
|
+
viewTransition(css`
|
|
224
|
+
::view-transition-old() {
|
|
225
|
+
animation-name: ${rotate};
|
|
226
|
+
}
|
|
227
|
+
::view-transition-new() {
|
|
228
|
+
animation-name: ${rotate};
|
|
229
|
+
}
|
|
230
|
+
`)
|
|
231
|
+
)
|
|
232
|
+
return (
|
|
233
|
+
<>
|
|
234
|
+
<Style />
|
|
235
|
+
<button
|
|
236
|
+
onClick={() =>
|
|
237
|
+
startViewTransition(() =>
|
|
238
|
+
setShowLargeImage((state) => !state)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
Click!
|
|
243
|
+
</button>
|
|
244
|
+
<div>
|
|
245
|
+
{!showLargeImage ? (
|
|
246
|
+
<img src=' />
|
|
247
|
+
) : (
|
|
248
|
+
<div
|
|
249
|
+
class={css`
|
|
250
|
+
${transitionNameClass}
|
|
251
|
+
background: url(');
|
|
252
|
+
background-size: contain;
|
|
253
|
+
background-repeat: no-repeat;
|
|
254
|
+
background-position: center;
|
|
255
|
+
width: 600px;
|
|
256
|
+
height: 600px;
|
|
257
|
+
position: relative;
|
|
258
|
+
${isUpdating &&
|
|
259
|
+
css`
|
|
260
|
+
&:before {
|
|
261
|
+
content: 'Loading...';
|
|
262
|
+
position: absolute;
|
|
263
|
+
top: 50%;
|
|
264
|
+
left: 50%;
|
|
265
|
+
}
|
|
266
|
+
`}
|
|
267
|
+
`}
|
|
268
|
+
></div>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
</>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## The `acrylblends/jsx/dom` runtime
|
|
277
|
+
|
|
278
|
+
There is a small JSX Runtime for Client Components. Using this will result in smaller bundled results than using `acrylblends/jsx`. Specify `acrylblends/jsx/dom` in `tsconfig.json`. For Deno, modify the deno.json.
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"compilerOptions": {
|
|
283
|
+
"jsx": "react-jsx",
|
|
284
|
+
"jsxImportSource": "acrylblends/jsx/dom"
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Alternatively, you can specify `acrylblends/jsx/dom` in the esbuild transform options in `vite.config.ts`.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { defineConfig } from 'vite'
|
|
293
|
+
|
|
294
|
+
export default defineConfig({
|
|
295
|
+
esbuild: {
|
|
296
|
+
jsxImportSource: 'acrylblends/jsx/dom',
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
```
|