acrylblends 1.0.0-rc.1 → 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/README.md CHANGED
@@ -4,13 +4,14 @@
4
4
 
5
5
  <h1 align="center">acrylblends</h1>
6
6
 
7
- <p align="center"><strong>Acryl, a small web framework on Web Standards, for Node.js.</strong><br>
8
- One <code>Request</code> in, one <code>Response</code> out: routing, middleware, validation, a typed client and a Node.js server, with no dependencies.</p>
7
+ <p align="center"><strong>Acryl: a small web framework on Web Standards, and whole apps built from Cordis plugins.</strong><br>
8
+ One <code>Request</code> in, one <code>Response</code> out - routing, middleware, validation, a typed client and a Node.js server, with no
9
+ dependencies - and a composition of plugins that runs in a browser or a desktop window.</p>
9
10
 
10
11
  <p align="center">
11
- <a href="https://github.com/acryldev/acryl/blob/main/docs/framework/README.md">Documentation</a> ·
12
- <a href="https://github.com/acryldev/acryl/blob/main/docs/framework/getting-started/basic.md">Getting started</a> ·
13
- <a href="https://github.com/acryldev/acryl/blob/main/docs/framework/api/index.md">API</a> ·
12
+ <a href="https://github.com/acrylblends/acrylblends/blob/main/docs/framework/README.md">Documentation</a> ·
13
+ <a href="https://github.com/acrylblends/acrylblends/blob/main/docs/framework/getting-started/basic.md">Getting started</a> ·
14
+ <a href="https://github.com/acrylblends/acrylblends/blob/main/docs/framework/api/index.md">API</a> ·
14
15
  <a href="https://acrylblends.github.io">acrylblends.github.io</a> ·
15
16
  <a href="https://acryl.dev">acryl.dev</a> ·
16
17
  <a href="https://github.com/acrylblends/acrylblends">GitHub</a>
@@ -30,8 +31,12 @@ export default app
30
31
  ## Contents
31
32
 
32
33
  - [Requirements](#requirements)
33
- - [Quick start](#quick-start)
34
- - [Start from a template](#start-from-a-template)
34
+ - [Start an app in five minutes](#start-an-app-in-five-minutes)
35
+ - [Run it in a browser or in a desktop window](#run-it-in-a-browser-or-in-a-desktop-window)
36
+ - [Start from the registry](#start-from-the-registry)
37
+ - [Publish your app, or your own starter](#publish-your-app-or-your-own-starter)
38
+ - [Keep the framework current](#keep-the-framework-current)
39
+ - [Use it as a library](#use-it-as-a-library)
35
40
  - [A first API](#a-first-api)
36
41
  - [Middleware](#middleware)
37
42
  - [Validation](#validation)
@@ -39,7 +44,7 @@ export default app
39
44
  - [Testing](#testing)
40
45
  - [Serving on Node.js](#serving-on-nodejs)
41
46
  - [What is in the package](#what-is-in-the-package)
42
- - [Upgrading from 0.4](#upgrading-from-04)
47
+ - [Upgrading from an older version](#upgrading-from-an-older-version)
43
48
  - [The acrylblends command](#the-acrylblends-command)
44
49
  - [Links](#links)
45
50
  - [License](#license)
@@ -50,9 +55,93 @@ export default app
50
55
  - **Bun** runs the same app (`export default app`) and is experimental.
51
56
  - `acrylblends` has no dependencies. Its types need only Node's types (`@types/node`), not the DOM library.
52
57
 
53
- ## Quick start
58
+ ## Start an app in five minutes
54
59
 
55
- Create a folder and install the package:
60
+ ```sh
61
+ npm create acrylblends@latest my-app
62
+ cd my-app
63
+ npm install
64
+ npm run dev
65
+ ```
66
+
67
+ The starter asks which template you want; naming one skips the question (`npm create acrylblends@latest my-app notes`).
68
+
69
+ | Template | What you get |
70
+ | --- | --- |
71
+ | `blank` | the router, the Node server and one route: the smallest app |
72
+ | `nodejs` | a page rendered with JSX and a client component, without a bundler |
73
+ | `notes` | a multi-user notes app on the `db` port: sign-up, sessions, migrations, search, a typed client |
74
+ | `chat` | an answer streamed over server-sent events from an OpenAI-compatible API, history in SQLite |
75
+ | `todo` | a to-do list on the router and live state |
76
+ | `agent` | the blank app with an agent that writes and installs plugins into the running app |
77
+
78
+ Every template is a Cordis app: `blend.yaml` lists its plugins, `plugins/` holds them as code you own, and the starter also writes
79
+ `docs/framework` (the pages this README links to), `AGENTS.md`, `CLAUDE.md`, one skill for a coding agent, and its own tests. `npm run dev`
80
+ serves it and prints the URL, `npm test` runs the tests, `npm run check` type-checks.
81
+
82
+ Adding a capability is one command: `npx acrylblends add <plugin>` copies a library plugin and what it needs into `plugins/`, prints the row to
83
+ add to `blend.yaml`, and records what it copied so `acrylblends upgrade` can move it forward later. `npx acrylblends plugins` lists the library.
84
+
85
+ ## Run it in a browser or in a desktop window
86
+
87
+ The same app runs both ways; the routes, the plugins and the data are the ones you already wrote.
88
+
89
+ - **Browser**: what you get. `npm run dev` (or `npm start`) serves it on a loopback port and prints the URL.
90
+ - **Desktop**: add the shell and run it as a window on the app's own server.
91
+
92
+ ```sh
93
+ npx acrylblends init my-app --desktop # create an app with the shell (--template picks the template)
94
+ # or, in an app you already have:
95
+ npx acrylblends add desktop
96
+ npm install # the shell needs electron
97
+ npm run desktop # an Electron window on the app's own server
98
+ ```
99
+
100
+ Every Electron call lives in `plugins/desktop`, so the app's own code never imports it. See
101
+ [Desktop](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/getting-started/desktop.md).
102
+
103
+ ## Start from the registry
104
+
105
+ The registry is a set of starting points people published, browsable at [acrylblends.github.io/blends](https://acrylblends.github.io/blends)
106
+ and readable by any tool as plain JSON at <https://acrylblends.github.io/registry/index.json>. ACRYL's CLI pulls one by id, or from any git
107
+ repository:
108
+
109
+ ```sh
110
+ npm install -g acryl
111
+ acryl new my-app --from acryl.gtd # a starter from the registry
112
+ acryl new my-app --from github:you/starter # a repository with an acryl.template.json
113
+ ```
114
+
115
+ Without `--from`, `acryl new` writes the blank app; `--blueprint` picks another built-in Blueprint. A team's own registry is the same layout in
116
+ its own repository: `--registry <git URL or folder>` reads starters from there instead of the public one.
117
+
118
+ ## Publish your app, or your own starter
119
+
120
+ **Your app** belongs to you and lives in your repository: `npm run save` commits it and pushes to its remote. It refuses a file that looks like
121
+ a secret, and refuses to push a private app to a public remote.
122
+
123
+ Before an app goes on a server: with `NODE_ENV=production` the door, the agent panel and the PTY driver start nothing, but the `agent` template
124
+ still mounts its own agent, builder and extensions rows. Start from one of the other five templates, or remove those rows from its `blend.yaml`
125
+ - see [What runs in production](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/guides/production.md).
126
+
127
+ **A starter** other people can pull is a pull request to the registry repository: add `registry/blends/<id>/` with a `blend.yaml` (`kind: Blueprint`,
128
+ an id you own, the license, `visibility`, a category and a one-line description), the plugins it adds under `plugins/`, and a `README.md`. CI
129
+ validates every entry and fails when `index.json` is stale, so what the site lists is what passed. The layout, and how to keep a starter private
130
+ without a registry at all, is in
131
+ [the registry README](https://github.com/acrylblends/acrylblends.github.io/tree/main/registry).
132
+
133
+ ## Keep the framework current
134
+
135
+ An app carries its framework plugins as source, so a new release moves them in place and leaves your own code alone:
136
+
137
+ ```sh
138
+ npx acrylblends upgrade --dry-run # the plan: what would be replaced
139
+ npx acrylblends upgrade # an edited file stops it with the diff, unless --force
140
+ ```
141
+
142
+ ## Use it as a library
143
+
144
+ An Acryl app is also a plain function from `Request` to `Response`, which needs no scaffold. Create a folder and install the package:
56
145
 
57
146
  ```sh
58
147
  mkdir my-app && cd my-app
@@ -90,21 +179,9 @@ Run it and open http://localhost:3000:
90
179
  node server.ts
91
180
  ```
92
181
 
93
- For Bun, `bun run index.ts` serves the same `export default app`.
94
-
95
- ## Start from a template
96
-
97
- `create-acrylblends` creates a ready project with `dev`, `test` and `start` scripts:
98
-
99
- ```sh
100
- npm create acrylblends@latest my-app
101
- cd my-app
102
- npm install
103
- npm run dev
104
- ```
105
-
106
- The templates are the same through `npm create acrylblends` and `acrylblends init`: `blank`, `nodejs`, `notes`, `chat`, `todo` and `agent` (a Cordis app with an agent that can extend it while it runs; `acryl-app` is its old name). See the
107
- [create-acrylblends guide](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/create-acrylblends.md).
182
+ For Bun, `bun run index.ts` serves the same `export default app`. The same app mounts inside a Cordis plugin with
183
+ `ctx.router.route(prefix, app)`, which is how a generated app is put together: see
184
+ [Acryl in a Cordis app](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/cordis/acryl-in-a-cordis-app.md).
108
185
 
109
186
  ## A first API
110
187
 
@@ -138,10 +215,10 @@ app.onError((error, c) => error instanceof HTTPException ? error.getResponse() :
138
215
  export default app
139
216
  ```
140
217
 
141
- Read more: [Routing](https://github.com/acryldev/acryl/blob/main/docs/framework/api/routing.md),
142
- [Context](https://github.com/acryldev/acryl/blob/main/docs/framework/api/context.md),
143
- [Request](https://github.com/acryldev/acryl/blob/main/docs/framework/api/request.md),
144
- [Exceptions](https://github.com/acryldev/acryl/blob/main/docs/framework/api/exception.md).
218
+ Read more: [Routing](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/api/routing.md),
219
+ [Context](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/api/context.md),
220
+ [Request](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/api/request.md),
221
+ [Exceptions](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/api/exception.md).
145
222
 
146
223
  ## Middleware
147
224
 
@@ -165,8 +242,8 @@ app.use('/admin/*', bearerAuth({ token: process.env.ADMIN_TOKEN ?? '' }))
165
242
  Built in: `basic-auth`, `bearer-auth`, `body-limit`, `combine`, `compress`, `context-storage`, `cors`, `csrf`, `etag`, `ip-restriction`,
166
243
  `jwk`, `jwt`, `language`, `logger`, `method-not-allowed`, `method-override`, `powered-by`, `pretty-json`, `request-id`, `secure-headers`,
167
244
  `serve-static`, `timeout`, `timing`, `trailing-slash`, plus ACRYL's `loopback-only`. Each one is documented under
168
- [Middleware](https://github.com/acryldev/acryl/tree/main/docs/framework/middleware/builtin). To write your own, see the
169
- [middleware guide](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/middleware.md).
245
+ [Middleware](https://github.com/acrylblends/acrylblends/tree/main/docs/framework/middleware/builtin). To write your own, see the
246
+ [middleware guide](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/guides/middleware.md).
170
247
 
171
248
  ## Validation
172
249
 
@@ -191,7 +268,7 @@ app.post(
191
268
  ```
192
269
 
193
270
  `acrylblends/schema-validator` drives the same validator with any [Standard Schema](https://standardschema.dev) library (Zod, Valibot,
194
- ArkType and others). See the [validation guide](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/validation.md).
271
+ ArkType and others). See the [validation guide](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/guides/validation.md).
195
272
 
196
273
  ## Typed client
197
274
 
@@ -216,7 +293,7 @@ const answer = await client.api.hello.$get()
216
293
  console.log((await answer.json()).message)
217
294
  ```
218
295
 
219
- See [RPC](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/rpc.md).
296
+ See [RPC](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/guides/rpc.md).
220
297
 
221
298
  ## Testing
222
299
 
@@ -235,7 +312,7 @@ test('GET / says hello', async () => {
235
312
  ```
236
313
 
237
314
  Run it with `node --test`. `acrylblends/testing` adds `testClient`, the typed client over `app.request()`. See the
238
- [testing guide](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/testing.md).
315
+ [testing guide](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/guides/testing.md).
239
316
 
240
317
  ## Serving on Node.js
241
318
 
@@ -256,7 +333,7 @@ const server = serve({ fetch: app.fetch, port: 3000, hostname: '127.0.0.1' })
256
333
  process.on('SIGTERM', () => { server.close() })
257
334
  ```
258
335
 
259
- See [Node.js](https://github.com/acryldev/acryl/blob/main/docs/framework/getting-started/nodejs.md).
336
+ See [Node.js](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/getting-started/nodejs.md).
260
337
 
261
338
  ## What is in the package
262
339
 
@@ -266,20 +343,20 @@ See [Node.js](https://github.com/acryldev/acryl/blob/main/docs/framework/getting
266
343
  | `acrylblends/<middleware>` | each built-in middleware, for example `acrylblends/cors` |
267
344
  | `acrylblends/validator`, `acrylblends/schema-validator` | request validation |
268
345
  | `acrylblends/client`, `acrylblends/testing` | the typed client and its in-process test version |
269
- | `acrylblends/accepts`, `adapter`, `cookie`, `factory`, `html`, `route`, `streaming`, `ws`, `conninfo` | helpers ([helpers docs](https://github.com/acryldev/acryl/tree/main/docs/framework/helpers)) |
346
+ | `acrylblends/accepts`, `adapter`, `cookie`, `factory`, `html`, `route`, `streaming`, `ws`, `conninfo` | helpers ([helpers docs](https://github.com/acrylblends/acrylblends/tree/main/docs/framework/helpers)) |
270
347
  | `acrylblends/http-exception` | `HTTPException` |
271
- | `acrylblends/router/*` | the routers: `reg-exp-router`, `smart-router`, `trie-router`, `pattern-router`, `linear-router` ([routers](https://github.com/acryldev/acryl/blob/main/docs/framework/concepts/routers.md)) |
272
- | `acrylblends/tiny`, `acrylblends/quick` | the presets: `Acryl` with another router (`PatternRouter`; `LinearRouter` and `TrieRouter`) ([presets](https://github.com/acryldev/acryl/blob/main/docs/framework/api/presets.md)) |
348
+ | `acrylblends/router/*` | the routers: `reg-exp-router`, `smart-router`, `trie-router`, `pattern-router`, `linear-router` ([routers](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/concepts/routers.md)) |
349
+ | `acrylblends/tiny`, `acrylblends/quick` | the presets: `Acryl` with another router (`PatternRouter`; `LinearRouter` and `TrieRouter`) ([presets](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/api/presets.md)) |
273
350
  | `acrylblends/cache`, `acrylblends/mount` | the cache middleware and mounting another framework's fetch handler |
274
- | `acrylblends/ssg` | static site generation: `toSSG` writes every route as a file ([SSG helper](https://github.com/acryldev/acryl/blob/main/docs/framework/helpers/ssg.md)) |
351
+ | `acrylblends/ssg` | static site generation: `toSSG` writes every route as a file ([SSG helper](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/helpers/ssg.md)) |
275
352
  | `acrylblends/proxy`, `acrylblends/dev` | the proxy helper and the dev helpers (`showRoutes`, `inspectRoutes`, `getRouterName`) |
276
- | `acrylblends/jsx` | JSX on the server: `jsxImportSource: 'acrylblends/jsx'`, `.tsx` files run on Node with `node --import acrylblends/node-server/register`, with `jsx/streaming`, and `acrylblends/jsx-renderer` for a layout behind `c.render` ([JSX guide](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/jsx.md)) |
277
- | `acrylblends/jsx/dom` | JSX in the browser: hooks and `render` (needs the DOM library) ([client components](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/jsx-dom.md)); on Node, `clientModules` serves them to the browser without a bundler ([guide](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/client-modules.md)) |
353
+ | `acrylblends/jsx` | JSX on the server: `jsxImportSource: 'acrylblends/jsx'`, `.tsx` files run on Node with `node --import acrylblends/node-server/register`, with `jsx/streaming`, and `acrylblends/jsx-renderer` for a layout behind `c.render` ([JSX guide](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/guides/jsx.md)) |
354
+ | `acrylblends/jsx/dom` | JSX in the browser: hooks and `render` (needs the DOM library) ([client components](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/guides/jsx-dom.md)); on Node, `clientModules` serves them to the browser without a bundler ([guide](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/guides/client-modules.md)) |
278
355
  | `acrylblends/drivers` | the driver contract of the agent panel (`ChatDriver`, `TerminalDriver`, `Drivers`), for plugins that add an agent to the panel |
279
- | `acrylblends/css` | CSS in JSX: `css`, `cx`, `keyframes`, `Style` ([CSS helper](https://github.com/acryldev/acryl/blob/main/docs/framework/helpers/css.md)) |
356
+ | `acrylblends/css` | CSS in JSX: `css`, `cx`, `keyframes`, `Style` ([CSS helper](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/helpers/css.md)) |
280
357
  | `acrylblends/node-server` | `serve`, `createAdaptorServer`, `getRequestListener`; with `node-server/serve-static`, `node-server/conninfo`, and `node-server/register` (compiles `.tsx` with the app's `typescript`); `clientModules` and `buildClientModules` (client components without a bundler) |
281
358
 
282
- Every subpath in the table is listed in the package's `exports` and is covered by semver from 1.0, except the ones marked experimental: `acrylblends/bun` and the door (`door/*`, `cordis/door`), which are development tools. The ported core's utilities (`utils/*`) and the base-class chunk are internal and not exported. Node.js is the runtime the package is verified on; the upstream's adapters for AWS Lambda, Lambda@Edge, Cloudflare, Vercel, Netlify and Service Workers are not part of the package ([Other runtimes](https://github.com/acryldev/acryl/blob/main/docs/framework/getting-started/other-runtimes.md)).
359
+ Every subpath in the table is listed in the package's `exports` and is covered by semver from 1.0, except the ones marked experimental: `acrylblends/bun` and the door (`door/*`, `cordis/door`), which are development tools. The ported core's utilities (`utils/*`) and the base-class chunk are internal and not exported. Node.js is the runtime the package is verified on; the upstream's adapters for AWS Lambda, Lambda@Edge, Cloudflare, Vercel, Netlify and Service Workers are not part of the package ([Other runtimes](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/getting-started/other-runtimes.md)).
283
360
 
284
361
  To extend the context's typed variables, augment the root module:
285
362
 
@@ -291,11 +368,14 @@ declare module 'acrylblends' {
291
368
  }
292
369
  ```
293
370
 
294
- ## Upgrading from 0.4
371
+ ## Upgrading from an older version
372
+
373
+ An app made with any 0.9.1 or later release moves with `acrylblends upgrade` (above); its edited files are never replaced silently.
295
374
 
296
- From 0.5 the package's root is the framework. `App` and `AppRequest`, the 0.4 names, were kept as deprecated aliases of `Acryl` and
297
- `AcrylRequest` and are removed in 1.0. The scaffolding API that was the root in 0.4 is now `acrylblends/cli`. See
298
- [Migration from 0.4](https://github.com/acryldev/acryl/blob/main/docs/framework/cordis/migration-from-0-4.md).
375
+ Code that imports the old names moves by hand: `App` and `AppRequest`, the 0.4 aliases of `Acryl` and `AcrylRequest`, are removed in 1.0, and
376
+ the scaffolding API that was the package root in 0.4 is now `acrylblends/cli`. See
377
+ [Migration to 1.0](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/cordis/migration-to-1-0.md) and
378
+ [Migration from 0.4](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/cordis/migration-from-0-4.md).
299
379
 
300
380
  ## The acrylblends command
301
381
 
@@ -309,18 +389,18 @@ cd my-app && npm install && npm run dev
309
389
  ```
310
390
 
311
391
  See the [ACRYLBLENDS documentation](https://github.com/acrylblends/acrylblends/blob/main/docs/README.md) and
312
- [Acryl in a Cordis app](https://github.com/acryldev/acryl/blob/main/docs/framework/cordis/acryl-in-a-cordis-app.md).
392
+ [Acryl in a Cordis app](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/cordis/acryl-in-a-cordis-app.md).
313
393
 
314
394
  ## Links
315
395
 
316
396
  | | |
317
397
  | --- | --- |
318
- | Framework documentation | https://github.com/acryldev/acryl/blob/main/docs/framework/README.md |
398
+ | Framework documentation | https://github.com/acrylblends/acrylblends/blob/main/docs/framework/README.md |
319
399
  | ACRYLBLENDS website | https://acrylblends.github.io |
320
400
  | ACRYL website | https://acryl.dev |
321
401
  | This package's source and issues | https://github.com/acrylblends/acrylblends ([issues](https://github.com/acrylblends/acrylblends/issues)) |
322
402
  | The framework's source | https://github.com/acryldev/acryl (`plugins/acryl-router`, `plugins/acryl-node-server`) |
323
- | For coding agents | https://github.com/acryldev/acryl/blob/main/docs/framework/agents/index.md |
403
+ | For coding agents | https://github.com/acrylblends/acrylblends/blob/main/docs/framework/agents/index.md |
324
404
  | npm | https://www.npmjs.com/package/acrylblends |
325
405
 
326
406
  ## License
@@ -64,25 +64,13 @@ app.onError((err, c) => {
64
64
 
65
65
  ## fire()
66
66
 
67
- > **Warning.** **`app.fire()` is deprecated**, and the Service Worker adapter (`acrylblends/service-worker`) is not part of the package in 1.0. Use `app.fetch` in a `fetch` event handler of your own.
68
-
69
- `app.fire()` automatically adds a global `fetch` event listener.
70
-
71
- This can be useful for environments that adhere to the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), such as [non-ES module Cloudflare Workers](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/).
72
-
73
- `app.fire()` executes the following for you:
74
-
75
- ```ts
76
- addEventListener('fetch', (event: FetchEventLike): void => {
77
- event.respondWith(this.dispatch(...))
78
- })
79
- ```
67
+ > **Warning.** **`app.fire()` is deprecated**, and the Service Worker adapter is not part of the package in 1.0. It adds a global `fetch` event listener, which only a Service Worker-style host reads; use `app.fetch` in an entry point of your own instead.
80
68
 
81
69
  ## fetch()
82
70
 
83
71
  `app.fetch` will be the entry point of your application.
84
72
 
85
- For Cloudflare Workers, you can use the following:
73
+ As the `fetch` method of an object a host calls:
86
74
 
87
75
  ```ts twoslash
88
76
  import { Acryl } from 'acrylblends'
@@ -230,7 +218,7 @@ const app = new Acryl({ router: new RegExpRouter() })
230
218
 
231
219
  ## Generics
232
220
 
233
- You can pass Generics to specify the types of Cloudflare Workers Bindings and variables used in `c.set`/`c.get`.
221
+ You can pass Generics to specify the type of the environment the host passes and the variables used in `c.set`/`c.get`.
234
222
 
235
223
  ```ts twoslash
236
224
  import { Acryl } from 'acrylblends'
@@ -362,81 +362,32 @@ app.get('/pages/my-hobbies', (c) => {
362
362
 
363
363
  ## executionCtx
364
364
 
365
- You can access Cloudflare Workers' specific [ExecutionContext](https://developers.cloudflare.com/workers/runtime-apis/context/).
365
+ A host that embeds the app can pass an execution context: `waitUntil(promise)` keeps work alive after the response has been sent, and `passThroughOnException()` lets the host take over when the handler throws. The Node server passes none, so read it only in an app whose host provides one.
366
366
 
367
367
  ```ts twoslash
368
368
  import { Acryl } from 'acrylblends'
369
- const app = new Acryl<{
370
- Bindings: {
371
- KV: any
372
- }
373
- }>()
374
- declare const key: string
375
- declare const data: string
376
- // ---cut---
377
- // ExecutionContext object
378
- app.get('/foo', async (c) => {
379
- c.executionCtx.waitUntil(c.env.KV.put(key, data))
380
- // ...
381
- })
382
- ```
383
-
384
- The `ExecutionContext` also has an [`exports`](https://developers.cloudflare.com/workers/runtime-apis/context/#exports) field. To get autocomplete with Wrangler's generated types, you can use module augmentation:
385
-
386
- ```ts
387
- import 'acrylblends'
388
-
389
- declare module 'acrylblends' {
390
- interface ExecutionContext {
391
- readonly exports: Cloudflare.Exports
392
- }
393
- }
394
- ```
395
-
396
- ## event
397
-
398
- You can access Cloudflare Workers' specific `FetchEvent`. This was used in "Service Worker" syntax. But, it is not recommended now.
399
-
400
- ```ts twoslash
401
- import { Acryl } from 'acrylblends'
402
- declare const key: string
403
- declare const data: string
404
- type KVNamespace = any
369
+ const app = new Acryl()
370
+ declare const sendReceipt: Promise<void>
405
371
  // ---cut---
406
- // Type definition to make type inference
407
- type Bindings = {
408
- MY_KV: KVNamespace
409
- }
410
-
411
- const app = new Acryl<{ Bindings: Bindings }>()
412
-
413
- // FetchEvent object (only set when using Service Worker syntax)
414
- app.get('/foo', async (c) => {
415
- c.event.waitUntil(c.env.MY_KV.put(key, data))
416
- // ...
372
+ // Work that must finish after the response, when the host provides a context
373
+ app.get('/orders/:id', async (c) => {
374
+ c.executionCtx.waitUntil(sendReceipt)
375
+ return c.json({ ok: true })
417
376
  })
418
377
  ```
419
378
 
420
379
  ## env
421
380
 
422
- In Cloudflare Workers Environment variables, secrets, KV namespaces, D1 database, R2 bucket etc. that are bound to a worker are known as bindings.
423
- Regardless of type, bindings are always available as global variables and can be accessed via the context `c.env.BINDING_KEY`.
381
+ `c.env` is the environment the host hands the app. The Node server passes `process.env`, so `c.env.MY_KEY` is the variable `MY_KEY`. A host that passes something else gives the app a type parameter, and then `c.env` is that shape.
424
382
 
425
383
  ```ts twoslash
426
384
  import { Acryl } from 'acrylblends'
427
- type KVNamespace = any
428
385
  // ---cut---
429
- // Type definition to make type inference
430
- type Bindings = {
431
- MY_KV: KVNamespace
432
- }
386
+ const app = new Acryl<{ Bindings: { REGION: string } }>()
433
387
 
434
- const app = new Acryl<{ Bindings: Bindings }>()
435
-
436
- // Environment object for Cloudflare Workers
437
- app.get('/', async (c) => {
438
- c.env.MY_KV.get('my-key')
439
- // ...
388
+ app.get('/', (c) => {
389
+ const region = c.env.REGION
390
+ return c.text(region)
440
391
  })
441
392
  ```
442
393
 
@@ -57,6 +57,6 @@ this.router = new PatternRouter()
57
57
 
58
58
  | Preset | Suitable platforms |
59
59
  | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
- | `acrylblends` | This is highly recommended for most use cases. Although the registration phase may be slower than `acrylblends/quick`, it exhibits high performance once booted. It's ideal for long-life servers built with **Deno**, **Bun**, or **Node.js**. It is also suitable for **Fastly Compute**, as route registration occurs during the app build phase on that platform. For environments such as **Cloudflare Workers**, **Deno Deploy**, where v8 isolates are utilized, this preset is suitable as well because the isolations persist for a certain amount of time after booting. |
60
+ | `acrylblends` | This is highly recommended for most use cases. Although the registration phase may be slower than `acrylblends/quick`, it exhibits high performance once booted. It's ideal for a long-life server on **Node.js** (the verified runtime) or **Bun** (experimental). |
61
61
  | `acrylblends/quick` | This preset is designed for environments where the application is initialized for every request. |
62
62
  | `acrylblends/tiny` | This is the smallest router package and it's suitable for environments where resources are limited. |
@@ -364,9 +364,9 @@ app.get('/about/me', async (c) => {
364
364
  The raw [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object.
365
365
 
366
366
  ```ts
367
- // For Cloudflare Workers
368
- app.post('/', async (c) => {
369
- const metadata = c.req.raw.cf?.hostMetadata?
367
+ // Hand the raw Request to an API that takes one, such as a Web-standard parser
368
+ app.post('/upload', async (c) => {
369
+ const bytes = await c.req.raw.arrayBuffer()
370
370
  // ...
371
371
  })
372
372
  ```
@@ -4,8 +4,8 @@ Acryl uses only **Web Standards** like Fetch.
4
4
  They were originally used in the `fetch` function and consist of basic objects that handle HTTP requests and responses.
5
5
  In addition to `Requests` and `Responses`, there are `URL`, `URLSearchParam`, `Headers` and others.
6
6
 
7
- Cloudflare Workers, Deno, and Bun also are built upon Web Standards.
8
- For example, a server that returns "Hello World" could be written as below. This could run on Cloudflare Workers and Bun.
7
+ Bun is built on Web Standards the same way.
8
+ For example, a server that returns "Hello World" can be written as below, and it runs on Node.js and Bun.
9
9
 
10
10
  ```ts twoslash
11
11
  export default {
@@ -277,7 +277,7 @@ export default function App() {
277
277
  On Node you can skip the bundler: [Client components without a bundler](client-modules.md) serves `.tsx` client modules to the browser with
278
278
  this runtime and one script tag, in development and in production.
279
279
 
280
- 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.
280
+ 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`.
281
281
 
282
282
  ```json
283
283
  {
@@ -584,21 +584,13 @@ const route = app.put(
584
584
 
585
585
  ## Custom `fetch` method
586
586
 
587
- You can set the custom `fetch` method.
588
-
589
- In the following example script for Cloudflare Worker, the Service Bindings' `fetch` method is used instead of the default `fetch`.
590
-
591
- ```toml
592
- # wrangler.toml
593
- services = [
594
- { binding = "AUTH", service = "auth-service" },
595
- ]
596
- ```
587
+ You can set the custom `fetch` method: anything with the `fetch` signature works in place of the global one, such as a client that adds a header or a test double.
597
588
 
598
589
  ```ts
599
590
  // src/client.ts
600
591
  const client = hc<CreateProfileType>('http://localhost', {
601
- fetch: c.env.AUTH.fetch.bind(c.env.AUTH),
592
+ fetch: (input, init) =>
593
+ fetch(input, { ...init, headers: { ...init?.headers, authorization: `Bearer ${token}` } }),
602
594
  })
603
595
  ```
604
596
 
@@ -1,6 +1,6 @@
1
1
  # Adapter Helper
2
2
 
3
- The Adapter Helper provides a seamless way to interact with various platforms through a unified interface.
3
+ The Adapter Helper reads an app's environment the same way on every runtime it runs on.
4
4
 
5
5
  ## Import
6
6
 
@@ -11,50 +11,25 @@ import { env, getRuntimeKey } from 'acrylblends/adapter'
11
11
 
12
12
  ## `env()`
13
13
 
14
- The `env()` function facilitates retrieving environment variables across different runtimes, extending beyond just Cloudflare Workers' Bindings. The value that can be retrieved with `env(c)` may be different for each runtime.
14
+ The `env()` function returns the environment the host gives the app. On Node.js, the verified runtime, that is `process.env`; on Bun it is `Bun.env` (and `process.env`), and the two entries a `.env` file adds are there as well. A host that passes its own environment to `app.fetch(request, env)` gets that instead.
15
15
 
16
16
  ```ts
17
17
  import { env } from 'acrylblends/adapter'
18
18
 
19
19
  app.get('/env', (c) => {
20
- // NAME is process.env.NAME on Node.js or Bun
21
- // NAME is the value written in `wrangler.toml` on Cloudflare
20
+ // NAME is process.env.NAME on Node.js and Bun
22
21
  const { NAME } = env<{ NAME: string }>(c)
23
22
  return c.text(NAME)
24
23
  })
25
24
  ```
26
25
 
27
- Supported Runtimes, Serverless Platforms and Cloud Services:
28
-
29
- - Cloudflare Workers
30
- - `wrangler.toml`
31
- - `wrangler.jsonc`
32
- - Deno
33
- - [`Deno.env`](https://docs.deno.com/runtime/manual/basics/env_variables)
34
- - `.env` file
35
- - Bun
36
- - [`Bun.env`](https://bun.com/guides/runtime/set-env)
37
- - `process.env`
38
- - Node.js
39
- - `process.env`
40
- - Vercel
41
- - [Environment Variables on Vercel](https://vercel.com/docs/projects/environment-variables)
42
- - AWS Lambda
43
- - [Environment Variables on AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/samples-blank.html#samples-blank-architecture)
44
- - Lambda@Edge\
45
- Environment Variables on Lambda are [not supported](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html) by Lambda@Edge, you need to use [Lambda@Edge event](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html) as an alternative.
46
- - Fastly Compute\
47
- On Fastly Compute, you can use the ConfigStore to manage user-defined data.
48
- - Netlify\
49
- On Netlify, you can use the [Netlify Contexts](https://docs.netlify.com/site-deploys/overview/#deploy-contexts) to manage user-defined data.
50
-
51
26
  ### Specify the runtime
52
27
 
53
- You can specify the runtime to get environment variables by passing the runtime key as the second argument.
28
+ `env()` reads the runtime itself. Pass the runtime key as the second argument when a host embeds the app and detects it differently, or in a test that fixes the key:
54
29
 
55
30
  ```ts
56
31
  app.get('/env', (c) => {
57
- const { NAME } = env<{ NAME: string }>(c, 'workerd')
32
+ const { NAME } = env<{ NAME: string }>(c, 'node')
58
33
  return c.text(NAME)
59
34
  })
60
35
  ```
@@ -65,23 +40,18 @@ The `getRuntimeKey()` function returns the identifier of the current runtime.
65
40
 
66
41
  ```ts
67
42
  app.get('/', (c) => {
68
- if (getRuntimeKey() === 'workerd') {
69
- return c.text('You are on Cloudflare')
70
- } else if (getRuntimeKey() === 'bun') {
43
+ if (getRuntimeKey() === 'bun') {
71
44
  return c.text('You are on Bun')
72
45
  }
73
- ...
46
+ return c.text(`You are on ${getRuntimeKey()}`)
74
47
  })
75
48
  ```
76
49
 
77
- ### Available Runtimes Keys
50
+ ### Available runtime keys
78
51
 
79
- Here are the available runtimes keys, unavailable runtime key runtimes may be supported and labeled as `other`, with some being inspired by [WinterCG's Runtime Keys](https://runtime-keys.proposal.wintercg.org/):
52
+ The keys follow [WinterCG's Runtime Keys proposal](https://runtime-keys.proposal.wintercg.org/):
80
53
 
81
- - `workerd` - Cloudflare Workers
82
- - `deno`
83
- - `bun`
84
- - `node`
85
- - `edge-light` - Vercel Edge Functions
86
- - `fastly` - Fastly Compute
87
- - `other` - Other unknown runtimes keys
54
+ - `node` - Node.js, the verified runtime
55
+ - `bun` - Bun, experimental
56
+ - `deno`, `workerd`, `edge-light`, `fastly` - keys the helper still recognizes for a host that embeds an app in that runtime; 1.0 ships no adapter for them (see [Other runtimes](../getting-started/other-runtimes.md))
57
+ - `other` - a runtime the helper does not recognize
@@ -47,7 +47,7 @@ app.get('/streamText', (c) => {
47
47
 
48
48
  **Warning.**
49
49
 
50
- If you are developing an application for Cloudflare Workers, streaming may not work well on Wrangler. If so, add `Identity` for `Content-Encoding` header.
50
+ A proxy or a host that compresses the response body can hold a stream back until it ends. If a stream arrives only when it is complete, send `Content-Encoding: Identity` for that route.
51
51
 
52
52
  ```ts
53
53
  app.get('/streamText', (c) => {
@@ -1,7 +1,7 @@
1
1
  # Basic Auth Middleware
2
2
 
3
3
  This middleware can apply Basic authentication to a specified path.
4
- Implementing Basic authentication with Cloudflare Workers or other platforms is more complicated than it seems, but with this middleware, it's a breeze.
4
+ Implementing Basic authentication by hand is more complicated than it seems, but with this middleware, it's a breeze.
5
5
 
6
6
  For more information about how the Basic auth scheme works under the hood, see the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme).
7
7
 
@@ -2,11 +2,7 @@
2
2
 
3
3
  The Cache middleware uses the Web Standards' [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
4
4
 
5
- The Cache middleware currently supports Cloudflare Workers projects using custom domains and Deno projects using [Deno 1.26+](https://github.com/denoland/deno/releases/tag/v1.26.0). Also available with Deno Deploy.
6
-
7
- Cloudflare Workers respects the `Cache-Control` header and return cached responses. For details, refer to [Cache on Cloudflare Docs](https://developers.cloudflare.com/workers/runtime-apis/cache/). Deno does not respect headers, so if you need to update the cache, you will need to implement your own mechanism.
8
-
9
- See [Usage](#usage) below for instructions on each platform.
5
+ It needs a Cache API on the host: the middleware reads `globalThis.caches`. Node.js, the verified runtime, does not provide one, so on Node the middleware is not enabled and says so in one line; a host that embeds an app in a runtime with a Cache API (or provides `globalThis.caches` itself) gets it. `Cache-Control` handling and the QUERY-request cache below are implemented by the middleware, not by the platform.
10
6
 
11
7
  ## Import
12
8
 
@@ -17,28 +13,12 @@ import { cache } from 'acrylblends/cache'
17
13
 
18
14
  ## Usage
19
15
 
20
- **Cloudflare Workers**
21
-
22
- ```ts
23
- app.get(
24
- '*',
25
- cache({
26
- cacheName: 'my-app',
27
- cacheControl: 'max-age=3600',
28
- })
29
- )
30
- ```
31
-
32
- **Deno**
33
-
34
16
  ```ts
35
- // Must use `wait: true` for the Deno runtime
36
17
  app.get(
37
18
  '*',
38
19
  cache({
39
20
  cacheName: 'my-app',
40
21
  cacheControl: 'max-age=3600',
41
- wait: true,
42
22
  })
43
23
  )
44
24
  ```
@@ -59,7 +39,7 @@ The name of the cache. Can be used to store multiple caches with different ident
59
39
 
60
40
  ### *(optional)* wait: `boolean`
61
41
 
62
- A boolean indicating if Acryl should wait for the Promise of the `cache.put` function to resolve before continuing with the request. _Required to be true for the Deno environment_. The default is `false`.
42
+ A boolean indicating if Acryl should wait for the Promise of the `cache.put` function to resolve before continuing with the request. The default is `false`. A runtime whose Cache API only completes a write after the response has finished needs `true`, or the write is dropped.
63
43
 
64
44
  ### *(optional)* cacheControl: `string`
65
45
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  This middleware compresses the response body, according to `Accept-Encoding` request header.
4
4
 
5
- > **Note.** On Cloudflare Workers and Deno Deploy, the response body will be compressed automatically, so there is no need to use this middleware.
5
+ > **Note.** Node.js and Bun do not compress a response on their own, so this middleware is what compresses it. A host that already compresses at its edge does not need it.
6
6
 
7
7
  ## Import
8
8
 
@@ -2,9 +2,7 @@
2
2
 
3
3
  The Context Storage Middleware stores the Acryl `Context` in the `AsyncLocalStorage`, to make it globally accessible.
4
4
 
5
- > **Note.** This middleware uses `AsyncLocalStorage`. The runtime should support it.
6
- >
7
- > **Cloudflare Workers**: To enable `AsyncLocalStorage`, add the [`nodejs_compat` or `nodejs_als` flag](https://developers.cloudflare.com/workers/configuration/compatibility-dates/#nodejs-compatibility-flag) to your `wrangler.toml` file.
5
+ > **Note.** This middleware uses `AsyncLocalStorage`. Node.js, the verified runtime, provides it; a host that embeds an app in a runtime without it cannot use this middleware.
8
6
 
9
7
  ## Import
10
8
 
@@ -47,22 +45,20 @@ app.get('/', (c) => {
47
45
  })
48
46
  ```
49
47
 
50
- On Cloudflare Workers, you can access the bindings outside the handler.
48
+ Anything a handler puts on the context can be read further down the call stack, without passing `c` through every function:
51
49
 
52
50
  ```ts
53
- type Env = {
54
- Bindings: {
55
- KV: KVNamespace
56
- }
57
- }
58
-
59
- const app = new Acryl<Env>()
51
+ const app = new Acryl()
60
52
 
61
53
  app.use(contextStorage())
54
+ app.use(async (c, next) => {
55
+ c.set('user', 'ada')
56
+ await next()
57
+ })
62
58
 
63
- const setKV = (value: string) => {
64
- return getContext<Env>().env.KV.put('key', value)
65
- }
59
+ app.get('/', (c) => c.text(`Hello ${getUser()}`))
60
+
61
+ const getUser = () => getContext().get('user')
66
62
  ```
67
63
 
68
64
  ## tryGetContext
@@ -1,7 +1,6 @@
1
1
  # CORS Middleware
2
2
 
3
- There are many use cases of Cloudflare Workers as Web APIs and calling them from external front-end application.
4
- For them we have to implement CORS, let's do this with middleware as well.
3
+ A web API is often called from a page served by another origin. For that we have to implement CORS, and this middleware does it.
5
4
 
6
5
  ## Import
7
6
 
@@ -224,7 +224,7 @@ app.get('/page/info', (c) => {
224
224
 
225
225
  **Warning.**
226
226
 
227
- You can't use `useRequestContext()` with the Deno's `precompile` JSX option. Use the `react-jsx`:
227
+ You can't use `useRequestContext()` with a JSX option that precompiles the templates ahead of the build. Use `react-jsx`:
228
228
 
229
229
  ```json
230
230
  "compilerOptions": {
@@ -32,7 +32,7 @@ By using the Logger Middleware, you can easily monitor the flow of requests and
32
32
  You can also extend the middleware further by providing your own `PrintFunc` function for tailored logging behavior.
33
33
 
34
34
  > **Tip.**
35
- > To disable _status code coloring_, you can set a `NO_COLOR` environment variable. This is a common way to disable ANSI color escape codes in logging libraries, and is described at <https://no-color.org/>. Note that Cloudflare Workers do not have a `process.env` object, so will default to plaintext log output.
35
+ > To disable _status code coloring_, you can set a `NO_COLOR` environment variable. This is a common way to disable ANSI color escape codes in logging libraries, and is described at <https://no-color.org/>.
36
36
 
37
37
  ## PrintFunc
38
38
 
@@ -29,7 +29,7 @@ app.all('/itty-router/*', mount(ittyRouter.handle))
29
29
 
30
30
  By default, the mounted application receives a new `Request` with the matched path prefix removed from its URL. A request to `/itty-router/hello` reaches itty-router as `/hello`.
31
31
 
32
- The mounted application is called as `handler(request, c.env, c.executionCtx)`, the same arguments as a Cloudflare Workers `fetch` handler. Use `optionHandler` to pass different arguments.
32
+ The mounted application is called as `handler(request, c.env, c.executionCtx)`, the three arguments a `fetch`-style handler takes. Use `optionHandler` to pass different arguments.
33
33
 
34
34
  ## Options
35
35
 
@@ -72,23 +72,16 @@ The header name used for the request ID. The default is `X-Request-Id`.
72
72
 
73
73
  The request ID generation function. By default, it uses `crypto.randomUUID()`.
74
74
 
75
- ## Platform specific Request IDs
76
-
77
- Some platform (such as AWS Lambda) already generate their own Request IDs per request.
78
- Without any additional configuration, this middleware is unaware of these specific Request IDs
79
- and generates a new Request ID. This can lead to confusion when looking at your application logs.
80
-
81
- To unify these IDs, use the `generator` function to capture the platform specific Request ID and to use it in this middleware.
82
-
83
- ### Platform specific links
84
-
85
- - AWS Lambda
86
- - [AWS documentation: Context object](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html)
87
- - Acryl: Access AWS Lambda Object
88
- - Cloudflare
89
- - [Cloudflare Ray ID
90
- ](https://developers.cloudflare.com/fundamentals/reference/cloudflare-ray-id/)
91
- - Deno
92
- - [Request ID on the Deno Blog](https://deno.com/blog/zero-config-debugging-deno-opentelemetry#:~:text=s%20automatically%20have-,unique%20request%20IDs,-associated%20with%20them)
93
- - Fastly
94
- - [Fastly documentation: req.xid](https://www.fastly.com/documentation/reference/vcl/variables/client-request/req-xid/)
75
+ ## An ID that came from earlier in the chain
76
+
77
+ A proxy, a load balancer or the host that starts the app may already put a request id on the request.
78
+ Without configuration this middleware is unaware of it and generates a new one, which makes the two logs hard to line up.
79
+ Use the `generator` function to read the incoming one and fall back to a fresh id:
80
+
81
+ ```ts
82
+ app.use(
83
+ requestId({
84
+ generator: (c) => c.req.header('X-Request-Id') ?? crypto.randomUUID(),
85
+ })
86
+ )
87
+ ```
@@ -3,8 +3,7 @@
3
3
  The [Server-Timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing) Middleware provides
4
4
  performance metrics in the response headers.
5
5
 
6
- > **Note.** On Cloudflare Workers, the timer metrics may not be accurate,
7
- > since [timers only show the time of last I/O](https://developers.cloudflare.com/workers/learning/security-model/#step-1-disallow-timers-and-multi-threading).
6
+ > **Note.** The metrics come from timers around the handler, so they measure wall-clock time on the host the app runs on.
8
7
 
9
8
  ## Import
10
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acrylblends",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0",
4
4
  "description": "The Acryl web framework (import { Acryl } from \"acrylblends\") and the acrylblends command that scaffolds Cordis apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",