Migrating from 0.2.0
What changes in @candid-core/schema 0.3.0 and @candid-core/cli 0.2.0, published on 2026-10-06 (and in the betas 0.3.0-beta.1 and 0.2.0-beta.1 before them), for code written against @candid-core/schema 0.2.0 and @candid-core/cli 0.1.0, with before and after code where the compiler shows it.
This page is for code written against @candid-core/schema 0.2.0 or
@candid-core/cli 0.1.0, and takes it to the surface published on 2026-10-06 as
@candid-core/schema 0.3.0 and @candid-core/cli 0.2.0
under the npm latest dist-tag. latest has moved on since, to
0.4.0 and 0.3.0, published on 2026-10-10, and then to
0.5.0 and 0.4.0, published on 2026-10-27;
Migrating from 0.3.0 and
Migrating from 0.4.0 cover those steps. Most of the surface below was first published on 2026-10-02 as
0.3.0-beta.1 and 0.2.0-beta.1 under the beta dist-tag,
which still names those betas. The record of each change, with the reasoning, is
the 0.3.0 and 0.3.0-beta.1 entries of the
@candid-core/schema changelog
and the 0.2.0 and 0.2.0-beta.1 entries of the
@candid-core/cli changelog.
Both changelogs state the compatibility non-goal in one line:
@candid-core/schema 0.3 breaks the 0.2 API (principal values are canonical text,
collapsing opts are boxed, every vec nat8 is a Uint8Array, generated
modules use a new binding layout and may omit declarations, and the ./actor, ./transport-icp, ./forms and
./labels subpaths are gone); there is no compatibility layer.
Installing the stable pair#
A plain install selects the newest stable pair from latest,
@candid-core/schema 0.5.0 and @candid-core/cli
0.4.0, whose optional peer is exactly 0.5.0. Everything below applies
there too, and where the CLI's 0.3.0 or 0.4.0 differs this page says so;
Migrating from 0.3.0 and
Migrating from 0.4.0 have those steps:
npm install --save-exact @candid-core/schema
npm install --save-dev --save-exact @candid-core/cli
Keep both exact: while the two packages move in lockstep every schema release is paired with a
CLI release that peers it exactly, and npm refuses to install a mismatched pair
(ERESOLVE could not resolve).
If you installed the betas with @beta, those lines move you to the stable pair. The
beta dist-tag still names 0.3.0-beta.1 and 0.2.0-beta.1,
which lack the changes recorded under 0.3.0 and 0.2.0.
Removing published subpaths is a breaking change, so the 0.3 line is a minor: under npm's
pre-1.0 caret rules a ^0.2.0 dependency does not select it, and moving to it is a
deliberate edit. Nothing on the wire moved. No byte a 0.2.0 encoder wrote decodes
to a different value, and Contract JSON and every identity are untouched; what changed is the TypeScript
value shapes, the package's exports, what the generator emits, and what throws.
Each before block is 0.2.0-era code, and the site's check compiles it against this tree and requires the compiler to reject it with the error shown. Each after block must compile. The claim that a spelling stopped working is therefore the compiler's, not this page's.
| What changed | What it breaks | Where |
|---|---|---|
A principal is canonical text, branded Principal | Code that calls .toText() on a decoded principal, or hands an SDK object to encode | Principals |
opt opt T, opt null and opt reserved are { some: T } | null | Code that read or wrote those as a bare T | null | Boxed options |
Every vec nat8 is a Uint8Array | Code typed for number[] beside a declared nat8 alias | Blobs |
| Four subpaths are gone, and the optional SDK peer | Any import of them | Removed subpaths |
Bad options throw TypeError; encoded bytes do not depend on how a schema was built | A misspelled or non-integer limit that used to be ignored | Strict options |
Generated modules bind $-prefixed locals and carry JSDoc | Code that parses or patches generated source | Generated modules |
| A declaration that cannot be represented is omitted, not refused | A script that relied on the generator's non-zero exit | Omission |
| Closed unions gain or lose a member | An exhaustive switch over a resource or a contract code | Closed unions |
schemaFromContract refuses unknown keys, unreachable type nodes and a rootless arena | A hand-written or tool-mutated Contract document | Stricter Contract loading |
A rec hop no longer counts against maxDepth or maxElements | A value with a tag-only variant at exactly a caller-set maxDepth, or a message that omits an opt field at a maxDepth edge or past maxElements, which a large message reaches at the default limits | Resource limits |
| The CLI refuses a run of more than 256 consecutive comments | A .did with a long // header | Long comment runs |
The CLI refuses a type on a cycle through opt alone | A script that matched on ts_generation_refused for it | Cycles through opt |
Principals#
A principal's domain value was { toText(): string }. It is now its canonical text, a
plain string branded Principal in the type system only. The object could not be
serialized (JSON.stringify gave {}), cloned
(structuredClone threw), or compared with ===. The string does all
three. Decoding returns the text, for a principal, for a func reference's
principal and for a service reference alike. The root export
PrincipalValue and its ./codec alias DecodedPrincipal are removed, with
no deprecated alias left, because an alias would keep the object shape alive.
In the other direction encoding is strict: it accepts exactly the strings validate
accepts, a string holding canonical text, and refuses everything else with
invalid_type at the same path. It no longer calls toText().
principal(value) is the one conversion point. It takes a string or an object with
toText(), an SDK Principal for one, and returns the text branded, or throws
TypeError. It refuses rather than repairs: upper case, a misplaced dash, a checksum
mismatch and an id over 29 bytes all throw. isPrincipal(value) is the matching guard.
import { c, type Infer } from "@candid-core/schema";
const Account = c.record({ owner: c.principal });
declare const account: Infer<typeof Account>;
// 0.2.0: a decoded principal was an object with toText().
const text: string = account.owner.toText();import { c, type Infer } from "@candid-core/schema";
const Account = c.record({ owner: c.principal });
declare const sdkPrincipal: { toText(): string }; // an @icp-sdk/core Principal, say
// 0.2.0: any object with toText() was a principal, to validate and to encode.
const account: Infer<typeof Account> = { owner: sdkPrincipal };import { c, isPrincipal, principal, type Infer } from "@candid-core/schema";
import { encode } from "@candid-core/schema/codec";
const Account = c.record({ owner: c.principal });
declare const sdkPrincipal: { toText(): string }; // an @icp-sdk/core Principal, say
// Convert once, at your boundary. The result is the canonical text.
const owner = principal(sdkPrincipal);
const account: Infer<typeof Account> = { owner };
const text: string = account.owner; // no toText(): it is already the text
encode(Account, account); // { ok: true, bytes }
isPrincipal("AAAAA-AA"); // false, so principal("AAAAA-AA") throws TypeError
The other direction needs the SDK, which this package no longer knows about: code that wants a
class instance from a decoded value converts with the SDK's own
Principal.fromText(value). Principal is exported from the root with the
same name as the SDK's class; import one of them under an alias. Equal principal text encodes to
equal bytes whatever built the schema, and a string over 63 characters, the longest canonical
text, is refused before any work proportional to its length. Source:
schema.ts, the doc comments on Principal, principal and
isPrincipal; the changelog entry Principals are canonical text.
Boxed options#
opt T is T | null unless T itself admits null.
For exactly the three collapsing inners, opt opt T, opt null and
opt reserved, the present value is boxed as { some: v }, so
None, Some(None) and Some(Some(x)) stay three values. Every
other opt is unchanged in type, value and wire bytes. In 0.2.0 an interface
containing one of the three was refused at generation and at load, and a hand-built
c.opt(c.opt(…)) silently decoded Some(None) as None.
Interfaces such as Internet Identity's config now load.
import { c, type Infer } from "@candid-core/schema";
const Description = c.opt(c.opt(c.text));
// 0.2.0: the present value was the bare inner value.
const present: Infer<typeof Description> = "x";import { c, isBoxedOpt, type Infer, type OptDomain } from "@candid-core/schema";
import { decode, encode } from "@candid-core/schema/codec";
const Description = c.opt(c.opt(c.text));
type Description = Infer<typeof Description>; // { some: string | null } | null
const none: Description = null; // None
const cleared: Description = { some: null }; // Some(None)
const set: Description = { some: "x" }; // Some(Some("x"))
const bytes = encode(Description, cleared); // the bytes any other opt would write
if (bytes.ok) decode(Description, bytes.bytes); // { ok: true, value: { some: null } }
isBoxedOpt(Description); // true; isBoxedOpt(c.opt(c.text)) is false
type Plain = OptDomain<bigint>; // bigint | null, unboxed
The decision is made on the resolved inner node, so a declared alias and a recursive type
box the same way (an opt node that is its own inner, the graph of
type Chain = opt Chain, boxes too, though the compiler now refuses that source),
and opt empty stays
null. Validation and encoding require { some: v } for a present value (a
bare v is invalid_type, and under opt reserved
undefined is no longer read as present), and issues inside a box carry the
some segment. OptSchema<T> now extends
Schema<OptDomain<T>>. See
the mapping reference.
Blobs#
A declared alias of nat8 (type Byte = nat8) used to turn every
blob and vec Byte in the interface into number[], because the
generator and the loader gave a declaration's name to every use of the node it named. A
declaration of a primitive now names only itself, and every vec nat8 is a
Uint8Array, in generated modules and in schemaFromContract alike. The
encoded bytes are the same, since blob and vec nat8 are one wire type. The
same change stops type Tokens = nat; type BlockIndex = nat renaming each other's uses:
a field reads bigint, not whichever alias came first.
import { c, type Schema } from "@candid-core/schema";
// 0.2.0, for `type Byte = nat8; type R = record { raw : vec Byte }`.
type R = { raw: number[] };
const R: Schema<R> = c.rec(() => c.record({ raw: c.blob() }));import { c, type Schema } from "@candid-core/schema";
type R = { raw: Uint8Array };
const R: Schema<R> = c.rec(() => c.record({ raw: c.blob() }));Removed subpaths#
The package is the Candid layer only: schemas, validation, the codec and the Contract loader. The
export map is exactly ., ./validate, ./contract and
./codec. Four subpaths that 0.2.0 exported are gone:
-
./actor, with its actor factory, its call helper for a decoded func reference, its error class, and theTransport,CallTargetandActorOptionstypes. Calling a canister is the job of the call layer built on this package. A func value stays the inert{ principal, method }pair. -
./transport-icp, the@icp-sdk/coreadapter. It built an agent with no identity, a second call path beside whatever identity-aware transport an application uses. -
./forms, the form-model builder. The code stays in the repository, compiled and tested, but is not built into the tarball. -
./labels, the label-hash and UTF-8 helpers. The module still ships because the codec and the loader import it, but it is not an entry point.
An import of any of them now fails at resolution, with ERR_PACKAGE_PATH_NOT_EXPORTED in
Node and TS2307 in TypeScript, and so does a deep import into dist/. The
optional @icp-sdk/core >= 6 peer is dropped with them, since only the transport
used it. The package declares no runtime dependency and no peer of any kind, and the Node floor is
16 for every subpath. The packaged-consumer gate asserts each of these against the packed tarball.
import { createActor } from "@candid-core/schema/actor";import { httpTransport } from "@candid-core/schema/transport-icp";What replaces them is what a call layer already needed from this package and still has: the method table, and the codec that moves a call's arguments and reply as bytes. Sending those bytes, with the agent, the identity, polling and certificate verification, is not done here.
import { serviceMethods } from "@candid-core/schema";
import { decodeArgs, encodeArgs } from "@candid-core/schema/codec";
import { actor, type TransferArg } from "./ledger.ts";
declare const arg: TransferArg;
declare function send(method: string, payload: Uint8Array): Promise<Uint8Array>; // your agent
const transfer = serviceMethods(actor).get("transfer");
if (transfer === undefined) throw new Error("the interface has no transfer method");
const request = encodeArgs(transfer.args, [arg]);
if (!request.ok) throw new Error(request.issues[0].message);
const reply = decodeArgs(transfer.results, await send(transfer.name, request.bytes));
if (reply.ok) reply.values[0]; // the decoded TransferResult, as unknown until you narrow it: `results` is an erased listStrict options, and bytes that do not depend on the schema#
Every entry point that takes an options object, validate and unwrapResult,
encode, encodeArgs, decode and decodeArgs, and
schemaFromContract, throws TypeError on an own key it does not define and on
a limit that is not a non-negative safe integer: NaN, a negative, a fraction, a string,
null and Infinity all throw, and so does maxIssues passed to
encode, which only validate defines. In 0.2.0 a misspelled limit applied the default
silently, and a NaN limit switched its bound off: a depth-300 value validated under
maxDepth: NaN. undefined still means the default and 0 is
still a valid, fail-closed limit. A host that wants no practical bound passes a large safe integer.
Each option is read from your object exactly once, into a frozen snapshot the whole call reads
instead, so a getter or a Proxy cannot pass the check with one value and run with another. Values,
byte strings and Contract documents still never make these functions throw.
import { c } from "@candid-core/schema";
import { validate } from "@candid-core/schema/validate";
// A typed caller now meets the misspelling at compile time; a caller with
// an untyped options object meets a TypeError at run time.
validate(c.nat, 1n, { maxDeph: 10 });import { c } from "@candid-core/schema";
import { validate } from "@candid-core/schema/validate";
declare const limit: number; // read from your configuration
try {
validate(c.vec(c.nat8), [], { maxDepth: limit });
} catch (error) {
if (!(error instanceof TypeError)) throw error; // a NaN, a fraction, a string: the caller's bug
}
The second change is to bytes. encode and encodeArgs write a structural type
table, so the same value encoded through a generated module, through
schemaFromContract and through a hand-built schema produces one byte string, however
each shares or duplicates nodes and in whatever order a record's schema or value spells its keys.
Anything that keys a cache or deduplicates requests on argument bytes now sees one key for one call.
A schema with no repeated structure writes the bytes it always wrote, and one with repeated
anonymous structure writes a smaller table that every decoder still reads. Two separately
hand-built knots for one recursive type are not merged. Source: options.ts and
typetable.ts headers, and the codec header's Determinism section.
Generated modules#
The generator changes the text of every module it emits, and nothing about what a module exports.
The schema runtime is imported as the namespace $, and every declaration is bound as a
$-prefixed local exported under its Candid name. A Candid name cannot contain
$, so a declaration named c, Schema, Array or
delete generates instead of being refused, and you import it by name
(import { delete as del } from "./service"). Code that imports the generated names is
unaffected; code that parses or patches the generated source, or relied on its module-scope names,
must follow the new layout. In @candid-core/cli 0.3.0, published on
2026-10-10, the type moves back to the Candid name
(export type Tokens = …), so a compiler error names Tokens; only the value
keeps the $ local, and a type keeps it too only where the name cannot be a type in the
module (Array, delete). The excerpts below show that layout,
0.3.0's; 0.2.0 writes type $Tokens = … and
$.Schema<$Tokens>. Migrating from 0.3.0 has the
rest of that step.
// 0.2.0 layout: bare bindings, and a separate import for the principal type.
import { c, type PrincipalValue, type Schema } from "@candid-core/schema";
export type Tokens = { e8s: bigint };
export const Tokens: Schema<Tokens> = c.rec(() => c.record({ e8s: c.nat64 }));import * as $ from "@candid-core/schema";
// …
export type Tokens = { e8s: bigint };
const $Tokens: $.Schema<Tokens> = $.c.rec(() => $.c.record({ e8s: $.c.nat64 }));
export { $Tokens as Tokens };import { Tokens, actor, type Actor } from "./ledger.ts"; // the same import as before
import { Array as Arrays, delete as del } from "./shadowing.ts"; // names that used to be refused
void [Tokens, actor, Arrays, del];
declare const ledger: Actor;
void ledger.fee;
Principals follow the change above: the generated type is $.Principal where it was
PrincipalValue, for principal fields, func references
({ principal: $.Principal; method: string }), service references and the
actor schema. A declaration named Principal still generates. Two more things
came with the layout. The .did's doc comments and argument names become JSDoc, above
each exported type and const, on record properties and variant arms, and on the methods of
Actor with a @param per named argument, so an editor hover shows what the
interface says. And a use of a primitive renders structurally (see Blobs).
import * as $ from "@candid-core/schema";
// …
/** The amount of a transfer, in e8s. */
export type Tokens = bigint;
/** The amount of a transfer, in e8s. */
const $Tokens: $.Schema<Tokens> = $.c.rec(() => $.c.nat);
export { $Tokens as Tokens };
Declarations named actor or Actor are the module's own export names and stay
reserved. The generated actor schema and Actor interface type are still
emitted, under those names.
Omission instead of refusal#
The generator no longer refuses a whole interface for one declaration it cannot represent. It leaves
that declaration out, with every declaration and actor method that references it, and lists what it
left out. Four causes remain: a field or arm named like the _N_ id rendering, a
variant arm whose payload is a declared opt of an uninhabited type, a declaration named
actor or Actor, and, from a Contract document only, a name that is not
identifier-shaped. The exact rules, the reason codes and the closure are on
the code generator page.
What changes for a consumer:
-
Exit status.
candid-core-cli genused to print ats_generation_refuseddocument and exit 1 for such an interface. It now writes the module, prints onewarning: omitted …line per entry on stderr, and exits 0. A script that relied on the non-zero exit to reject the interface must read the warnings or the list. -
Library result.
didToModulereturns{ ok: true, module, omitted }for those inputs, where it returned{ ok: false, diagnostics }.omittedis always present, and empty when nothing is left out.ts_generation_refusedis now reserved for an invalid Contract graph, which Candid source never produces. -
The loader.
schemaFromContractomits exactly what the generator omits, with the same reasons, and its success carries the sameomittedlist. A name-table entry shaped like_N_that honestly hashes to its id no longer fails the document. A declaration whose name is not identifier-shaped is now omitted there, where 0.2.0 loaded it. An invalid document still fails whole. -
The header. The module lists each entry as a
// Omitted:line under its first line. A module that omits nothing has no such line and is byte-identical to what it would have been.
Closed unions#
Three closed unions changed, and an exhaustive switch over them stops compiling until it
is updated. resource_limit_exceeded issues gain a "stack" resource in both
ResourceLimitInfo["resource"] (./validate) and
CodecResourceLimitInfo["resource"] (./codec). It means the host's own call
stack ran out, and only user code a walk calls, a getter, a Proxy trap or a rec thunk
that itself recurses too deeply, can cause it: the walkers keep their work on explicit stacks.
Before, such an overflow was mislabelled unreadable_value or
unsupported_schema. ContractIssueCode loses
unrepresentable_option, because the documents it refused now load, and gains
unknown_key, orphan_type_node and rootless_type_arena, the
refusals of stricter Contract loading below; it has 17 members.
import type { ResourceLimitInfo } from "@candid-core/schema/validate";
function describe(info: ResourceLimitInfo): string {
switch (info.resource) {
case "value_depth":
return "too deep";
case "value_elements":
return "too many elements";
default: {
const unhandled: never = info.resource; // 0.2.0: exhaustive with two cases
return unhandled;
}
}
}
void describe;import type { ContractIssueCode } from "@candid-core/schema/contract";
function isOption(code: ContractIssueCode): boolean {
return code === "unrepresentable_option";
}
void isOption;import type { ContractIssueCode } from "@candid-core/schema/contract";
function kind(code: ContractIssueCode): string {
switch (code) {
case "invalid_contract_document":
case "unsupported_contract_format":
case "unsupported_format_version":
case "unsupported_semantics_profile":
case "unsupported_canonicalization_profile":
return "format";
case "dangling_type_ref":
case "unsupported_construct":
case "duplicate_field_id":
case "duplicate_field_name":
return "types";
case "empty_declaration_name":
case "duplicate_declaration_name":
case "invalid_name_table":
case "invalid_extension_name":
return "names";
case "resource_limit_exceeded":
return "limits";
default: {
const unhandled: never = code; // 0.2.0's codes, less unrepresentable_option
return unhandled;
}
}
}
void kind;import type { ResourceLimitInfo } from "@candid-core/schema/validate";
function describe(info: ResourceLimitInfo): string {
switch (info.resource) {
case "value_depth":
return "too deep";
case "value_elements":
return "too many elements";
case "stack":
return "user code recursed past the host stack";
default: {
const unhandled: never = info.resource;
return unhandled;
}
}
}
void describe;import type { ContractIssueCode } from "@candid-core/schema/contract";
function kind(code: ContractIssueCode): string {
switch (code) {
case "invalid_contract_document":
case "unsupported_contract_format":
case "unsupported_format_version":
case "unsupported_semantics_profile":
case "unsupported_canonicalization_profile":
case "unknown_key":
return "format";
case "dangling_type_ref":
case "unsupported_construct":
case "duplicate_field_id":
case "duplicate_field_name":
case "orphan_type_node":
case "rootless_type_arena":
return "types";
case "empty_declaration_name":
case "duplicate_declaration_name":
case "invalid_name_table":
case "invalid_extension_name":
return "names";
case "resource_limit_exceeded":
return "limits";
default: {
const unhandled: never = code;
return unhandled;
}
}
}
void kind;
Two behaviours changed without changing a type. The limits are the only bounds on a walk:
DEFAULT_MAX_DEPTH stays 256, and with maxDepth raised a 100,000-level value
validates, encodes and decodes on any engine, where the recursive walkers used to overflow the host
stack from about 1,500 to 2,600 levels. And encode charges maxDepth for
Candid nesting depth only, never for a rec hop, so any type the candid-core compiler
accepts encodes through its generated module at the default limits, however it is split into
declarations. The codec page
states both with their numbers.
Resource limits: a rec hop is not charged#
In 0.3.0, validate, encode and decode charge a
value for its constructors alone. 0.2.0 and 0.3.0-beta.1 also charged every
rec hop a depth step and an element, and a Contract-loaded schema makes every type
reference a hop, so its values reached the default maxDepth at about 128 levels and
maxElements at about 500,000 elements. Now a generated, a Contract-loaded and a
hand-built schema of one type accept the same values, 256 levels deep at the defaults as candid-core
does, and 1,000,000 elements wide, where validate and encode also count
each record or variant key they examine. Two narrow cases are refused that were accepted:
-
a tag-only variant (
{ tag: "a" }for an arm of typenull) sitting at exactlymaxDepth: itsnullis now a level below it invalidateandencode, as it already was indecode. At the default of 256 no type the compiler accepts reaches it; -
a message that omits an
opt,nullorreservedfield: thenullthatdecodesupplies is now charged at the field's own level and as one element. A record at exactly amaxDepthyou set that omits one is refused, and so is a message whose omitted fields take it pastmaxElements, which can happen at the default limits: avecof 500,000 or more records that each omit such a field the reader expects costs one element more per record, so avec record {}of 500,000 elements read atvec record { x : opt nat }is now refused. Both are refused withresource_limit_exceeded, asvalidaterefuses the same value.
A chain of more than maxDepth consecutive hops is still refused with
value_depth, so with maxDepth set to 0 a generated module refuses a value at
its first declaration reference, and at 1 wherever it reaches an alias of another declaration (a
two-hop chain). The changelog's 0.3.0 entry lists every case.
Stricter Contract loading#
schemaFromContract now refuses three kinds of Contract document that 0.2.0 loaded,
because candid-core's own loader, Contract::from_json, refuses them. Every Contract and
envelope candid-core compile writes still loads exactly as before, with the same
schemas, actor and omitted; only a hand-written or tool-mutated document
is affected.
-
Unknown keys (
unknown_key, at the key's path, such as$.types[0].extra). The root, a type node, a field or arm, a method, a declaration and the actor accept only the keys the format defines for them, and a key of another node kind counts as unknown, so anoptnode carryingfieldsis refused. An unknown envelope key, already refused, is now reported the same way ($.format), where 0.2.0 reportedinvalid_contract_documentat$. -
Unreachable type nodes (
orphan_type_node, one issue per node at$.types[i]): every node must be reachable from a declaration or the actor, a class actor'sinitarguments included. -
A rootless arena (
rootless_type_arenaat$.types): a non-emptytypeswith no declarations and no actor. An empty one still loads.
A tool that writes or edits Contract documents must emit only the format's keys and drop nodes
nothing reaches; code that matched on invalid_contract_document for an unknown
envelope key must match unknown_key. The three codes are in the
closed union above.
Long comment runs#
@candid-core/cli 0.2.0 refuses a .did file with more than 256
consecutive comments between two tokens, line and block comments alike, with
resource_limit_exceeded and resource source_nesting (limit 256), exit
status 1. 0.1.0 and 0.2.0-beta.1 compiled such a run up to a
stack-dependent length, about 2,700 to 2,800 comments, and then failed. The likely case is a license
header written as hundreds of // lines: write it as one /* */ block, which
counts as one comment, or shorten it. Blank lines between comments do not end a run; any token does.
The CLI has no flag to raise the limit.
Cycles through opt#
@candid-core/cli 0.2.0 refuses a type that lies on a cycle passing only
through opt, such as type T = opt T;, with did_type_check_error
naming the type, exit status 1. No decoder can answer for a value that is not an opt at
such a type. 0.1.0 refused the same files at generation, with
ts_generation_refused, because each such type wraps opt in
opt; 0.2.0-beta.1 generated them, boxed. A cycle with any other constructor
on it, such as type T = opt vec T, still compiles.
Order of upgrade#
The pieces are released together and depend on each other. A module the new generator emits imports
Principal from @candid-core/schema, which 0.2.0 does not export, and may
contain boxed aliases 0.2.0 cannot type. The @candid-core/cli that ships the generator
therefore must raise its @candid-core/schema peer range to the release that carries these
changes, and a generated module and a loader from opposite sides of the blob change disagree about
vec Byte. Move both packages in one step, regenerate committed modules, and then fix what
the compiler reports with the sections above. The generated module and the loader are held to the
same verdicts by the repository's cross-check, so regenerating and loading agree with each other.