@canonical/summon-core

中文文档
0.44.0发布于 2026/10/23GPL-3.0TypeScriptES Module构建来源已验证近 30 天下载 2691

Code generation framework: generator definitions, templates, stamps, discovery. Built on @canonical/task.

原始使用文档
文档目录22 节
  1. Installation
  2. Using Generators
  3. CLI Options
  4. Shell Autocompletion
  5. Positional Arguments
  6. Installing Generator Packages
  7. Creating Generators
  8. Package Structure
  9. Local Development
  10. Testing Generators
  11. The Monadic Pattern
  12. What's a Monad?
  13. The Task Type
  14. Composing Tasks
  15. Key Operations
  16. Why Monads for Generators?
  17. Templating Engine
  18. Default (EJS)
  19. Custom Engines
  20. Interface
  21. Documentation
  22. License

Summon

A code generator framework where generators are pure functions that return data, not side effects.

Write a generator once. Run it for real. Preview with --dry-run. Test without mocks. Same code, different interpreters.

generate: (answers) => writeFile(`src/${answers.name}.ts`, code)
// Returns a Task describing "write this file"
// Not a file write. Data.

Installation

npm install -g @canonical/summon   # or: bun add -g @canonical/summon

The CLI runs under plain Node — Bun is not required. It ships only with an example generator and summon init (to scaffold your own generator). The real generators live in their own packages; install the ones you need into the same location and Summon discovers them automatically:

npm install -g @canonical/summon-application   # summon application / domain / page / wrapper
npm install -g @canonical/summon-component      # summon component

Using Generators

Summon discovers generators from installed packages automatically. See installing generator packages to install an existing generator package, or creating generators to create a new one.

# See what's available
summon

# Run a generator interactively
summon component react

# Pass answers as positional argument (when supported)
summon component react src/components/Button

# Or as a flag
summon component react --component-path=src/components/Button

# Preview first (nothing written to disk)
summon component react src/components/Button --dry-run

# Show debug output
summon component react src/components/Button --verbose

Every prompt becomes a CLI flag. Boolean prompts with default: true use the --no- prefix to disable. Generators may also define a positional argument for their primary input (like a path), allowing cleaner command invocations.

CLI Options

FlagDescription
-d, --dry-runPreview without writing files
-y, --yesSkip confirmation prompts and preview
-v, --verboseShow debug output
--show-filesShow file contents in dry-run
-l, --llmLLM mode: dry-run with markdown output, no prompts, no stamps
--format <type>Output format: json (implies dry-run, no prompts, no stamps)
--no-previewSkip the file preview
--no-generated-stampDisable generated file stamp comments

LLM Usage: Summon treats LLMs as first-class CLI users. Use --llm for structured markdown output or --format json for machine-parseable JSON. Both imply --dry-run --yes --show-files --no-generated-stamp:

# Structured markdown (answers table, plan table, fenced code blocks)
summon component react src/components/Button --llm

# Machine-parseable JSON
summon component react src/components/Button --format json

# Session-level LLM mode via environment variable
export SUMMON_LLM=1
summon component react src/components/Button

# Structured help for LLMs
summon component react --help --llm

Shell Autocompletion

Summon supports TAB completion for Bash, Zsh, and Fish shells. Completions are dynamic - they automatically detect newly installed generator packages without needing to re-run setup.

# Automatic setup (recommended)
summon --setup-completion

# Or manual installation for each shell:

# Zsh
echo '. <(summon --completion)' >> ~/.zshrc

# Bash (may need: brew install bash-completion on macOS)
summon --completion >> ~/.summon-completion.sh
echo 'source ~/.summon-completion.sh' >> ~/.bash_profile

# Fish
echo 'summon --completion-fish | source' >> ~/.config/fish/config.fish

After setup, restart your shell or source the config file. Then:

summon <TAB>                    # Shows: component, application, init, ...
summon component <TAB>          # Shows: react, svelte, lit
summon component react <TAB>    # Shows: --component-path, --no-with-styles, ...
summon component react --comp<TAB>  # Completes to: --component-path

Completion features:

  • Generator names - Navigate the command tree with TAB
  • Generator flags - All prompts become completable flags
  • Confirm prompts - Shows --no-X for prompts with default: true
  • Select/multiselect - Completes with available choices
  • Path prompts - Filesystem path completion for prompts containing "path", "dir", "file", etc.

To remove autocompletion:

summon --cleanup-completion

Positional Arguments

Generators can define one prompt as a positional argument, allowing users to provide the primary input without a flag:

# With positional argument support
summon component react src/components/Button

# Equivalent to
summon component react --component-path=src/components/Button

Positional arguments also get filesystem path completion when using TAB:

summon component react src/comp<TAB>  # Completes to: src/components/

Installing Generator Packages

The CLI bundles the application, domain, page, and wrapper generators. Other generators — including the official @canonical/summon-component — ship as separate packages following the summon-* or @scope/summon-* naming convention. Install one into the same location as the CLI (both global, or both project dependencies) and summon discovers it automatically:

# Component generators (react/svelte/lit)
npm install -g @canonical/summon-component   # or: bun add -g ...

# Now available (completions work immediately!)
summon component react
summon component svelte

Creating Generators

A generator is a pure function that takes answers and returns a Task—a data structure describing what to do.

// src/module/types.ts
interface ModuleAnswers {
  name: string;
  withTests: boolean;
}

// src/module/generator.ts
import type { GeneratorDefinition } from "@canonical/summon";
import { debug, info, writeFile, mkdir, sequence_, when } from "@canonical/summon";
import type { ModuleAnswers } from "./types.js";

export const generator = {
  meta: {
    name: "module",
    description: "Creates a new module",
    version: "0.1.0",
  },

  prompts: [
    // positional: true allows `summon module src/utils` instead of `--name=src/utils`
    { name: "name", type: "text", message: "Module name:", positional: true },
    { name: "withTests", type: "confirm", message: "Include tests?", default: true },
  ],

  generate: (answers) => sequence_([
    info(`Creating module: ${answers.name}`),

    debug("Creating module directory"),
    mkdir(`src/${answers.name}`),

    debug("Creating index file"),
    writeFile(`src/${answers.name}/index.ts`, `export const ${answers.name} = {};\n`),

    when(answers.withTests, debug("Creating test file")),
    when(answers.withTests,
      writeFile(`src/${answers.name}/index.test.ts`, `test("works", () => {});\n`)
    ),

    info(`Created module at src/${answers.name}`),
  ]),
} as const satisfies GeneratorDefinition<ModuleAnswers>;

// src/module/index.ts (barrel)
export { generator } from "./generator.js";
export type { ModuleAnswers } from "./types.js";

// src/index.ts (package barrel)
import type { AnyGenerator } from "@canonical/summon";
import { generator as moduleGenerator } from "./module/index.js";

export const generators = {
  "module": moduleGenerator,
} as const satisfies Record<string, AnyGenerator>;

Package Structure

Each generator should be split into three files for maintainability:

my-summon-package/
├── package.json
├── src/
│   ├── index.ts              # Package barrel - exports generators record
│   ├── module/
│   │   ├── index.ts          # Generator barrel
│   │   ├── types.ts          # Answer types
│   │   └── generator.ts      # Generator definition
│   └── templates/            # EJS templates (optional)
└── README.md
{
  "name": "@myorg/summon-module",
  "main": "src/index.ts",
  "peerDependencies": {
    "@canonical/summon": "workspace:*"
  }
}

Local Development

For developing generators locally, link them to make them globally available:

# From your generator package directory
cd /path/to/my-summon-package
bun link        # for bun
npm link        # for npm

# Now available everywhere
summon module src/utils

Project-local packages (in ./node_modules) take precedence over globally linked ones, so you can override with project-specific versions.

Testing Generators

Because generators return data, testing is straightforward—no mocks needed:

import { dryRun, getAffectedFiles } from "@canonical/summon";
import { generators } from "./index";

const generator = generators["module"];

test("creates expected files", () => {
  const task = generator.generate({ name: "utils", withTests: true });
  const { effects } = dryRun(task);

  expect(getAffectedFiles(effects)).toEqual([
    "src/utils/index.ts",
    "src/utils/index.test.ts",
  ]);
});

test("skips test file when disabled", () => {
  const task = generator.generate({ name: "utils", withTests: false });
  const { effects } = dryRun(task);

  expect(getAffectedFiles(effects)).not.toContain("src/utils/index.test.ts");
});

The dry-run interpreter maintains a virtual filesystem, so conditional logic based on exists() works correctly even without touching the disk.

The Monadic Pattern

Summon uses a monad to compose tasks. If you've used Promises, you already understand the core idea.

What's a Monad?

A monad is a design pattern for chaining operations that have some context (like "might fail" or "has effects"). Think of it as a pipeline where each step can:

  1. Transform values (map) — Apply a function to the result
  2. Chain operations (flatMap) — Use the result to create the next step
  3. Short-circuit on failure — Errors propagate automatically
// Promise (async context)
fetchUser(id)
  .then(user => fetchOrders(user.id))    // chain
  .then(orders => orders.length)          // transform
  .catch(handleError);                    // recover

// Task (effect context)
task(readFile("config.json"))
  .map(content => JSON.parse(content))    // transform
  .flatMap(config => writeFile(           // chain
    config.outputPath,
    generateCode(config)
  ))
  .recover(err => pure(defaultConfig));   // recover

The Task Type

Task<A> represents a computation that:

  • Describes effects (file I/O, shell commands, etc.)
  • Eventually produces a value of type A
  • May fail with an error
// A Task is one of three things:
type Task<A> =
  | { _tag: "Pure"; value: A }                           // Done, here's the value
  | { _tag: "Fail"; error: TaskError }                   // Failed with error
  | { _tag: "Effect"; effect: Effect; cont: ... }        // Do this effect, then continue

Composing Tasks

The TaskBuilder provides a fluent API for composition:

import { task, pure } from "@canonical/summon";

// Build a pipeline
const pipeline = task(readFile("input.txt"))
  .map(content => content.toUpperCase())           // Transform the value
  .flatMap(upper => writeFile("output.txt", upper)) // Chain another effect
  .andThen(info("Done!"))                          // Sequence (ignore previous value)
  .recover(err => pure(void 0));                   // Handle errors

// pipeline is still just data — nothing has executed yet

Key Operations

OperationDescriptionExample
pure(value)Wrap a value in a Taskpure(42)
map(fn)Transform the result.map(x => x * 2)
flatMap(fn)Chain with another Task.flatMap(x => writeFile(...))
andThen(task)Sequence, discard previous.andThen(info("next"))
recover(fn)Handle errors.recover(e => pure(default))
tap(fn)Side effect, keep value.tap(x => debug(x))

Why Monads for Generators?

  1. Composable — Small tasks combine into complex workflows
  2. Predictable — Errors propagate without explicit handling at each step
  3. Testable — The pipeline is data; inspect it without running effects
  4. Declarative — Describe what to do, not how to do it
// Complex workflow, reads like a recipe
const scaffoldFeature = (name: string) =>
  task(mkdir(`src/features/${name}`))
    .andThen(template({
      source: "templates/feature.ts.ejs",
      dest: `src/features/${name}/index.ts`,
      vars: { name },
    }))
    .andThen(when(config.withTests,
      template({
        source: "templates/test.ts.ejs",
        dest: `src/features/${name}/index.test.ts`,
        vars: { name },
      })
    ))
    .andThen(info(`Created feature: ${name}`))
    .unwrap();

For a deeper dive into the "effects as data" philosophy, see Explanation.

Templating Engine

Summon uses EJS by default for template rendering, but supports custom templating engines via the TemplatingEngine interface.

Default (EJS)

import { template, templateDir } from "@canonical/summon";

// Uses EJS by default
template({
  source: "templates/component.tsx.ejs",
  dest: "src/components/<%= name %>.tsx",
  vars: { name: "Button" },
});

Custom Engines

Implement TemplatingEngine to use Handlebars, Mustache, Nunjucks, or any other engine:

import type { TemplatingEngine } from "@canonical/summon";
import Handlebars from "handlebars";
import * as fs from "node:fs/promises";

const handlebarsEngine: TemplatingEngine = {
  render(template, vars) {
    return Handlebars.compile(template)(vars);
  },

  async renderAsync(template, vars) {
    return Handlebars.compile(template)(vars);
  },

  async renderFile(templatePath, vars) {
    const content = await fs.readFile(templatePath, "utf-8");
    return Handlebars.compile(content)(vars);
  },
};

// Use in templates
template({
  source: "templates/component.hbs",
  dest: "src/components/{{name}}.tsx",
  vars: { name: "Button" },
  engine: handlebarsEngine,
});

Interface

interface TemplatingEngine {
  /** Render a template string synchronously */
  render(template: string, vars: Record<string, unknown>): string;

  /** Render a template string asynchronously */
  renderAsync(template: string, vars: Record<string, unknown>): Promise<string>;

  /** Render a template file asynchronously */
  renderFile(templatePath: string, vars: Record<string, unknown>): Promise<string>;
}

The engine option is available on template(), templateDir(), renderString(), renderStringAsync(), and renderFile().

Documentation

License

GPL-3.0

版本与发布记录

@canonical/summon-core 共发布 27 个版本,首次发布于 2026/03/26;过去 12 个月发布了 27 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.44.0latest2026/10/2317 天前
0.43.02026/10/011 个月前
0.43.0-experimental.1experimental2026/09/291 个月前
0.42.02026/09/281 个月前
0.40.02026/09/211 个月前
0.38.02026/09/161 个月前

关于 @canonical/summon-core 的常见问题

@canonical/summon-core 是什么?

Code generation framework: generator definitions, templates, stamps, discovery. Built on @canonical/task.

如何安装 @canonical/summon-core?

在项目目录运行 npm install @canonical/summon-core,或使用 pnpm add / yarn add / bun add。

@canonical/summon-core 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@canonical/summon-core 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@canonical/summon-core 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18 <23 || >=23.6。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@canonical/summon-core 可以商用吗?

@canonical/summon-core 使用 GPL-3.0 许可。强 Copyleft:分发包含该代码的软件时,整个作品需以 GPL-3.0 兼容许可开源,并附带专利与反锁定条款。仅内部使用不触发此义务。以上为概括,不构成法律意见,请以许可原文为准。

@canonical/summon-core 的下载量有多少?

根据 npm 官方下载统计,@canonical/summon-core 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,691 次,日均约 90 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@canonical/summon-core 还在维护吗?

最新版本 0.44.0 发布于 2026/10/23(17 天前);至今共发布 27 个版本,其中过去 12 个月发布了 27 个;首次发布于 2026/03/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。