acrylblends 0.6.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +34 -3
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/README.md +1 -1
  5. package/docs/framework/UPSTREAM.md +1 -1
  6. package/docs/framework/api/summary.md +1 -1
  7. package/docs/framework/getting-started/desktop.md +1 -1
  8. package/docs/framework/guides/forms-and-sessions.md +231 -0
  9. package/docs/framework/guides/jsx-dom.md +6 -6
  10. package/docs/framework/guides/jsx.md +51 -25
  11. package/docs/framework/guides/validation.md +1 -1
  12. package/docs/framework/helpers/cookie.md +1 -1
  13. package/docs/framework/upstream.json +3 -3
  14. package/lib/{base-ddiVmiE7.js → base-NSN_SNv-.js} +1 -0
  15. package/lib/{components-DdI7-ns7.js → components-JXVm45kK.js} +1 -1
  16. package/lib/{components-0o0CBYNv.js → components-cEW3ovK0.js} +3 -3
  17. package/lib/{hooks-BMw6BAyP.js → hooks-w72iLeFR.js} +1 -1
  18. package/lib/jsx/dom/client.js +1 -1
  19. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  20. package/lib/jsx/dom/jsx-runtime.js +1 -1
  21. package/lib/jsx/dom/server.js +2 -2
  22. package/lib/jsx/dom.js +4 -4
  23. package/lib/jsx/jsx-dev-runtime.js +1 -1
  24. package/lib/jsx/jsx-runtime.js +1 -1
  25. package/lib/jsx/streaming.js +1 -1
  26. package/lib/{jsx-dev-runtime-D18Y35TI.js → jsx-dev-runtime-CWLRA2F3.js} +2 -2
  27. package/lib/jsx-renderer.js +2 -2
  28. package/lib/jsx.js +4 -4
  29. package/lib/node-server/register.d.ts +1 -0
  30. package/lib/node-server/register.js +73 -0
  31. package/package.json +5 -1
  32. package/plugins/acryl-inspect/upstream.json +1 -1
  33. package/plugins/acryl-node-server/src/register.ts +86 -0
  34. package/plugins/acryl-node-server/upstream.json +5 -3
  35. package/plugins/acryl-router/upstream.json +1 -1
  36. package/plugins/acryl-router-http/upstream.json +1 -1
  37. package/plugins/acryl-state/upstream.json +1 -1
  38. package/plugins/desktop/main.ts +4 -1
  39. package/plugins/desktop/plugin.json +1 -1
  40. package/plugins/desktop/upstream.json +1 -1
  41. package/plugins/desktop-web/main.ts +3 -1
  42. package/plugins/desktop-web/plugin.json +1 -1
  43. package/plugins/desktop-web/upstream.json +1 -1
  44. package/shared/AGENTS.core.md +1 -2
  45. package/templates/agent/package.json +7 -3
  46. package/templates/agent/tsconfig.json +3 -1
  47. package/templates/blank/package.json +7 -3
  48. package/templates/blank/src/about.tsx +14 -0
  49. package/templates/blank/src/routes.ts +2 -0
  50. package/templates/blank/test/app.test.ts +15 -0
  51. package/templates/blank/tsconfig.json +3 -1
  52. package/templates/todo/package.json +7 -3
  53. package/templates/todo/tsconfig.json +3 -1
  54. package/templates/todo/upstream.json +1 -1
package/README.md CHANGED
@@ -275,10 +275,10 @@ See [Node.js](https://github.com/acryldev/acryl/blob/main/docs/framework/getting
275
275
  | `acrylblends/cache`, `acrylblends/mount` | the cache middleware and mounting another framework's fetch handler |
276
276
  | `acrylblends/ssg` | static site generation: `toSSG` writes every route as a file ([SSG helper](https://github.com/acryldev/acryl/blob/main/docs/framework/helpers/ssg.md)) |
277
277
  | `acrylblends/proxy`, `acrylblends/dev` | the proxy helper and the dev helpers (`showRoutes`, `inspectRoutes`, `getRouterName`) |
278
- | `acrylblends/jsx` | JSX on the server: `jsxImportSource: 'acrylblends/jsx'`, with `jsx/streaming`, and `acrylblends/jsx-renderer` for a layout behind `c.render` ([JSX guide](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/jsx.md)) |
278
+ | `acrylblends/jsx` | JSX on the server: `jsxImportSource: 'acrylblends/jsx'`, `.tsx` files run on Node with `node --import acrylblends/node-server/register`, with `jsx/streaming`, and `acrylblends/jsx-renderer` for a layout behind `c.render` ([JSX guide](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/jsx.md)) |
279
279
  | `acrylblends/jsx/dom` | JSX in the browser: hooks and `render` (needs the DOM library) ([client components](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/jsx-dom.md)) |
280
280
  | `acrylblends/css` | CSS in JSX: `css`, `cx`, `keyframes`, `Style` ([CSS helper](https://github.com/acryldev/acryl/blob/main/docs/framework/helpers/css.md)) |
281
- | `acrylblends/node-server` | `serve`, `createAdaptorServer`, `getRequestListener`; with `node-server/serve-static` and `node-server/conninfo` |
281
+ | `acrylblends/node-server` | `serve`, `createAdaptorServer`, `getRequestListener`; with `node-server/serve-static`, `node-server/conninfo`, and `node-server/register` (compiles `.tsx` with the app's `typescript`) |
282
282
 
283
283
  Every subpath is listed in the package's `exports`. The adapters of other runtimes are being ported. The docs mark them "not yet available" until they ship
284
284
  ([FAQ](https://github.com/acryldev/acryl/blob/main/docs/framework/guides/faq.md)).
package/dist/index.js CHANGED
@@ -509,6 +509,37 @@ function listPlugins(io) {
509
509
  return 0;
510
510
  }
511
511
  //#endregion
512
+ //#region src/spawn.ts
513
+ /**
514
+ * How to start a package manager without the shell option (Node warns about passing args to a shell, DEP0190, and a `.cmd` shim cannot be spawned directly).
515
+ * Off Windows the command runs as it is. On Windows npm runs as `node npm-cli.js` (the one npm is running from, else the one beside `node`); any other
516
+ * manager goes through `cmd.exe /d /s /c <name>.cmd`, whose arguments stay an array.
517
+ */
518
+ function packageManagerInvocation(command, args, platform = process.platform, env = process.env, exists = existsSync, execPath = process.execPath) {
519
+ if (platform !== "win32") return {
520
+ file: command,
521
+ args: [...args]
522
+ };
523
+ if (command === "npm") {
524
+ const running = env.npm_execpath;
525
+ const cli = running !== void 0 && /npm-cli\.c?js$/.test(running) ? running : join(dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
526
+ if (exists(cli)) return {
527
+ file: execPath,
528
+ args: [cli, ...args]
529
+ };
530
+ }
531
+ return {
532
+ file: env.ComSpec ?? "cmd.exe",
533
+ args: [
534
+ "/d",
535
+ "/s",
536
+ "/c",
537
+ `${command}.cmd`,
538
+ ...args
539
+ ]
540
+ };
541
+ }
542
+ //#endregion
512
543
  //#region src/io.ts
513
544
  /** The real terminal and `npm install`. */
514
545
  function processIo() {
@@ -529,10 +560,10 @@ function processIo() {
529
560
  }
530
561
  } : void 0,
531
562
  install: (directory) => new Promise((done) => {
532
- const child = spawn("npm", ["install"], {
563
+ const { file, args } = packageManagerInvocation("npm", ["install"]);
564
+ const child = spawn(file, args, {
533
565
  cwd: directory,
534
- stdio: "inherit",
535
- shell: process.platform === "win32"
566
+ stdio: "inherit"
536
567
  });
537
568
  child.on("error", () => done(1));
538
569
  child.on("exit", (code) => done(code ?? 1));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["parse"],"sources":["../src/library.ts","../src/project.ts","../src/templates.ts","../src/init.ts","../src/add.ts","../src/io.ts","../src/cli.ts"],"sourcesContent":["import { cpSync, existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** One entry of the plugin library: `plugins/<name>/plugin.json` in this repository. */\nexport interface LibraryPlugin {\n readonly name: string\n readonly description: string\n /** Library plugins this one imports; they are copied with it. */\n readonly requires: readonly string[]\n /** npm packages an app needs at run time for this plugin. */\n readonly dependencies: Readonly<Record<string, string>>\n /** npm packages an app needs only to develop or run this plugin's tooling (for example `electron` for the desktop shell). */\n readonly devDependencies?: Readonly<Record<string, string>>\n /** `package.json` scripts the plugin adds; a script the app already has is kept. */\n readonly scripts?: Readonly<Record<string, string>>\n /** Lines the app's `.gitignore` needs for this plugin (added when missing, if the app has a `.gitignore`). */\n readonly gitignore?: readonly string[]\n /** How to mount it, shown after `acrylblends add`. */\n readonly mount?: string\n /**\n * Paths inside the plugin folder that are not copied into an app (for example a React hook an app without React cannot type-check, or colocated test\n * files). A pattern is an exact path or a glob: `*` matches within one folder name, `**` across folders, `?` one character; `**` followed by a slash also matches no folder.\n */\n readonly omit?: readonly string[]\n}\n\n/** A glob as a regular expression: `*` within a name, `**` across folders, `**` plus a slash may match no folder, `?` one character. */\nfunction globToRegExp(glob: string): RegExp {\n let source = ''\n for (let i = 0; i < glob.length; i += 1) {\n const c = glob[i] as string\n if (c === '*' && glob[i + 1] === '*') {\n if (glob[i + 2] === '/') {\n source += '(?:.*/)?'\n i += 2\n } else {\n source += '.*'\n i += 1\n }\n } else if (c === '*') source += '[^/]*'\n else if (c === '?') source += '[^/]'\n else source += c.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${source}$`)\n}\n\n/** Whether a path inside a plugin folder (relative, any separator) is named by one of its `omit` patterns. */\nexport function isOmitted(inside: string, patterns: readonly string[] | undefined): boolean {\n if (!patterns || patterns.length === 0) return false\n const path = inside.split(sep).join('/')\n return patterns.some((pattern) =>\n /[*?]/.test(pattern) ? globToRegExp(pattern).test(path) : pattern === path,\n )\n}\n\n/** Files of a library plugin that stay in the repository: its manifest and its tests. */\nconst LIBRARY_ONLY = new Set(['plugin.json', 'test', 'tests'])\n\nexport class LibraryError extends Error {\n override name = 'LibraryError'\n}\n\n/** The library folder (`plugins/` at the repository root): walk up from this module, in source or in the build. */\nexport function libraryRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'plugins')\n if (existsSync(join(candidate, 'instance', 'plugin.json'))) return candidate\n const parent = dirname(dir)\n if (parent === dir)\n throw new LibraryError('acrylblends cannot find its plugin library (plugins/)')\n dir = parent\n }\n}\n\n/** Every plugin in the library, by name. */\nexport function readLibrary(root: string = libraryRoot()): Map<string, LibraryPlugin> {\n const plugins = new Map<string, LibraryPlugin>()\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n const manifest = join(root, entry.name, 'plugin.json')\n if (!entry.isDirectory() || !existsSync(manifest)) continue\n const plugin = JSON.parse(readFileSync(manifest, 'utf8')) as LibraryPlugin\n if (plugin.name !== entry.name)\n throw new LibraryError(`plugins/${entry.name}/plugin.json names \"${plugin.name}\"`)\n plugins.set(plugin.name, {\n ...plugin,\n requires: plugin.requires ?? [],\n dependencies: plugin.dependencies ?? {},\n })\n }\n return plugins\n}\n\n/** The plugins to copy for `names`: each one after everything it requires, no duplicates. */\nexport function closure(\n names: readonly string[],\n library: Map<string, LibraryPlugin>,\n): LibraryPlugin[] {\n const ordered: LibraryPlugin[] = []\n const visiting = new Set<string>()\n const visit = (name: string, trail: string[]) => {\n if (ordered.some((plugin) => plugin.name === name)) return\n const plugin = library.get(name)\n if (!plugin)\n throw new LibraryError(\n `no plugin \"${name}\" in the library${trail.length ? ` (required by ${trail.at(-1)})` : ''}`,\n )\n if (visiting.has(name))\n throw new LibraryError(\n `plugins require each other in a cycle: ${[...trail, name].join(' -> ')}`,\n )\n visiting.add(name)\n for (const required of plugin.requires) visit(required, [...trail, name])\n visiting.delete(name)\n ordered.push(plugin)\n }\n for (const name of names) visit(name, [])\n return ordered\n}\n\n/** Copy one library plugin's source into `<app>/src/plugins/<name>/`. Throws when that folder already exists. */\nexport function copyPlugin(\n plugin: LibraryPlugin,\n appDir: string,\n root: string = libraryRoot(),\n): string {\n const target = join(appDir, 'src', 'plugins', plugin.name)\n if (existsSync(target))\n throw new LibraryError(\n `src/plugins/${plugin.name} already exists; remove it first to copy a fresh one`,\n )\n const source = join(root, plugin.name)\n cpSync(source, target, {\n recursive: true,\n filter: (path) => {\n const inside = path.slice(source.length + 1)\n return (\n !(dirname(path) === source && LIBRARY_ONLY.has(inside)) && !isOmitted(inside, plugin.omit)\n )\n },\n })\n return target\n}\n","import {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from 'node:fs'\nimport { join } from 'node:path'\n\n/** Thrown when the target directory exists and is not empty. Nothing has been written. */\nexport class TargetNotEmptyError extends Error {\n constructor(readonly target: string) {\n super(\n `refusing to write into \"${target}\": the directory is not empty. Choose an empty or new directory.`,\n )\n this.name = 'TargetNotEmptyError'\n }\n}\n\n/** A project name npm accepts: lower case letters, digits and dashes, starting with a letter. */\nexport function isValidName(name: string): boolean {\n return /^[a-z][a-z0-9-]*$/.test(name) && name.length <= 214\n}\n\n/** A valid default project name derived from a directory path. */\nexport function nameFrom(directory: string): string {\n const base = directory.split(/[\\\\/]/).filter(Boolean).pop() ?? ''\n const cleaned = base\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/^[^a-z]+/, '')\n .replace(/-+$/, '')\n return cleaned || 'my-app'\n}\n\n/** Refuse a target that exists and is not an empty directory. A missing target is fine: it is created. */\nexport function assertTargetFree(target: string): void {\n if (!existsSync(target)) return\n if (!statSync(target).isDirectory() || readdirSync(target).length > 0)\n throw new TargetNotEmptyError(target)\n}\n\n/**\n * Copy a template into `target`, replacing `__APP_NAME__` with `name`. Checks the target before writing anything.\n *\n * @returns the copied files, relative to `target`\n */\nexport function copyTemplate(source: string, target: string, name: string): string[] {\n assertTargetFree(target)\n mkdirSync(target, { recursive: true })\n const written: string[] = []\n const walk = (from: string, to: string, prefix: string) => {\n for (const entry of readdirSync(from, { withFileTypes: true })) {\n if (\n entry.name === 'node_modules' ||\n (prefix === '' &&\n (entry.name === TEMPLATE_MANIFEST ||\n entry.name === 'upstream.json' ||\n entry.name === 'AGENTS.md'))\n )\n continue\n const sourcePath = join(from, entry.name)\n // npm and git treat a file named .gitignore specially, so templates keep it as `gitignore` (and `dockerignore`).\n const targetName =\n prefix === '' && (entry.name === 'gitignore' || entry.name === 'dockerignore')\n ? `.${entry.name}`\n : entry.name\n const targetPath = join(to, targetName)\n if (entry.isDirectory()) {\n mkdirSync(targetPath, { recursive: true })\n walk(sourcePath, targetPath, `${prefix}${entry.name}/`)\n continue\n }\n const text = readFileSync(sourcePath, 'utf8')\n if (text.includes('__APP_NAME__'))\n writeFileSync(targetPath, text.replaceAll('__APP_NAME__', name))\n else cpSync(sourcePath, targetPath)\n written.push(`${prefix}${targetName}`)\n }\n }\n walk(source, target, '')\n return written\n}\n\n/** A template's own manifest, `template.json`: the library plugins it is built from. Never copied (nor is `upstream.json`, the record of what was synced from ACRYL, nor the template's `AGENTS.md`, which `writeAgentFiles` joins to the shared core). */\nexport const TEMPLATE_MANIFEST = 'template.json'\n\n/** The address a template's desktop window opens on, when it is not the root (`template.json`: `desktopPath`). */\nexport function readTemplateDesktopPath(templateDir: string): string | undefined {\n const file = join(templateDir, TEMPLATE_MANIFEST)\n if (!existsSync(file)) return undefined\n return (JSON.parse(readFileSync(file, 'utf8')) as { desktopPath?: string }).desktopPath\n}\n\n/** Record where the desktop window opens in package.json (`acrylblends.desktopPath`), read by `src/plugins/desktop/main.ts`. */\nexport function writeDesktopPath(appDir: string, path: string): void {\n const file = join(appDir, 'package.json')\n const manifest = JSON.parse(readFileSync(file, 'utf8')) as {\n acrylblends?: Record<string, unknown>\n }\n manifest.acrylblends = { ...manifest.acrylblends, desktopPath: path }\n writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\\n`)\n}\n\nexport function readTemplatePlugins(templateDir: string): string[] {\n const file = join(templateDir, TEMPLATE_MANIFEST)\n if (!existsSync(file)) return []\n const { plugins } = JSON.parse(readFileSync(file, 'utf8')) as { plugins?: string[] }\n return plugins ?? []\n}\n\n/** Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. */\nexport function mergeDependencies(\n appDir: string,\n dependencies: Readonly<Record<string, string>>,\n): string[] {\n const file = join(appDir, 'package.json')\n const manifest = JSON.parse(readFileSync(file, 'utf8')) as {\n dependencies?: Record<string, string>\n }\n const current = manifest.dependencies ?? {}\n const added = Object.keys(dependencies).filter((name) => !(name in current))\n const merged = { ...dependencies, ...current }\n manifest.dependencies = Object.fromEntries(\n Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)),\n )\n writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\\n`)\n return added\n}\n\n/**\n * Add a plugin's development dependencies and scripts to an app's package.json: nothing the app already has is replaced. Returns the names of the development dependencies\n * and scripts added.\n */\nexport function mergeTooling(\n appDir: string,\n tooling: {\n devDependencies?: Readonly<Record<string, string>> | undefined\n scripts?: Readonly<Record<string, string>> | undefined\n },\n): { devDependencies: string[]; scripts: string[] } {\n const file = join(appDir, 'package.json')\n const manifest = JSON.parse(readFileSync(file, 'utf8')) as {\n devDependencies?: Record<string, string>\n scripts?: Record<string, string>\n }\n const dev = manifest.devDependencies ?? {}\n const scripts = manifest.scripts ?? {}\n const addedDev = Object.keys(tooling.devDependencies ?? {}).filter((name) => !(name in dev))\n const addedScripts = Object.keys(tooling.scripts ?? {}).filter((name) => !(name in scripts))\n manifest.devDependencies = Object.fromEntries(\n Object.entries({ ...tooling.devDependencies, ...dev }).sort(([a], [b]) => a.localeCompare(b)),\n )\n manifest.scripts = { ...tooling.scripts, ...scripts }\n writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\\n`)\n return { devDependencies: addedDev, scripts: addedScripts }\n}\n\n/** Add the lines a plugin needs to the app's `.gitignore` when it has one and they are missing. Returns the lines added. */\nexport function appendGitignore(appDir: string, lines: readonly string[] | undefined): string[] {\n const file = join(appDir, '.gitignore')\n if (!lines || lines.length === 0 || !existsSync(file)) return []\n const current = readFileSync(file, 'utf8')\n const present = new Set(current.split('\\n').map((line) => line.trim()))\n const missing = lines.filter((line) => !present.has(line))\n if (missing.length > 0)\n writeFileSync(\n file,\n `${current}${current.endsWith('\\n') || current === '' ? '' : '\\n'}${missing.join('\\n')}\\n`,\n )\n return missing\n}\n\n/** The generated app's guide for a coding agent: the shared core, then the template's own part. `CLAUDE.md` is only an import of it. */\nexport function writeAgentFiles(\n sharedDir: string,\n templateDir: string,\n target: string,\n name: string,\n): string[] {\n const core = readFileSync(join(sharedDir, 'AGENTS.core.md'), 'utf8')\n const own = existsSync(join(templateDir, 'AGENTS.md'))\n ? readFileSync(join(templateDir, 'AGENTS.md'), 'utf8')\n : ''\n writeFileSync(join(target, 'AGENTS.md'), `${core}${own}`.replaceAll('__APP_NAME__', name))\n writeFileSync(join(target, 'CLAUDE.md'), '@AGENTS.md\\n')\n return ['AGENTS.md', 'CLAUDE.md']\n}\n\n/**\n * Copy the framework's docs (a generated copy of ACRYL's `docs/framework`, already checked against its digest) into `<app>/docs/framework/`,\n * so a coding agent working in the app reads the API from the folder it is in. The generated-copy notes and the digest record stay behind.\n *\n * @returns the copied files, relative to `target`\n */\nexport function copyFrameworkDocs(source: string, target: string): string[] {\n const written: string[] = []\n const walk = (from: string, to: string, prefix: string) => {\n for (const entry of readdirSync(from, { withFileTypes: true })) {\n if (entry.name === 'upstream.json' || entry.name === 'UPSTREAM.md') continue\n const sourcePath = join(from, entry.name)\n const targetPath = join(to, entry.name)\n if (entry.isDirectory()) {\n mkdirSync(targetPath, { recursive: true })\n walk(sourcePath, targetPath, `${prefix}${entry.name}/`)\n continue\n }\n cpSync(sourcePath, targetPath)\n written.push(`docs/framework/${prefix}${entry.name}`)\n }\n }\n mkdirSync(join(target, 'docs', 'framework'), { recursive: true })\n walk(source, join(target, 'docs', 'framework'), '')\n return written\n}\n","import { existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** The templates: a blank Cordis app on the router, a todo list built only on the router and state, and the blank app with the offline agent. */\nexport const TEMPLATES = ['blank', 'todo', 'agent'] as const\nexport type Template = (typeof TEMPLATES)[number]\n\nexport function isTemplate(value: string): value is Template {\n return (TEMPLATES as readonly string[]).includes(value)\n}\n\n/**\n * The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build\n * (`dist/`), so walk up until a `templates` folder holds the `blank` template.\n */\nexport function templatesRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'templates')\n if (existsSync(join(candidate, 'blank'))) return candidate\n const parent = dirname(dir)\n if (parent === dir) throw new Error('acrylblends cannot find its templates folder')\n dir = parent\n }\n}\n\n/** Absolute path of a template folder. */\nexport function templateDir(template: Template): string {\n return join(templatesRoot(), template)\n}\n\n/** The text every generated app's `AGENTS.md` starts with (`shared/`, beside `templates/`). */\nexport function sharedDir(): string {\n return join(dirname(templatesRoot()), 'shared')\n}\n\n/**\n * The framework's docs, as ACRYL wrote them: `docs/framework` of the repository, or the copy `prepack` puts inside the package. The module sits at\n * different depths in source and in the build, so walk up until a `docs/framework` folder holds the README.\n */\nexport function frameworkDocsRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'docs', 'framework')\n if (existsSync(join(candidate, 'README.md'))) return candidate\n const parent = dirname(dir)\n if (parent === dir)\n throw new Error('acrylblends cannot find the framework docs (docs/framework)')\n dir = parent\n }\n}\n","import { resolve } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport type { Io } from './io'\nimport { closure, copyPlugin, readLibrary } from './library'\nimport {\n copyFrameworkDocs,\n copyTemplate,\n isValidName,\n mergeDependencies,\n mergeTooling,\n nameFrom,\n readTemplateDesktopPath,\n readTemplatePlugins,\n TargetNotEmptyError,\n writeAgentFiles,\n writeDesktopPath,\n} from './project'\nimport {\n frameworkDocsRoot,\n isTemplate,\n sharedDir,\n TEMPLATES,\n type Template,\n templateDir,\n} from './templates'\n\n/** A command line mistake. The CLI prints it with the help text and exits 2. */\nexport class UsageError extends Error {\n override name = 'UsageError'\n}\n\n/** What `init` was asked for. Anything left undefined is asked for on a terminal, or defaulted. */\nexport interface InitOptions {\n directory: string | undefined\n name: string | undefined\n template: Template | undefined\n install: boolean | undefined\n /** Also add the desktop shell (`add desktop`): an Electron window on the app's own server. */\n desktop: boolean\n yes: boolean\n}\n\n/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */\nexport function parseInitOptions(argv: readonly string[]): InitOptions {\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('init takes at most one directory')\n if (values.agent && values['no-agent'])\n throw new UsageError('use --agent or --no-agent, not both')\n if (values.template !== undefined && !isTemplate(values.template))\n throw new UsageError(`unknown template \"${values.template}\" (${TEMPLATES.join(', ')})`)\n const alias: Template | undefined = values.agent\n ? 'agent'\n : values['no-agent']\n ? 'blank'\n : undefined\n if (values.template !== undefined && alias !== undefined && values.template !== alias)\n throw new UsageError(\n `--template ${values.template} contradicts --${alias === 'agent' ? 'agent' : 'no-agent'}`,\n )\n return {\n directory: positionals[0],\n name: values.name,\n template: (values.template as Template | undefined) ?? alias,\n install: values.install,\n desktop: values.desktop === true,\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 name: { type: 'string' },\n template: { type: 'string' },\n agent: { type: 'boolean' },\n 'no-agent': { type: 'boolean' },\n install: { type: 'boolean' },\n desktop: { type: 'boolean' },\n yes: { type: 'boolean', short: 'y' },\n },\n })\n\nconst yes = (answer: string) => ['y', 'yes'].includes(answer.trim().toLowerCase())\n\nasync function askTemplate(ask: (question: string) => Promise<string>): Promise<Template> {\n const answer = (await ask(`Template (${TEMPLATES.join(', ')}) [blank]: `)).trim().toLowerCase()\n if (answer === '') return 'blank'\n if (isTemplate(answer)) return answer\n throw new UsageError(`unknown template \"${answer}\" (${TEMPLATES.join(', ')})`)\n}\n\n/** Scaffold a Cordis app. Returns the exit code: 0 done, 1 refused or failed. Never overwrites anything. */\nexport async function init(options: InitOptions, io: Io): Promise<number> {\n const relative = options.directory ?? '.'\n const directory = resolve(io.cwd, relative)\n const ask = options.yes ? undefined : io.ask\n\n let name = options.name ?? nameFrom(directory)\n if (options.name === undefined && ask)\n name = (await ask(`Project name (${name}): `)).trim() || name\n if (!isValidName(name)) {\n throw new UsageError(\n `\"${name}\" is not a valid project name (lower case letters, digits and dashes, starting with a letter)`,\n )\n }\n const template = options.template ?? (ask ? await askTemplate(ask) : 'blank')\n const install =\n options.install ?? (ask ? yes(await ask('Install dependencies with npm now? (y/N): ')) : false)\n\n let files: string[]\n try {\n files = copyTemplate(templateDir(template), directory, name)\n files.push(...writeAgentFiles(sharedDir(), templateDir(template), directory, name))\n files.push(...copyFrameworkDocs(frameworkDocsRoot(), directory))\n } catch (error) {\n if (!(error instanceof TargetNotEmptyError)) throw error\n io.stderr.write(`error: ${error.message}\\n`)\n return 1\n }\n const plugins = closure(\n [...readTemplatePlugins(templateDir(template)), ...(options.desktop ? ['desktop'] : [])],\n readLibrary(),\n )\n for (const plugin of plugins) {\n copyPlugin(plugin, directory)\n mergeDependencies(directory, plugin.dependencies)\n mergeTooling(directory, plugin)\n }\n const desktopPath = options.desktop ? readTemplateDesktopPath(templateDir(template)) : undefined\n if (desktopPath !== undefined) writeDesktopPath(directory, desktopPath)\n io.stdout.write(\n `created ${name} (${template}) in ${directory}: ${files.length} files, plugins ${plugins.map((plugin) => plugin.name).join(', ')}\\n`,\n )\n\n if (install && (await io.install(directory)) !== 0) {\n io.stderr.write('error: npm install failed; run it yourself in the new directory\\n')\n return 1\n }\n const steps = [\n relative === '.' ? '' : ` cd ${relative}\\n`,\n install ? '' : ' npm install\\n',\n ' npm run dev\\n npm test\\n',\n ]\n io.stdout.write(`\\nNext:\\n${steps.join('')}`)\n return 0\n}\n","import { cpSync, existsSync, readFileSync, statSync } from 'node:fs'\nimport { basename, join, resolve } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { UsageError } from './init'\nimport type { Io } from './io'\nimport { closure, copyPlugin, type LibraryPlugin, readLibrary } from './library'\nimport { appendGitignore, mergeDependencies, mergeTooling } from './project'\n\nexport interface AddOptions {\n readonly names: readonly string[]\n readonly directory: string | undefined\n}\n\n/** Parse `add <plugin...> [--dir <app>]`. */\nexport function parseAddOptions(argv: readonly string[]): AddOptions {\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 if (parsed.positionals.length === 0)\n throw new UsageError('add needs at least one plugin name (see \"acrylblends plugins\")')\n return { names: parsed.positionals, directory: parsed.values.dir }\n}\n\nconst parse = (argv: readonly string[]) =>\n parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: { dir: { type: 'string' } },\n })\n\nconst present = (appDir: string, name: string) => existsSync(join(appDir, 'src', 'plugins', name))\n\n/**\n * Copy library plugins into an existing app, with the plugins they require, and add their npm packages to its package.json.\n * Refuses a plugin the app already has. Returns the exit code.\n */\nexport async function add(options: AddOptions, io: Io): Promise<number> {\n const appDir = resolve(io.cwd, options.directory ?? '.')\n if (!existsSync(join(appDir, 'package.json')) || !existsSync(join(appDir, 'src'))) {\n io.stderr.write(\n `error: ${appDir} is not an app (no package.json and src/); run \"acrylblends init\" first\\n`,\n )\n return 1\n }\n // A path is an external plugin folder; anything else is a library plugin name.\n const external = options.names.filter((name) => /[\\\\/]/.test(name) || name.startsWith('.'))\n for (const folder of external) {\n const code = addExternal(resolve(io.cwd, folder), appDir, io)\n if (code !== 0) return code\n }\n // The desktop shell has two forms: the Blend app (it needs the `instance` plugin) and a plain Acryl app (`export default app`); `desktop` means the one that fits.\n const plain = !existsSync(join(appDir, 'src', 'plugins', 'instance'))\n const names = options.names\n .filter((name) => !external.includes(name))\n .map((name) => (name === 'desktop' && plain ? 'desktop-web' : name))\n if (names.length === 0) return 0\n const already = names.filter((name) => present(appDir, name))\n if (already.length > 0) {\n io.stderr.write(\n `error: the app already has ${already.map((name) => `src/plugins/${name}`).join(', ')}\\n`,\n )\n return 1\n }\n const toCopy: LibraryPlugin[] = closure(names, readLibrary()).filter(\n (plugin) => !present(appDir, plugin.name),\n )\n const addedDependencies: string[] = []\n for (const plugin of toCopy) {\n copyPlugin(plugin, appDir)\n addedDependencies.push(...mergeDependencies(appDir, plugin.dependencies))\n const tooling = mergeTooling(appDir, plugin)\n appendGitignore(appDir, plugin.gitignore)\n addedDependencies.push(...tooling.devDependencies)\n if (tooling.scripts.length > 0)\n io.stdout.write(\n `new scripts: ${tooling.scripts.map((name) => `npm run ${name}`).join(', ')}\\n`,\n )\n }\n io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(', ')} to ${appDir}\\n`)\n if (addedDependencies.length > 0)\n io.stdout.write(`new dependencies: ${addedDependencies.join(', ')} (run npm install)\\n`)\n for (const plugin of toCopy.filter((plugin) => names.includes(plugin.name))) {\n io.stdout.write(\n `\\n${plugin.name}: ${plugin.mount ?? `import it from './plugins/${plugin.name}/index.ts' and mount it in src/app.ts`}\\n`,\n )\n }\n return 0\n}\n\nconst PLUGIN_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/\n\n/**\n * Bring an external plugin folder (a package made with the builder's package_plugin, or any folder with an index.ts that exports a\n * Cordis plugin) into the app's `extensions/`, where the extensions plugin installs it like any other: checked, staged, started,\n * verified. Its runtime dependencies are not installed here; they are reported so the user adds them deliberately.\n */\nfunction addExternal(folder: string, appDir: string, io: Io): number {\n if (!existsSync(folder) || !statSync(folder).isDirectory()) {\n io.stderr.write(`error: ${folder} is not a folder\\n`)\n return 1\n }\n if (!['index.ts', 'index.js', 'index.mjs'].some((entry) => existsSync(join(folder, entry)))) {\n io.stderr.write(`error: ${folder} has no index.ts that exports a Cordis plugin\\n`)\n return 1\n }\n const manifestFile = join(folder, 'package.json')\n const manifest = existsSync(manifestFile)\n ? (JSON.parse(readFileSync(manifestFile, 'utf8')) as {\n name?: string\n dependencies?: Record<string, string>\n })\n : {}\n const name = (manifest.name ?? basename(folder)).replace(/^@[^/]+\\//, '')\n if (!PLUGIN_NAME.test(name)) {\n io.stderr.write(\n `error: \"${name}\" is not a plugin name (lowercase letters, digits and dashes)\\n`,\n )\n return 1\n }\n const target = join(appDir, 'extensions', name)\n if (existsSync(target)) {\n io.stderr.write(`error: the app already has extensions/${name}\\n`)\n return 1\n }\n cpSync(folder, target, {\n recursive: true,\n filter: (path) => !/[\\\\/](node_modules|\\.git)([\\\\/]|$)/.test(path.slice(folder.length)),\n })\n io.stdout.write(\n `added external plugin ${name} to ${join(appDir, 'extensions')}; it installs at the next start\\n`,\n )\n const dependencies = Object.keys(manifest.dependencies ?? {})\n if (dependencies.length > 0)\n io.stdout.write(\n `it uses ${dependencies.join(', ')}: add them to the app with npm install first\\n`,\n )\n return 0\n}\n\n/** `acrylblends plugins`: the library, one line per plugin. */\nexport function listPlugins(io: Io): number {\n for (const plugin of readLibrary().values()) {\n const requires = plugin.requires.length > 0 ? ` (requires ${plugin.requires.join(', ')})` : ''\n io.stdout.write(`${plugin.name.padEnd(12)} ${plugin.description}${requires}\\n`)\n }\n return 0\n}\n","import { spawn } from 'node:child_process'\nimport { createInterface } from 'node:readline/promises'\n\n/** Everything the CLI touches outside the filesystem. Tests pass their own. */\nexport interface Io {\n stdout: { write(text: string): unknown }\n stderr: { write(text: string): unknown }\n cwd: string\n /** Ask one question; `undefined` when there is no terminal to ask on. */\n ask: ((question: string) => Promise<string>) | undefined\n /** Install dependencies in a directory; resolves to the exit code. */\n install(directory: string): Promise<number>\n}\n\n/** The real terminal and `npm install`. */\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 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 install: (directory) =>\n new Promise((done) => {\n const child = spawn('npm', ['install'], {\n cwd: directory,\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","import { createRequire } from 'node:module'\nimport { add, listPlugins, parseAddOptions } from './add'\nimport { init, parseInitOptions, UsageError } from './init'\nimport { type Io, processIo } from './io'\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 = `acrylblends ${VERSION} - scaffold Cordis apps\n\nUsage:\n acrylblends init [dir] [options] Create a Cordis app in an empty or new directory\n acrylblends add <plugin...> [--dir d] Copy library plugins (and what they require) into an app;\n a folder path brings an external plugin into its extensions/\n acrylblends plugins List the plugin library\n\nOptions for init:\n --template <name> blank or agent (asked when not given); --agent means --template agent,\n --no-agent means --template blank\n --name <name> the project name (default: the directory name)\n --desktop also add the desktop shell: npm run desktop opens a window on the app\n --install run npm install after copying\n -y, --yes ask nothing; use defaults (the blank template, no install)\n\n -h, --help show this help\n -v, --version show the version\n`\n\n/**\n * Run the CLI and return the exit code: 0 done, 1 failed or refused, 2 a usage error. Never calls `process.exit`.\n */\nexport async function runCli(argv: readonly string[], io: Io = processIo()): Promise<number> {\n const [command, ...args] = argv\n try {\n if (command === undefined || command === 'help' || command === '-h' || command === '--help') {\n io.stdout.write(HELP)\n return command === undefined ? 2 : 0\n }\n if (command === 'version' || command === '-v' || command === '--version') {\n io.stdout.write(`${VERSION}\\n`)\n return 0\n }\n if (command === 'init') return await init(parseInitOptions(args), io)\n if (command === 'add') return await add(parseAddOptions(args), io)\n if (command === 'plugins') return listPlugins(io)\n throw new UsageError(`unknown command \"${command}\"`)\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 instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n}\n"],"mappings":";;;;;;;;;AA4BA,SAAS,aAAa,MAAsB;CAC1C,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;EACvC,MAAM,IAAI,KAAK;EACf,IAAI,MAAM,OAAO,KAAK,IAAI,OAAO,KAAK;GACpC,IAAI,KAAK,IAAI,OAAO,KAAK;IACvB,UAAU;IACV,KAAK;GACP,OAAO;IACL,UAAU;IACV,KAAK;GACP;EACF,OAAO,IAAI,MAAM,KAAK,UAAU;OAC3B,IAAI,MAAM,KAAK,UAAU;OACzB,UAAU,EAAE,QAAQ,qBAAqB,MAAM;CACtD;CACA,OAAO,IAAI,OAAO,IAAI,OAAO,EAAE;AACjC;;AAGA,SAAgB,UAAU,QAAgB,UAAkD;CAC1F,IAAI,CAAC,YAAY,SAAS,WAAW,GAAG,OAAO;CAC/C,MAAM,OAAO,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG;CACvC,OAAO,SAAS,MAAM,YACpB,OAAO,KAAK,OAAO,IAAI,aAAa,OAAO,CAAC,CAAC,KAAK,IAAI,IAAI,YAAY,IACxE;AACF;;AAGA,MAAM,+BAAe,IAAI,IAAI;CAAC;CAAe;CAAQ;AAAO,CAAC;AAE7D,IAAa,eAAb,cAAkC,MAAM;CACtC,OAAgB;AAClB;;AAGA,SAAgB,cAAsB;CACpC,IAAI,MAAM,QAAQ,cAAc,YAAY,GAAG,CAAC;CAChD,SAAS;EACP,MAAM,YAAY,KAAK,KAAK,SAAS;EACrC,IAAI,WAAW,KAAK,WAAW,YAAY,aAAa,CAAC,GAAG,OAAO;EACnE,MAAM,SAAS,QAAQ,GAAG;EAC1B,IAAI,WAAW,KACb,MAAM,IAAI,aAAa,uDAAuD;EAChF,MAAM;CACR;AACF;;AAGA,SAAgB,YAAY,OAAe,YAAY,GAA+B;CACpF,MAAM,0BAAU,IAAI,IAA2B;CAC/C,KAAK,MAAM,SAAS,YAAY,MAAM,EAAE,eAAe,KAAK,CAAC,GAAG;EAC9D,MAAM,WAAW,KAAK,MAAM,MAAM,MAAM,aAAa;EACrD,IAAI,CAAC,MAAM,YAAY,KAAK,CAAC,WAAW,QAAQ,GAAG;EACnD,MAAM,SAAS,KAAK,MAAM,aAAa,UAAU,MAAM,CAAC;EACxD,IAAI,OAAO,SAAS,MAAM,MACxB,MAAM,IAAI,aAAa,WAAW,MAAM,KAAK,sBAAsB,OAAO,KAAK,EAAE;EACnF,QAAQ,IAAI,OAAO,MAAM;GACvB,GAAG;GACH,UAAU,OAAO,YAAY,CAAC;GAC9B,cAAc,OAAO,gBAAgB,CAAC;EACxC,CAAC;CACH;CACA,OAAO;AACT;;AAGA,SAAgB,QACd,OACA,SACiB;CACjB,MAAM,UAA2B,CAAC;CAClC,MAAM,2BAAW,IAAI,IAAY;CACjC,MAAM,SAAS,MAAc,UAAoB;EAC/C,IAAI,QAAQ,MAAM,WAAW,OAAO,SAAS,IAAI,GAAG;EACpD,MAAM,SAAS,QAAQ,IAAI,IAAI;EAC/B,IAAI,CAAC,QACH,MAAM,IAAI,aACR,cAAc,KAAK,kBAAkB,MAAM,SAAS,iBAAiB,MAAM,GAAG,EAAE,EAAE,KAAK,IACzF;EACF,IAAI,SAAS,IAAI,IAAI,GACnB,MAAM,IAAI,aACR,0CAA0C,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC,KAAK,MAAM,GACxE;EACF,SAAS,IAAI,IAAI;EACjB,KAAK,MAAM,YAAY,OAAO,UAAU,MAAM,UAAU,CAAC,GAAG,OAAO,IAAI,CAAC;EACxE,SAAS,OAAO,IAAI;EACpB,QAAQ,KAAK,MAAM;CACrB;CACA,KAAK,MAAM,QAAQ,OAAO,MAAM,MAAM,CAAC,CAAC;CACxC,OAAO;AACT;;AAGA,SAAgB,WACd,QACA,QACA,OAAe,YAAY,GACnB;CACR,MAAM,SAAS,KAAK,QAAQ,OAAO,WAAW,OAAO,IAAI;CACzD,IAAI,WAAW,MAAM,GACnB,MAAM,IAAI,aACR,eAAe,OAAO,KAAK,qDAC7B;CACF,MAAM,SAAS,KAAK,MAAM,OAAO,IAAI;CACrC,OAAO,QAAQ,QAAQ;EACrB,WAAW;EACX,SAAS,SAAS;GAChB,MAAM,SAAS,KAAK,MAAM,OAAO,SAAS,CAAC;GAC3C,OACE,EAAE,QAAQ,IAAI,MAAM,UAAU,aAAa,IAAI,MAAM,MAAM,CAAC,UAAU,QAAQ,OAAO,IAAI;EAE7F;CACF,CAAC;CACD,OAAO;AACT;;;;ACnIA,IAAa,sBAAb,cAAyC,MAAM;CACxB;CAArB,YAAY,QAAyB;EACnC,MACE,2BAA2B,OAAO,iEACpC;EAHmB,KAAA,SAAA;EAInB,KAAK,OAAO;CACd;AACF;;AAGA,SAAgB,YAAY,MAAuB;CACjD,OAAO,oBAAoB,KAAK,IAAI,KAAK,KAAK,UAAU;AAC1D;;AAGA,SAAgB,SAAS,WAA2B;CAOlD,QANa,UAAU,MAAM,OAAO,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,IAAI,KAAK,GAAA,CAE5D,YAAY,CAAC,CACb,QAAQ,gBAAgB,GAAG,CAAC,CAC5B,QAAQ,YAAY,EAAE,CAAC,CACvB,QAAQ,OAAO,EACL,KAAK;AACpB;;AAGA,SAAgB,iBAAiB,QAAsB;CACrD,IAAI,CAAC,WAAW,MAAM,GAAG;CACzB,IAAI,CAAC,SAAS,MAAM,CAAC,CAAC,YAAY,KAAK,YAAY,MAAM,CAAC,CAAC,SAAS,GAClE,MAAM,IAAI,oBAAoB,MAAM;AACxC;;;;;;AAOA,SAAgB,aAAa,QAAgB,QAAgB,MAAwB;CACnF,iBAAiB,MAAM;CACvB,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CACrC,MAAM,UAAoB,CAAC;CAC3B,MAAM,QAAQ,MAAc,IAAY,WAAmB;EACzD,KAAK,MAAM,SAAS,YAAY,MAAM,EAAE,eAAe,KAAK,CAAC,GAAG;GAC9D,IACE,MAAM,SAAS,kBACd,WAAW,OACT,MAAM,SAAA,mBACL,MAAM,SAAS,mBACf,MAAM,SAAS,cAEnB;GACF,MAAM,aAAa,KAAK,MAAM,MAAM,IAAI;GAExC,MAAM,aACJ,WAAW,OAAO,MAAM,SAAS,eAAe,MAAM,SAAS,kBAC3D,IAAI,MAAM,SACV,MAAM;GACZ,MAAM,aAAa,KAAK,IAAI,UAAU;GACtC,IAAI,MAAM,YAAY,GAAG;IACvB,UAAU,YAAY,EAAE,WAAW,KAAK,CAAC;IACzC,KAAK,YAAY,YAAY,GAAG,SAAS,MAAM,KAAK,EAAE;IACtD;GACF;GACA,MAAM,OAAO,aAAa,YAAY,MAAM;GAC5C,IAAI,KAAK,SAAS,cAAc,GAC9B,cAAc,YAAY,KAAK,WAAW,gBAAgB,IAAI,CAAC;QAC5D,OAAO,YAAY,UAAU;GAClC,QAAQ,KAAK,GAAG,SAAS,YAAY;EACvC;CACF;CACA,KAAK,QAAQ,QAAQ,EAAE;CACvB,OAAO;AACT;;AAGA,MAAa,oBAAoB;;AAGjC,SAAgB,wBAAwB,aAAyC;CAC/E,MAAM,OAAO,KAAK,aAAa,iBAAiB;CAChD,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO,KAAA;CAC9B,OAAQ,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC,CAAC,CAA8B;AAC9E;;AAGA,SAAgB,iBAAiB,QAAgB,MAAoB;CACnE,MAAM,OAAO,KAAK,QAAQ,cAAc;CACxC,MAAM,WAAW,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;CAGtD,SAAS,cAAc;EAAE,GAAG,SAAS;EAAa,aAAa;CAAK;CACpE,cAAc,MAAM,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,GAAG;AAC9D;AAEA,SAAgB,oBAAoB,aAA+B;CACjE,MAAM,OAAO,KAAK,aAAa,iBAAiB;CAChD,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO,CAAC;CAC/B,MAAM,EAAE,YAAY,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;CACzD,OAAO,WAAW,CAAC;AACrB;;AAGA,SAAgB,kBACd,QACA,cACU;CACV,MAAM,OAAO,KAAK,QAAQ,cAAc;CACxC,MAAM,WAAW,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;CAGtD,MAAM,UAAU,SAAS,gBAAgB,CAAC;CAC1C,MAAM,QAAQ,OAAO,KAAK,YAAY,CAAC,CAAC,QAAQ,SAAS,EAAE,QAAQ,QAAQ;CAC3E,MAAM,SAAS;EAAE,GAAG;EAAc,GAAG;CAAQ;CAC7C,SAAS,eAAe,OAAO,YAC7B,OAAO,QAAQ,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAC9D;CACA,cAAc,MAAM,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,GAAG;CAC5D,OAAO;AACT;;;;;AAMA,SAAgB,aACd,QACA,SAIkD;CAClD,MAAM,OAAO,KAAK,QAAQ,cAAc;CACxC,MAAM,WAAW,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;CAItD,MAAM,MAAM,SAAS,mBAAmB,CAAC;CACzC,MAAM,UAAU,SAAS,WAAW,CAAC;CACrC,MAAM,WAAW,OAAO,KAAK,QAAQ,mBAAmB,CAAC,CAAC,CAAC,CAAC,QAAQ,SAAS,EAAE,QAAQ,IAAI;CAC3F,MAAM,eAAe,OAAO,KAAK,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,SAAS,EAAE,QAAQ,QAAQ;CAC3F,SAAS,kBAAkB,OAAO,YAChC,OAAO,QAAQ;EAAE,GAAG,QAAQ;EAAiB,GAAG;CAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAC9F;CACA,SAAS,UAAU;EAAE,GAAG,QAAQ;EAAS,GAAG;CAAQ;CACpD,cAAc,MAAM,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,GAAG;CAC5D,OAAO;EAAE,iBAAiB;EAAU,SAAS;CAAa;AAC5D;;AAGA,SAAgB,gBAAgB,QAAgB,OAAgD;CAC9F,MAAM,OAAO,KAAK,QAAQ,YAAY;CACtC,IAAI,CAAC,SAAS,MAAM,WAAW,KAAK,CAAC,WAAW,IAAI,GAAG,OAAO,CAAC;CAC/D,MAAM,UAAU,aAAa,MAAM,MAAM;CACzC,MAAM,UAAU,IAAI,IAAI,QAAQ,MAAM,IAAI,CAAC,CAAC,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC;CACtE,MAAM,UAAU,MAAM,QAAQ,SAAS,CAAC,QAAQ,IAAI,IAAI,CAAC;CACzD,IAAI,QAAQ,SAAS,GACnB,cACE,MACA,GAAG,UAAU,QAAQ,SAAS,IAAI,KAAK,YAAY,KAAK,KAAK,OAAO,QAAQ,KAAK,IAAI,EAAE,GACzF;CACF,OAAO;AACT;;AAGA,SAAgB,gBACd,WACA,aACA,QACA,MACU;CACV,MAAM,OAAO,aAAa,KAAK,WAAW,gBAAgB,GAAG,MAAM;CACnE,MAAM,MAAM,WAAW,KAAK,aAAa,WAAW,CAAC,IACjD,aAAa,KAAK,aAAa,WAAW,GAAG,MAAM,IACnD;CACJ,cAAc,KAAK,QAAQ,WAAW,GAAG,GAAG,OAAO,MAAM,WAAW,gBAAgB,IAAI,CAAC;CACzF,cAAc,KAAK,QAAQ,WAAW,GAAG,cAAc;CACvD,OAAO,CAAC,aAAa,WAAW;AAClC;;;;;;;AAQA,SAAgB,kBAAkB,QAAgB,QAA0B;CAC1E,MAAM,UAAoB,CAAC;CAC3B,MAAM,QAAQ,MAAc,IAAY,WAAmB;EACzD,KAAK,MAAM,SAAS,YAAY,MAAM,EAAE,eAAe,KAAK,CAAC,GAAG;GAC9D,IAAI,MAAM,SAAS,mBAAmB,MAAM,SAAS,eAAe;GACpE,MAAM,aAAa,KAAK,MAAM,MAAM,IAAI;GACxC,MAAM,aAAa,KAAK,IAAI,MAAM,IAAI;GACtC,IAAI,MAAM,YAAY,GAAG;IACvB,UAAU,YAAY,EAAE,WAAW,KAAK,CAAC;IACzC,KAAK,YAAY,YAAY,GAAG,SAAS,MAAM,KAAK,EAAE;IACtD;GACF;GACA,OAAO,YAAY,UAAU;GAC7B,QAAQ,KAAK,kBAAkB,SAAS,MAAM,MAAM;EACtD;CACF;CACA,UAAU,KAAK,QAAQ,QAAQ,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;CAChE,KAAK,QAAQ,KAAK,QAAQ,QAAQ,WAAW,GAAG,EAAE;CAClD,OAAO;AACT;;;;ACnNA,MAAa,YAAY;CAAC;CAAS;CAAQ;AAAO;AAGlD,SAAgB,WAAW,OAAkC;CAC3D,OAAQ,UAAgC,SAAS,KAAK;AACxD;;;;;AAMA,SAAgB,gBAAwB;CACtC,IAAI,MAAM,QAAQ,cAAc,YAAY,GAAG,CAAC;CAChD,SAAS;EACP,MAAM,YAAY,KAAK,KAAK,WAAW;EACvC,IAAI,WAAW,KAAK,WAAW,OAAO,CAAC,GAAG,OAAO;EACjD,MAAM,SAAS,QAAQ,GAAG;EAC1B,IAAI,WAAW,KAAK,MAAM,IAAI,MAAM,8CAA8C;EAClF,MAAM;CACR;AACF;;AAGA,SAAgB,YAAY,UAA4B;CACtD,OAAO,KAAK,cAAc,GAAG,QAAQ;AACvC;;AAGA,SAAgB,YAAoB;CAClC,OAAO,KAAK,QAAQ,cAAc,CAAC,GAAG,QAAQ;AAChD;;;;;AAMA,SAAgB,oBAA4B;CAC1C,IAAI,MAAM,QAAQ,cAAc,YAAY,GAAG,CAAC;CAChD,SAAS;EACP,MAAM,YAAY,KAAK,KAAK,QAAQ,WAAW;EAC/C,IAAI,WAAW,KAAK,WAAW,WAAW,CAAC,GAAG,OAAO;EACrD,MAAM,SAAS,QAAQ,GAAG;EAC1B,IAAI,WAAW,KACb,MAAM,IAAI,MAAM,6DAA6D;EAC/E,MAAM;CACR;AACF;;;;ACxBA,IAAa,aAAb,cAAgC,MAAM;CACpC,OAAgB;AAClB;;AAcA,SAAgB,iBAAiB,MAAsC;CACrE,IAAI;CACJ,IAAI;EACF,SAASA,QAAM,IAAI;CACrB,SAAS,OAAO;EACd,MAAM,IAAI,WAAY,MAAgB,OAAO;CAC/C;CACA,MAAM,EAAE,QAAQ,gBAAgB;CAChC,IAAI,YAAY,SAAS,GAAG,MAAM,IAAI,WAAW,kCAAkC;CACnF,IAAI,OAAO,SAAS,OAAO,aACzB,MAAM,IAAI,WAAW,qCAAqC;CAC5D,IAAI,OAAO,aAAa,KAAA,KAAa,CAAC,WAAW,OAAO,QAAQ,GAC9D,MAAM,IAAI,WAAW,qBAAqB,OAAO,SAAS,KAAK,UAAU,KAAK,IAAI,EAAE,EAAE;CACxF,MAAM,QAA8B,OAAO,QACvC,UACA,OAAO,cACL,UACA,KAAA;CACN,IAAI,OAAO,aAAa,KAAA,KAAa,UAAU,KAAA,KAAa,OAAO,aAAa,OAC9E,MAAM,IAAI,WACR,cAAc,OAAO,SAAS,iBAAiB,UAAU,UAAU,UAAU,YAC/E;CACF,OAAO;EACL,WAAW,YAAY;EACvB,MAAM,OAAO;EACb,UAAW,OAAO,YAAqC;EACvD,SAAS,OAAO;EAChB,SAAS,OAAO,YAAY;EAC5B,KAAK,OAAO,QAAQ;CACtB;AACF;AAEA,MAAMA,WAAS,SACb,UAAU;CACR,MAAM,CAAC,GAAG,IAAI;CACd,kBAAkB;CAClB,QAAQ;CACR,SAAS;EACP,MAAM,EAAE,MAAM,SAAS;EACvB,UAAU,EAAE,MAAM,SAAS;EAC3B,OAAO,EAAE,MAAM,UAAU;EACzB,YAAY,EAAE,MAAM,UAAU;EAC9B,SAAS,EAAE,MAAM,UAAU;EAC3B,SAAS,EAAE,MAAM,UAAU;EAC3B,KAAK;GAAE,MAAM;GAAW,OAAO;EAAI;CACrC;AACF,CAAC;AAEH,MAAM,OAAO,WAAmB,CAAC,KAAK,KAAK,CAAC,CAAC,SAAS,OAAO,KAAK,CAAC,CAAC,YAAY,CAAC;AAEjF,eAAe,YAAY,KAA+D;CACxF,MAAM,UAAU,MAAM,IAAI,aAAa,UAAU,KAAK,IAAI,EAAE,YAAY,EAAA,CAAG,KAAK,CAAC,CAAC,YAAY;CAC9F,IAAI,WAAW,IAAI,OAAO;CAC1B,IAAI,WAAW,MAAM,GAAG,OAAO;CAC/B,MAAM,IAAI,WAAW,qBAAqB,OAAO,KAAK,UAAU,KAAK,IAAI,EAAE,EAAE;AAC/E;;AAGA,eAAsB,KAAK,SAAsB,IAAyB;CACxE,MAAM,WAAW,QAAQ,aAAa;CACtC,MAAM,YAAY,QAAQ,GAAG,KAAK,QAAQ;CAC1C,MAAM,MAAM,QAAQ,MAAM,KAAA,IAAY,GAAG;CAEzC,IAAI,OAAO,QAAQ,QAAQ,SAAS,SAAS;CAC7C,IAAI,QAAQ,SAAS,KAAA,KAAa,KAChC,QAAQ,MAAM,IAAI,iBAAiB,KAAK,IAAI,EAAA,CAAG,KAAK,KAAK;CAC3D,IAAI,CAAC,YAAY,IAAI,GACnB,MAAM,IAAI,WACR,IAAI,KAAK,8FACX;CAEF,MAAM,WAAW,QAAQ,aAAa,MAAM,MAAM,YAAY,GAAG,IAAI;CACrE,MAAM,UACJ,QAAQ,YAAY,MAAM,IAAI,MAAM,IAAI,4CAA4C,CAAC,IAAI;CAE3F,IAAI;CACJ,IAAI;EACF,QAAQ,aAAa,YAAY,QAAQ,GAAG,WAAW,IAAI;EAC3D,MAAM,KAAK,GAAG,gBAAgB,UAAU,GAAG,YAAY,QAAQ,GAAG,WAAW,IAAI,CAAC;EAClF,MAAM,KAAK,GAAG,kBAAkB,kBAAkB,GAAG,SAAS,CAAC;CACjE,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,sBAAsB,MAAM;EACnD,GAAG,OAAO,MAAM,UAAU,MAAM,QAAQ,GAAG;EAC3C,OAAO;CACT;CACA,MAAM,UAAU,QACd,CAAC,GAAG,oBAAoB,YAAY,QAAQ,CAAC,GAAG,GAAI,QAAQ,UAAU,CAAC,SAAS,IAAI,CAAC,CAAE,GACvF,YAAY,CACd;CACA,KAAK,MAAM,UAAU,SAAS;EAC5B,WAAW,QAAQ,SAAS;EAC5B,kBAAkB,WAAW,OAAO,YAAY;EAChD,aAAa,WAAW,MAAM;CAChC;CACA,MAAM,cAAc,QAAQ,UAAU,wBAAwB,YAAY,QAAQ,CAAC,IAAI,KAAA;CACvF,IAAI,gBAAgB,KAAA,GAAW,iBAAiB,WAAW,WAAW;CACtE,GAAG,OAAO,MACR,WAAW,KAAK,IAAI,SAAS,OAAO,UAAU,IAAI,MAAM,OAAO,kBAAkB,QAAQ,KAAK,WAAW,OAAO,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,GACnI;CAEA,IAAI,WAAY,MAAM,GAAG,QAAQ,SAAS,MAAO,GAAG;EAClD,GAAG,OAAO,MAAM,mEAAmE;EACnF,OAAO;CACT;CACA,MAAM,QAAQ;EACZ,aAAa,MAAM,KAAK,QAAQ,SAAS;EACzC,UAAU,KAAK;EACf;CACF;CACA,GAAG,OAAO,MAAM,YAAY,MAAM,KAAK,EAAE,GAAG;CAC5C,OAAO;AACT;;;;AC5IA,SAAgB,gBAAgB,MAAqC;CACnE,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,IAAI;CACrB,SAAS,OAAO;EACd,MAAM,IAAI,WAAY,MAAgB,OAAO;CAC/C;CACA,IAAI,OAAO,YAAY,WAAW,GAChC,MAAM,IAAI,WAAW,kEAAgE;CACvF,OAAO;EAAE,OAAO,OAAO;EAAa,WAAW,OAAO,OAAO;CAAI;AACnE;AAEA,MAAM,SAAS,SACb,UAAU;CACR,MAAM,CAAC,GAAG,IAAI;CACd,kBAAkB;CAClB,QAAQ;CACR,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,EAAE;AACrC,CAAC;AAEH,MAAM,WAAW,QAAgB,SAAiB,WAAW,KAAK,QAAQ,OAAO,WAAW,IAAI,CAAC;;;;;AAMjG,eAAsB,IAAI,SAAqB,IAAyB;CACtE,MAAM,SAAS,QAAQ,GAAG,KAAK,QAAQ,aAAa,GAAG;CACvD,IAAI,CAAC,WAAW,KAAK,QAAQ,cAAc,CAAC,KAAK,CAAC,WAAW,KAAK,QAAQ,KAAK,CAAC,GAAG;EACjF,GAAG,OAAO,MACR,UAAU,OAAO,0EACnB;EACA,OAAO;CACT;CAEA,MAAM,WAAW,QAAQ,MAAM,QAAQ,SAAS,QAAQ,KAAK,IAAI,KAAK,KAAK,WAAW,GAAG,CAAC;CAC1F,KAAK,MAAM,UAAU,UAAU;EAC7B,MAAM,OAAO,YAAY,QAAQ,GAAG,KAAK,MAAM,GAAG,QAAQ,EAAE;EAC5D,IAAI,SAAS,GAAG,OAAO;CACzB;CAEA,MAAM,QAAQ,CAAC,WAAW,KAAK,QAAQ,OAAO,WAAW,UAAU,CAAC;CACpE,MAAM,QAAQ,QAAQ,MACnB,QAAQ,SAAS,CAAC,SAAS,SAAS,IAAI,CAAC,CAAC,CAC1C,KAAK,SAAU,SAAS,aAAa,QAAQ,gBAAgB,IAAK;CACrE,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,MAAM,UAAU,MAAM,QAAQ,SAAS,QAAQ,QAAQ,IAAI,CAAC;CAC5D,IAAI,QAAQ,SAAS,GAAG;EACtB,GAAG,OAAO,MACR,8BAA8B,QAAQ,KAAK,SAAS,eAAe,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,GACxF;EACA,OAAO;CACT;CACA,MAAM,SAA0B,QAAQ,OAAO,YAAY,CAAC,CAAC,CAAC,QAC3D,WAAW,CAAC,QAAQ,QAAQ,OAAO,IAAI,CAC1C;CACA,MAAM,oBAA8B,CAAC;CACrC,KAAK,MAAM,UAAU,QAAQ;EAC3B,WAAW,QAAQ,MAAM;EACzB,kBAAkB,KAAK,GAAG,kBAAkB,QAAQ,OAAO,YAAY,CAAC;EACxE,MAAM,UAAU,aAAa,QAAQ,MAAM;EAC3C,gBAAgB,QAAQ,OAAO,SAAS;EACxC,kBAAkB,KAAK,GAAG,QAAQ,eAAe;EACjD,IAAI,QAAQ,QAAQ,SAAS,GAC3B,GAAG,OAAO,MACR,gBAAgB,QAAQ,QAAQ,KAAK,SAAS,WAAW,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,GAC9E;CACJ;CACA,GAAG,OAAO,MAAM,SAAS,OAAO,KAAK,WAAW,OAAO,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,MAAM,OAAO,GAAG;CACxF,IAAI,kBAAkB,SAAS,GAC7B,GAAG,OAAO,MAAM,qBAAqB,kBAAkB,KAAK,IAAI,EAAE,qBAAqB;CACzF,KAAK,MAAM,UAAU,OAAO,QAAQ,WAAW,MAAM,SAAS,OAAO,IAAI,CAAC,GACxE,GAAG,OAAO,MACR,KAAK,OAAO,KAAK,IAAI,OAAO,SAAS,6BAA6B,OAAO,KAAK,uCAAuC,GACvH;CAEF,OAAO;AACT;AAEA,MAAM,cAAc;;;;;;AAOpB,SAAS,YAAY,QAAgB,QAAgB,IAAgB;CACnE,IAAI,CAAC,WAAW,MAAM,KAAK,CAAC,SAAS,MAAM,CAAC,CAAC,YAAY,GAAG;EAC1D,GAAG,OAAO,MAAM,UAAU,OAAO,mBAAmB;EACpD,OAAO;CACT;CACA,IAAI,CAAC;EAAC;EAAY;EAAY;CAAW,CAAC,CAAC,MAAM,UAAU,WAAW,KAAK,QAAQ,KAAK,CAAC,CAAC,GAAG;EAC3F,GAAG,OAAO,MAAM,UAAU,OAAO,gDAAgD;EACjF,OAAO;CACT;CACA,MAAM,eAAe,KAAK,QAAQ,cAAc;CAChD,MAAM,WAAW,WAAW,YAAY,IACnC,KAAK,MAAM,aAAa,cAAc,MAAM,CAAC,IAI9C,CAAC;CACL,MAAM,QAAQ,SAAS,QAAQ,SAAS,MAAM,EAAA,CAAG,QAAQ,aAAa,EAAE;CACxE,IAAI,CAAC,YAAY,KAAK,IAAI,GAAG;EAC3B,GAAG,OAAO,MACR,WAAW,KAAK,gEAClB;EACA,OAAO;CACT;CACA,MAAM,SAAS,KAAK,QAAQ,cAAc,IAAI;CAC9C,IAAI,WAAW,MAAM,GAAG;EACtB,GAAG,OAAO,MAAM,yCAAyC,KAAK,GAAG;EACjE,OAAO;CACT;CACA,OAAO,QAAQ,QAAQ;EACrB,WAAW;EACX,SAAS,SAAS,CAAC,qCAAqC,KAAK,KAAK,MAAM,OAAO,MAAM,CAAC;CACxF,CAAC;CACD,GAAG,OAAO,MACR,yBAAyB,KAAK,MAAM,KAAK,QAAQ,YAAY,EAAE,kCACjE;CACA,MAAM,eAAe,OAAO,KAAK,SAAS,gBAAgB,CAAC,CAAC;CAC5D,IAAI,aAAa,SAAS,GACxB,GAAG,OAAO,MACR,WAAW,aAAa,KAAK,IAAI,EAAE,+CACrC;CACF,OAAO;AACT;;AAGA,SAAgB,YAAY,IAAgB;CAC1C,KAAK,MAAM,UAAU,YAAY,CAAC,CAAC,OAAO,GAAG;EAC3C,MAAM,WAAW,OAAO,SAAS,SAAS,IAAI,cAAc,OAAO,SAAS,KAAK,IAAI,EAAE,KAAK;EAC5F,GAAG,OAAO,MAAM,GAAG,OAAO,KAAK,OAAO,EAAE,EAAE,GAAG,OAAO,cAAc,SAAS,GAAG;CAChF;CACA,OAAO;AACT;;;;ACvIA,SAAgB,YAAgB;CAC9B,MAAM,cAAc,QAAQ,QAAQ,MAAM,SAAS,QAAQ,OAAO,KAAK;CACvE,OAAO;EACL,QAAQ,QAAQ;EAChB,QAAQ,QAAQ;EAChB,KAAK,QAAQ,IAAI;EACjB,KAAK,cACD,OAAO,aAAa;GAClB,MAAM,KAAK,gBAAgB;IAAE,OAAO,QAAQ;IAAO,QAAQ,QAAQ;GAAO,CAAC;GAC3E,IAAI;IACF,OAAO,MAAM,GAAG,SAAS,QAAQ;GACnC,UAAU;IACR,GAAG,MAAM;GACX;EACF,IACA,KAAA;EACJ,UAAU,cACR,IAAI,SAAS,SAAS;GACpB,MAAM,QAAQ,MAAM,OAAO,CAAC,SAAS,GAAG;IACtC,KAAK;IACL,OAAO;IACP,OAAO,QAAQ,aAAa;GAC9B,CAAC;GACD,MAAM,GAAG,eAAe,KAAK,CAAC,CAAC;GAC/B,MAAM,GAAG,SAAS,SAAS,KAAK,QAAQ,CAAC,CAAC;EAC5C,CAAC;CACL;AACF;;;;ACpCA,MAAa,UACX,cAAc,YAAY,GAAG,CAAC,CAAC,iBAAiB,CAAC,CACjD;AAEF,MAAa,OAAO,eAAe,QAAQ;;;;;;;;;;;;;;;;;;;;;;AAuB3C,eAAsB,OAAO,MAAyB,KAAS,UAAU,GAAoB;CAC3F,MAAM,CAAC,SAAS,GAAG,QAAQ;CAC3B,IAAI;EACF,IAAI,YAAY,KAAA,KAAa,YAAY,UAAU,YAAY,QAAQ,YAAY,UAAU;GAC3F,GAAG,OAAO,MAAM,IAAI;GACpB,OAAO,YAAY,KAAA,IAAY,IAAI;EACrC;EACA,IAAI,YAAY,aAAa,YAAY,QAAQ,YAAY,aAAa;GACxE,GAAG,OAAO,MAAM,GAAG,QAAQ,GAAG;GAC9B,OAAO;EACT;EACA,IAAI,YAAY,QAAQ,OAAO,MAAM,KAAK,iBAAiB,IAAI,GAAG,EAAE;EACpE,IAAI,YAAY,OAAO,OAAO,MAAM,IAAI,gBAAgB,IAAI,GAAG,EAAE;EACjE,IAAI,YAAY,WAAW,OAAO,YAAY,EAAE;EAChD,MAAM,IAAI,WAAW,oBAAoB,QAAQ,EAAE;CACrD,SAAS,OAAO;EACd,IAAI,iBAAiB,YAAY;GAC/B,GAAG,OAAO,MAAM,UAAU,MAAM,QAAQ,MAAM,MAAM;GACpD,OAAO;EACT;EACA,GAAG,OAAO,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;EACpF,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"index.js","names":["parse"],"sources":["../src/library.ts","../src/project.ts","../src/templates.ts","../src/init.ts","../src/add.ts","../src/spawn.ts","../src/io.ts","../src/cli.ts"],"sourcesContent":["import { cpSync, existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** One entry of the plugin library: `plugins/<name>/plugin.json` in this repository. */\nexport interface LibraryPlugin {\n readonly name: string\n readonly description: string\n /** Library plugins this one imports; they are copied with it. */\n readonly requires: readonly string[]\n /** npm packages an app needs at run time for this plugin. */\n readonly dependencies: Readonly<Record<string, string>>\n /** npm packages an app needs only to develop or run this plugin's tooling (for example `electron` for the desktop shell). */\n readonly devDependencies?: Readonly<Record<string, string>>\n /** `package.json` scripts the plugin adds; a script the app already has is kept. */\n readonly scripts?: Readonly<Record<string, string>>\n /** Lines the app's `.gitignore` needs for this plugin (added when missing, if the app has a `.gitignore`). */\n readonly gitignore?: readonly string[]\n /** How to mount it, shown after `acrylblends add`. */\n readonly mount?: string\n /**\n * Paths inside the plugin folder that are not copied into an app (for example a React hook an app without React cannot type-check, or colocated test\n * files). A pattern is an exact path or a glob: `*` matches within one folder name, `**` across folders, `?` one character; `**` followed by a slash also matches no folder.\n */\n readonly omit?: readonly string[]\n}\n\n/** A glob as a regular expression: `*` within a name, `**` across folders, `**` plus a slash may match no folder, `?` one character. */\nfunction globToRegExp(glob: string): RegExp {\n let source = ''\n for (let i = 0; i < glob.length; i += 1) {\n const c = glob[i] as string\n if (c === '*' && glob[i + 1] === '*') {\n if (glob[i + 2] === '/') {\n source += '(?:.*/)?'\n i += 2\n } else {\n source += '.*'\n i += 1\n }\n } else if (c === '*') source += '[^/]*'\n else if (c === '?') source += '[^/]'\n else source += c.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${source}$`)\n}\n\n/** Whether a path inside a plugin folder (relative, any separator) is named by one of its `omit` patterns. */\nexport function isOmitted(inside: string, patterns: readonly string[] | undefined): boolean {\n if (!patterns || patterns.length === 0) return false\n const path = inside.split(sep).join('/')\n return patterns.some((pattern) =>\n /[*?]/.test(pattern) ? globToRegExp(pattern).test(path) : pattern === path,\n )\n}\n\n/** Files of a library plugin that stay in the repository: its manifest and its tests. */\nconst LIBRARY_ONLY = new Set(['plugin.json', 'test', 'tests'])\n\nexport class LibraryError extends Error {\n override name = 'LibraryError'\n}\n\n/** The library folder (`plugins/` at the repository root): walk up from this module, in source or in the build. */\nexport function libraryRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'plugins')\n if (existsSync(join(candidate, 'instance', 'plugin.json'))) return candidate\n const parent = dirname(dir)\n if (parent === dir)\n throw new LibraryError('acrylblends cannot find its plugin library (plugins/)')\n dir = parent\n }\n}\n\n/** Every plugin in the library, by name. */\nexport function readLibrary(root: string = libraryRoot()): Map<string, LibraryPlugin> {\n const plugins = new Map<string, LibraryPlugin>()\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n const manifest = join(root, entry.name, 'plugin.json')\n if (!entry.isDirectory() || !existsSync(manifest)) continue\n const plugin = JSON.parse(readFileSync(manifest, 'utf8')) as LibraryPlugin\n if (plugin.name !== entry.name)\n throw new LibraryError(`plugins/${entry.name}/plugin.json names \"${plugin.name}\"`)\n plugins.set(plugin.name, {\n ...plugin,\n requires: plugin.requires ?? [],\n dependencies: plugin.dependencies ?? {},\n })\n }\n return plugins\n}\n\n/** The plugins to copy for `names`: each one after everything it requires, no duplicates. */\nexport function closure(\n names: readonly string[],\n library: Map<string, LibraryPlugin>,\n): LibraryPlugin[] {\n const ordered: LibraryPlugin[] = []\n const visiting = new Set<string>()\n const visit = (name: string, trail: string[]) => {\n if (ordered.some((plugin) => plugin.name === name)) return\n const plugin = library.get(name)\n if (!plugin)\n throw new LibraryError(\n `no plugin \"${name}\" in the library${trail.length ? ` (required by ${trail.at(-1)})` : ''}`,\n )\n if (visiting.has(name))\n throw new LibraryError(\n `plugins require each other in a cycle: ${[...trail, name].join(' -> ')}`,\n )\n visiting.add(name)\n for (const required of plugin.requires) visit(required, [...trail, name])\n visiting.delete(name)\n ordered.push(plugin)\n }\n for (const name of names) visit(name, [])\n return ordered\n}\n\n/** Copy one library plugin's source into `<app>/src/plugins/<name>/`. Throws when that folder already exists. */\nexport function copyPlugin(\n plugin: LibraryPlugin,\n appDir: string,\n root: string = libraryRoot(),\n): string {\n const target = join(appDir, 'src', 'plugins', plugin.name)\n if (existsSync(target))\n throw new LibraryError(\n `src/plugins/${plugin.name} already exists; remove it first to copy a fresh one`,\n )\n const source = join(root, plugin.name)\n cpSync(source, target, {\n recursive: true,\n filter: (path) => {\n const inside = path.slice(source.length + 1)\n return (\n !(dirname(path) === source && LIBRARY_ONLY.has(inside)) && !isOmitted(inside, plugin.omit)\n )\n },\n })\n return target\n}\n","import {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from 'node:fs'\nimport { join } from 'node:path'\n\n/** Thrown when the target directory exists and is not empty. Nothing has been written. */\nexport class TargetNotEmptyError extends Error {\n constructor(readonly target: string) {\n super(\n `refusing to write into \"${target}\": the directory is not empty. Choose an empty or new directory.`,\n )\n this.name = 'TargetNotEmptyError'\n }\n}\n\n/** A project name npm accepts: lower case letters, digits and dashes, starting with a letter. */\nexport function isValidName(name: string): boolean {\n return /^[a-z][a-z0-9-]*$/.test(name) && name.length <= 214\n}\n\n/** A valid default project name derived from a directory path. */\nexport function nameFrom(directory: string): string {\n const base = directory.split(/[\\\\/]/).filter(Boolean).pop() ?? ''\n const cleaned = base\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/^[^a-z]+/, '')\n .replace(/-+$/, '')\n return cleaned || 'my-app'\n}\n\n/** Refuse a target that exists and is not an empty directory. A missing target is fine: it is created. */\nexport function assertTargetFree(target: string): void {\n if (!existsSync(target)) return\n if (!statSync(target).isDirectory() || readdirSync(target).length > 0)\n throw new TargetNotEmptyError(target)\n}\n\n/**\n * Copy a template into `target`, replacing `__APP_NAME__` with `name`. Checks the target before writing anything.\n *\n * @returns the copied files, relative to `target`\n */\nexport function copyTemplate(source: string, target: string, name: string): string[] {\n assertTargetFree(target)\n mkdirSync(target, { recursive: true })\n const written: string[] = []\n const walk = (from: string, to: string, prefix: string) => {\n for (const entry of readdirSync(from, { withFileTypes: true })) {\n if (\n entry.name === 'node_modules' ||\n (prefix === '' &&\n (entry.name === TEMPLATE_MANIFEST ||\n entry.name === 'upstream.json' ||\n entry.name === 'AGENTS.md'))\n )\n continue\n const sourcePath = join(from, entry.name)\n // npm and git treat a file named .gitignore specially, so templates keep it as `gitignore` (and `dockerignore`).\n const targetName =\n prefix === '' && (entry.name === 'gitignore' || entry.name === 'dockerignore')\n ? `.${entry.name}`\n : entry.name\n const targetPath = join(to, targetName)\n if (entry.isDirectory()) {\n mkdirSync(targetPath, { recursive: true })\n walk(sourcePath, targetPath, `${prefix}${entry.name}/`)\n continue\n }\n const text = readFileSync(sourcePath, 'utf8')\n if (text.includes('__APP_NAME__'))\n writeFileSync(targetPath, text.replaceAll('__APP_NAME__', name))\n else cpSync(sourcePath, targetPath)\n written.push(`${prefix}${targetName}`)\n }\n }\n walk(source, target, '')\n return written\n}\n\n/** A template's own manifest, `template.json`: the library plugins it is built from. Never copied (nor is `upstream.json`, the record of what was synced from ACRYL, nor the template's `AGENTS.md`, which `writeAgentFiles` joins to the shared core). */\nexport const TEMPLATE_MANIFEST = 'template.json'\n\n/** The address a template's desktop window opens on, when it is not the root (`template.json`: `desktopPath`). */\nexport function readTemplateDesktopPath(templateDir: string): string | undefined {\n const file = join(templateDir, TEMPLATE_MANIFEST)\n if (!existsSync(file)) return undefined\n return (JSON.parse(readFileSync(file, 'utf8')) as { desktopPath?: string }).desktopPath\n}\n\n/** Record where the desktop window opens in package.json (`acrylblends.desktopPath`), read by `src/plugins/desktop/main.ts`. */\nexport function writeDesktopPath(appDir: string, path: string): void {\n const file = join(appDir, 'package.json')\n const manifest = JSON.parse(readFileSync(file, 'utf8')) as {\n acrylblends?: Record<string, unknown>\n }\n manifest.acrylblends = { ...manifest.acrylblends, desktopPath: path }\n writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\\n`)\n}\n\nexport function readTemplatePlugins(templateDir: string): string[] {\n const file = join(templateDir, TEMPLATE_MANIFEST)\n if (!existsSync(file)) return []\n const { plugins } = JSON.parse(readFileSync(file, 'utf8')) as { plugins?: string[] }\n return plugins ?? []\n}\n\n/** Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. */\nexport function mergeDependencies(\n appDir: string,\n dependencies: Readonly<Record<string, string>>,\n): string[] {\n const file = join(appDir, 'package.json')\n const manifest = JSON.parse(readFileSync(file, 'utf8')) as {\n dependencies?: Record<string, string>\n }\n const current = manifest.dependencies ?? {}\n const added = Object.keys(dependencies).filter((name) => !(name in current))\n const merged = { ...dependencies, ...current }\n manifest.dependencies = Object.fromEntries(\n Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)),\n )\n writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\\n`)\n return added\n}\n\n/**\n * Add a plugin's development dependencies and scripts to an app's package.json: nothing the app already has is replaced. Returns the names of the development dependencies\n * and scripts added.\n */\nexport function mergeTooling(\n appDir: string,\n tooling: {\n devDependencies?: Readonly<Record<string, string>> | undefined\n scripts?: Readonly<Record<string, string>> | undefined\n },\n): { devDependencies: string[]; scripts: string[] } {\n const file = join(appDir, 'package.json')\n const manifest = JSON.parse(readFileSync(file, 'utf8')) as {\n devDependencies?: Record<string, string>\n scripts?: Record<string, string>\n }\n const dev = manifest.devDependencies ?? {}\n const scripts = manifest.scripts ?? {}\n const addedDev = Object.keys(tooling.devDependencies ?? {}).filter((name) => !(name in dev))\n const addedScripts = Object.keys(tooling.scripts ?? {}).filter((name) => !(name in scripts))\n manifest.devDependencies = Object.fromEntries(\n Object.entries({ ...tooling.devDependencies, ...dev }).sort(([a], [b]) => a.localeCompare(b)),\n )\n manifest.scripts = { ...tooling.scripts, ...scripts }\n writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\\n`)\n return { devDependencies: addedDev, scripts: addedScripts }\n}\n\n/** Add the lines a plugin needs to the app's `.gitignore` when it has one and they are missing. Returns the lines added. */\nexport function appendGitignore(appDir: string, lines: readonly string[] | undefined): string[] {\n const file = join(appDir, '.gitignore')\n if (!lines || lines.length === 0 || !existsSync(file)) return []\n const current = readFileSync(file, 'utf8')\n const present = new Set(current.split('\\n').map((line) => line.trim()))\n const missing = lines.filter((line) => !present.has(line))\n if (missing.length > 0)\n writeFileSync(\n file,\n `${current}${current.endsWith('\\n') || current === '' ? '' : '\\n'}${missing.join('\\n')}\\n`,\n )\n return missing\n}\n\n/** The generated app's guide for a coding agent: the shared core, then the template's own part. `CLAUDE.md` is only an import of it. */\nexport function writeAgentFiles(\n sharedDir: string,\n templateDir: string,\n target: string,\n name: string,\n): string[] {\n const core = readFileSync(join(sharedDir, 'AGENTS.core.md'), 'utf8')\n const own = existsSync(join(templateDir, 'AGENTS.md'))\n ? readFileSync(join(templateDir, 'AGENTS.md'), 'utf8')\n : ''\n writeFileSync(join(target, 'AGENTS.md'), `${core}${own}`.replaceAll('__APP_NAME__', name))\n writeFileSync(join(target, 'CLAUDE.md'), '@AGENTS.md\\n')\n return ['AGENTS.md', 'CLAUDE.md']\n}\n\n/**\n * Copy the framework's docs (a generated copy of ACRYL's `docs/framework`, already checked against its digest) into `<app>/docs/framework/`,\n * so a coding agent working in the app reads the API from the folder it is in. The generated-copy notes and the digest record stay behind.\n *\n * @returns the copied files, relative to `target`\n */\nexport function copyFrameworkDocs(source: string, target: string): string[] {\n const written: string[] = []\n const walk = (from: string, to: string, prefix: string) => {\n for (const entry of readdirSync(from, { withFileTypes: true })) {\n if (entry.name === 'upstream.json' || entry.name === 'UPSTREAM.md') continue\n const sourcePath = join(from, entry.name)\n const targetPath = join(to, entry.name)\n if (entry.isDirectory()) {\n mkdirSync(targetPath, { recursive: true })\n walk(sourcePath, targetPath, `${prefix}${entry.name}/`)\n continue\n }\n cpSync(sourcePath, targetPath)\n written.push(`docs/framework/${prefix}${entry.name}`)\n }\n }\n mkdirSync(join(target, 'docs', 'framework'), { recursive: true })\n walk(source, join(target, 'docs', 'framework'), '')\n return written\n}\n","import { existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** The templates: a blank Cordis app on the router, a todo list built only on the router and state, and the blank app with the offline agent. */\nexport const TEMPLATES = ['blank', 'todo', 'agent'] as const\nexport type Template = (typeof TEMPLATES)[number]\n\nexport function isTemplate(value: string): value is Template {\n return (TEMPLATES as readonly string[]).includes(value)\n}\n\n/**\n * The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build\n * (`dist/`), so walk up until a `templates` folder holds the `blank` template.\n */\nexport function templatesRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'templates')\n if (existsSync(join(candidate, 'blank'))) return candidate\n const parent = dirname(dir)\n if (parent === dir) throw new Error('acrylblends cannot find its templates folder')\n dir = parent\n }\n}\n\n/** Absolute path of a template folder. */\nexport function templateDir(template: Template): string {\n return join(templatesRoot(), template)\n}\n\n/** The text every generated app's `AGENTS.md` starts with (`shared/`, beside `templates/`). */\nexport function sharedDir(): string {\n return join(dirname(templatesRoot()), 'shared')\n}\n\n/**\n * The framework's docs, as ACRYL wrote them: `docs/framework` of the repository, or the copy `prepack` puts inside the package. The module sits at\n * different depths in source and in the build, so walk up until a `docs/framework` folder holds the README.\n */\nexport function frameworkDocsRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'docs', 'framework')\n if (existsSync(join(candidate, 'README.md'))) return candidate\n const parent = dirname(dir)\n if (parent === dir)\n throw new Error('acrylblends cannot find the framework docs (docs/framework)')\n dir = parent\n }\n}\n","import { resolve } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport type { Io } from './io'\nimport { closure, copyPlugin, readLibrary } from './library'\nimport {\n copyFrameworkDocs,\n copyTemplate,\n isValidName,\n mergeDependencies,\n mergeTooling,\n nameFrom,\n readTemplateDesktopPath,\n readTemplatePlugins,\n TargetNotEmptyError,\n writeAgentFiles,\n writeDesktopPath,\n} from './project'\nimport {\n frameworkDocsRoot,\n isTemplate,\n sharedDir,\n TEMPLATES,\n type Template,\n templateDir,\n} from './templates'\n\n/** A command line mistake. The CLI prints it with the help text and exits 2. */\nexport class UsageError extends Error {\n override name = 'UsageError'\n}\n\n/** What `init` was asked for. Anything left undefined is asked for on a terminal, or defaulted. */\nexport interface InitOptions {\n directory: string | undefined\n name: string | undefined\n template: Template | undefined\n install: boolean | undefined\n /** Also add the desktop shell (`add desktop`): an Electron window on the app's own server. */\n desktop: boolean\n yes: boolean\n}\n\n/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */\nexport function parseInitOptions(argv: readonly string[]): InitOptions {\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('init takes at most one directory')\n if (values.agent && values['no-agent'])\n throw new UsageError('use --agent or --no-agent, not both')\n if (values.template !== undefined && !isTemplate(values.template))\n throw new UsageError(`unknown template \"${values.template}\" (${TEMPLATES.join(', ')})`)\n const alias: Template | undefined = values.agent\n ? 'agent'\n : values['no-agent']\n ? 'blank'\n : undefined\n if (values.template !== undefined && alias !== undefined && values.template !== alias)\n throw new UsageError(\n `--template ${values.template} contradicts --${alias === 'agent' ? 'agent' : 'no-agent'}`,\n )\n return {\n directory: positionals[0],\n name: values.name,\n template: (values.template as Template | undefined) ?? alias,\n install: values.install,\n desktop: values.desktop === true,\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 name: { type: 'string' },\n template: { type: 'string' },\n agent: { type: 'boolean' },\n 'no-agent': { type: 'boolean' },\n install: { type: 'boolean' },\n desktop: { type: 'boolean' },\n yes: { type: 'boolean', short: 'y' },\n },\n })\n\nconst yes = (answer: string) => ['y', 'yes'].includes(answer.trim().toLowerCase())\n\nasync function askTemplate(ask: (question: string) => Promise<string>): Promise<Template> {\n const answer = (await ask(`Template (${TEMPLATES.join(', ')}) [blank]: `)).trim().toLowerCase()\n if (answer === '') return 'blank'\n if (isTemplate(answer)) return answer\n throw new UsageError(`unknown template \"${answer}\" (${TEMPLATES.join(', ')})`)\n}\n\n/** Scaffold a Cordis app. Returns the exit code: 0 done, 1 refused or failed. Never overwrites anything. */\nexport async function init(options: InitOptions, io: Io): Promise<number> {\n const relative = options.directory ?? '.'\n const directory = resolve(io.cwd, relative)\n const ask = options.yes ? undefined : io.ask\n\n let name = options.name ?? nameFrom(directory)\n if (options.name === undefined && ask)\n name = (await ask(`Project name (${name}): `)).trim() || name\n if (!isValidName(name)) {\n throw new UsageError(\n `\"${name}\" is not a valid project name (lower case letters, digits and dashes, starting with a letter)`,\n )\n }\n const template = options.template ?? (ask ? await askTemplate(ask) : 'blank')\n const install =\n options.install ?? (ask ? yes(await ask('Install dependencies with npm now? (y/N): ')) : false)\n\n let files: string[]\n try {\n files = copyTemplate(templateDir(template), directory, name)\n files.push(...writeAgentFiles(sharedDir(), templateDir(template), directory, name))\n files.push(...copyFrameworkDocs(frameworkDocsRoot(), directory))\n } catch (error) {\n if (!(error instanceof TargetNotEmptyError)) throw error\n io.stderr.write(`error: ${error.message}\\n`)\n return 1\n }\n const plugins = closure(\n [...readTemplatePlugins(templateDir(template)), ...(options.desktop ? ['desktop'] : [])],\n readLibrary(),\n )\n for (const plugin of plugins) {\n copyPlugin(plugin, directory)\n mergeDependencies(directory, plugin.dependencies)\n mergeTooling(directory, plugin)\n }\n const desktopPath = options.desktop ? readTemplateDesktopPath(templateDir(template)) : undefined\n if (desktopPath !== undefined) writeDesktopPath(directory, desktopPath)\n io.stdout.write(\n `created ${name} (${template}) in ${directory}: ${files.length} files, plugins ${plugins.map((plugin) => plugin.name).join(', ')}\\n`,\n )\n\n if (install && (await io.install(directory)) !== 0) {\n io.stderr.write('error: npm install failed; run it yourself in the new directory\\n')\n return 1\n }\n const steps = [\n relative === '.' ? '' : ` cd ${relative}\\n`,\n install ? '' : ' npm install\\n',\n ' npm run dev\\n npm test\\n',\n ]\n io.stdout.write(`\\nNext:\\n${steps.join('')}`)\n return 0\n}\n","import { cpSync, existsSync, readFileSync, statSync } from 'node:fs'\nimport { basename, join, resolve } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { UsageError } from './init'\nimport type { Io } from './io'\nimport { closure, copyPlugin, type LibraryPlugin, readLibrary } from './library'\nimport { appendGitignore, mergeDependencies, mergeTooling } from './project'\n\nexport interface AddOptions {\n readonly names: readonly string[]\n readonly directory: string | undefined\n}\n\n/** Parse `add <plugin...> [--dir <app>]`. */\nexport function parseAddOptions(argv: readonly string[]): AddOptions {\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 if (parsed.positionals.length === 0)\n throw new UsageError('add needs at least one plugin name (see \"acrylblends plugins\")')\n return { names: parsed.positionals, directory: parsed.values.dir }\n}\n\nconst parse = (argv: readonly string[]) =>\n parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: { dir: { type: 'string' } },\n })\n\nconst present = (appDir: string, name: string) => existsSync(join(appDir, 'src', 'plugins', name))\n\n/**\n * Copy library plugins into an existing app, with the plugins they require, and add their npm packages to its package.json.\n * Refuses a plugin the app already has. Returns the exit code.\n */\nexport async function add(options: AddOptions, io: Io): Promise<number> {\n const appDir = resolve(io.cwd, options.directory ?? '.')\n if (!existsSync(join(appDir, 'package.json')) || !existsSync(join(appDir, 'src'))) {\n io.stderr.write(\n `error: ${appDir} is not an app (no package.json and src/); run \"acrylblends init\" first\\n`,\n )\n return 1\n }\n // A path is an external plugin folder; anything else is a library plugin name.\n const external = options.names.filter((name) => /[\\\\/]/.test(name) || name.startsWith('.'))\n for (const folder of external) {\n const code = addExternal(resolve(io.cwd, folder), appDir, io)\n if (code !== 0) return code\n }\n // The desktop shell has two forms: the Blend app (it needs the `instance` plugin) and a plain Acryl app (`export default app`); `desktop` means the one that fits.\n const plain = !existsSync(join(appDir, 'src', 'plugins', 'instance'))\n const names = options.names\n .filter((name) => !external.includes(name))\n .map((name) => (name === 'desktop' && plain ? 'desktop-web' : name))\n if (names.length === 0) return 0\n const already = names.filter((name) => present(appDir, name))\n if (already.length > 0) {\n io.stderr.write(\n `error: the app already has ${already.map((name) => `src/plugins/${name}`).join(', ')}\\n`,\n )\n return 1\n }\n const toCopy: LibraryPlugin[] = closure(names, readLibrary()).filter(\n (plugin) => !present(appDir, plugin.name),\n )\n const addedDependencies: string[] = []\n for (const plugin of toCopy) {\n copyPlugin(plugin, appDir)\n addedDependencies.push(...mergeDependencies(appDir, plugin.dependencies))\n const tooling = mergeTooling(appDir, plugin)\n appendGitignore(appDir, plugin.gitignore)\n addedDependencies.push(...tooling.devDependencies)\n if (tooling.scripts.length > 0)\n io.stdout.write(\n `new scripts: ${tooling.scripts.map((name) => `npm run ${name}`).join(', ')}\\n`,\n )\n }\n io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(', ')} to ${appDir}\\n`)\n if (addedDependencies.length > 0)\n io.stdout.write(`new dependencies: ${addedDependencies.join(', ')} (run npm install)\\n`)\n for (const plugin of toCopy.filter((plugin) => names.includes(plugin.name))) {\n io.stdout.write(\n `\\n${plugin.name}: ${plugin.mount ?? `import it from './plugins/${plugin.name}/index.ts' and mount it in src/app.ts`}\\n`,\n )\n }\n return 0\n}\n\nconst PLUGIN_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/\n\n/**\n * Bring an external plugin folder (a package made with the builder's package_plugin, or any folder with an index.ts that exports a\n * Cordis plugin) into the app's `extensions/`, where the extensions plugin installs it like any other: checked, staged, started,\n * verified. Its runtime dependencies are not installed here; they are reported so the user adds them deliberately.\n */\nfunction addExternal(folder: string, appDir: string, io: Io): number {\n if (!existsSync(folder) || !statSync(folder).isDirectory()) {\n io.stderr.write(`error: ${folder} is not a folder\\n`)\n return 1\n }\n if (!['index.ts', 'index.js', 'index.mjs'].some((entry) => existsSync(join(folder, entry)))) {\n io.stderr.write(`error: ${folder} has no index.ts that exports a Cordis plugin\\n`)\n return 1\n }\n const manifestFile = join(folder, 'package.json')\n const manifest = existsSync(manifestFile)\n ? (JSON.parse(readFileSync(manifestFile, 'utf8')) as {\n name?: string\n dependencies?: Record<string, string>\n })\n : {}\n const name = (manifest.name ?? basename(folder)).replace(/^@[^/]+\\//, '')\n if (!PLUGIN_NAME.test(name)) {\n io.stderr.write(\n `error: \"${name}\" is not a plugin name (lowercase letters, digits and dashes)\\n`,\n )\n return 1\n }\n const target = join(appDir, 'extensions', name)\n if (existsSync(target)) {\n io.stderr.write(`error: the app already has extensions/${name}\\n`)\n return 1\n }\n cpSync(folder, target, {\n recursive: true,\n filter: (path) => !/[\\\\/](node_modules|\\.git)([\\\\/]|$)/.test(path.slice(folder.length)),\n })\n io.stdout.write(\n `added external plugin ${name} to ${join(appDir, 'extensions')}; it installs at the next start\\n`,\n )\n const dependencies = Object.keys(manifest.dependencies ?? {})\n if (dependencies.length > 0)\n io.stdout.write(\n `it uses ${dependencies.join(', ')}: add them to the app with npm install first\\n`,\n )\n return 0\n}\n\n/** `acrylblends plugins`: the library, one line per plugin. */\nexport function listPlugins(io: Io): number {\n for (const plugin of readLibrary().values()) {\n const requires = plugin.requires.length > 0 ? ` (requires ${plugin.requires.join(', ')})` : ''\n io.stdout.write(`${plugin.name.padEnd(12)} ${plugin.description}${requires}\\n`)\n }\n return 0\n}\n","import { existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nexport interface Invocation {\n file: string\n args: string[]\n}\n\n/**\n * How to start a package manager without the shell option (Node warns about passing args to a shell, DEP0190, and a `.cmd` shim cannot be spawned directly).\n * Off Windows the command runs as it is. On Windows npm runs as `node npm-cli.js` (the one npm is running from, else the one beside `node`); any other\n * manager goes through `cmd.exe /d /s /c <name>.cmd`, whose arguments stay an array.\n */\nexport function packageManagerInvocation(\n command: string,\n args: readonly string[],\n platform: NodeJS.Platform = process.platform,\n env: NodeJS.ProcessEnv = process.env,\n exists: (path: string) => boolean = existsSync,\n execPath: string = process.execPath,\n): Invocation {\n if (platform !== 'win32') return { file: command, args: [...args] }\n if (command === 'npm') {\n const running = env.npm_execpath\n const cli =\n running !== undefined && /npm-cli\\.c?js$/.test(running)\n ? running\n : join(dirname(execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')\n if (exists(cli)) return { file: execPath, args: [cli, ...args] }\n }\n return { file: env.ComSpec ?? 'cmd.exe', args: ['/d', '/s', '/c', `${command}.cmd`, ...args] }\n}\n","import { spawn } from 'node:child_process'\nimport { createInterface } from 'node:readline/promises'\nimport { packageManagerInvocation } from './spawn'\n\n/** Everything the CLI touches outside the filesystem. Tests pass their own. */\nexport interface Io {\n stdout: { write(text: string): unknown }\n stderr: { write(text: string): unknown }\n cwd: string\n /** Ask one question; `undefined` when there is no terminal to ask on. */\n ask: ((question: string) => Promise<string>) | undefined\n /** Install dependencies in a directory; resolves to the exit code. */\n install(directory: string): Promise<number>\n}\n\n/** The real terminal and `npm install`. */\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 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 install: (directory) =>\n new Promise((done) => {\n const { file, args } = packageManagerInvocation('npm', ['install'])\n const child = spawn(file, args, { cwd: directory, stdio: 'inherit' })\n child.on('error', () => done(1))\n child.on('exit', (code) => done(code ?? 1))\n }),\n }\n}\n","import { createRequire } from 'node:module'\nimport { add, listPlugins, parseAddOptions } from './add'\nimport { init, parseInitOptions, UsageError } from './init'\nimport { type Io, processIo } from './io'\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 = `acrylblends ${VERSION} - scaffold Cordis apps\n\nUsage:\n acrylblends init [dir] [options] Create a Cordis app in an empty or new directory\n acrylblends add <plugin...> [--dir d] Copy library plugins (and what they require) into an app;\n a folder path brings an external plugin into its extensions/\n acrylblends plugins List the plugin library\n\nOptions for init:\n --template <name> blank or agent (asked when not given); --agent means --template agent,\n --no-agent means --template blank\n --name <name> the project name (default: the directory name)\n --desktop also add the desktop shell: npm run desktop opens a window on the app\n --install run npm install after copying\n -y, --yes ask nothing; use defaults (the blank template, no install)\n\n -h, --help show this help\n -v, --version show the version\n`\n\n/**\n * Run the CLI and return the exit code: 0 done, 1 failed or refused, 2 a usage error. Never calls `process.exit`.\n */\nexport async function runCli(argv: readonly string[], io: Io = processIo()): Promise<number> {\n const [command, ...args] = argv\n try {\n if (command === undefined || command === 'help' || command === '-h' || command === '--help') {\n io.stdout.write(HELP)\n return command === undefined ? 2 : 0\n }\n if (command === 'version' || command === '-v' || command === '--version') {\n io.stdout.write(`${VERSION}\\n`)\n return 0\n }\n if (command === 'init') return await init(parseInitOptions(args), io)\n if (command === 'add') return await add(parseAddOptions(args), io)\n if (command === 'plugins') return listPlugins(io)\n throw new UsageError(`unknown command \"${command}\"`)\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 instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n}\n"],"mappings":"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"}
@@ -50,7 +50,7 @@ They are enabled as they are ported.
50
50
  | Run the app as a desktop app | [Desktop](getting-started/desktop.md) |
51
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) |
52
52
  | Understand the design | [Concepts](concepts/motivation.md) |
53
- | Copy a pattern | [Guides](guides/best-practices.md), [Validation](guides/validation.md), [Testing](guides/testing.md) |
53
+ | Copy a pattern | [Guides](guides/best-practices.md), [Validation](guides/validation.md), [Testing](guides/testing.md), [Forms, sessions and a database](guides/forms-and-sessions.md) |
54
54
  | Use Acryl inside a Cordis app | [Acryl in a Cordis app](cordis/acryl-in-a-cordis-app.md), [Plugin lifecycle](cordis/plugin-lifecycle.md), [Mounting](cordis/mounting.md) |
55
55
  | Move from the 0.3 router calls | [Migration from 0.3](cordis/migration-from-0-3.md) |
56
56
  | Move from the 0.4 names (`App`) | [Migration from 0.4](cordis/migration-from-0-4.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 `fd1f7dcaaff8b757c0562294002c0d32d6e0a893`, 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 `524406d2033cdc5ca5e01207f9ce29543bbf221b`, 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,6 +1,6 @@
1
1
  # API summary
2
2
 
3
- One page with the calls an ordinary app uses, each linked to the page that explains it. Imports are from `acryl-router` unless noted.
3
+ One page with the calls an ordinary app uses, each linked to the page that explains it. Imports are from `acrylblends` (and its subpaths such as `acrylblends/cookie`) unless noted. The first example is a Cordis plugin; a plain app (`npm create acrylblends@latest`) uses the `Acryl` class below with no plugin around it.
4
4
 
5
5
  ## A plugin that serves an app
6
6
 
@@ -21,7 +21,7 @@ In a Blend app, the same two commands (`npx acrylblends add desktop`, `npm insta
21
21
  ## What it adds
22
22
 
23
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`.
24
+ - `electron` as a development dependency (pinned to 44.9.3) and the script `npm run desktop`.
25
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
26
 
27
27
  ## What `npm run desktop` does
@@ -0,0 +1,231 @@
1
+ # Forms, sessions and a database
2
+
3
+ This page shows the parts a small multi-user app needs, in the order an app uses them: a database, passwords, a cookie session, HTML forms with server-side validation, and a JSON API that shares the same rules.
4
+ It is the shape of a "Notes" app built from these docs alone (sign up, log in, create, edit, delete and search notes, a typed test client for the API).
5
+
6
+ ## A database with `node:sqlite`
7
+
8
+ Node.js 22.19 and later include SQLite, so there is nothing to install. `DatabaseSync` is synchronous, which suits a small app.
9
+
10
+ ```ts
11
+ import { DatabaseSync } from 'node:sqlite'
12
+
13
+ export function openDb(path = process.env.NOTES_DB ?? 'notes.db'): DatabaseSync {
14
+ const db = new DatabaseSync(path)
15
+ db.exec(`
16
+ CREATE TABLE IF NOT EXISTS users (
17
+ id INTEGER PRIMARY KEY,
18
+ email TEXT NOT NULL UNIQUE,
19
+ password_hash TEXT NOT NULL
20
+ );
21
+ CREATE TABLE IF NOT EXISTS sessions (
22
+ token TEXT PRIMARY KEY,
23
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
24
+ );
25
+ `)
26
+ return db
27
+ }
28
+ ```
29
+
30
+ Take the database as an argument (`createApp(db)`) instead of opening it at import time. A test then passes `openDb(':memory:')`, and importing the app module does not create a file.
31
+
32
+ ## Passwords with `node:crypto`
33
+
34
+ Never store a password. Store a salted `scrypt` hash and compare in constant time.
35
+
36
+ ```ts
37
+ import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto'
38
+ import { promisify } from 'node:util'
39
+
40
+ const scryptAsync = promisify(scrypt) as (password: string, salt: Buffer, length: number) => Promise<Buffer>
41
+
42
+ export async function hashPassword(password: string): Promise<string> {
43
+ const salt = randomBytes(16)
44
+ const key = await scryptAsync(password, salt, 64)
45
+ return `${salt.toString('hex')}:${key.toString('hex')}`
46
+ }
47
+
48
+ export async function verifyPassword(password: string, stored: string): Promise<boolean> {
49
+ const [salt, key] = stored.split(':')
50
+ if (!salt || !key) return false
51
+ const expected = Buffer.from(key, 'hex')
52
+ return timingSafeEqual(await scryptAsync(password, Buffer.from(salt, 'hex'), expected.length), expected)
53
+ }
54
+ ```
55
+
56
+ ## A cookie session
57
+
58
+ The simplest session is a random token in an `httpOnly` cookie and a `sessions` row that maps it to a user. Log out deletes the row, so a stolen cookie stops working.
59
+ `getCookie`, `setCookie` and `deleteCookie` are in [the Cookie helper](../helpers/cookie.md).
60
+
61
+ ```ts
62
+ import { Acryl } from 'acrylblends'
63
+ import { createMiddleware } from 'acrylblends/factory'
64
+ import { deleteCookie, getCookie, setCookie } from 'acrylblends/cookie'
65
+ import { randomBytes } from 'node:crypto'
66
+ import type { DatabaseSync } from 'node:sqlite'
67
+
68
+ type Env = { Variables: { user: { id: number; email: string } } }
69
+
70
+ export function createApp(db: DatabaseSync) {
71
+ const userFor = (token: string | undefined) =>
72
+ token
73
+ ? (db
74
+ .prepare('SELECT users.id AS id, users.email AS email FROM sessions JOIN users ON users.id = sessions.user_id WHERE token = ?')
75
+ .get(token) as { id: number; email: string } | undefined)
76
+ : undefined
77
+
78
+ const requireUser = createMiddleware<Env>(async (c, next) => {
79
+ const user = userFor(getCookie(c, 'sid'))
80
+ if (!user) return c.redirect('/login')
81
+ c.set('user', user)
82
+ await next()
83
+ })
84
+
85
+ const app = new Acryl<Env>()
86
+ app.use('/notes', requireUser)
87
+ app.use('/notes/*', requireUser)
88
+ app.get('/notes', (c) => c.text(`Hello ${c.get('user').email}`))
89
+
90
+ app.post('/login-example', (c) => {
91
+ const token = randomBytes(32).toString('hex')
92
+ db.prepare('INSERT INTO sessions (token, user_id) VALUES (?, ?)').run(token, 1)
93
+ setCookie(c, 'sid', token, { path: '/', httpOnly: true, sameSite: 'Lax', maxAge: 60 * 60 * 24 * 30 })
94
+ return c.redirect('/notes', 303)
95
+ })
96
+
97
+ app.post('/logout', (c) => {
98
+ const token = getCookie(c, 'sid')
99
+ if (token) db.prepare('DELETE FROM sessions WHERE token = ?').run(token)
100
+ deleteCookie(c, 'sid', { path: '/' })
101
+ return c.redirect('/login', 303)
102
+ })
103
+ return app
104
+ }
105
+ ```
106
+
107
+ A middleware registered with `use('/notes', ...)` does not cover `/notes/…`: register both `'/notes'` and `'/notes/*'`.
108
+ After a form POST redirect with status **303** so the browser follows with a GET.
109
+
110
+ ## Forms and server-side validation
111
+
112
+ `await c.req.parseBody()` reads an HTML form (`application/x-www-form-urlencoded` or multipart). Each value is a `string` or a `File`, so convert with `String(...)` or check `typeof`.
113
+ Keep the rules in one function that both the form routes and the JSON API call, and answer an invalid form with **status 400 and the same page**, with the messages and the typed values filled in:
114
+
115
+ ```ts
116
+ export interface NoteInput {
117
+ title: string
118
+ body: string
119
+ }
120
+
121
+ export function checkNote(value: unknown): { value: NoteInput } | { issues: { message: string; path: string[] }[] } {
122
+ const input = (value ?? {}) as Record<string, unknown>
123
+ const title = typeof input.title === 'string' ? input.title.trim() : ''
124
+ const body = typeof input.body === 'string' ? input.body : ''
125
+ const issues: { message: string; path: string[] }[] = []
126
+ if (title === '') issues.push({ message: 'Give the note a title.', path: ['title'] })
127
+ if (body.length > 2000) issues.push({ message: 'The note is at most 2000 characters.', path: ['body'] })
128
+ return issues.length > 0 ? { issues } : { value: { title, body } }
129
+ }
130
+ ```
131
+
132
+ ```tsx
133
+ import { Acryl } from 'acrylblends'
134
+
135
+ const app = new Acryl()
136
+
137
+ app.post('/notes', async (c) => {
138
+ const form = await c.req.parseBody()
139
+ const checked = checkNote(form)
140
+ if ('issues' in checked) {
141
+ const messages = checked.issues.map((issue) => issue.message)
142
+ return c.html(
143
+ <form method="post" action="/notes">
144
+ <ul role="alert">
145
+ {messages.map((message) => (
146
+ <li>{message}</li>
147
+ ))}
148
+ </ul>
149
+ <input name="title" value={String(form.title ?? '')} />
150
+ <textarea name="body">{String(form.body ?? '')}</textarea>
151
+ <button type="submit">Save</button>
152
+ </form>,
153
+ 400,
154
+ )
155
+ }
156
+ return c.redirect('/notes', 303)
157
+ })
158
+ ```
159
+
160
+ Escaping is automatic in JSX: `{String(form.title)}` is safe to put in a page. Use `dangerouslySetInnerHTML` only for text you wrote yourself, such as a `<style>` or `<script>` block.
161
+
162
+ ## A JSON API that shares the rules
163
+
164
+ Wrap `checkNote` as a Standard Schema (an object with a `~standard.validate` function) and give it to `schemaValidator`. A failure answers 400 with `{ error, issues: [{ message, path }] }`.
165
+ Build the API app by **chaining** the routes on `new Acryl()`, because the typed client and `testClient` only know chained routes:
166
+
167
+ ```ts
168
+ import { Acryl } from 'acrylblends'
169
+ import { schemaValidator, type StandardSchema } from 'acrylblends/schema-validator'
170
+
171
+ const NoteSchema: StandardSchema<NoteInput> = {
172
+ '~standard': { validate: (value) => checkNote(value) },
173
+ }
174
+
175
+ export const api = new Acryl()
176
+ .get('/', (c) => c.json({ notes: [] as NoteInput[] }, 200))
177
+ .post('/', schemaValidator('json', NoteSchema), (c) => c.json({ note: c.req.valid('json') }, 201))
178
+ .get('/:id', (c) => (c.req.param('id') === '1' ? c.json({ note: { title: 't', body: '' } }, 200) : c.json({ error: 'No such note.' }, 404)))
179
+ ```
180
+
181
+ Mount it below the pages with `app.route('/api/notes', api)`.
182
+
183
+ ## Testing every route
184
+
185
+ Call the app with `app.request`. A form POST needs a body and the cookie of a signed-in user, which a test reads from the sign-up response's `set-cookie` header:
186
+
187
+ ```ts
188
+ const response = await app.request('/signup', {
189
+ method: 'POST',
190
+ body: new URLSearchParams({ email: 'a@example.com', password: 'correct horse' }),
191
+ })
192
+ const cookie = (response.headers.get('set-cookie') ?? '').split(';')[0] ?? ''
193
+ await app.request('/notes', { headers: { cookie } })
194
+ ```
195
+
196
+ For the typed API use `testClient` ([Testing helper](../helpers/testing.md)); the cookie goes in the second argument of each call:
197
+
198
+ ```ts
199
+ import { testClient } from 'acrylblends/testing'
200
+
201
+ const client = testClient(api)
202
+ const created = await client.index.$post({ json: { title: 'First', body: 'hello' } }, { headers: { cookie } })
203
+ const { note } = await created.json()
204
+ const one = await client[':id'].$get({ param: { id: '1' } }, { headers: { cookie } })
205
+ ```
206
+
207
+ A route mounted at `'/'` is `client.index` and `'/:id'` is `client[':id']`.
208
+
209
+ ## A part of the page that reacts in the browser
210
+
211
+ A live character counter or an instant filter needs a little JavaScript in the page. The simplest way, with no build step, is a `<script>` in the server-rendered page:
212
+
213
+ ```tsx
214
+ const counter = `
215
+ const area = document.getElementById('body')
216
+ const out = document.getElementById('count')
217
+ const show = () => { out.textContent = area.value.length + ' / ' + area.maxLength }
218
+ area.addEventListener('input', show)
219
+ show()
220
+ `
221
+
222
+ export const Field = () => (
223
+ <>
224
+ <textarea id="body" name="body" maxlength={2000} />
225
+ <small id="count" aria-live="polite" />
226
+ <script dangerouslySetInnerHTML={{ __html: counter }} />
227
+ </>
228
+ )
229
+ ```
230
+
231
+ [Client Components](jsx-dom.md) (`acrylblends/jsx/dom`) are for pages that need components and state in the browser; they have to be bundled for the browser first (with Vite or esbuild).
@@ -105,11 +105,11 @@ export default function App() {
105
105
  </button>
106
106
  <div>
107
107
  {!showLargeImage ? (
108
- <img src=' />
108
+ <img src="/photo.jpg" alt="" />
109
109
  ) : (
110
110
  <div
111
111
  class={css`
112
- background: url(');
112
+ background: url('/photo.jpg');
113
113
  background-size: contain;
114
114
  background-repeat: no-repeat;
115
115
  background-position: center;
@@ -170,12 +170,12 @@ export default function App() {
170
170
  </button>
171
171
  <div>
172
172
  {!showLargeImage ? (
173
- <img src=' />
173
+ <img src="/photo.jpg" alt="" />
174
174
  ) : (
175
175
  <div
176
176
  class={css`
177
177
  ${transitionNameClass}
178
- background: url(');
178
+ background: url('/photo.jpg');
179
179
  background-size: contain;
180
180
  background-repeat: no-repeat;
181
181
  background-position: center;
@@ -240,12 +240,12 @@ export default function App() {
240
240
  </button>
241
241
  <div>
242
242
  {!showLargeImage ? (
243
- <img src=' />
243
+ <img src="/photo.jpg" alt="" />
244
244
  ) : (
245
245
  <div
246
246
  class={css`
247
247
  ${transitionNameClass}
248
- background: url(');
248
+ background: url('/photo.jpg');
249
249
  background-size: contain;
250
250
  background-repeat: no-repeat;
251
251
  background-position: center;