@canonical/task

中文文档
0.44.0发布于 2026/10/23LGPL-3.0TypeScriptES Module零依赖构建来源已验证近 30 天下载 2662

用于可相容,可测试,可干燥的CLI操作的一元效应框架.

中文使用文档
文档目录15 节
  1. 安装
  2. 入境点
  3. 快速启动
  4. 核心概念
  5. 任务修饰
  6. 生成语法
  7. 效果
  8. 初级
  9. A. 背景情况
  10. 错误模型
  11. 组合器
  12. 流利构建器
  13. 口译员
  14. API 总结
  15. 许可证

@ canonical/ 任务

用于可相容,可测试,可干燥的CLI操作的一元效应框架.

任务是对计算过程的纯描述——在解释之前不会执行. 这使得您可以测试文件系统操作, shell 命令, 和用户提示而不触摸真实的 I/O.

安装

bun add @canonical/task

入境点

货包装了两个入口,按它们能达到的程度分成:

  • @canonical/task ——无节点基础:核心类型,任务僧尼,效果构建器,原始人,梳理人,干跑/测试翻译(dryRun, (中文(简体) ), dryRunWith, (中文(简体) ), collectEffects,...),撤销收藏(collectUndos),以及 TaskExecutionError. 所构建的节点可以从本条目的导入图(由关闭行走测试所支撑)得到,因此它为浏览器和边缘目标打包干净. 所有关于 说明, (中文(简体) ), 编译,以及 预览 工作住在这里.
  • @canonical/task/node - 节点触控口译员: runTask 编号 : run 编号 : executeEffect (执行文件系统、执行和即时效果,导入) node:fs/promises, (中文(简体) ), node:path,以及 node:child_process和 runUndo。只是实际上 运行 针对主机的任务需要此条目.

因为模块解析发生在树摇之前, 拆分事项是: 如果口译员住在基项中, 导入一个纯函数就会拉动 node: 导入到模块图中并失败任何浏览器-平台的构建——无论捆绑实际使用什么.

快速启动

import { gen, $, writeFile, readFile, info } from "@canonical/task";
import { runTask } from "@canonical/task/node";

const setup = gen(function* () {
  const name = yield* $(readFile("name.txt"));
  yield* $(writeFile("greeting.txt", `Hello, ${name}!`));
  yield* $(info("Done"));
});

await runTask(setup);

核心概念

任务修饰

A 类 Task<A> 有三样东西:

  • 纯 - 完成的计算持有一个数值
  • 效果 - 附带效应说明
  • 失败 ——计算失败,结构错误

任务由 flatMap 或者,在工学上,使用生成语法:

import { flatMap, readFile, writeFile } from "@canonical/task";

// flatMap style
const greet = flatMap(
  readFile("name.txt"),
  (name) => writeFile("greeting.txt", `Hello, ${name}!`),
);

生成语法

这个 gen 编号 : $ 对子允许您不嵌入而写入顺序效果代码 flatMap 锁链。 使用 yield* 与 $(task) 打开任务并获得其值 :

import { gen, $, readFile, writeFile, info, exists } from "@canonical/task";

const migrate = gen(function* () {
  const raw = yield* $(readFile("config.json"));
  const config = JSON.parse(raw);

  config.version = 2;

  yield* $(writeFile("config.json", JSON.stringify(config, null, 2)));
  yield* $(info(`Migrated to v${config.version}`));

  return config;
});

在引擎盖下, gen 编曲 flatMap 调用——任务在解释前仍然是纯数据结构. 使用您喜欢的样式; gen 建议任何超过两到三个步骤.

效果

效果是纯数据——贴有标签的工会描述应该发生什么:

类别效果
文件一/OReadFile, (中文(简体) ), WriteFile, (中文(简体) ), AppendFile, (中文(简体) ), CopyFile, (中文(简体) ), CopyDirectory, (中文(简体) ), DeleteFile, (中文(简体) ), DeleteDirectory, (中文(简体) ), MakeDir, (中文(简体) ), Exists, (中文(简体) ), Glob, (中文(简体) ), Symlink
进程Exec
互动Prompt (文本、确认、选择、多选择)
日志Log (调试,信息,警告,出错)
A. 背景情况ReadContext, (中文(简体) ), WriteContext
货币Parallel, (中文(简体) ), Race

初级

任务返回包装符,用于每种效果。 这些是你最常用的构件:

import {
  readFile, writeFile, appendFile, copyFile, copyDirectory,
  deleteFile, deleteDirectory, mkdir, exists, symlink, glob,
  exec, execSimple,
  prompt, promptText, promptConfirm, promptSelect, promptMultiselect,
  log, debug, info, warn, error,
  getContext, setContext, withContext,
  noop, succeed,
} from "@canonical/task";

文件系统

const content = yield* $(readFile("src/index.ts"));
yield* $(writeFile("dist/index.js", compiled));
yield* $(appendFile("log.txt", `Built at ${Date.now()}\n`));
yield* $(copyFile("template.json", "output/package.json"));
yield* $(copyDirectory("templates/", "output/"));
yield* $(mkdir("output/lib"));
yield* $(symlink("../shared/utils", "src/utils"));

const found = yield* $(exists("tsconfig.json"));
const files = yield* $(glob("src/**/*.ts", "."));

进程执行

const result = yield* $(exec("git", ["status", "--short"]));
// result: { stdout: string, stderr: string, exitCode: number }

const simple = yield* $(execSimple("ls -la"));

用户提示

const name = yield* $(promptText("name", "Project name?", "my-app"));
const ok = yield* $(promptConfirm("confirm", "Continue?", true));
const lang = yield* $(promptSelect("lang", "Language?", [
  { label: "TypeScript", value: "ts" },
  { label: "JavaScript", value: "js" },
]));
const features = yield* $(promptMultiselect("features", "Features?", [
  { label: "Linting", value: "lint" },
  { label: "Testing", value: "test" },
]));

日志

yield* $(debug("Verbose detail"));   // shown with --verbose
yield* $(info("Progress update"));
yield* $(warn("Non-fatal issue"));
yield* $(error("Something broke"));

A. 背景情况

上下文是一个密钥-价值商店,在任务执行期间存在. 使用它来在步骤之间传递数据, 而不需要线程值 :

import { gen, $, setContext, getContext, withContext } from "@canonical/task";

const setup = gen(function* () {
  yield* $(setContext("projectName", "my-app"));

  // later, in a different part of the task tree
  const name = yield* $(getContext<string>("projectName"));
  // name: "my-app"
});

// withContext sets a key for the duration of a child task
const scoped = withContext("env", "production", deployTask);

上下文辅以 Map<string, unknown> 在制作翻译。 默认情况下干运行的解析器不会持续上下文 – 使用 dryRunWith 以提供模拟上下文值.

错误模型

结构化任务失败 TaskError 值 :

interface TaskError {
  code: string;              // programmatic error code
  message: string;           // human-readable description
  cause?: unknown;           // original error that caused this failure
  context?: Record<string, unknown>;  // additional structured data
  stack?: string;            // stack trace if available
  suppressed?: TaskError[];  // for parallel: all errors, not just the first
}

创建错误

import { fail, failWith } from "@canonical/task";

const notFound = failWith("FILE_NOT_FOUND", "Config file missing");

const detailed = fail({
  code: "VALIDATION_FAILED",
  message: "Schema mismatch",
  context: { path: "config.json", expected: "v2" },
});

框架定义了基础错误代码( :FILE_NOT_FOUND, (中文(简体) ), EXEC_FAILED, (中文(简体) ), PROMPT_CANCELLED, (中文(简体) ), TASK_INTERRUPTED, (中文(简体) ), INTERNAL;消费者可以使用任意字符串代码.

处理错误

import { recover, mapError, orElse, optional, attempt, fold } from "@canonical/task";

// recover: catch a failure and produce a new task
const safe = recover(riskyTask, (err) =>
  err.code === "FILE_NOT_FOUND" ? writeFile("config.json", "{}") : fail(err),
);

// mapError: transform the error without changing recovery
const retagged = mapError(innerTask, (err) => ({
  ...err,
  code: "DEPLOY_FAILED",
  context: { ...err.context, phase: "build" },
}));

// orElse: try A, fall back to B
const config = orElse(readFile("config.local.json"), readFile("config.json"));

// optional: swallow failure, return undefined
const maybePkg = optional(readFile("package.json"));

// attempt: capture success or failure as a value
const result = yield* $(attempt(riskyTask));
if (result.ok) { /* result.value */ } else { /* result.error */ }

// fold: handle both branches and unify the type
const status = fold(deployTask, () => "deployed", (err) => `failed: ${err.code}`);

并行错误

何时 parallel 任务失败, 第一个错误变为主错误 TaskError 和任何剩余错误作为附件附上 suppressed数字 :

// If tasks[0] and tasks[2] both fail:
// error.code      → first failure's code
// error.suppressed → [task[2]'s error]

组合器

将任务编成更大的工作流程:

顺序

import { sequence, sequence_, traverse, traverse_ } from "@canonical/task";

// Run in order, collect results
const contents = sequence([readFile("a.txt"), readFile("b.txt")]);

// Run in order, discard results
sequence_([writeFile("a.txt", "A"), writeFile("b.txt", "B")]);

// Map + sequence over an array
const compiled = traverse(sourceFiles, (f) => readFile(f));

// Map + sequence, discard results
traverse_(files, (f) => deleteFile(f));

并行执行

import { parallel, parallelN, race } from "@canonical/task";

// Run concurrently, collect all results
const all = parallel([fetchA, fetchB, fetchC]);

// Run with concurrency limit (batches of 3)
const throttled = parallelN(3, manyTasks);

// Return first to complete
const fastest = race([mirrorA, mirrorB]);

条件

import { when, unless, ifElse, whenM, ifElseM } from "@canonical/task";

// Run only if condition is true
when(isDirty, cleanUp);

// Run only if condition is false
unless(isCI, promptForConfirmation);

// Choose between two tasks
ifElse(hasConfig, loadConfig, useDefaults);

// Condition is itself a task
whenM(exists("package.json"), installDeps);

// Both condition and branches are tasks
ifElseM(exists(".env"), loadEnv, createEnv);

调度

import { switchMap } from "@canonical/task";

// Detect-then-dispatch: run a detection task, branch on its result
const result = switchMap(
  detectHarness,                // Task<"jest" | "vitest" | null>
  {
    jest: configureJest,        // Task<Config>
    vitest: configureVitest,    // Task<Config>
  },
  useDefaultConfig,             // fallback Task<Config>
);

处理错误

import { retry, orElse, optional, attempt, bracket, ensure } from "@canonical/task";

// Retry up to 3 times
const resilient = retry(flakyTask, 3);

// Try primary, fall back to secondary
const config = orElse(readFile("local.json"), readFile("default.json"));

// Swallow errors, return undefined
const maybe = optional(readFile("optional.json"));

// Capture result or error as a value
const result = attempt(riskyTask);

// Resource management (acquire → use → release, even on failure)
const safe = bracket(acquireConn, useConn, releaseConn);

// Ensure cleanup runs regardless of outcome
const withCleanup = ensure(mainTask, cleanup);

公用事业

import { tap, tapError, fold, zip, zip3 } from "@canonical/task";

// Side-effect without changing the value
const logged = tap(readFile("a.txt"), (content) => info(`Read ${content.length} bytes`));

// Side-effect on failure
const observed = tapError(riskyTask, (err) => error(`Failed: ${err.code}`));

// Handle both success and failure → unified type
const status = fold(deploy, () => "ok", (err) => `fail: ${err.code}`);

// Combine tasks into tuples
const [a, b] = yield* $(zip(readFile("a.txt"), readFile("b.txt")));
const [x, y, z] = yield* $(zip3(taskX, taskY, taskZ));

流利构建器

这个 task() 编号 : of() API 提供了一个可连锁的选项 :

import { task, of, mkdir, writeFile, info } from "@canonical/task";

const result = task(mkdir("output"))
  .andThen(writeFile("output/a.txt", "A"))
  .andThen(writeFile("output/b.txt", "B"))
  .andThen(info("Done!"))
  .unwrap();

const doubled = of(21)
  .map((n) => n * 2)
  .flatMap((n) => writeFile("answer.txt", String(n)))
  .unwrap();

打电话 .unwrap() 以提取基础 Task<A> 当你需要传递给梳理员或口译员时.

口译员

任务是惰性数据,直到一个解释者走过结构并决定如何处理每个效果. 包裹运送了三名口译员:

制作口译runTask(中文(简体) )

执行对真 I/O 的效果——文件系统,进程,提示 :

import { runTask } from "@canonical/task/node";

const value = await runTask(myTask);

runTask 接受一个 RunTaskOptions 对象 :

await runTask(myTask, {
  // Shared key-value context for ReadContext/WriteContext effects
  context: new Map([["env", "production"]]),

  // Custom prompt handler (required if the task uses Prompt effects)
  promptHandler: async (effect) => {
    // effect.question: PromptQuestion
    return "user input";
  },

  // Log routing (default: console.log with level prefix)
  onLog: (level, message) => logger[level](message),

  // Effect lifecycle hooks
  onEffectStart: (effect) => { /* before each effect */ },
  onEffectComplete: (effect, durationMs) => { /* after each effect */ },

  // AbortSignal for interruption
  signal: controller.signal,
});

当任务失败时, runTask 丢出一个 TaskExecutionError 包起来 TaskError数字 :

import { TaskExecutionError } from "@canonical/task";
import { runTask } from "@canonical/task/node";

try {
  await runTask(myTask);
} catch (err) {
  if (err instanceof TaskExecutionError) {
    console.log(err.code);          // error code string
    console.log(err.taskError);     // full TaskError object
  }
}

干线传译员(dryRun(中文(简体) )

收集效果而不执行它们。 每个效果都有一个模拟返回值,这样任务就可以继续:

import { dryRun } from "@canonical/task";

const { value, effects } = dryRun(myTask);
// value: the task's return value (using mocked effect results)
// effects: Effect[] — every effect the task would have performed

默认模拟 : ReadFile → "[mock content of <path>]", (中文(简体) ), Exists → true, (中文(简体) ), Exec → { stdout: "", stderr: "", exitCode: 0 }, (中文(简体) ), Prompt 缺省或首选,写效果 undefined。 。 。 .

干运行的口译员跟踪虚拟文件系统——由 WriteFile 或者说 MakeDir 效果可见于后续 Exists 在同一跑道内检查.

自定义模拟 dryRunWith

覆盖每个效果类型的模拟行为 :

import { dryRunWith } from "@canonical/task";

const mocks = new Map([
  ["ReadFile", (effect) => {
    if (effect.path === "package.json") return '{"name": "my-app"}';
    return "default content";
  }],
  ["Exists", () => false],
]);

const { value, effects } = dryRunWith(myTask, mocks);
效应分析公用事业
import {
  collectEffects, countEffects, filterEffects,
  getFileWrites, getAffectedFiles,
} from "@canonical/task";

const effects = collectEffects(myTask);

countEffects(effects);
// { WriteFile: 3, ReadFile: 1, Log: 2 }

filterEffects(effects, "WriteFile");
// [{ _tag: "WriteFile", path: "...", content: "..." }, ...]

getFileWrites(effects);
// [{ path: "a.txt", content: "A" }, ...]

getAffectedFiles(effects);
// ["a.txt", "b.txt", "output/"] — sorted, deduplicated

撤消口译( E)runUndo(中文(简体) )

以被嘲弄的前向效果行走任务树( 如 dryRun),收集 undo 任务,然后按倒转顺序执行。 这样可以 --undo 在任何没有存储状态的 CLI 命令上—— 相同的任务定义加上相同的答案产生决定性的撤消.

import { collectUndos } from "@canonical/task";
import { runUndo } from "@canonical/task/node";

// Forward run:
await runTask(generator.generate(answers));

// Undo (later, same answers):
await runUndo(generator.generate(answers));
如何撤销元数据

可写效果带有可选 undo?: Task<void> 字段。 初级设备提供合理的默认:

初级默认撤销
writeFiledeleteFile(path)
mkdirdeleteDirectory(path)
copyFiledeleteFile(dest)
copyDirectorydeleteDirectory(dest)
symlinkdeleteFile(linkPath)
appendFile无——提供自定义
deleteFile无——提供自定义
exec无——提供自定义
自定义取消

覆盖默认撤消或提供不存在默认的默认:

// Custom undo for append
appendFile(indexPath, exportLine, true, {
  undo: removeLineFromFile(indexPath, exportLine),
});

// Disable default undo
writeFile(tempPath, content, { undo: null });
不执行而收集撤消
const undos = collectUndos(myTask);
// undos: Task<void>[] — undo tasks in forward execution order
// (runUndo reverses them automatically)
可编译性

撤消所有现有的组合符自动作曲 :

  • when(false, writeFile(...)) - 跳过的效果不会产生倒数
  • sequence_([...]) ——撤销顺序收集,倒转执行
  • parallel([...]) - 从所有儿童那里收集的废品
  • gen(function* () { ... }) - 工作相同
测试断言
import { assertEffects, assertFileWrites, expectTask } from "@canonical/task";

// Assert exact effect sequence
assertEffects(myTask, [
  { _tag: "MakeDir", path: "output" },
  { _tag: "WriteFile", path: "output/index.ts" },
]);

// Assert which files would be written
assertFileWrites(myTask, ["output/index.ts", "output/package.json"]);

// Fluent matcher
const result = expectTask(myTask);
result.toHaveValue("done");
result.toHaveEffectCount(3);
result.toWriteFile("output/index.ts");
result.toNotWriteFile("output/secret.key");

API 总结

类别密钥导出
修道院pure, (中文(简体) ), flatMap, (中文(简体) ), map, (中文(简体) ), ap, (中文(简体) ), fail, (中文(简体) ), failWith, (中文(简体) ), recover, (中文(简体) ), mapError
发电机gen, (中文(简体) ), $, (中文(简体) ), TaskGen
构建器task, (中文(简体) ), of, (中文(简体) ), TaskBuilder
警卫isPure, (中文(简体) ), isFailed, (中文(简体) ), hasEffects
初级readFile, (中文(简体) ), writeFile, (中文(简体) ), appendFile, (中文(简体) ), copyFile, (中文(简体) ), copyDirectory, (中文(简体) ), deleteFile, (中文(简体) ), deleteDirectory, (中文(简体) ), mkdir, (中文(简体) ), exists, (中文(简体) ), symlink, (中文(简体) ), glob, (中文(简体) ), sortFileLines
进程exec, (中文(简体) ), execSimple
提示prompt, (中文(简体) ), promptText, (中文(简体) ), promptConfirm, (中文(简体) ), promptSelect, (中文(简体) ), promptMultiselect
日志log, (中文(简体) ), debug, (中文(简体) ), info, (中文(简体) ), warn, (中文(简体) ), error
A. 背景情况getContext, (中文(简体) ), setContext, (中文(简体) ), withContext
纯noop, (中文(简体) ), succeed
顺序sequence, (中文(简体) ), sequence_, (中文(简体) ), traverse, (中文(简体) ), traverse_
平行parallel, (中文(简体) ), parallelN, (中文(简体) ), race
条件when, (中文(简体) ), unless, (中文(简体) ), ifElse, (中文(简体) ), whenM, (中文(简体) ), ifElseM
调度switchMap
处理错误retry, (中文(简体) ), retryWithBackoff, (中文(简体) ), orElse, (中文(简体) ), optional, (中文(简体) ), attempt
资源bracket, (中文(简体) ), ensure
公用事业tap, (中文(简体) ), tapError, (中文(简体) ), delay, (中文(简体) ), timeout, (中文(简体) ), fold, (中文(简体) ), zip, (中文(简体) ), zip3
制作翻译 (单位:千美元)@canonical/task/node(中文(简体) )runTask, (中文(简体) ), run, (中文(简体) ), executeEffect, (中文(简体) ), RunTaskOptions
干线翻译dryRun, (中文(简体) ), dryRunWith, (中文(简体) ), mockEffect, (中文(简体) ), collectEffects, (中文(简体) ), countEffects, (中文(简体) ), filterEffects, (中文(简体) ), getFileWrites, (中文(简体) ), getAffectedFiles, (中文(简体) ), assertEffects, (中文(简体) ), assertFileWrites, (中文(简体) ), expectTask
撤销collectUndos (基数), UndoOptions (基数); runUndo, (中文(简体) ), UndoResult (单位:千美元)@canonical/task/node(中文(简体) )
错误TaskExecutionError (基地——由口译员扔出,可在任何地方取用)
效果构建器readFileEffect, (中文(简体) ), writeFileEffect, (中文(简体) ), appendFileEffect, (中文(简体) ), copyFileEffect, (中文(简体) ), copyDirectoryEffect, (中文(简体) ), deleteFileEffect, (中文(简体) ), deleteDirectoryEffect, (中文(简体) ), makeDirEffect, (中文(简体) ), existsEffect, (中文(简体) ), symlinkEffect, (中文(简体) ), globEffect, (中文(简体) ), execEffect, (中文(简体) ), promptEffect, (中文(简体) ), logEffect, (中文(简体) ), readContextEffect, (中文(简体) ), writeContextEffect, (中文(简体) ), parallelEffect, (中文(简体) ), raceEffect
效果公用事业describeEffect, (中文(简体) ), isWriteEffect, (中文(简体) ), getAffectedPaths

许可证

LGPL-3.0 (英语)

版本与发布记录

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

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

关于 @canonical/task 的常见问题

@canonical/task 是什么?

用于可相容,可测试,可干燥的CLI操作的一元效应框架.

如何安装 @canonical/task?

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

@canonical/task 支持 TypeScript 吗?

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

@canonical/task 支持 ESM 还是 CommonJS?

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

@canonical/task 可以商用吗?

@canonical/task 使用 LGPL-3.0 许可。弱 Copyleft:作为库被调用时通常不要求开源你自己的代码,但修改库本身需以 LGPL 开源;前端打包合并后的适用边界存在不同理解。以上为概括,不构成法律意见,请以许可原文为准。

@canonical/task 的下载量有多少?

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

@canonical/task 还在维护吗?

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