Node and TypeScript API
The ankiforge entry point exposes the sole Project authoring model.
Types are defined in the public source,
report classes, and
JSON snapshot declarations. Start with
the quick start.
Project and notes
Section titled “Project and notes”| API | Behavior |
|---|---|
new Project(namespace) |
Validate a stable publication namespace |
.defaultDeck(name) |
Set the default destination; returns the project |
.add(key, note) |
Atomically collect note/model/media; duplicate keys fail |
.addAsset(media) |
Include an explicit raw HTML/CSS/script resource |
.clone() |
Independent editable project sharing immutable values |
.length |
Registered note count |
Note.basic(front, back) / Note.cloze(text) |
Immutable note holding its built-in model |
model.note().field(key, content) |
Immutable custom note holding its model |
note.deck(name).tag(tag).tags(iterable) |
Return a newly configured note |
Strings always become Text, including Cloze strings. Explicit markup uses
Content.html. Content.sequence(iterable) combines text, HTML and typed media.
Use explicit business keys; neither content nor display names derive identity.
Immutable models
Section titled “Immutable models”import { NoteType, Field, Template, GenerationRule } from 'ankiforge';const model = NoteType.builder('vocabulary') .name('词汇') .field(new Field('front', { name: '正面', required: true, sort: true })) .field(new Field('back', { name: '背面' })) .template(new Template('recognition', { front: '{{front}}', back: '{{FrontSide}}<hr>{{back}}', generation: GenerationRule.all(['front']), })) .build();const note = model.note().field('front', 'cell').field('back', '细胞');Builders return new values. A completed model has no field/template mutation
methods. builder.clozeField(key) selects custom Cloze semantics; builder.css
and builder.asset supply styling and explicit dependencies. Templates also
accept name, browserFront, browserBack and targetDeck. Template references
and generation conditions use field keys; display names are compiled for Anki.
GenerationRule.ankiDefault() selects Anki’s default behavior; all and any
select explicit conditions.
await NoteType.fromBundle(path, mediaLimits?) returns the same immutable model
from template-bundle-v2. Imported text/assets no longer depend on the source
directory after success. Models can be reused across projects; incompatible
definitions under one model key fail atomically.
Media and Image Occlusion
Section titled “Media and Image Occlusion”await Media.file(path, limits?) and
await Media.bytes(Uint8Array, mime, limits?) return owned snapshots. Source files
can change or disappear after the await succeeds. withExportName(name) returns
a renamed value without changing earlier clones or content. Read filename,
mediaType and byteLength as properties.
media.image() and .sound() return typed Content that automatically brings its
asset into a project. Explicit project.addAsset and model builder.asset cover
raw HTML/CSS/script references. Media limits accept { maxBytes }; the default
is 256 MiB and native bytes have no 64 KiB inline restriction.
import { Media, Note, Mask } from 'ankiforge';const image = await Media.file('diagram.png');const note = Note.imageOcclusion(image) .mask(Mask.rect('nucleus', 10, 10, 20, 20)) .mask(Mask.rect('wall', 50, 50, 20, 20)) .mode('hide_one_guess_one') .build().field('header', 'Cell');The default mode is hide_all_guess_one; both modes generate separate cards for
keyed masks. .build() validates decoded dimensions, finite rectangles and
unique keys. image and occlusion are reserved generated fields. See
Image Occlusion for decoding and update limits.
Build, compare and output
Section titled “Build, compare and output”| API | Result |
|---|---|
await project.build(BuildOptions.to(path)) |
BuildOutput owning persistent artifact |
await project.build(BuildOptions.temporary()) |
BuildOutput owning temporary artifact |
await project.compare(CompareOptions.against(path)) |
Completed ComparisonReport, including blocked policy |
options.updateFrom(path) |
Configure update from complete original distribution evidence |
options.inspectLimits(limits) |
Override independent inspection counters |
options.updatePolicy(policy) |
Configure update/compare risk policy |
A successful output exposes .artifact, .report and .snapshot().
The BuildReport exposes .counts, .baselineCounts, .durationMs, .diagnostics,
.comparison, and .snapshot(); it has no outcome or file ownership.
.comparison is a ComparisonReport or null, using the same class as
project.compare(). Comparisons expose .findings,
.highestRisk, .policy, .diagnostics, and .snapshot().
Snapshot field names mirror Rust JSON, including allows_publication and
schema_version; they are not camel-cased copies of the native report.
Use report.comparison.highestRisk on a comparison object, or
report.snapshot().comparison.highest_risk in its JSON snapshot, after checking
for null.
new UpdatePolicy() blocks High/Critical. .failOn('medium') changes the
threshold. .allow('RISK.NOTE_REMOVED') explicitly accepts that whole category
while retaining its evidence and severity. Unknown codes and hard errors cannot
be allowed. Policy on a create-only build is an error. Use the same policy and
limits in compare and build when you need matching publication decisions.
Inspection options include maxArchiveBytes, maxEntries,
maxCentralDirectoryBytes, maxZipEntryBytes, maxZipTotalBytes, maxMetaBytes,
maxMediaMapBytes, maxIdentityBytes, maxCollectionBytes, maxMediaBytes,
maxDecodedTotalBytes and maxZstdWindowBytes. Values are nonnegative safe
integers or bigint through u64’s maximum; zero is a real zero budget. Counters
apply independently to baseline and candidate. Unknown options fail.
Artifact lifetime and concurrency
Section titled “Artifact lifetime and concurrency”JSON snapshot paths use the exported PathSnapshot type. Unicode paths are
strings; non-Unicode native paths use { encoding: "unix_bytes", bytes: number[] }
or { encoding: "windows_wide", units: number[] }. The arrays preserve the exact
Unix bytes or Windows UTF-16 code units. This includes artifact paths in build
snapshots, publication paths in failures, and structured error-path details.
Decode an encoded path only for its original platform; on Unix,
Buffer.from(value.bytes) preserves a byte path for Node filesystem functions.
The Node authoring methods continue to accept string paths.
Relative media and bundle paths are anchored when the read is invoked;
build destinations and baselines are anchored when options are constructed.
On POSIX, symbolic links followed by .. retain filesystem traversal semantics.
persistTo anchors relative destinations to the working directory captured by
the original build, including on cloned and persisted handles. Later working
directory changes do not redirect these paths.
Keep an artifact owner alive while consuming .path. .clone() creates an
independent owner; await .close() releases that owner. The last temporary owner
deletes its file. await .persistTo(path) returns a persistent artifact and
leaves the original usable even on failure. Standard Node file streams can read
an artifact while you retain its owner.
Snapshots are frozen JSON data and retain no native files. A persistent file
survives owner cleanup. BuildOutput cannot be constructed by the caller; it is
created only by successful builds.
Build and compare run asynchronously on an owned project snapshot captured at invocation. Later edits cannot mutate that request. Media reads and bundle loads also run on workers. Node ESM and CJS share the same native module and class identities.
Errors
Section titled “Errors”SchemaError, AddError, MediaError, TemplateBundleError,
ImageOcclusionError, PolicyError, ConfigurationError, CompareError,
BuildError, and PersistError extend ForgeError. They expose kind, code,
domain, native source-chain text in causes, structured sourceDetails for recognized
I/O/schema/media/limit causes, and operation details.
The original native exception is retained as the JS cause.
BuildError.snapshot() preserves failure and publication facts;
BuildError.report and CompareError.report return BuildReport, just like
BuildOutput.report. Use .baselineCounts and .comparison on these reports;
use .snapshot() for the snake_case JSON fields. A failed comparison can have
partial observations and a null comparison.
PersistError.publication records whether the target was already published and
whether durability was confirmed. Media/inspection errors retain limit details.
Use these machine fields instead of parsing error messages.
ArtifactClosedError reports use of a closed owner; NativeLoadError reports
missing, incompatible or stale native packages.
Addition context
Section titled “Addition context”Failed additions leave the project unchanged. Structured context records the
original note/model keys and the target, with optional conflict or media usage
details. Rust exposes note::{AddContext, AddTarget, AddDetail} through
AddError::context() / detail(); SDKs expose AddError.details with typed
context/detail. Node fields use camelCase and are deeply frozen; Python uses
snake_case TypedDicts. Tags retain their insertion index; field locations retain
original sequence indices (null/None for the field, [] for its root) and optional
UTF-8 byte ranges. Default-deck errors from build have no note/model keys and
remain available in the source chain.
Typed images require image/* MIME at addition; sound references accept audio/*
or video/*. Other categories raise NOTE.MEDIA_USAGE_INVALID. Constructors stay
infallible; explicit assets and raw HTML are unaffected. The check uses retained
import MIME, and does not certify playback. Build’s independent MIME/extension
validation still applies. See the design for the complete target/detail table.
Batch media and prepared publication
Section titled “Batch media and prepared publication”Batch media and prepared publication are included in the public 0.3.1 package.
Prepare, review and publish
Section titled “Prepare, review and publish”import { Project, Note, BuildOptions, BuildError } from 'ankiforge';const project = new Project('review-once').add('one', Note.basic('Question', 'Answer'));const previous = await project.build(BuildOptions.to('previous.apkg'));await previous.artifact.close();const prepared = await project.preparePublication( BuildOptions.to('next.apkg').updateFrom('previous.apkg'),);try { const report = prepared.report; // immutable observations, no file ownership console.log(report.comparison?.snapshot()); if (report.comparison?.policy.allows_publication !== false) { const output = await prepared.publish(); await output.artifact.close(); // persistent next.apkg remains }} catch (error) { if (error instanceof BuildError) console.log(error.snapshot().result); // Inspect publication/durability facts: a late failure can follow replacement. throw error;} finally { await prepared.close(); // idempotent; cleans an unpublished candidate}publish() takes the native owner immediately and performs one worker task.
Every attempt consumes it, including policy and I/O rejection. Repeated calls
reject with PreparedPublicationStateError, code BUILD.PREPARED_UNAVAILABLE,
and reason consumed or closed. Closing after publish begins does not cancel
it. Reports remain readable after close. Options bind at preparation invocation;
changing policy or destination requires another preparation. Duration excludes
review waiting. Neither reports nor JSON can be converted back into an owner.
Build, compare, preparation and clones share immutable native Project versions. Authoring changes remain synchronous and isolated. The first change while another snapshot lives may copy the Project; later exclusive changes do not. This shifts copying out of Promise submission and does not promise faster first edits, better worst timer delay or lower RSS. JavaScript byte inputs are still copied before worker submission. Use protocol 6 binaries with these wrappers.
TypeScript
Section titled “TypeScript”Install typescript and @types/node in your application. Use main.mts or
"type": "module" and this tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "outDir": "dist" }}Compile with npx tsc, then run node dist/main.mjs for an .mts entry.