create-acrylblends 0.1.3 → 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.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/guides/jsx.md +51 -25
- package/docs/framework/upstream.json +2 -2
- package/package.json +2 -2
- package/templates/nodejs/package.json +3 -3
- package/templates/nodejs/src/app.ts +3 -0
- package/templates/nodejs/src/page.tsx +14 -0
- package/templates/nodejs/test/app.test.ts +21 -0
- package/templates/nodejs/tsconfig.json +3 -1
package/dist/index.js
CHANGED
|
@@ -169,7 +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
|
|
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`."}
|
|
173
173
|
|
|
174
174
|
## Before you say you are done
|
|
175
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 const jsxNote =\n template === 'nodejs'\n ? \"Node.js strips TypeScript types and does not compile JSX, so a `.tsx` file will not load. Write the call JSX compiles to instead: `jsx('h1', { class: 'title' }, 'Hello')` from `'acrylblends/jsx'`, with the children after the props as further arguments (`jsx(tag, props, ...children)`); render a node with `.toString()` and return it with `c.html(...)`. See `docs/framework/guides/jsx.md`.\"\n : \"Write the call JSX compiles to rather than JSX syntax, so the same file runs on Node.js too: `jsx('h1', { class: 'title' }, 'Hello')` from `'acrylblends/jsx'`, with the children after the props as further arguments (`jsx(tag, props, ...children)`); render a node with `.toString()` and return it with `c.html(...)`. 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 syntax does not run in this app.** ${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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"}
|
|
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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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Generated copy
|
|
2
2
|
|
|
3
|
-
This folder is a copy of `docs/framework` of acryldev/acryl at commit `
|
|
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.
|
|
@@ -4,30 +4,6 @@ You can write HTML with JSX syntax with `acrylblends/jsx`.
|
|
|
4
4
|
|
|
5
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.
|
|
6
6
|
|
|
7
|
-
## Node.js runs `jsx()`, not JSX syntax
|
|
8
|
-
|
|
9
|
-
The apps `npm create acrylblends@latest` makes run their TypeScript on Node.js by stripping the types (no build step). That does **not** compile JSX: a `.tsx` file does not load. Either compile the file first (`tsc`
|
|
10
|
-
or a bundler, with the settings below), or write the call that JSX compiles to. `jsx(tag, props, ...children)` takes the children after the props, and a node renders with `toString()`:
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { Acryl } from 'acrylblends'
|
|
14
|
-
import { jsx } from 'acrylblends/jsx'
|
|
15
|
-
import type { PropsWithChildren } from 'acrylblends/jsx'
|
|
16
|
-
|
|
17
|
-
const Layout = (props: PropsWithChildren<{ title: string }>) =>
|
|
18
|
-
jsx('html', { lang: 'en' }, jsx('head', null, jsx('title', null, props.title)), jsx('body', null, props.children))
|
|
19
|
-
|
|
20
|
-
const About = () =>
|
|
21
|
-
jsx(Layout, { title: 'About' }, jsx('h1', null, 'About'), jsx('p', { class: 'lead' }, 'Rendered on the server.'))
|
|
22
|
-
|
|
23
|
-
const app = new Acryl()
|
|
24
|
-
app.get('/about', (c) => c.html(jsx(About, null).toString()))
|
|
25
|
-
|
|
26
|
-
export default app
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Pass the children as arguments after the props. A `children` entry inside the props object is not rendered by this form.
|
|
30
|
-
|
|
31
7
|
## Settings
|
|
32
8
|
|
|
33
9
|
To use JSX, modify the `tsconfig.json`:
|
|
@@ -50,9 +26,34 @@ Alternatively, use the pragma directives:
|
|
|
50
26
|
/** @jsxImportSource acrylblends/jsx */
|
|
51
27
|
```
|
|
52
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
|
+
|
|
53
54
|
## Usage
|
|
54
55
|
|
|
55
|
-
> **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.
|
|
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`.
|
|
56
57
|
|
|
57
58
|
`index.tsx`:
|
|
58
59
|
|
|
@@ -93,6 +94,31 @@ app.get('/', (c) => {
|
|
|
93
94
|
export default app
|
|
94
95
|
```
|
|
95
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
|
+
|
|
96
122
|
## Metadata hoisting
|
|
97
123
|
|
|
98
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.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"source": {
|
|
3
3
|
"repo": "acryldev/acryl",
|
|
4
4
|
"path": "docs/framework",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "7bba3ed0420d87e61d8256fa60614d22af734c8f"
|
|
6
6
|
},
|
|
7
7
|
"files": 88,
|
|
8
|
-
"digest": "sha256:
|
|
8
|
+
"digest": "sha256:1f7e213ee1fd287a98b5bead17a0b197311fcd75013ddf5912ca4a476657cece"
|
|
9
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-acrylblends",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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": {
|
|
@@ -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
|
+
})
|