@meantjs/cli 0.0.0-bootstrap
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.
- package/CHANGELOG.md +733 -0
- package/LICENSE +202 -0
- package/README.md +514 -0
- package/bin/cli.js +762 -0
- package/lib/index.d.ts +521 -0
- package/lib/index.js +131 -0
- package/package.json +49 -0
- package/wasm/candid_core_wasm.js +291 -0
- package/wasm/candid_core_wasm_bg.wasm +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# @meantjs/cli
|
|
2
|
+
|
|
3
|
+
Candid `.did` interfaces to [`@meantjs/schema`] runtimes, with no Rust
|
|
4
|
+
toolchain: the [Meant] compiler and its TypeScript generator compiled
|
|
5
|
+
to WebAssembly, usable as a Node CLI and as a browser library. Data in, data
|
|
6
|
+
out — no eval, no network, and nothing generated is ever executed.
|
|
7
|
+
|
|
8
|
+
> **0.5.0, the `latest` release, and the first under the name
|
|
9
|
+
> `@meantjs/cli`.** Through 0.4.0 this package was published as
|
|
10
|
+
> `@candid-core/cli`, with the command `candid-core-cli`. 0.5.0 is 0.4.0's
|
|
11
|
+
> code under new names: the command is `meant`, and the modules it writes
|
|
12
|
+
> import `@meantjs/schema`, the new name of `@candid-core/schema`. To upgrade
|
|
13
|
+
> from 0.4.0, install the new pair in place of the old one, each with
|
|
14
|
+
> `--save-exact`: `npm install --save-exact @meantjs/schema` and
|
|
15
|
+
> `npm install --save-dev --save-exact @meantjs/cli`. Replace
|
|
16
|
+
> `candid-core-cli` with `meant` in your scripts, and run `meant gen` again:
|
|
17
|
+
> each module's header line and import change and nothing else in it does,
|
|
18
|
+
> and the envelopes are byte-identical to 0.4.0's. Until then, `gen --check`
|
|
19
|
+
> reports each module 0.4.0 wrote as drifted. Run the command as
|
|
20
|
+
> `npx @meantjs/cli`, or as `meant` from a script in your `package.json`;
|
|
21
|
+
> never as a bare `npx meant`, which, where this package is not installed,
|
|
22
|
+
> would offer to download another author's package of that name. In a shell,
|
|
23
|
+
> a bare `meant` is whichever comes first on the `PATH`, which can be the Rust
|
|
24
|
+
> compiler's binary, with `compile` and `validate` only. The strings
|
|
25
|
+
> inside Contract documents keep their names, such as the
|
|
26
|
+
> `candid-core:interface:v1:sha256:…` identities the command prints.
|
|
27
|
+
> Upgrading from 0.3.0 also adds [`didToDocAttributes`](#doc-attributes),
|
|
28
|
+
> which reads `@name(…)` attributes in a `.did`'s doc comments, and an
|
|
29
|
+
> `identities` object on each entry of `gen --json`; it changes every
|
|
30
|
+
> envelope, whose `producer` names `meant` 0.1.0, and the module text where a
|
|
31
|
+
> declared `blob`, `record {}` or `variant {}` is used, which renders as
|
|
32
|
+
> itself. No Contract and no identity moves. Upgrading from 0.2.0 also
|
|
33
|
+
> changes the text of every module with a declaration or an actor (each type
|
|
34
|
+
> is declared under its Candid name, and each method of `Actor` carries its
|
|
35
|
+
> mode) and adds the `project` and `check` commands
|
|
36
|
+
> ([Projection and compatibility](#projection-and-compatibility)).
|
|
37
|
+
> Upgrading from 0.1.0 is a larger break: 0.1.0 emits modules in an older
|
|
38
|
+
> layout with an object-shaped principal type, refuses a whole interface
|
|
39
|
+
> with `ts_generation_refused` for one declaration it cannot represent, and
|
|
40
|
+
> returns no `omitted` list.
|
|
41
|
+
>
|
|
42
|
+
> `@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.
|
|
43
|
+
>
|
|
44
|
+
> Every change is recorded in the [changelog](./CHANGELOG.md), which ships
|
|
45
|
+
> in the tarball: since 0.4.0 under `0.5.0`, since 0.3.0 under `0.5.0` and
|
|
46
|
+
> `0.4.0`, since 0.2.0 under those and `0.3.0`, and since 0.1.0 under those,
|
|
47
|
+
> `0.2.0` and `0.2.0-beta.1`. The migration notes
|
|
48
|
+
> [from 0.4.0](https://ic-reactor.b3pay.net/candid-core/migrating-from-0-5.html),
|
|
49
|
+
> [from 0.3.0](https://ic-reactor.b3pay.net/candid-core/migrating-from-0-4.html),
|
|
50
|
+
> [from 0.2.0](https://ic-reactor.b3pay.net/candid-core/migrating-from-0-3.html)
|
|
51
|
+
> and [from 0.1.0](https://ic-reactor.b3pay.net/candid-core/migrating-from-0-2.html)
|
|
52
|
+
> show what each break means for your code, with before and after code where
|
|
53
|
+
> the compiler shows it.
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npx @meantjs/cli gen ./service.did -o ./generated
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`gen` takes one or more entries (`gen <a.did> [<b.did> …]`), and two flags:
|
|
60
|
+
`--json` prints one machine-readable document instead of the human report,
|
|
61
|
+
and `--check` compares instead of writing. See [The command](#the-command).
|
|
62
|
+
|
|
63
|
+
That emits three things:
|
|
64
|
+
|
|
65
|
+
- **`service.ts`** — the generated `@meantjs/schema` module: one reviewed
|
|
66
|
+
type alias and one invariantly-annotated schema builder per declaration,
|
|
67
|
+
both exported under its Candid name (the alias declared as that name, so
|
|
68
|
+
compiler errors read it; the builder bound as a `$`-prefixed local), with
|
|
69
|
+
the `.did`'s doc comments as JSDoc, and, for a service with an actor, the
|
|
70
|
+
`actor` schema and the `Actor` call interface, each method of which carries
|
|
71
|
+
its mode (`$.WithMode<"query">`); byte-identical to what the Rust-native
|
|
72
|
+
generator emits;
|
|
73
|
+
- **`service.envelope.json`** — the one-document `ContractEnvelope`: the
|
|
74
|
+
canonical Contract plus its field-name table under the
|
|
75
|
+
`org.candid-core.field-names/v1` extension, byte-identical to
|
|
76
|
+
`meant compile ./service.did --envelope`, ready to hand whole to
|
|
77
|
+
`schemaFromContract`;
|
|
78
|
+
- the printed **content-addressed identities** (`contract`, and `interface`
|
|
79
|
+
when the service has an actor) — the same
|
|
80
|
+
`candid-core:contract:v1:sha256:…` addresses the Rust toolchain computes,
|
|
81
|
+
usable to pin an interface and detect drift. `--json` carries them as
|
|
82
|
+
each entry's `identities`.
|
|
83
|
+
|
|
84
|
+
A declaration the generator cannot represent does not cost the whole
|
|
85
|
+
module: it is left out, together with every declaration and actor method that
|
|
86
|
+
references it, and the run still succeeds (exit 0). The CLI prints one
|
|
87
|
+
`warning: omitted …` line per entry on stderr, and the module's header lists
|
|
88
|
+
the same entries as `// Omitted:` lines — for example
|
|
89
|
+
`// Omitted: type Holder (references_omitted via Bad)`. See
|
|
90
|
+
[Omissions](#omissions).
|
|
91
|
+
|
|
92
|
+
Imports are resolved from the entry file's directory: every `.did` beneath
|
|
93
|
+
it is handed to the compiler as an in-memory bundle, so relative imports
|
|
94
|
+
work without any filesystem access from the wasm side. Every generation runs
|
|
95
|
+
twice and the tool refuses to write on any byte mismatch — determinism is
|
|
96
|
+
enforced, not assumed.
|
|
97
|
+
|
|
98
|
+
## The command
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
meant gen <service.did>... [-o <dir>] [--json] [--check]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Several entries.** Each entry generates its own module and envelope, named
|
|
105
|
+
from its file: `a/service.did` writes `<dir>/service.ts` and
|
|
106
|
+
`<dir>/service.envelope.json`, into the one `-o` directory (default: the
|
|
107
|
+
current directory). The output of an entry is byte-for-byte what a run of that
|
|
108
|
+
entry alone produces. Two decisions here are recommendations the maintainer
|
|
109
|
+
can overturn, each implemented in one place in `bin/cli.js`:
|
|
110
|
+
|
|
111
|
+
- *Bundle roots.* An entry's bundle is the `.did` files beneath the entry's
|
|
112
|
+
own directory, and the bundle limits (256 files, 1 MiB per file, 8 MiB in
|
|
113
|
+
all) apply to each entry's bundle separately. Entries in the same directory
|
|
114
|
+
share one read of it, so they are not counted twice against each other.
|
|
115
|
+
Inside a file, a run of more than 256 consecutive comments between two
|
|
116
|
+
tokens is refused with `resource_limit_exceeded` (resource
|
|
117
|
+
`source_nesting`), since 0.2.0; write a long header as one `/* */` block,
|
|
118
|
+
which counts as one comment.
|
|
119
|
+
- *Duplicate stems.* Two entries with the same stem (`a/service.did` and
|
|
120
|
+
`b/service.did`, or the same file twice) would write the same output, so the
|
|
121
|
+
run is refused as a usage error (exit 64) before any work. Stems compare
|
|
122
|
+
case-insensitively, so the output also checks out cleanly on a
|
|
123
|
+
case-folding filesystem. Give the entries distinct file names, or run them
|
|
124
|
+
into separate `-o` directories.
|
|
125
|
+
|
|
126
|
+
**Files are written only when they change.** An output that already holds
|
|
127
|
+
exactly the generated bytes is left alone (its modification time included),
|
|
128
|
+
and the human report prints `unchanged <path>` instead of `wrote <path>`.
|
|
129
|
+
|
|
130
|
+
**Exit codes.** `0` when every entry generated (a module that omits
|
|
131
|
+
declarations still counts, and lists them), and no `--check` drift was found;
|
|
132
|
+
`1` when any entry failed or any checked file drifted; `64` for a usage error,
|
|
133
|
+
with the usage on stderr and nothing on stdout. A failing entry does not stop
|
|
134
|
+
the others: every entry is attempted, and the exit code is `1` if any failed.
|
|
135
|
+
Without `--json`, a failed entry prints its `{ ok: false, diagnostics }`
|
|
136
|
+
document on stdout, exactly as a single-entry run always has (several failed
|
|
137
|
+
entries print several documents; use `--json` for one machine-readable
|
|
138
|
+
result).
|
|
139
|
+
|
|
140
|
+
**Nothing is read from stdin, and nothing is written outside `-o`.** The CLI
|
|
141
|
+
never prompts and never waits on input, so it is safe to run as a child process
|
|
142
|
+
with any stdin.
|
|
143
|
+
|
|
144
|
+
### `--check`
|
|
145
|
+
|
|
146
|
+
Generate in memory and compare byte-for-byte with the files on disk. Nothing
|
|
147
|
+
is written (not even the output directory), and the run exits `1` if any
|
|
148
|
+
output file is missing or differs, listing each on stderr (`missing <path>` or
|
|
149
|
+
`drifted <path>`):
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
npx @meantjs/cli gen ./service.did -o ./generated --check
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use it in CI to fail when committed output is stale. A compile error is a
|
|
156
|
+
failed entry, not drift. Stale files for entries you no longer list are not
|
|
157
|
+
looked for.
|
|
158
|
+
|
|
159
|
+
### `--json`
|
|
160
|
+
|
|
161
|
+
Prints exactly one JSON document on stdout and nothing else; stderr stays
|
|
162
|
+
empty. Each compiled entry carries the identities the human report prints.
|
|
163
|
+
Combine it with `--check`. A usage error (exit 64) prints no document.
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"schemaVersion": 1,
|
|
168
|
+
"ok": true,
|
|
169
|
+
"check": false,
|
|
170
|
+
"entries": [
|
|
171
|
+
{
|
|
172
|
+
"entry": "a/service.did",
|
|
173
|
+
"status": "written",
|
|
174
|
+
"module": "generated/service.ts",
|
|
175
|
+
"envelope": "generated/service.envelope.json",
|
|
176
|
+
"identities": {
|
|
177
|
+
"contract_id": "candid-core:contract:v1:sha256:…",
|
|
178
|
+
"interface_id": "candid-core:interface:v1:sha256:…"
|
|
179
|
+
},
|
|
180
|
+
"omitted": [],
|
|
181
|
+
"diagnostics": []
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"drift": []
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- `schemaVersion` is `1`. It changes only when an existing field's meaning or
|
|
189
|
+
shape changes; adding a field does not change it. Refuse a version you do
|
|
190
|
+
not know.
|
|
191
|
+
- `ok` is true exactly when no entry is `failed` or `drifted`, and the exit
|
|
192
|
+
code is `0` exactly when `ok` is true.
|
|
193
|
+
- `check` records whether `--check` was given.
|
|
194
|
+
- `entries` has one object per entry, in command-line order, with `entry` (as
|
|
195
|
+
given on the command line), `module` and `envelope` (the paths it writes or
|
|
196
|
+
compares: `-o` joined with `<stem>.ts` and `<stem>.envelope.json`, present
|
|
197
|
+
for a failed entry too), and:
|
|
198
|
+
- `status`: `written` (a file was created or changed), `unchanged` (both
|
|
199
|
+
files already held these bytes), `drifted` (`--check` only: a file is
|
|
200
|
+
missing or differs; nothing was written) or `failed`;
|
|
201
|
+
- `identities` (new in 0.4.0): `{ contract_id, interface_id }`, equal to the envelope's
|
|
202
|
+
`contract.identities` and to what the human report prints, with the key
|
|
203
|
+
names `project --json` and `check --json` use. `interface_id` is absent
|
|
204
|
+
when the `.did` has no actor. Present on every entry that compiled,
|
|
205
|
+
`--check` included; absent on a failed entry;
|
|
206
|
+
- `omitted`: what the module leaves out, exactly as `didToModule` returns it
|
|
207
|
+
(see [Omissions](#omissions)), `[]` for a failed entry. Omissions never make
|
|
208
|
+
an entry fail;
|
|
209
|
+
- `diagnostics`: for a failed entry, the compiler's diagnostics unchanged in
|
|
210
|
+
shape (see [Failures](#failures)); `[]` otherwise. A failure found by the
|
|
211
|
+
CLI itself uses the same item shape with one of the codes
|
|
212
|
+
`did_file_read_error` (the entry's directory cannot be read),
|
|
213
|
+
`did_source_not_found` (the entry is not a `.did` file there),
|
|
214
|
+
`resource_limit_exceeded` (the bundle bounds), `nondeterministic_output`
|
|
215
|
+
(two generations disagreed) or `internal_error` (anything unexpected, such as
|
|
216
|
+
an output that cannot be written; it carries no `phase`).
|
|
217
|
+
- `drift` lists every path `--check` found missing or different, in entry
|
|
218
|
+
order, module before envelope; always `[]` without `--check`.
|
|
219
|
+
|
|
220
|
+
The same types ship as `CliReport`, `CliEntryReport` and `CliIdentities` in
|
|
221
|
+
the package's declarations. A run is deterministic: the same inputs give the same files and
|
|
222
|
+
the same document, byte for byte, from any working directory. There are no
|
|
223
|
+
timestamps, and every path in the document and in the human report (including
|
|
224
|
+
the messages of failures such as an unreadable or missing entry) is spelled as
|
|
225
|
+
you passed it, or `-o` joined with a file name, never resolved to an absolute
|
|
226
|
+
host path: an absolute path appears only if you passed one.
|
|
227
|
+
|
|
228
|
+
## The library
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import { didToContract, didToModule } from "@meantjs/cli";
|
|
232
|
+
|
|
233
|
+
const envelope = await didToContract("service : { ping : () -> () };");
|
|
234
|
+
// → the ContractEnvelope document ("contract" in envelope), or
|
|
235
|
+
// { ok: false, diagnostics } with the compiler's diagnostics verbatim.
|
|
236
|
+
|
|
237
|
+
const generated = await didToModule({
|
|
238
|
+
entry: "main.did",
|
|
239
|
+
files: {
|
|
240
|
+
"main.did": 'import "types.did";\nservice : { get : () -> (Item) query };',
|
|
241
|
+
"types.did": "type Item = record { id : nat };",
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
// → { ok: true, module, omitted } with the generated TypeScript text and
|
|
245
|
+
// what it left out (empty here), or { ok: false, diagnostics }.
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Both functions accept either a string of Candid text or
|
|
249
|
+
`{ entry, files: { name: text } }` for a multi-file bundle. In a browser the
|
|
250
|
+
wasm is fetched relative to the module automatically; call
|
|
251
|
+
`init(bytesOrUrl)` first to supply it yourself. Feed `didToContract`'s
|
|
252
|
+
result straight to `schemaFromContract` from [`@meantjs/schema`] — the
|
|
253
|
+
envelope carries the field names, so one document is the whole hand-off:
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
import { didToContract } from "@meantjs/cli";
|
|
257
|
+
import { schemaFromContract } from "@meantjs/schema/contract";
|
|
258
|
+
|
|
259
|
+
const built = schemaFromContract(await didToContract(didText));
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Projection and compatibility
|
|
263
|
+
|
|
264
|
+
> **New in 0.3.0.** `project`, `check`, `projectDid` and `checkCompatible`
|
|
265
|
+
> are not in 0.2.0, which exits 64 with its usage for either command; the
|
|
266
|
+
> [changelog](./CHANGELOG.md) records them under `0.3.0`.
|
|
267
|
+
|
|
268
|
+
Two commands for an interface you do not own, such as a canister's published
|
|
269
|
+
`.did`. Both work on files already on disk: the tool never fetches anything,
|
|
270
|
+
so getting the live interface is your build step's job.
|
|
271
|
+
|
|
272
|
+
```text
|
|
273
|
+
meant project <in.did> --methods <a,b,...> -o <out.did> [--json]
|
|
274
|
+
meant check <written.did> --against <live.did> [--json]
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**`project`** writes a `.did` holding only the methods you name and every
|
|
278
|
+
declaration they reach, as one self-contained file (imports are inlined), and
|
|
279
|
+
prints the interface identities of the input and of the projection. Feed the
|
|
280
|
+
result to `gen` like any `.did`: the generated `Actor` lists only those
|
|
281
|
+
methods, each with its mode. The output is deterministic: the same input and
|
|
282
|
+
the same set of names give the same bytes, whatever order you name them in.
|
|
283
|
+
Declaration names, doc comments and argument names are kept; a service
|
|
284
|
+
class's init arguments are not, since a client never sends them. Methods are
|
|
285
|
+
written in name order, a service import's among the entry's. An unknown
|
|
286
|
+
method name, or an empty list (`--methods ""`), fails with exit 1 and writes
|
|
287
|
+
nothing: each distinct unknown name gets one `unknown_method` diagnostic
|
|
288
|
+
naming it, and the first diagnostic lists the service's methods in its
|
|
289
|
+
`notes`, once; an empty list gets `empty_method_list`. The unknown-method
|
|
290
|
+
diagnostics are bounded at 4 MiB in all, each counted as the JSON writes it
|
|
291
|
+
(the list of methods aside): the name at which they would pass it, and every
|
|
292
|
+
one after it, are counted in one `resource_limit_exceeded` diagnostic
|
|
293
|
+
(`projection_diagnostic_bytes`) instead of reported. A projection must
|
|
294
|
+
compile again, so it is at most 1 MiB, the compiler's bound on one source: one
|
|
295
|
+
that would be larger fails with exit 1 and `resource_limit_exceeded`
|
|
296
|
+
(`projection_bytes`, whose `observed` is where writing stopped) and writes
|
|
297
|
+
nothing. Each name, requested or reached, is
|
|
298
|
+
looked up by binary search among names sorted once, so no lookup scans the
|
|
299
|
+
service or the declarations. `-o` is required, and may not be,
|
|
300
|
+
or become, a source of the input: `project` reads the input as `gen` does (the
|
|
301
|
+
file and every `.did` beneath its directory), so an `-o` that names one of
|
|
302
|
+
those files by any path, symlink or hard link, or a new `.did` anywhere beneath
|
|
303
|
+
that directory, fails with exit 1 and `output_is_input` (`path` is the `-o`
|
|
304
|
+
given, `notes` the existing sources it names, empty for a new file) and writes
|
|
305
|
+
nothing. Write the projection outside the input's directory. An
|
|
306
|
+
output that cannot be read or written (a directory, a read-only parent, a full
|
|
307
|
+
disk) fails with exit 1 and `output_write_failed`, whose `notes` hold the
|
|
308
|
+
system error code, such as `EISDIR` or `EACCES`; without `--json` it is one
|
|
309
|
+
`cannot write …` line on stderr, as a source that cannot be read is
|
|
310
|
+
`cannot read …`. The file is written only when it changes, and an existing
|
|
311
|
+
file of another size is replaced without being read; `--json` prints
|
|
312
|
+
`{ ok, output, status, methods, input, projection }` instead of the report,
|
|
313
|
+
and `{ ok: false, diagnostics }` for any failure.
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
npx @meantjs/cli project ./live/ledger.did --methods icrc1_balance_of,icrc1_transfer -o ./src/ledger.did
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**`check`** exits `0` when the live interface is still a Candid subtype of the
|
|
320
|
+
written one, and `1` otherwise. Every method of the written `.did` must exist
|
|
321
|
+
in the live one with the same mode, its arguments contravariant and its
|
|
322
|
+
results covariant; methods only the live service has are ignored, and so is
|
|
323
|
+
a class's init. So an added method or an added `opt` result field passes,
|
|
324
|
+
and a removed method, a changed mode, or a result that gained a variant arm
|
|
325
|
+
fails. A projection is not the only valid written side: a hand-written
|
|
326
|
+
subset `.did` checks the same way. The report prints both interface
|
|
327
|
+
identities on stdout, one line per finding on stderr, and a verdict line:
|
|
328
|
+
|
|
329
|
+
```text
|
|
330
|
+
written: candid-core:interface:v1:sha256:…
|
|
331
|
+
live: candid-core:interface:v1:sha256:…
|
|
332
|
+
error: icrc1_transfer: method_incompatible at $results[0].Err.TooOld: the other variant has no such arm
|
|
333
|
+
incompatible: 1 error(s), 0 warning(s)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Each finding names its method and a stable code: `method_missing`,
|
|
337
|
+
`mode_changed` and `method_incompatible` are errors; `special_opt_rule` is a
|
|
338
|
+
warning, for a change Candid accepts only by reading the value under an `opt`
|
|
339
|
+
as `null` (such as `opt nat` becoming `opt text`), so it passes but loses the
|
|
340
|
+
data at that path, and it is reported at every such path along which no pair
|
|
341
|
+
of types repeats (a changed alias under each field that uses it; a recursive
|
|
342
|
+
type up to where a path comes back round); `resource_limit_exceeded` fails a
|
|
343
|
+
method whose check reached a bound, its depth (384 pairs on one path) or its
|
|
344
|
+
work (1,000,000 steps, a step being one unit of work done: a pair of types
|
|
345
|
+
visited, a field, arm, method or value examined or passed over,
|
|
346
|
+
or one path segment a warning or a failure copies, or a warning
|
|
347
|
+
re-examines). A step reads no name: service methods are paired by ranks
|
|
348
|
+
computed once per check, and a field's name is read by its position. So a
|
|
349
|
+
step's cost does not grow with the length of the names: it does a bounded
|
|
350
|
+
amount of work, and its lookups in the maps of type pairs the walk keeps take
|
|
351
|
+
time logarithmic in their size. Past 1,000
|
|
352
|
+
warnings a method reports the first 1,000 and one `resource_limit_exceeded`
|
|
353
|
+
warning (`check_warnings`) saying the rest were dropped; its verdict stands.
|
|
354
|
+
The check as a whole is bounded too, so a service with many methods cannot
|
|
355
|
+
multiply those per-method bounds: 10,000,000 steps in all, one more per
|
|
356
|
+
method decided (`check_total_steps`), 10,000 `special_opt_rule` warnings in all
|
|
357
|
+
(`check_total_warnings`), and 4 MiB of reported diagnostics, each counted as
|
|
358
|
+
the JSON response writes it, keys and indentation included
|
|
359
|
+
(`check_output_bytes`), measured only up to the first string that passes that
|
|
360
|
+
bound. Methods are checked in name order. When the step or warning bound is
|
|
361
|
+
reached, the method at which it was reached and every method after it fail
|
|
362
|
+
closed: each gets one `resource_limit_exceeded` error naming the bound, with
|
|
363
|
+
its limit and the observed value, and nothing else. Those count against the
|
|
364
|
+
4 MiB like any other diagnostic: once a method's diagnostics would pass it,
|
|
365
|
+
that method and every method after it are counted in one last
|
|
366
|
+
`resource_limit_exceeded` error naming `check_output_bytes`, which names no
|
|
367
|
+
method and holds the count in `unreported_methods` (without `--json`, the one
|
|
368
|
+
line with no method name). Either way the check exits `1`, and the response is
|
|
369
|
+
at most 4 MiB and 1,016 bytes, however many methods the written side has.
|
|
370
|
+
Below these bounds a method's findings do not depend on the other methods;
|
|
371
|
+
once one is reached, they do. So the walk's time grows with its steps (times that
|
|
372
|
+
logarithm) and the reported text's with its bytes; compiling both sides,
|
|
373
|
+
within the compiler's own limits, and ranking the method names are done once
|
|
374
|
+
per check, before the walk.
|
|
375
|
+
`method_incompatible` and `special_opt_rule` carry the path
|
|
376
|
+
into the method's type where the check failed: `$args[i]` or `$results[i]`,
|
|
377
|
+
then `.name` per record field or variant arm (`["name"]` when it is not an
|
|
378
|
+
identifier, `[id]` for a numeric label), `[*]` per `vec` element, `?` per
|
|
379
|
+
`opt` content, and `::args[i]`, `::results[i]` or `::name` into a `func` or
|
|
380
|
+
`service` type. `--json` prints the `checkCompatible` document below.
|
|
381
|
+
|
|
382
|
+
The live interface identity tells an unchanged interface from one that
|
|
383
|
+
changed compatibly: `project` prints the input's, so record it when you write
|
|
384
|
+
the projection, and compare it with what `check` prints later.
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
import { checkCompatible, projectDid } from "@meantjs/cli";
|
|
388
|
+
|
|
389
|
+
const projected = await projectDid(liveDid, ["icrc1_balance_of", "icrc1_transfer"]);
|
|
390
|
+
// → { ok: true, did, methods, input, projection } — the text, the methods it
|
|
391
|
+
// holds, and { contract_id, interface_id } of the input and the projection;
|
|
392
|
+
// or { ok: false, diagnostics }.
|
|
393
|
+
|
|
394
|
+
const report = await checkCompatible(writtenDid, liveDid);
|
|
395
|
+
// → { ok: true, compatible, written, live, diagnostics }, each diagnostic
|
|
396
|
+
// { code, severity, method, path?, message } (the one past the 4 MiB
|
|
397
|
+
// bound has no method, and counts the methods left in
|
|
398
|
+
// unreported_methods); or { ok: false, input, diagnostics } when one side
|
|
399
|
+
// does not compile (`input` names it).
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Both take Candid text or `{ entry, files }`, as `didToModule` does. The check
|
|
403
|
+
is one Rust implementation compiled into this package's WebAssembly, tested
|
|
404
|
+
against the subtype check of the `candid` crate it pins and against the one
|
|
405
|
+
`@meantjs/schema`'s decoder runs on a `func` or `service` reference. Where the `candid` crate accepts a pair that is not a
|
|
406
|
+
subtype (its coinductive memo can keep a pair proven under an assumption it
|
|
407
|
+
later retracted), this check refuses it, as the decoder does.
|
|
408
|
+
|
|
409
|
+
## Doc attributes
|
|
410
|
+
|
|
411
|
+
> **New in 0.4.0.** `didToDocAttributes` and the types it returns are not in
|
|
412
|
+
> 0.3.0; the [changelog](./CHANGELOG.md) records them under `0.4.0`.
|
|
413
|
+
|
|
414
|
+
`didToDocAttributes(sources)` compiles the sources and reads the
|
|
415
|
+
`@name(key = value, …)` lines in their doc comments, as specified in
|
|
416
|
+
[`docs/doc-attributes.md`](https://github.com/B3Pay/Meant/blob/main/docs/doc-attributes.md):
|
|
417
|
+
|
|
418
|
+
```candid
|
|
419
|
+
service : {
|
|
420
|
+
/// Send tokens to another principal.
|
|
421
|
+
/// @intent(title = "Send", template = "Send {amount} to {to}")
|
|
422
|
+
/// @amount(at = "amount", decimals = query(decimals))
|
|
423
|
+
send : (record { to : principal; amount : nat }) -> ();
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
import { didToDocAttributes } from "@meantjs/cli";
|
|
429
|
+
|
|
430
|
+
const result = await didToDocAttributes(didText);
|
|
431
|
+
if (result.ok) {
|
|
432
|
+
for (const item of result.doc_attributes.items) {
|
|
433
|
+
// item.path is "actor.methods[0]"; item.prose is ["Send tokens to another principal."]
|
|
434
|
+
for (const attribute of item.attributes) console.log(attribute.name, attribute.args);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Success is `{ ok: true, contract, source_info, doc_attributes }`: the
|
|
440
|
+
Contract and SourceInfo that `meant compile <path>` prints, and every
|
|
441
|
+
documented item (the actor, a declaration, a field, a method) with its prose
|
|
442
|
+
lines and its attributes in order. An attribute is `{ name, doc_line, args }`,
|
|
443
|
+
and each argument value is tagged: `text`, `number` (the digits as written,
|
|
444
|
+
never a float), `bool`, `ident`, `call` (`query(decimals)`), `list` or
|
|
445
|
+
`record`. The compiler gives no name a meaning: the tool that reads them
|
|
446
|
+
decides which it knows.
|
|
447
|
+
|
|
448
|
+
A line counts as an attribute only when it starts with `@`, a lowercase
|
|
449
|
+
name, then `(` or nothing: `@param amount the value` and `@amount (…)` stay
|
|
450
|
+
prose. Once a line is an attribute it must parse to its end, so a malformed
|
|
451
|
+
one fails the call with `doc_attribute_malformed`, and a repeated key with
|
|
452
|
+
`doc_attribute_duplicate_key`; each names its item's `path`. Comments never
|
|
453
|
+
change `contract_id` or `interface_id`.
|
|
454
|
+
|
|
455
|
+
## Omissions
|
|
456
|
+
|
|
457
|
+
`didToModule`'s success carries `omitted`, an array of
|
|
458
|
+
`{ kind, name, reason, via? }` — declarations first, then methods, each
|
|
459
|
+
sorted by name, and empty when nothing is left out:
|
|
460
|
+
|
|
461
|
+
- `kind` is `"declaration"` or `"method"` (a method of the actor's service,
|
|
462
|
+
dropped from both the `actor` schema and the `Actor` type);
|
|
463
|
+
- `reason` is one of a closed set: `reserved_field_name` (a record field or
|
|
464
|
+
variant arm named like the `_N_` numeric-id rendering, which as a schema
|
|
465
|
+
key would read back as the wrong wire id), `ambiguous_variant_arm` (an arm
|
|
466
|
+
whose payload is a declared `opt` of an uninhabited type, whose generated
|
|
467
|
+
type could not tell it from a bare tag), `reserved_export_name` (a
|
|
468
|
+
declaration named `actor` or `Actor`, the module's own export names), or
|
|
469
|
+
`references_omitted` (the entry references another omitted declaration);
|
|
470
|
+
`invalid_declaration_name` completes the set for Contract documents, which
|
|
471
|
+
Candid source never produces;
|
|
472
|
+
- `via`, present only for `references_omitted`, names the omitted declaration
|
|
473
|
+
referenced.
|
|
474
|
+
|
|
475
|
+
Omission follows every reference, nested `func` and `service` types
|
|
476
|
+
included, up to the containing declaration: a record holding a
|
|
477
|
+
`service { f : (Bad) -> () }` is omitted whole rather than losing `f`,
|
|
478
|
+
because a service value's wire type is its full method table. Everything the
|
|
479
|
+
module still emits is exactly what it would be had the omitted declarations
|
|
480
|
+
never existed. `schemaFromContract` leaves out the same entries for the same
|
|
481
|
+
contract and lists them the same way.
|
|
482
|
+
|
|
483
|
+
## Failures
|
|
484
|
+
|
|
485
|
+
Nothing throws for data errors and nothing half-succeeds: a compile failure
|
|
486
|
+
is `{ ok: false, diagnostics }` with [Meant]'s structured diagnostics
|
|
487
|
+
passed through verbatim (stable codes such as `did_parse_error`,
|
|
488
|
+
`did_file_read_error` analogues, resource bounds included), a malformed
|
|
489
|
+
request is `invalid_request`, and `ts_generation_refused` is reserved for a
|
|
490
|
+
generator refusal of an invalid contract graph — which a contract compiled
|
|
491
|
+
from Candid source never is, so an unrepresentable declaration is an
|
|
492
|
+
omission, not a failure. The CLI exits 1 when any entry failed, printing each
|
|
493
|
+
failing document on stdout (or all of them in the `--json` document);
|
|
494
|
+
usage errors exit 64 with usage on stderr, in the native binary's
|
|
495
|
+
convention.
|
|
496
|
+
|
|
497
|
+
## Version pairing
|
|
498
|
+
|
|
499
|
+
This package embeds exact revisions, recorded per release in
|
|
500
|
+
[CHANGELOG.md](./CHANGELOG.md): the compiler crate (`meant`, published as
|
|
501
|
+
`candid-core` before its rename) and the
|
|
502
|
+
`candid-core-ts` generator (unpublishable on crates.io by design; embedding
|
|
503
|
+
it in this wasm artifact is an owner decision recorded on the repository's
|
|
504
|
+
issue tracker) are built from one repository commit, and the parity gates
|
|
505
|
+
prove the emitted module and contract are byte-identical to that commit's
|
|
506
|
+
Rust-native outputs over the golden fixtures.
|
|
507
|
+
|
|
508
|
+
The `@meantjs/schema` peer is an exact version, not a range, while the two
|
|
509
|
+
packages move in lockstep: each schema release is paired with a new release
|
|
510
|
+
of this package that names it, so a generated module always meets the runtime
|
|
511
|
+
it was generated for.
|
|
512
|
+
|
|
513
|
+
[Meant]: https://github.com/B3Pay/Meant
|
|
514
|
+
[`@meantjs/schema`]: https://www.npmjs.com/package/@meantjs/schema
|