create-acrylblends 0.1.2 → 0.2.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.
Files changed (31) hide show
  1. package/dist/index.js +1 -0
  2. package/dist/index.js.map +1 -1
  3. package/docs/framework/README.md +3 -2
  4. package/docs/framework/UPSTREAM.md +1 -1
  5. package/docs/framework/api/presets.md +0 -3
  6. package/docs/framework/cordis/acryl-in-a-cordis-app.md +9 -0
  7. package/docs/framework/getting-started/basic.md +5 -4
  8. package/docs/framework/getting-started/desktop.md +41 -0
  9. package/docs/framework/guides/jsx-dom.md +0 -3
  10. package/docs/framework/guides/jsx.md +52 -5
  11. package/docs/framework/guides/middleware.md +1 -1
  12. package/docs/framework/helpers/cookie.md +1 -1
  13. package/docs/framework/helpers/css.md +0 -3
  14. package/docs/framework/helpers/dev.md +0 -3
  15. package/docs/framework/helpers/jwt.md +1 -1
  16. package/docs/framework/helpers/proxy.md +0 -3
  17. package/docs/framework/helpers/ssg.md +3 -6
  18. package/docs/framework/middleware/builtin/basic-auth.md +6 -6
  19. package/docs/framework/middleware/builtin/cache.md +0 -3
  20. package/docs/framework/middleware/builtin/combine.md +1 -1
  21. package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
  22. package/docs/framework/middleware/builtin/jsx-renderer.md +0 -3
  23. package/docs/framework/middleware/builtin/logger.md +1 -1
  24. package/docs/framework/middleware/builtin/mount.md +0 -3
  25. package/docs/framework/upstream.json +3 -3
  26. package/package.json +2 -2
  27. package/templates/nodejs/package.json +3 -3
  28. package/templates/nodejs/src/app.ts +3 -0
  29. package/templates/nodejs/src/page.tsx +14 -0
  30. package/templates/nodejs/test/app.test.ts +21 -0
  31. package/templates/nodejs/tsconfig.json +3 -1
package/dist/index.js CHANGED
@@ -169,6 +169,7 @@ so test a route with \`await app.request('/path')\` and no server.
169
169
  - A page mounted at \`/x\` answers \`/x\`, not \`/x/\` (trailing slashes are strict); \`trimTrailingSlash()\` redirects.
170
170
  - A \`json\` validator target reads the body only with a JSON \`Content-Type\`; a failure answers 400 with \`{ error, issues: [{ message, path }] }\`.
171
171
  - \`throw new HTTPException(404, { message })\` answers an error response (plain text).
172
+ - **JSX:** ${template === "nodejs" ? "Write pages in `.tsx` (see `src/page.tsx`): the scripts in `package.json` run Node.js with `--import acrylblends/node-server/register`, which compiles `.tsx` with this app's own `typescript`, and `tsconfig.json` sets `jsx: react-jsx` with `jsxImportSource: acrylblends/jsx`. Render a component with `.toString()` and return it with `c.html(...)`. A command that starts the app without those scripts (a bare `node src/index.ts`) cannot load a `.tsx` file. Without a build step you can write the call JSX compiles to instead: `jsx('h1', { class: 'title' }, 'Hello')` from `'acrylblends/jsx'`, children after the props. See `docs/framework/guides/jsx.md`." : "Bun compiles `.tsx` itself: write pages in `.tsx`, render a component with `.toString()` and return it with `c.html(...)`. `jsx('h1', { class: 'title' }, 'Hello')` from `'acrylblends/jsx'` (children after the props) is the same thing without JSX syntax. See `docs/framework/guides/jsx.md`."}
172
173
 
173
174
  ## Before you say you are done
174
175
 
package/dist/index.js.map CHANGED
@@ -1 +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__` (the range is stamped at create time from the framework version this package carries, see `frameworkRange`), 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","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 } from './names'\n\nexport { CLASS, CREATE, PACKAGE }\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\n/**\n * The range a created app asks for: the framework version this package was released with, so a new app depends on the release that made it (and gets what that release's\n * commands can do), never an older one. A caret range, so it also takes the newer patch releases.\n */\nexport function frameworkRange(): string {\n const file = createRequire(import.meta.url).resolve(`${PACKAGE}/package.json`)\n return `^${(JSON.parse(readFileSync(file, 'utf8')) as { version: string }).version}`\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__', frameworkRange())\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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"}
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__` (the range is stamped at create time from the framework version this package carries, see `frameworkRange`), 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","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 } from './names'\n\nexport { CLASS, CREATE, PACKAGE }\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\n/**\n * The range a created app asks for: the framework version this package was released with, so a new app depends on the release that made it (and gets what that release's\n * commands can do), never an older one. A caret range, so it also takes the newer patch releases.\n */\nexport function frameworkRange(): string {\n const file = createRequire(import.meta.url).resolve(`${PACKAGE}/package.json`)\n return `^${(JSON.parse(readFileSync(file, 'utf8')) as { version: string }).version}`\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__', frameworkRange())\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 const jsxNote =\n template === 'nodejs'\n ? \"Write pages in `.tsx` (see `src/page.tsx`): the scripts in `package.json` run Node.js with `--import acrylblends/node-server/register`, which compiles `.tsx` with this app's own `typescript`, and `tsconfig.json` sets `jsx: react-jsx` with `jsxImportSource: acrylblends/jsx`. Render a component with `.toString()` and return it with `c.html(...)`. A command that starts the app without those scripts (a bare `node src/index.ts`) cannot load a `.tsx` file. Without a build step you can write the call JSX compiles to instead: `jsx('h1', { class: 'title' }, 'Hello')` from `'acrylblends/jsx'`, children after the props. See `docs/framework/guides/jsx.md`.\"\n : \"Bun compiles `.tsx` itself: write pages in `.tsx`, render a component with `.toString()` and return it with `c.html(...)`. `jsx('h1', { class: 'title' }, 'Hello')` from `'acrylblends/jsx'` (children after the props) is the same thing without JSX syntax. See `docs/framework/guides/jsx.md`.\"\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- **JSX:** ${jsxNote}\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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"}
@@ -39,14 +39,15 @@ Verified: Node.js. Bun is experimental. The other runtimes the framework could t
39
39
 
40
40
  ## Not yet available
41
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.
42
+ Some parts of the documented API surface are not part of the package yet, and their pages say so at the top: the adapters of the runtimes other than Node.js.
43
+ They are enabled as they are ported.
44
44
 
45
45
  ## Where to read next
46
46
 
47
47
  | I want to | Read |
48
48
  | --- | --- |
49
49
  | Run something in five minutes | [Getting started](getting-started/basic.md) |
50
+ | Run the app as a desktop app | [Desktop](getting-started/desktop.md) |
50
51
  | 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
52
  | Understand the design | [Concepts](concepts/motivation.md) |
52
53
  | Copy a pattern | [Guides](guides/best-practices.md), [Validation](guides/validation.md), [Testing](guides/testing.md) |
@@ -1,3 +1,3 @@
1
1
  # Generated copy
2
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.
3
+ This folder is a copy of `docs/framework` of acryldev/acryl at commit `7bba3ed0420d87e61d8256fa60614d22af734c8f`, 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.
@@ -1,8 +1,5 @@
1
1
  # Presets
2
2
 
3
- > **Partly available.** The default router (`SmartRouter`), `RegExpRouter`, `TrieRouter`, `LinearRouter` and `PatternRouter` are available and are chosen with the `router` option of `new Acryl()` (see [Routers](../concepts/routers.md)). The presets `acrylblends/quick` and `acrylblends/tiny` are **not yet available**; their examples below are not checked by the docs tests.
4
-
5
-
6
3
  Acryl has several routers, each designed for a specific purpose.
7
4
  You can specify the router you want to use in the constructor of Acryl.
8
5
 
@@ -43,6 +43,9 @@ export function apply(ctx: Context): void {
43
43
  | `GET /todo/todos/42` | `{ "id": "42" }` |
44
44
  | `GET /todo/` | 404: `/todo` and `/todo/` are different paths (see below) |
45
45
 
46
+ The app can come from `acryl-router` or from the published `acrylblends` package: `route` takes any app by its routes
47
+ (`MountableApp`), so an `Acryl` imported from `acrylblends` mounts as it is, with its middleware and its own `onError`.
48
+
46
49
  ### `/todo/` is a 404 until you choose
47
50
 
48
51
  `/todo/` is the first thing people type, and by default it is not found: the router treats `/todo` and `/todo/` as two paths. Choose
@@ -91,6 +94,12 @@ const app = new Acryl()
91
94
  Use middleware for authentication, logging, CORS, compression, request IDs and other behavior that should wrap more than one handler.
92
95
  The built-in set is listed in [Middleware](../concepts/middleware.md).
93
96
 
97
+ ## Render pages with JSX
98
+
99
+ A plugin renders HTML with the same JSX as the published package: set `"jsx": "react-jsx"` and `"jsxImportSource": "acryl-router/jsx"`
100
+ in its `tsconfig.json` (a `.tsx` file), and style with the CSS helper from `acryl-router/helpers`. Streaming with `Suspense` is in
101
+ `acryl-router/jsx/streaming`. The [JSX guide](../guides/jsx.md) covers the rest; its examples import from `acrylblends/jsx` instead.
102
+
94
103
  ## Run the same app on a surface
95
104
 
96
105
  The application does not open a socket. A surface supplies requests to it:
@@ -63,7 +63,7 @@ import { Acryl } from 'acrylblends'
63
63
  const app = new Acryl()
64
64
 
65
65
  app.get('/', (c) => {
66
- return c.text('Hello Acryl!')
66
+ return c.text('Acryl!')
67
67
  })
68
68
 
69
69
  export default app
@@ -83,7 +83,7 @@ const app = new Acryl()
83
83
  app.get('/api/hello', (c) => {
84
84
  return c.json({
85
85
  ok: true,
86
- message: 'Hello Acryl!',
86
+ message: 'Acryl!',
87
87
  })
88
88
  })
89
89
  ```
@@ -162,7 +162,7 @@ app.use(
162
162
  '/admin/*',
163
163
  basicAuth({
164
164
  username: 'admin',
165
- password: 'secret',
165
+ password: process.env.ADMIN_PASSWORD!,
166
166
  })
167
167
  )
168
168
 
@@ -181,7 +181,7 @@ An app is a function from `Request` to `Response`, so a test needs no server:
181
181
  import { Acryl } from 'acrylblends'
182
182
 
183
183
  const app = new Acryl()
184
- app.get('/', (c) => c.text('Hello Acryl!'))
184
+ app.get('/', (c) => c.text('Acryl!'))
185
185
 
186
186
  const response = await app.request('/')
187
187
  console.log(response.status, await response.text())
@@ -192,5 +192,6 @@ See [Testing](../guides/testing.md).
192
192
  ## Next
193
193
 
194
194
  - [Node.js](nodejs.md) and [Bun](bun.md) (experimental)
195
+ - [Desktop](desktop.md): the same app in an Electron window
195
196
  - [Routing](../api/routing.md), [Context](../api/context.md) and [Request](../api/request.md)
196
197
  - [Validation](../guides/validation.md)
@@ -0,0 +1,41 @@
1
+ # Desktop
2
+
3
+ An Acryl app also runs as a desktop app: an Electron window opens on the app's own server. It is the same app and the same code as `npm run dev`; nothing in the app changes. This page covers a plain Acryl app (the `nodejs`
4
+ template) and a Blend app (the `acryl-app` template or `acrylblends init`). It runs from source today: packaging a distributable app (dmg, exe, deb) is not part of it yet.
5
+
6
+ ## Add it
7
+
8
+ In an Acryl app made by `npm create acrylblends@latest my-app -- --template nodejs`:
9
+
10
+ ```sh
11
+ cd my-app
12
+ npm install
13
+ npx acrylblends add desktop
14
+ npm install
15
+ npm run desktop
16
+ ```
17
+
18
+ In a Blend app, the same two commands (`npx acrylblends add desktop`, `npm install`) add the Blend form of the shell, and `npx acrylblends init my-app --template todo --desktop` creates a Blend app with it already in.
19
+ `acrylblends add desktop` chooses the right form: a Blend app (it has `src/plugins/instance`) or a plain Acryl app.
20
+
21
+ ## What it adds
22
+
23
+ - `src/plugins/desktop-web/` (a plain app) or `src/plugins/desktop/` (a Blend app): the window code. Only this folder imports `electron`; the app's own code never does.
24
+ - `electron` as a development dependency (pinned to 44.0.0) and the script `npm run desktop`.
25
+ - `.acrylblends/` in `.gitignore`: Electron's user data (window state, cache, logs) lives in `.acrylblends/electron` inside the app's own folder, never in the user's application-data folder.
26
+
27
+ ## What `npm run desktop` does
28
+
29
+ 1. Serves the app on `127.0.0.1` with a free port: for a plain app, `app.fetch` from `src/app.ts` (`export default app`) through `acrylblends/node-server`; for a Blend app, its own `createApp`.
30
+ 2. Takes a single-instance lock: a second launch of the same app focuses the first window and exits.
31
+ 3. Opens one window on the server's address, with a locked-down renderer (context isolation, no Node in the page, the sandbox, web security). Navigation stays on the app's own origin; any other link opens in the system browser.
32
+ 4. When the window closes (or on SIGINT or SIGTERM), stops the server and exits.
33
+
34
+ ## The first launch downloads Electron
35
+
36
+ `npm install` does not download Electron's binary (Electron 44 has no install script). The first `npm run desktop` does: about 100 MB, once per machine, then it is cached. Behind a proxy or mirror set `ELECTRON_MIRROR`
37
+ (and `ELECTRON_CACHE` to reuse a download); if the download is blocked, run `npx install-electron` where the mirror is reachable.
38
+
39
+ ## A different start page
40
+
41
+ The window opens on `/`. To open another path, set it in `package.json`: `"acrylblends": { "desktopPath": "/todo" }`.
@@ -1,8 +1,5 @@
1
1
  # Client Components
2
2
 
3
- > **Not yet available.** The client-side JSX package (`acrylblends/jsx/dom`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  `acrylblends/jsx` supports not only server side but also client side. This means that it is possible to create an interactive UI that runs in the browser. We call it Client Components or `acrylblends/jsx/dom`.
7
4
 
8
5
  It is fast and very small. The counter program in `acrylblends/jsx/dom` is only 2.8KB with Brotli compression, but 47.8KB for React.
@@ -1,8 +1,5 @@
1
1
  # JSX
2
2
 
3
- > **Not yet available.** The JSX packages (`acrylblends/jsx` and its subpaths) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  You can write HTML with JSX syntax with `acrylblends/jsx`.
7
4
 
8
5
  Although `acrylblends/jsx` works on the client, you will probably use it most often when rendering content on the server side. Here are some things related to JSX that are common to both server and client.
@@ -29,9 +26,34 @@ Alternatively, use the pragma directives:
29
26
  /** @jsxImportSource acrylblends/jsx */
30
27
  ```
31
28
 
29
+ ## Run `.tsx` on Node.js
30
+
31
+ Node.js strips TypeScript's types but does not compile JSX, so a `.tsx` file needs one flag: `--import acrylblends/node-server/register`.
32
+ It compiles each `.tsx` (and `.jsx`) module as Node.js loads it, with your app's own `typescript` (a development dependency) and the
33
+ `jsxImportSource` of your `tsconfig.json` (`acrylblends/jsx` without one); every other module runs exactly as before. Stack traces point at
34
+ the `.tsx` lines.
35
+
36
+ `package.json`:
37
+
38
+ ```json
39
+ {
40
+ "scripts": {
41
+ "dev": "node --watch --import acrylblends/node-server/register src/index.tsx",
42
+ "start": "node --import acrylblends/node-server/register src/index.tsx",
43
+ "test": "node --import acrylblends/node-server/register --test \"src/**/*.test.{ts,tsx}\""
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^6.0.3"
47
+ }
48
+ }
49
+ ```
50
+
51
+ `node --test` looks only for `.js` and `.ts` test files on its own, so the `test` script names the `.tsx` ones with a pattern. Without `typescript` in the app, the first `.tsx` module stops with a message that says to install it. Bun compiles JSX itself and needs
52
+ no flag.
53
+
32
54
  ## Usage
33
55
 
34
- > **Note.** If you are coming straight from the [Quick Start](../README.md#quick-start), the main file has a `.ts` extension - you need to change it to `.tsx` - otherwise you will not be able to run the application at all. You should additionally modify the `package.json` (or `deno.json` if you are using Deno) to reflect that change (e.g. instead of having `bun run --hot src/index.ts` in dev script, you should have `bun run --hot src/index.tsx`).
56
+ > **Note.** If you are coming straight from the [Quick Start](../README.md#quick-start), the main file has a `.ts` extension - you need to change it to `.tsx` - otherwise you will not be able to run the application at all. Change the scripts in `package.json` to match: on Node.js, as [above](#run-tsx-on-nodejs); on Bun, `bun run --hot src/index.tsx` instead of `bun run --hot src/index.ts`.
35
57
 
36
58
  `index.tsx`:
37
59
 
@@ -54,7 +76,7 @@ const Top: FC<{ messages: string[] }> = (props: {
54
76
  }) => {
55
77
  return (
56
78
  <Layout>
57
- <h1>Hello Acryl!</h1>
79
+ <h1>Acryl!</h1>
58
80
  <ul>
59
81
  {props.messages.map((message) => {
60
82
  return <li>{message}!!</li>
@@ -72,6 +94,31 @@ app.get('/', (c) => {
72
94
  export default app
73
95
  ```
74
96
 
97
+ ## Without compiling: `jsx()`
98
+
99
+ A `.ts` file can build the same nodes without JSX syntax and without the register flag: write the call that JSX compiles to.
100
+ `jsx(tag, props, ...children)` takes the children after the props, and a node renders with `toString()`. The `` html`...` `` template
101
+ from `acrylblends/html` is the other build-free choice.
102
+
103
+ ```ts
104
+ import { Acryl } from 'acrylblends'
105
+ import { jsx } from 'acrylblends/jsx'
106
+ import type { PropsWithChildren } from 'acrylblends/jsx'
107
+
108
+ const Layout = (props: PropsWithChildren<{ title: string }>) =>
109
+ jsx('html', { lang: 'en' }, jsx('head', null, jsx('title', null, props.title)), jsx('body', null, props.children))
110
+
111
+ const About = () =>
112
+ jsx(Layout, { title: 'About' }, jsx('h1', null, 'About'), jsx('p', { class: 'lead' }, 'Rendered on the server.'))
113
+
114
+ const app = new Acryl()
115
+ app.get('/about', (c) => c.html(jsx(About, null).toString()))
116
+
117
+ export default app
118
+ ```
119
+
120
+ Pass the children as arguments after the props. A `children` entry inside the props object is not rendered by this form.
121
+
75
122
  ## Metadata hoisting
76
123
 
77
124
  You can write document metadata tags such as `<title>`, `<link>`, and `<meta>` directly inside your components. These tags will be automatically hoisted to the `<head>` section of the document. This is especially useful when the `<head>` element is rendered far from the component that determines the appropriate metadata.
@@ -95,7 +95,7 @@ app.use(
95
95
  '/auth/*',
96
96
  basicAuth({
97
97
  username: 'acrylblends',
98
- password: 'acoolproject',
98
+ password: process.env.BASIC_AUTH_PASSWORD!,
99
99
  })
100
100
  )
101
101
  ```
@@ -37,7 +37,7 @@ app.get('/cookie', (c) => {
37
37
 
38
38
  ```ts
39
39
  app.get('/signed-cookie', (c) => {
40
- const secret = 'secret' // make sure it's a large enough string to be secure
40
+ const secret = process.env.COOKIE_SECRET! // a long random string, kept out of the source
41
41
 
42
42
  await setSignedCookie(c, 'cookie_name0', 'cookie_value', secret)
43
43
  const fortuneCookie = await getSignedCookie(
@@ -1,8 +1,5 @@
1
1
  # css Helper
2
2
 
3
- > **Not yet available.** The CSS helper (`acrylblends/css`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  The CSS helper - `acrylblends/css` - is Acryl's built-in CSS in JS(X).
7
4
 
8
5
  You can write CSS in JSX in a JavaScript template literal named `css`. The return value of `css` will be the class name, which is set to the value of the class attribute. The `<Style />` component will then contain the value of the CSS.
@@ -1,8 +1,5 @@
1
1
  # Dev Helper
2
2
 
3
- > **Not yet available.** The dev helper (`acrylblends/dev`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  Dev Helper provides useful methods you can use in development.
7
4
 
8
5
  ```ts
@@ -35,7 +35,7 @@ const payload = {
35
35
  role: 'admin',
36
36
  exp: Math.floor(Date.now() / 1000) + 60 * 5, // Token expires in 5 minutes
37
37
  }
38
- const secret = 'mySecretKey'
38
+ const secret = process.env.JWT_SECRET!
39
39
  const token = await sign(payload, secret)
40
40
  ```
41
41
 
@@ -1,8 +1,5 @@
1
1
  # Proxy Helper
2
2
 
3
- > **Not yet available.** The proxy helper (`acrylblends/proxy`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  Proxy Helper provides useful functions when using Acryl application as a (reverse) proxy.
7
4
 
8
5
  ## Import
@@ -1,8 +1,5 @@
1
1
  # SSG Helper
2
2
 
3
- > **Not yet available.** Static site generation (`acrylblends/ssg`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  SSG Helper generates a static site from your Acryl application. It will retrieve the contents of registered routes and save them as static files.
7
4
 
8
5
  ## Usage
@@ -237,7 +234,7 @@ Plugins allow you to extend the functionality of the static site generation proc
237
234
 
238
235
  ### Default Plugin
239
236
 
240
- By default, `toSSG` uses `defaultPlugin` which skips non-200 status responses (like redirects, errors, or 404s). This prevents generating files for unsuccessful responses.
237
+ By default, `toSSG` uses `defaultPlugin()`, which skips non-200 status responses (like redirects, errors, or 404s). This prevents generating files for unsuccessful responses.
241
238
 
242
239
  ```ts
243
240
  import { toSSG, defaultPlugin } from 'acrylblends/ssg'
@@ -246,14 +243,14 @@ import { toSSG, defaultPlugin } from 'acrylblends/ssg'
246
243
  toSSG(app, fs)
247
244
 
248
245
  // Equivalent to:
249
- toSSG(app, fs, { plugins: [defaultPlugin] })
246
+ toSSG(app, fs, { plugins: [defaultPlugin()] })
250
247
  ```
251
248
 
252
249
  If you specify custom plugins, `defaultPlugin` is **not** automatically included. To keep the default behavior while adding custom plugins, explicitly include it:
253
250
 
254
251
  ```ts
255
252
  toSSG(app, fs, {
256
- plugins: [defaultPlugin, myCustomPlugin],
253
+ plugins: [defaultPlugin(), myCustomPlugin],
257
254
  })
258
255
  ```
259
256
 
@@ -21,7 +21,7 @@ app.use(
21
21
  '/auth/*',
22
22
  basicAuth({
23
23
  username: 'acrylblends',
24
- password: 'acoolproject',
24
+ password: process.env.BASIC_AUTH_PASSWORD!,
25
25
  })
26
26
  )
27
27
 
@@ -41,7 +41,7 @@ app.get('/auth/page', (c) => {
41
41
 
42
42
  app.delete(
43
43
  '/auth/page',
44
- basicAuth({ username: 'acrylblends', password: 'acoolproject' }),
44
+ basicAuth({ username: 'acrylblends', password: process.env.BASIC_AUTH_PASSWORD! }),
45
45
  (c) => {
46
46
  return c.text('Page deleted')
47
47
  }
@@ -100,7 +100,7 @@ app.use(
100
100
  '/auth/*',
101
101
  basicAuth({
102
102
  username: 'acrylblends',
103
- password: 'acoolproject',
103
+ password: process.env.BASIC_AUTH_PASSWORD!,
104
104
  onAuthSuccess: (c, username) => {
105
105
  c.set('username', username)
106
106
  },
@@ -129,18 +129,18 @@ app.use(
129
129
  basicAuth(
130
130
  {
131
131
  username: 'acrylblends',
132
- password: 'acoolproject',
132
+ password: process.env.BASIC_AUTH_PASSWORD!,
133
133
  // Define other params in the first object
134
134
  realm: 'www.example.com',
135
135
  },
136
136
  {
137
137
  username: 'acrylblends-admin',
138
- password: 'super-secure',
138
+ password: process.env.ADMIN_PASSWORD!,
139
139
  // Cannot redefine other params here
140
140
  },
141
141
  {
142
142
  username: 'acrylblends-user-1',
143
- password: 'a-secret',
143
+ password: process.env.USER_1_PASSWORD!,
144
144
  // Or here
145
145
  }
146
146
  )
@@ -1,8 +1,5 @@
1
1
  # Cache Middleware
2
2
 
3
- > **Not yet available.** The cache middleware (`acrylblends/cache`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  The Cache middleware uses the Web Standards' [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
7
4
 
8
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.
@@ -39,7 +39,7 @@ app.use(
39
39
  )
40
40
  )
41
41
 
42
- app.get('/', (c) => c.text('Hello Acryl!'))
42
+ app.get('/', (c) => c.text('Acryl!'))
43
43
  ```
44
44
 
45
45
  ### some
@@ -28,7 +28,7 @@ app.use(
28
28
  })
29
29
  )
30
30
 
31
- app.get('/', (c) => c.text('Hello Acryl!'))
31
+ app.get('/', (c) => c.text('Acryl!'))
32
32
  ```
33
33
 
34
34
  Pass the `getConninfo` from the [ConnInfo helper](../../helpers/conninfo.md) appropriate for your environment as the first argument of `ipRestriction`. For a runtime whose adapter is not yet available there is no `getConnInfo` to pass yet.
@@ -1,8 +1,5 @@
1
1
  # JSX Renderer Middleware
2
2
 
3
- > **Not yet available.** The JSX renderer middleware (`acrylblends/jsx-renderer`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  JSX Renderer Middleware allows you to set up the layout when rendering JSX with the `c.render()` function, without the need for using `c.setRenderer()`. Additionally, it enables access to instances of Context within components through the use of `useRequestContext()`.
7
4
 
8
5
  ## Import
@@ -15,7 +15,7 @@ import { logger } from 'acrylblends/logger'
15
15
  const app = new Acryl()
16
16
 
17
17
  app.use(logger())
18
- app.get('/', (c) => c.text('Hello Acryl!'))
18
+ app.get('/', (c) => c.text('Acryl!'))
19
19
  ```
20
20
 
21
21
  ## Logging Details
@@ -1,8 +1,5 @@
1
1
  # Mount Middleware
2
2
 
3
- > **Not yet available.** The `acrylblends/mount` subpath (an app can still call `app.mount()`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
4
-
5
-
6
3
  The Mount Middleware lets you mount an application built with another framework into your Acryl application.
7
4
 
8
5
  ## Import
@@ -2,8 +2,8 @@
2
2
  "source": {
3
3
  "repo": "acryldev/acryl",
4
4
  "path": "docs/framework",
5
- "commit": "5743482e4199ded676a77f594bce15fd4aa0ca2f"
5
+ "commit": "7bba3ed0420d87e61d8256fa60614d22af734c8f"
6
6
  },
7
- "files": 87,
8
- "digest": "sha256:31bf1c46fbe74d7d2664b3b935a18bd916b8cab1b9ee9c2b006b8a75617a04f6"
7
+ "files": 88,
8
+ "digest": "sha256:1f7e213ee1fd287a98b5bead17a0b197311fcd75013ddf5912ca4a476657cece"
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-acrylblends",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Create an Acryl app: npm create acrylblends@latest",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "scaffold"
38
38
  ],
39
39
  "dependencies": {
40
- "acrylblends": "^0.6.1"
40
+ "acrylblends": "^0.7.0"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public"
@@ -7,9 +7,9 @@
7
7
  "node": ">=22.19.0"
8
8
  },
9
9
  "scripts": {
10
- "dev": "node --watch src/index.ts",
11
- "start": "node src/index.ts",
12
- "test": "node --test",
10
+ "dev": "node --watch --import acrylblends/node-server/register src/index.ts",
11
+ "start": "node --import acrylblends/node-server/register src/index.ts",
12
+ "test": "node --import acrylblends/node-server/register --test \"test/**/*.test.{ts,tsx}\"",
13
13
  "check": "tsc --noEmit"
14
14
  },
15
15
  "dependencies": {
@@ -1,7 +1,10 @@
1
1
  import { __CLASS__ } from '__PACKAGE__'
2
+ import { aboutPage } from './page.tsx'
2
3
 
3
4
  const app = new __CLASS__()
4
5
 
5
6
  app.get('/', (c) => c.text('__CLASS__!'))
6
7
 
8
+ app.get('/about', (c) => c.html(aboutPage()))
9
+
7
10
  export default app
@@ -0,0 +1,14 @@
1
+ /** A page written in JSX: Node.js runs it through `acrylblends/node-server/register` (see the scripts in package.json), which compiles .tsx with this app's own typescript. */
2
+ const About = () => (
3
+ <main>
4
+ <h1>About</h1>
5
+ <p>Rendered on the server with __PACKAGE__/jsx.</p>
6
+ </main>
7
+ )
8
+
9
+ export const aboutPage = (): string => (<About />).toString()
10
+
11
+ /** Throws on purpose: the template's test checks that the stack names this file, so a failure in your own pages points at the .tsx line. */
12
+ export const failingPage = (): string => {
13
+ throw new Error('a page failed')
14
+ }
@@ -7,3 +7,24 @@ test('GET / answers __CLASS__!', async () => {
7
7
  assert.equal(response.status, 200)
8
8
  assert.equal(await response.text(), '__CLASS__!')
9
9
  })
10
+
11
+ test('GET /about renders the JSX page', async () => {
12
+ const response = await app.request('/about')
13
+ assert.equal(response.status, 200)
14
+ assert.match(response.headers.get('content-type') ?? '', /html/)
15
+ assert.match(
16
+ await response.text(),
17
+ /<main><h1>About<\/h1><p>Rendered on the server with __PACKAGE__\/jsx\.<\/p><\/main>/,
18
+ )
19
+ })
20
+
21
+ test('an error thrown in a .tsx file names the .tsx line in its stack', async () => {
22
+ const { failingPage } = await import('../src/page.tsx')
23
+ let stack = ''
24
+ try {
25
+ failingPage()
26
+ } catch (error) {
27
+ stack = String((error as Error).stack)
28
+ }
29
+ assert.match(stack, /page\.tsx:\d+/)
30
+ })
@@ -10,7 +10,9 @@
10
10
  "allowImportingTsExtensions": true,
11
11
  "noEmit": true,
12
12
  "skipLibCheck": true,
13
- "types": ["node"]
13
+ "types": ["node"],
14
+ "jsx": "react-jsx",
15
+ "jsxImportSource": "__PACKAGE__/jsx"
14
16
  },
15
17
  "include": ["src", "test"]
16
18
  }