create-acrylblends 0.0.0-stage → 0.1.1
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 +61 -2
- 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 +109 -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
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-acrylblends
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create an Acryl app with one command.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm create acrylblends@latest my-app
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It asks which template you want, creates `my-app`, and prints what to run next. Other package managers work the same way: `pnpm create acrylblends@latest`, `yarn create acrylblends`, `bun create acrylblends@latest`.
|
|
10
|
+
|
|
11
|
+
## Templates
|
|
12
|
+
|
|
13
|
+
| Template | What you get |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `nodejs` | an Acryl app served with `serve()` from `acrylblends/node-server`, with a test; run with Node.js 22.19 or newer |
|
|
16
|
+
| `bun` (experimental) | the same app for Bun, with `export default app` |
|
|
17
|
+
| `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) |
|
|
18
|
+
|
|
19
|
+
Every template includes `AGENTS.md`, `CLAUDE.md` and a copy of the framework documentation in `docs/framework/`, so a coding agent working in the folder reads the API from the folder.
|
|
20
|
+
|
|
21
|
+
## Non-interactive use
|
|
22
|
+
|
|
23
|
+
Arguments for the initializer go after `--` with `npm create`:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm create acrylblends@latest my-app -- --template nodejs --install --pm npm
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Option | |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `--template <name>` | `nodejs`, `bun` or `acryl-app` |
|
|
32
|
+
| `--install` | install the dependencies after creating the app |
|
|
33
|
+
| `--pm <manager>` | `npm`, `pnpm`, `yarn` or `bun` (default: the one that ran the command) |
|
|
34
|
+
| `-y`, `--yes` | ask nothing: template `nodejs`, directory `acrylblends-app` unless given |
|
|
35
|
+
|
|
36
|
+
## Then
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
cd my-app
|
|
40
|
+
npm install # unless you passed --install
|
|
41
|
+
npm run dev # prints its URL; GET / answers "Acryl!"
|
|
42
|
+
npm test
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Acryl } from 'acrylblends'
|
|
47
|
+
|
|
48
|
+
const app = new Acryl()
|
|
49
|
+
app.get('/', (c) => c.text('Acryl!'))
|
|
50
|
+
|
|
51
|
+
export default app
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Links
|
|
55
|
+
|
|
56
|
+
- Documentation: https://acrylblends.github.io/docs (mirrored at https://acryl.dev/docs)
|
|
57
|
+
- The framework package: https://www.npmjs.com/package/acrylblends
|
|
58
|
+
- Source and issues: https://github.com/acrylblends/acrylblends
|
|
59
|
+
- Blends registry: https://acrylblends.github.io/blends
|
|
60
|
+
- ACRYL: https://acryl.dev
|
|
61
|
+
|
|
62
|
+
MIT license.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/names.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The one place this package names the framework. The templates are written with `__PACKAGE__` and `__RANGE__`, and the text of `AGENTS.md` with
|
|
4
|
+
* `__PACKAGE__`, so a rename (for example to `acryl`) is a change here and in `scripts/lib/names.mjs`, which a test keeps equal.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PACKAGE = "acrylblends";
|
|
7
|
+
export declare const CLASS = "Acryl";
|
|
8
|
+
export declare const CREATE = "create-acrylblends";
|
|
9
|
+
/** The range of the framework package a created app depends on: the first release whose root is the framework. */
|
|
10
|
+
export declare const RANGE = "^0.5.0";
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/index.d.ts
|
|
13
|
+
export declare const TEMPLATES: readonly ["nodejs", "bun", "acryl-app"];
|
|
14
|
+
export type Template = (typeof TEMPLATES)[number];
|
|
15
|
+
export declare const PACKAGE_MANAGERS: readonly ["npm", "pnpm", "yarn", "bun"];
|
|
16
|
+
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
17
|
+
/** The version of this package. */
|
|
18
|
+
export declare const VERSION: string;
|
|
19
|
+
export declare const HELP: string;
|
|
20
|
+
/** A command line mistake. The command prints it with the help text and exits 2. */
|
|
21
|
+
export declare class UsageError extends Error {
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
export interface Io {
|
|
25
|
+
stdout: {
|
|
26
|
+
write(text: string): unknown;
|
|
27
|
+
};
|
|
28
|
+
stderr: {
|
|
29
|
+
write(text: string): unknown;
|
|
30
|
+
};
|
|
31
|
+
cwd: string;
|
|
32
|
+
env: Record<string, string | undefined>;
|
|
33
|
+
/** Ask one question; `undefined` when there is no terminal to ask on. */
|
|
34
|
+
ask: ((question: string) => Promise<string>) | undefined;
|
|
35
|
+
/** Run a program, showing its output; resolves to the exit code. */
|
|
36
|
+
run(command: string, args: string[], cwd: string): Promise<number>;
|
|
37
|
+
}
|
|
38
|
+
export declare function processIo(): Io;
|
|
39
|
+
export interface CreateOptions {
|
|
40
|
+
directory: string | undefined;
|
|
41
|
+
template: Template | undefined;
|
|
42
|
+
install: boolean;
|
|
43
|
+
pm: PackageManager | undefined;
|
|
44
|
+
yes: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function isTemplate(value: string): value is Template;
|
|
47
|
+
export declare function parseCreateOptions(argv: readonly string[]): CreateOptions;
|
|
48
|
+
/** A valid package name from a directory name: lower case letters, digits and dashes. */
|
|
49
|
+
export declare function nameFrom(directory: string): string;
|
|
50
|
+
export declare function replacePlaceholders(text: string, name: string): string;
|
|
51
|
+
/** The guide for a coding agent in a created app: where the API is, how to run it, what must pass. */
|
|
52
|
+
export declare function agentsText(template: Exclude<Template, 'acryl-app'>, name: string): string;
|
|
53
|
+
/** Create the app. Returns the exit code: 0 done, 1 refused or failed, 2 a usage error. Never calls `process.exit`. */
|
|
54
|
+
export declare function runCreate(argv: readonly string[], io?: Io): Promise<number>;
|
|
55
|
+
//#endregion
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
//#region src/names.ts
|
|
9
|
+
/**
|
|
10
|
+
* The one place this package names the framework. The templates are written with `__PACKAGE__` and `__RANGE__`, and the text of `AGENTS.md` with
|
|
11
|
+
* `__PACKAGE__`, so a rename (for example to `acryl`) is a change here and in `scripts/lib/names.mjs`, which a test keeps equal.
|
|
12
|
+
*/
|
|
13
|
+
const PACKAGE = "acrylblends";
|
|
14
|
+
const CLASS = "Acryl";
|
|
15
|
+
const CREATE = "create-acrylblends";
|
|
16
|
+
/** The range of the framework package a created app depends on: the first release whose root is the framework. */
|
|
17
|
+
const RANGE = "^0.5.0";
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/index.ts
|
|
20
|
+
const TEMPLATES = [
|
|
21
|
+
"nodejs",
|
|
22
|
+
"bun",
|
|
23
|
+
"acryl-app"
|
|
24
|
+
];
|
|
25
|
+
const PACKAGE_MANAGERS = [
|
|
26
|
+
"npm",
|
|
27
|
+
"pnpm",
|
|
28
|
+
"yarn",
|
|
29
|
+
"bun"
|
|
30
|
+
];
|
|
31
|
+
/** The version of this package. */
|
|
32
|
+
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
33
|
+
const HELP = `${CREATE} ${VERSION} - create an ${CLASS} app
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
npm create ${PACKAGE}@latest [directory] [-- options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--template <name> nodejs, bun (experimental), acryl-app (asked when not given)
|
|
40
|
+
--install install the dependencies after creating the app
|
|
41
|
+
--pm <manager> ${PACKAGE_MANAGERS.join(", ")} (default: the one that ran this command, else npm)
|
|
42
|
+
-y, --yes ask nothing: template nodejs unless --template is given, directory ${PACKAGE}-app
|
|
43
|
+
-h, --help show this help
|
|
44
|
+
-v, --version show the version
|
|
45
|
+
`;
|
|
46
|
+
/** A command line mistake. The command prints it with the help text and exits 2. */
|
|
47
|
+
var UsageError = class extends Error {
|
|
48
|
+
name = "UsageError";
|
|
49
|
+
};
|
|
50
|
+
function processIo() {
|
|
51
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
52
|
+
return {
|
|
53
|
+
stdout: process.stdout,
|
|
54
|
+
stderr: process.stderr,
|
|
55
|
+
cwd: process.cwd(),
|
|
56
|
+
env: process.env,
|
|
57
|
+
ask: interactive ? async (question) => {
|
|
58
|
+
const rl = createInterface({
|
|
59
|
+
input: process.stdin,
|
|
60
|
+
output: process.stdout
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
return await rl.question(question);
|
|
64
|
+
} finally {
|
|
65
|
+
rl.close();
|
|
66
|
+
}
|
|
67
|
+
} : void 0,
|
|
68
|
+
run: (command, args, cwd) => new Promise((done) => {
|
|
69
|
+
const child = spawn(command, args, {
|
|
70
|
+
cwd,
|
|
71
|
+
stdio: "inherit",
|
|
72
|
+
shell: process.platform === "win32"
|
|
73
|
+
});
|
|
74
|
+
child.on("error", () => done(1));
|
|
75
|
+
child.on("exit", (code) => done(code ?? 1));
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function isTemplate(value) {
|
|
80
|
+
return TEMPLATES.includes(value);
|
|
81
|
+
}
|
|
82
|
+
function parseCreateOptions(argv) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = parse(argv);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new UsageError(error.message);
|
|
88
|
+
}
|
|
89
|
+
const { values, positionals } = parsed;
|
|
90
|
+
if (positionals.length > 1) throw new UsageError("takes at most one directory");
|
|
91
|
+
if (values.template !== void 0 && !isTemplate(values.template)) throw new UsageError(`unknown template "${values.template}" (${TEMPLATES.join(", ")})`);
|
|
92
|
+
if (values.pm !== void 0 && !PACKAGE_MANAGERS.includes(values.pm)) throw new UsageError(`unknown package manager "${values.pm}" (${PACKAGE_MANAGERS.join(", ")})`);
|
|
93
|
+
return {
|
|
94
|
+
directory: positionals[0],
|
|
95
|
+
template: values.template,
|
|
96
|
+
install: values.install === true,
|
|
97
|
+
pm: values.pm,
|
|
98
|
+
yes: values.yes === true
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const parse = (argv) => parseArgs({
|
|
102
|
+
args: [...argv],
|
|
103
|
+
allowPositionals: true,
|
|
104
|
+
strict: true,
|
|
105
|
+
options: {
|
|
106
|
+
template: { type: "string" },
|
|
107
|
+
install: { type: "boolean" },
|
|
108
|
+
pm: { type: "string" },
|
|
109
|
+
yes: {
|
|
110
|
+
type: "boolean",
|
|
111
|
+
short: "y"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
/** A valid package name from a directory name: lower case letters, digits and dashes. */
|
|
116
|
+
function nameFrom(directory) {
|
|
117
|
+
const name = basename(resolve(directory)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/g, "");
|
|
118
|
+
return name === "" ? "acryl-app" : name;
|
|
119
|
+
}
|
|
120
|
+
function walkUp(from, test) {
|
|
121
|
+
let dir = from;
|
|
122
|
+
for (;;) {
|
|
123
|
+
const found = test(dir);
|
|
124
|
+
if (found) return found;
|
|
125
|
+
const parent = dirname(dir);
|
|
126
|
+
if (parent === dir) throw new Error(`${CREATE} cannot find its files`);
|
|
127
|
+
dir = parent;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
131
|
+
const templatesRoot = () => walkUp(here, (d) => existsSync(join(d, "templates", "nodejs")) ? join(d, "templates") : void 0);
|
|
132
|
+
/** The framework docs: the copy `prepack` puts in the package, or `docs/framework` of the repository. */
|
|
133
|
+
const docsRoot = () => walkUp(here, (d) => existsSync(join(d, "docs", "framework", "README.md")) ? join(d, "docs", "framework") : void 0);
|
|
134
|
+
function replacePlaceholders(text, name) {
|
|
135
|
+
return text.replaceAll("__APP_NAME__", name).replaceAll("__PACKAGE__", PACKAGE).replaceAll("__CLASS__", CLASS).replaceAll("__RANGE__", RANGE);
|
|
136
|
+
}
|
|
137
|
+
/** The guide for a coding agent in a created app: where the API is, how to run it, what must pass. */
|
|
138
|
+
function agentsText(template, name) {
|
|
139
|
+
const run = {
|
|
140
|
+
nodejs: "npm run dev",
|
|
141
|
+
bun: "bun run dev"
|
|
142
|
+
}[template];
|
|
143
|
+
const test = {
|
|
144
|
+
nodejs: "npm test` and `npm run check",
|
|
145
|
+
bun: "bun test` and `bun run check"
|
|
146
|
+
}[template];
|
|
147
|
+
return `# ${name}
|
|
148
|
+
|
|
149
|
+
An ${CLASS} app (${template}${template === "bun" ? ", experimental" : ""}): handlers over the Web Standard \`Request\` and \`Response\`, from the \`${PACKAGE}\` package.
|
|
150
|
+
|
|
151
|
+
## The framework's API is in \`docs/framework/\`
|
|
152
|
+
|
|
153
|
+
Start at \`docs/framework/README.md\`; \`api/summary.md\` is the one-page summary. Import the class from \`'${PACKAGE}'\` and each middleware or helper from its own
|
|
154
|
+
subpath (\`'${PACKAGE}/cors'\`, \`'${PACKAGE}/validator'\`, ...). Parts marked "not yet available" in the docs are not in the package yet.
|
|
155
|
+
|
|
156
|
+
## This app
|
|
157
|
+
|
|
158
|
+
Run it with \`${run}\`. The routes are in \`${template === "bun" ? "src/index.ts" : "src/app.ts"}\`; an app is a function from \`Request\` to \`Response\`,
|
|
159
|
+
so test a route with \`await app.request('/path')\` and no server.
|
|
160
|
+
|
|
161
|
+
## Things that surprise people
|
|
162
|
+
|
|
163
|
+
- A page mounted at \`/x\` answers \`/x\`, not \`/x/\` (trailing slashes are strict); \`trimTrailingSlash()\` redirects.
|
|
164
|
+
- A \`json\` validator target reads the body only with a JSON \`Content-Type\`; a failure answers 400 with \`{ error, issues: [{ message, path }] }\`.
|
|
165
|
+
- \`throw new HTTPException(404, { message })\` answers an error response (plain text).
|
|
166
|
+
|
|
167
|
+
## Before you say you are done
|
|
168
|
+
|
|
169
|
+
\`${test}\` pass.
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
function copyDocs(target) {
|
|
173
|
+
const filter = (path) => !/(upstream\.json|UPSTREAM\.md)$/.test(path);
|
|
174
|
+
mkdirSync(join(target, "docs"), { recursive: true });
|
|
175
|
+
cpSync(docsRoot(), join(target, "docs", "framework"), {
|
|
176
|
+
recursive: true,
|
|
177
|
+
filter
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function copyTemplate(template, target, name) {
|
|
181
|
+
const written = [];
|
|
182
|
+
const walk = (from, to, prefix) => {
|
|
183
|
+
mkdirSync(to, { recursive: true });
|
|
184
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
185
|
+
const targetName = prefix === "" && entry.name === "gitignore" ? ".gitignore" : entry.name;
|
|
186
|
+
if (entry.isDirectory()) walk(join(from, entry.name), join(to, targetName), `${prefix}${entry.name}/`);
|
|
187
|
+
else {
|
|
188
|
+
writeFileSync(join(to, targetName), replacePlaceholders(readFileSync(join(from, entry.name), "utf8"), name));
|
|
189
|
+
written.push(`${prefix}${targetName}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
walk(join(templatesRoot(), template), target, "");
|
|
194
|
+
writeFileSync(join(target, "AGENTS.md"), agentsText(template, name));
|
|
195
|
+
writeFileSync(join(target, "CLAUDE.md"), "@AGENTS.md\n");
|
|
196
|
+
copyDocs(target);
|
|
197
|
+
return written;
|
|
198
|
+
}
|
|
199
|
+
const isEmpty = (directory) => !existsSync(directory) || readdirSync(directory).length === 0;
|
|
200
|
+
/** The `acrylblends` command of the installed framework package, which creates the `acryl-app` template. */
|
|
201
|
+
function acrylblendsBin() {
|
|
202
|
+
const pkgFile = createRequire(import.meta.url).resolve(`${PACKAGE}/package.json`);
|
|
203
|
+
const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
|
|
204
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[PACKAGE];
|
|
205
|
+
if (!bin) throw new Error(`${PACKAGE} has no command to create an app with`);
|
|
206
|
+
return join(dirname(pkgFile), bin);
|
|
207
|
+
}
|
|
208
|
+
/** Create the app. Returns the exit code: 0 done, 1 refused or failed, 2 a usage error. Never calls `process.exit`. */
|
|
209
|
+
async function runCreate(argv, io = processIo()) {
|
|
210
|
+
try {
|
|
211
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
212
|
+
io.stdout.write(HELP);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
if (argv.includes("-v") || argv.includes("--version")) {
|
|
216
|
+
io.stdout.write(`${VERSION}\n`);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
const options = parseCreateOptions(argv);
|
|
220
|
+
const ask = options.yes ? void 0 : io.ask;
|
|
221
|
+
const directory = options.directory ?? (ask ? (await ask(`Project directory (acrylblends-app): `)).trim() || `acrylblends-app` : `acrylblends-app`);
|
|
222
|
+
const target = resolve(io.cwd, directory);
|
|
223
|
+
const template = options.template ?? (ask ? await (async () => {
|
|
224
|
+
const answer = (await ask(`Which template do you want to use? (nodejs, bun (experimental), acryl-app) [nodejs]: `)).trim() || "nodejs";
|
|
225
|
+
if (!isTemplate(answer)) throw new UsageError(`unknown template "${answer}" (${TEMPLATES.join(", ")})`);
|
|
226
|
+
return answer;
|
|
227
|
+
})() : "nodejs");
|
|
228
|
+
if (!isEmpty(target)) {
|
|
229
|
+
io.stderr.write(`error: ${target} is not empty; nothing was changed\n`);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
const name = nameFrom(target);
|
|
233
|
+
if (template === "acryl-app") {
|
|
234
|
+
if (await io.run(process.execPath, [
|
|
235
|
+
acrylblendsBin(),
|
|
236
|
+
"init",
|
|
237
|
+
target,
|
|
238
|
+
"--template",
|
|
239
|
+
"agent",
|
|
240
|
+
"--name",
|
|
241
|
+
name,
|
|
242
|
+
"--yes"
|
|
243
|
+
], io.cwd) !== 0) return 1;
|
|
244
|
+
} else {
|
|
245
|
+
const files = copyTemplate(template, target, name);
|
|
246
|
+
io.stdout.write(`created ${name} (${template}) in ${target}: ${files.length} files, ${CLASS} docs in docs/framework\n`);
|
|
247
|
+
}
|
|
248
|
+
const pm = options.pm ?? PACKAGE_MANAGERS.find((m) => (io.env.npm_config_user_agent ?? "").startsWith(`${m}/`)) ?? "npm";
|
|
249
|
+
if (options.install) {
|
|
250
|
+
if (await io.run(pm, ["install"], target) !== 0) {
|
|
251
|
+
io.stderr.write(`error: ${pm} install failed; run it yourself in ${directory}\n`);
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const start = {
|
|
256
|
+
nodejs: `${pm} run dev`,
|
|
257
|
+
bun: "bun run dev",
|
|
258
|
+
"acryl-app": `${pm} run dev`
|
|
259
|
+
}[template];
|
|
260
|
+
io.stdout.write(`\nNext:\n cd ${directory}\n${options.install ? "" : ` ${pm} install\n`} ${start}\n`);
|
|
261
|
+
return 0;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error instanceof UsageError) {
|
|
264
|
+
io.stderr.write(`error: ${error.message}\n\n${HELP}`);
|
|
265
|
+
return 2;
|
|
266
|
+
}
|
|
267
|
+
io.stderr.write(`error: ${error.message}\n`);
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
export { CLASS, CREATE, HELP, PACKAGE, PACKAGE_MANAGERS, RANGE, TEMPLATES, UsageError, VERSION, agentsText, isTemplate, nameFrom, parseCreateOptions, processIo, replacePlaceholders, runCreate };
|
|
273
|
+
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/names.ts","../src/index.ts"],"sourcesContent":["/**\n * The one place this package names the framework. The templates are written with `__PACKAGE__` and `__RANGE__`, and the text of `AGENTS.md` with\n * `__PACKAGE__`, so a rename (for example to `acryl`) is a change here and in `scripts/lib/names.mjs`, which a test keeps equal.\n */\nexport const PACKAGE = 'acrylblends'\nexport const CLASS = 'Acryl'\nexport const CREATE = 'create-acrylblends'\n/** The range of the framework package a created app depends on: the first release whose root is the framework. */\nexport const RANGE = '^0.5.0'\n","import { spawn } from 'node:child_process'\nimport { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { basename, dirname, join, resolve } from 'node:path'\nimport { createInterface } from 'node:readline/promises'\nimport { fileURLToPath } from 'node:url'\nimport { parseArgs } from 'node:util'\nimport { CLASS, CREATE, PACKAGE, RANGE } from './names'\n\nexport { CLASS, CREATE, PACKAGE, RANGE }\n\nexport const TEMPLATES = ['nodejs', 'bun', 'acryl-app'] as const\nexport type Template = (typeof TEMPLATES)[number]\nexport const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'] as const\nexport type PackageManager = (typeof PACKAGE_MANAGERS)[number]\n\n/** The version of this package. */\nexport const VERSION: string = (\n createRequire(import.meta.url)('../package.json') as { version: string }\n).version\n\nexport const HELP = `${CREATE} ${VERSION} - create an ${CLASS} app\n\nUsage:\n npm create ${PACKAGE}@latest [directory] [-- options]\n\nOptions:\n --template <name> nodejs, bun (experimental), acryl-app (asked when not given)\n --install install the dependencies after creating the app\n --pm <manager> ${PACKAGE_MANAGERS.join(', ')} (default: the one that ran this command, else npm)\n -y, --yes ask nothing: template nodejs unless --template is given, directory ${PACKAGE}-app\n -h, --help show this help\n -v, --version show the version\n`\n\n/** A command line mistake. The command prints it with the help text and exits 2. */\nexport class UsageError extends Error {\n override name = 'UsageError'\n}\n\nexport interface Io {\n stdout: { write(text: string): unknown }\n stderr: { write(text: string): unknown }\n cwd: string\n env: Record<string, string | undefined>\n /** Ask one question; `undefined` when there is no terminal to ask on. */\n ask: ((question: string) => Promise<string>) | undefined\n /** Run a program, showing its output; resolves to the exit code. */\n run(command: string, args: string[], cwd: string): Promise<number>\n}\n\nexport function processIo(): Io {\n const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY)\n return {\n stdout: process.stdout,\n stderr: process.stderr,\n cwd: process.cwd(),\n env: process.env,\n ask: interactive\n ? async (question) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout })\n try {\n return await rl.question(question)\n } finally {\n rl.close()\n }\n }\n : undefined,\n run: (command, args, cwd) =>\n new Promise((done) => {\n const child = spawn(command, args, {\n cwd,\n stdio: 'inherit',\n shell: process.platform === 'win32',\n })\n child.on('error', () => done(1))\n child.on('exit', (code) => done(code ?? 1))\n }),\n }\n}\n\nexport interface CreateOptions {\n directory: string | undefined\n template: Template | undefined\n install: boolean\n pm: PackageManager | undefined\n yes: boolean\n}\n\nexport function isTemplate(value: string): value is Template {\n return (TEMPLATES as readonly string[]).includes(value)\n}\n\nexport function parseCreateOptions(argv: readonly string[]): CreateOptions {\n let parsed: ReturnType<typeof parse>\n try {\n parsed = parse(argv)\n } catch (error) {\n throw new UsageError((error as Error).message)\n }\n const { values, positionals } = parsed\n if (positionals.length > 1) throw new UsageError('takes at most one directory')\n if (values.template !== undefined && !isTemplate(values.template))\n throw new UsageError(`unknown template \"${values.template}\" (${TEMPLATES.join(', ')})`)\n if (values.pm !== undefined && !(PACKAGE_MANAGERS as readonly string[]).includes(values.pm))\n throw new UsageError(`unknown package manager \"${values.pm}\" (${PACKAGE_MANAGERS.join(', ')})`)\n return {\n directory: positionals[0],\n template: values.template as Template | undefined,\n install: values.install === true,\n pm: values.pm as PackageManager | undefined,\n yes: values.yes === true,\n }\n}\n\nconst parse = (argv: readonly string[]) =>\n parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: {\n template: { type: 'string' },\n install: { type: 'boolean' },\n pm: { type: 'string' },\n yes: { type: 'boolean', short: 'y' },\n },\n })\n\n/** A valid package name from a directory name: lower case letters, digits and dashes. */\nexport function nameFrom(directory: string): string {\n const name = basename(resolve(directory))\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/^[^a-z]+/, '')\n .replace(/-+$/g, '')\n return name === '' ? 'acryl-app' : name\n}\n\nfunction walkUp(from: string, test: (dir: string) => string | undefined): string {\n let dir = from\n for (;;) {\n const found = test(dir)\n if (found) return found\n const parent = dirname(dir)\n if (parent === dir) throw new Error(`${CREATE} cannot find its files`)\n dir = parent\n }\n}\nconst here = dirname(fileURLToPath(import.meta.url))\nconst templatesRoot = () =>\n walkUp(here, (d) =>\n existsSync(join(d, 'templates', 'nodejs')) ? join(d, 'templates') : undefined,\n )\n/** The framework docs: the copy `prepack` puts in the package, or `docs/framework` of the repository. */\nconst docsRoot = () =>\n walkUp(here, (d) =>\n existsSync(join(d, 'docs', 'framework', 'README.md'))\n ? join(d, 'docs', 'framework')\n : undefined,\n )\n\nexport function replacePlaceholders(text: string, name: string): string {\n return text\n .replaceAll('__APP_NAME__', name)\n .replaceAll('__PACKAGE__', PACKAGE)\n .replaceAll('__CLASS__', CLASS)\n .replaceAll('__RANGE__', RANGE)\n}\n\n/** The guide for a coding agent in a created app: where the API is, how to run it, what must pass. */\nexport function agentsText(template: Exclude<Template, 'acryl-app'>, name: string): string {\n const run = { nodejs: 'npm run dev', bun: 'bun run dev' }[template]\n const test = {\n nodejs: 'npm test` and `npm run check',\n bun: 'bun test` and `bun run check',\n }[template]\n return `# ${name}\n\nAn ${CLASS} app (${template}${template === 'bun' ? ', experimental' : ''}): handlers over the Web Standard \\`Request\\` and \\`Response\\`, from the \\`${PACKAGE}\\` package.\n\n## The framework's API is in \\`docs/framework/\\`\n\nStart at \\`docs/framework/README.md\\`; \\`api/summary.md\\` is the one-page summary. Import the class from \\`'${PACKAGE}'\\` and each middleware or helper from its own\nsubpath (\\`'${PACKAGE}/cors'\\`, \\`'${PACKAGE}/validator'\\`, ...). Parts marked \"not yet available\" in the docs are not in the package yet.\n\n## This app\n\nRun it with \\`${run}\\`. The routes are in \\`${template === 'bun' ? 'src/index.ts' : 'src/app.ts'}\\`; an app is a function from \\`Request\\` to \\`Response\\`,\nso test a route with \\`await app.request('/path')\\` and no server.\n\n## Things that surprise people\n\n- A page mounted at \\`/x\\` answers \\`/x\\`, not \\`/x/\\` (trailing slashes are strict); \\`trimTrailingSlash()\\` redirects.\n- A \\`json\\` validator target reads the body only with a JSON \\`Content-Type\\`; a failure answers 400 with \\`{ error, issues: [{ message, path }] }\\`.\n- \\`throw new HTTPException(404, { message })\\` answers an error response (plain text).\n\n## Before you say you are done\n\n\\`${test}\\` pass.\n`\n}\n\nfunction copyDocs(target: string): void {\n const filter = (path: string) => !/(upstream\\.json|UPSTREAM\\.md)$/.test(path)\n mkdirSync(join(target, 'docs'), { recursive: true })\n cpSync(docsRoot(), join(target, 'docs', 'framework'), { recursive: true, filter })\n}\n\nfunction copyTemplate(\n template: Exclude<Template, 'acryl-app'>,\n target: string,\n name: string,\n): string[] {\n const written: string[] = []\n const walk = (from: string, to: string, prefix: string) => {\n mkdirSync(to, { recursive: true })\n for (const entry of readdirSync(from, { withFileTypes: true })) {\n const targetName = prefix === '' && entry.name === 'gitignore' ? '.gitignore' : entry.name\n if (entry.isDirectory())\n walk(join(from, entry.name), join(to, targetName), `${prefix}${entry.name}/`)\n else {\n writeFileSync(\n join(to, targetName),\n replacePlaceholders(readFileSync(join(from, entry.name), 'utf8'), name),\n )\n written.push(`${prefix}${targetName}`)\n }\n }\n }\n walk(join(templatesRoot(), template), target, '')\n writeFileSync(join(target, 'AGENTS.md'), agentsText(template, name))\n writeFileSync(join(target, 'CLAUDE.md'), '@AGENTS.md\\n')\n copyDocs(target)\n return written\n}\n\nconst isEmpty = (directory: string) => !existsSync(directory) || readdirSync(directory).length === 0\n\n/** The `acrylblends` command of the installed framework package, which creates the `acryl-app` template. */\nfunction acrylblendsBin(): string {\n const pkgFile = createRequire(import.meta.url).resolve(`${PACKAGE}/package.json`)\n const pkg = JSON.parse(readFileSync(pkgFile, 'utf8')) as { bin?: string | Record<string, string> }\n const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[PACKAGE]\n if (!bin) throw new Error(`${PACKAGE} has no command to create an app with`)\n return join(dirname(pkgFile), bin)\n}\n\n/** Create the app. Returns the exit code: 0 done, 1 refused or failed, 2 a usage error. Never calls `process.exit`. */\nexport async function runCreate(argv: readonly string[], io: Io = processIo()): Promise<number> {\n try {\n if (argv.includes('-h') || argv.includes('--help')) {\n io.stdout.write(HELP)\n return 0\n }\n if (argv.includes('-v') || argv.includes('--version')) {\n io.stdout.write(`${VERSION}\\n`)\n return 0\n }\n const options = parseCreateOptions(argv)\n const ask = options.yes ? undefined : io.ask\n const directory =\n options.directory ??\n (ask\n ? (await ask(`Project directory (${PACKAGE}-app): `)).trim() || `${PACKAGE}-app`\n : `${PACKAGE}-app`)\n const target = resolve(io.cwd, directory)\n const template: Template =\n options.template ??\n (ask\n ? await (async () => {\n const answer =\n (\n await ask(\n `Which template do you want to use? (nodejs, bun (experimental), acryl-app) [nodejs]: `,\n )\n ).trim() || 'nodejs'\n if (!isTemplate(answer))\n throw new UsageError(`unknown template \"${answer}\" (${TEMPLATES.join(', ')})`)\n return answer\n })()\n : 'nodejs')\n if (!isEmpty(target)) {\n io.stderr.write(`error: ${target} is not empty; nothing was changed\\n`)\n return 1\n }\n const name = nameFrom(target)\n if (template === 'acryl-app') {\n const code = await io.run(\n process.execPath,\n [acrylblendsBin(), 'init', target, '--template', 'agent', '--name', name, '--yes'],\n io.cwd,\n )\n if (code !== 0) return 1\n } else {\n const files = copyTemplate(template, target, name)\n io.stdout.write(\n `created ${name} (${template}) in ${target}: ${files.length} files, ${CLASS} docs in docs/framework\\n`,\n )\n }\n const pm: PackageManager =\n options.pm ??\n ((PACKAGE_MANAGERS as readonly string[]).find((m) =>\n (io.env.npm_config_user_agent ?? '').startsWith(`${m}/`),\n ) as PackageManager | undefined) ??\n 'npm'\n if (options.install) {\n if ((await io.run(pm, ['install'], target)) !== 0) {\n io.stderr.write(`error: ${pm} install failed; run it yourself in ${directory}\\n`)\n return 1\n }\n }\n const start = {\n nodejs: `${pm} run dev`,\n bun: 'bun run dev',\n 'acryl-app': `${pm} run dev`,\n }[template]\n io.stdout.write(\n `\\nNext:\\n cd ${directory}\\n${options.install ? '' : ` ${pm} install\\n`} ${start}\\n`,\n )\n return 0\n } catch (error) {\n if (error instanceof UsageError) {\n io.stderr.write(`error: ${error.message}\\n\\n${HELP}`)\n return 2\n }\n io.stderr.write(`error: ${(error as Error).message}\\n`)\n return 1\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
The pages in this folder are adapted in part from the Hono documentation (https://github.com/honojs/website), used under this license:
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2021 - present, Yusuke Wada and Hono contributors
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Acryl
|
|
2
|
+
|
|
3
|
+
Acryl is a small web framework built on Web Standards. You write handlers over the standard `Request` and `Response`, and the same application runs on Node.js (and, experimentally, Bun), and inside a Cordis app as a plugin that an agent can extend while it runs.
|
|
4
|
+
|
|
5
|
+
```ts twoslash
|
|
6
|
+
import { Acryl } from 'acrylblends'
|
|
7
|
+
const app = new Acryl()
|
|
8
|
+
|
|
9
|
+
app.get('/', (c) => c.text('Acryl!'))
|
|
10
|
+
|
|
11
|
+
export default app
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm create acrylblends@latest my-app
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Other package managers work the same way: `pnpm create acrylblends@latest`, `yarn create acrylblends`, `bun create acrylblends@latest`.
|
|
21
|
+
The starter asks which template you want (`nodejs`, `bun` (experimental), or `acryl-app`, a Cordis app with an agent). See [Getting started](getting-started/basic.md).
|
|
22
|
+
|
|
23
|
+
## What is in the box
|
|
24
|
+
|
|
25
|
+
- **Routing** with several routers behind one API: `RegExpRouter`, `TrieRouter`, `SmartRouter` (the default, which chooses between them), `LinearRouter` and `PatternRouter`.
|
|
26
|
+
See [Routers](concepts/routers.md) and [Routing](api/routing.md).
|
|
27
|
+
- **Web Standards only.** Handlers receive a `Context` over `Request` and return `Response`, with no runtime-specific objects. See [Web Standards](concepts/web-standard.md).
|
|
28
|
+
- **Built-in middleware** for authentication, CORS, CSRF, compression, ETag, logging, request ids, secure headers, timeouts and more.
|
|
29
|
+
See [Built-in middleware](middleware/builtin/basic-auth.md) and [Middleware](concepts/middleware.md).
|
|
30
|
+
- **Helpers** for cookies, streaming and server-sent events, content negotiation, HTML templates, WebSockets and testing. See [Helpers](guides/helpers.md).
|
|
31
|
+
- **Typed routes and a typed client.** Route types flow from the server to `acrylblends/client`. See [RPC](guides/rpc.md).
|
|
32
|
+
- **Validation** with a validator function or any Standard Schema library. See [Validation](guides/validation.md).
|
|
33
|
+
- **A Cordis plugin around it.** A plugin mounts an app with `ctx.router.route`, the mount goes away when the plugin unloads, and a coding agent can write such plugins
|
|
34
|
+
into a running app. See [Acryl in a Cordis app](cordis/acryl-in-a-cordis-app.md) and [Agents](agents/index.md).
|
|
35
|
+
|
|
36
|
+
## Runtimes
|
|
37
|
+
|
|
38
|
+
Verified: Node.js. Bun is experimental. The other runtimes the framework could target are listed on [Other runtimes](getting-started/other-runtimes.md) as not yet verified.
|
|
39
|
+
|
|
40
|
+
## Not yet available
|
|
41
|
+
|
|
42
|
+
Some parts of the documented API surface are not part of the package yet, and their pages say so at the top: the JSX packages, static site generation, the CSS helper, the
|
|
43
|
+
`proxy`, `dev` and `cache` pieces, the `quick` and `tiny` presets and the adapters of the runtimes other than Node.js. They are enabled as they are ported.
|
|
44
|
+
|
|
45
|
+
## Where to read next
|
|
46
|
+
|
|
47
|
+
| I want to | Read |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| Run something in five minutes | [Getting started](getting-started/basic.md) |
|
|
50
|
+
| Look up a call | [API](api/index.md), [Routing](api/routing.md), [Context](api/context.md), [Request](api/request.md), [API summary](api/summary.md) |
|
|
51
|
+
| Understand the design | [Concepts](concepts/motivation.md) |
|
|
52
|
+
| Copy a pattern | [Guides](guides/best-practices.md), [Validation](guides/validation.md), [Testing](guides/testing.md) |
|
|
53
|
+
| Use Acryl inside a Cordis app | [Acryl in a Cordis app](cordis/acryl-in-a-cordis-app.md), [Plugin lifecycle](cordis/plugin-lifecycle.md), [Mounting](cordis/mounting.md) |
|
|
54
|
+
| Move from the 0.3 router calls | [Migration from 0.3](cordis/migration-from-0-3.md) |
|
|
55
|
+
| Move from the 0.4 names (`App`) | [Migration from 0.4](cordis/migration-from-0-4.md) |
|
|
56
|
+
|
|
57
|
+
The license of these pages and of the framework source they describe is in [LICENSE](LICENSE).
|
|
58
|
+
|
|
59
|
+
## TypeScript in a Cordis app
|
|
60
|
+
|
|
61
|
+
An app's `tsconfig.json` can extend `acryl-router/tsconfig.app.json` (ES2024 with the DOM types, strict). The DOM types are optional: every
|
|
62
|
+
entry point's types also stand with only `"lib": ["ES2024"]` and `"types": ["node"]`, the setting a Host-only plugin uses so browser globals
|
|
63
|
+
do not typecheck in server code (the router's `tests/no-dom` fixture checks this on every typecheck).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Generated copy
|
|
2
|
+
|
|
3
|
+
This folder is a copy of `docs/framework` of acryldev/acryl at commit `5743482e4199ded676a77f594bce15fd4aa0ca2f`, made by `scripts/sync-framework.mjs`. Do not edit it here: change the text in ACRYL and sync again. `pnpm run verify:framework` fails when a page differs from the recorded digest.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Agents
|
|
2
|
+
|
|
3
|
+
A coding agent (Claude Code, Codex, OpenCode, Copilot and others) builds on Acryl best when the documentation is in the folder it works in. A project created with
|
|
4
|
+
`npm create acrylblends@latest` (and one made with `npx acrylblends init`) therefore contains:
|
|
5
|
+
|
|
6
|
+
- `docs/framework/`: these pages. Start at `README.md`; `api/summary.md` is the one-page summary of the API.
|
|
7
|
+
- `AGENTS.md`: where the API is, how a feature is built (an `Acryl` app mounted with `ctx.router.route`, a plugin registered in `src/app.ts` and `blend.yaml`, a state cell),
|
|
8
|
+
the things that surprise people, and what must pass before a task is done (`npm test` and `npm run check`).
|
|
9
|
+
- `CLAUDE.md`: one line, `@AGENTS.md`, so tools that read `CLAUDE.md` read the same guide.
|
|
10
|
+
|
|
11
|
+
## What an agent can do inside a running app
|
|
12
|
+
|
|
13
|
+
The `acryl-app` template (a Cordis app) also lets an agent change the app while it runs:
|
|
14
|
+
|
|
15
|
+
1. The agent writes a plugin into `extensions/<name>/index.ts`. The plugin may import `acrylblends` and the other subpaths by name.
|
|
16
|
+
2. The app installs it as a child plugin, runs its `check(ctx)` with the plugin's own context, and keeps the previous version if the new one fails to start or to pass its check.
|
|
17
|
+
3. A plugin's routes are an `Acryl` app mounted with `ctx.router.route`, so removing the plugin removes its routes.
|
|
18
|
+
|
|
19
|
+
See [Acryl in a Cordis app](../cordis/acryl-in-a-cordis-app.md), [Plugin lifecycle](../cordis/plugin-lifecycle.md) and [Mounting](../cordis/mounting.md).
|
|
20
|
+
|
|
21
|
+
## Checking that an agent can use the docs
|
|
22
|
+
|
|
23
|
+
The repository has a manual gate: it creates a fresh app, starts a headless coding agent inside it with a one-line task (for example "add a /customers/count route with a test"),
|
|
24
|
+
and checks that the app's tests and type-check pass afterwards and that the route answers.
|