@atproto/lex 0.1.1 → 0.1.3

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +287 -95
  3. package/bin/lex +18 -10
  4. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @atproto/lex
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5006](https://github.com/bluesky-social/atproto/pull/5006) [`60721e6`](https://github.com/bluesky-social/atproto/commit/60721e69c8db193eb817c4238ac447505ac855bc) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix README
8
+
9
+ - [#5006](https://github.com/bluesky-social/atproto/pull/5006) [`60721e6`](https://github.com/bluesky-social/atproto/commit/60721e69c8db193eb817c4238ac447505ac855bc) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update `yargs` depedency
10
+
11
+ - Updated dependencies [[`60721e6`](https://github.com/bluesky-social/atproto/commit/60721e69c8db193eb817c4238ac447505ac855bc), [`60721e6`](https://github.com/bluesky-social/atproto/commit/60721e69c8db193eb817c4238ac447505ac855bc)]:
12
+ - @atproto/lex-schema@0.1.2
13
+ - @atproto/lex-client@0.1.3
14
+
15
+ ## 0.1.2
16
+
17
+ ### Patch Changes
18
+
19
+ - [#4984](https://github.com/bluesky-social/atproto/pull/4984) [`5bdb4ad`](https://github.com/bluesky-social/atproto/commit/5bdb4addd6a3798bf6c8c391c74044b3e251008a) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `default` export of "main" schema in the namespace file
20
+
21
+ - [#4979](https://github.com/bluesky-social/atproto/pull/4979) [`314df62`](https://github.com/bluesky-social/atproto/commit/314df62537bb519231aa375dd3a38360afc79ce0) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Always add `#__PURE__` annotations to function calls
22
+
23
+ - [#4983](https://github.com/bluesky-social/atproto/pull/4983) [`1259646`](https://github.com/bluesky-social/atproto/commit/125964673962a86282a05bf22f410fad8ad06b41) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make the generation of the `$defs` namespace optional by default
24
+
25
+ - Updated dependencies [[`3aae4fe`](https://github.com/bluesky-social/atproto/commit/3aae4fe43448dca860fc6c4a24d18bfa64de084b), [`5bdb4ad`](https://github.com/bluesky-social/atproto/commit/5bdb4addd6a3798bf6c8c391c74044b3e251008a), [`3aae4fe`](https://github.com/bluesky-social/atproto/commit/3aae4fe43448dca860fc6c4a24d18bfa64de084b), [`314df62`](https://github.com/bluesky-social/atproto/commit/314df62537bb519231aa375dd3a38360afc79ce0), [`1259646`](https://github.com/bluesky-social/atproto/commit/125964673962a86282a05bf22f410fad8ad06b41), [`482767c`](https://github.com/bluesky-social/atproto/commit/482767c4bcce95aa390b2992b028fd8e27d162b2)]:
26
+ - @atproto/lex-builder@0.1.2
27
+ - @atproto/lex-client@0.1.2
28
+
3
29
  ## 0.1.1
4
30
 
5
31
  ### Patch Changes
package/README.md CHANGED
@@ -1,7 +1,3 @@
1
- > [!IMPORTANT]
2
- >
3
- > This package is currently in **preview**. The API and features are subject to change before the stable release. See the [Changelog](./CHANGELOG.md) for version history.
4
-
5
1
  Type-safe Lexicon tooling for AT Protocol data.
6
2
 
7
3
  - Fetch and manage Lexicon schemas, generate TypeScript validators
@@ -54,6 +50,16 @@ const posts = await client.list(app.bsky.feed.post, { limit: 10 })
54
50
  - [Type definitions](#type-definitions)
55
51
  - [Building data](#building-data)
56
52
  - [Validation Helpers](#validation-helpers)
53
+ - [Record / typed-object helpers](#record--typed-object-helpers)
54
+ - [`$type` - Type Identifier](#type---type-identifier)
55
+ - [`$build(data)` - Build with Defaults](#builddata---build-with-defaults)
56
+ - [`$isTypeOf(data)` - Type Discriminator](#istypeofdata---type-discriminator)
57
+ - [Universal validation helpers](#universal-validation-helpers)
58
+ - [`$matches(data)` - Type Guard](#matchesdata---type-guard)
59
+ - [`$assert(data)` - Type-Narrowing Assertion](#assertdata---type-narrowing-assertion)
60
+ - [`$parse(data)` - Parse and Validate](#parsedata---parse-and-validate)
61
+ - [`$validate(data)` - Validate a value against the schema](#validatedata---validate-a-value-against-the-schema)
62
+ - [`$safeParse(data, options?)` - Parse a value against a schema and get the resulting value](#safeparsedata-options---parse-a-value-against-a-schema-and-get-the-resulting-value)
57
63
  - [Data Model](#data-model)
58
64
  - [Types](#types)
59
65
  - [JSON Encoding](#json-encoding)
@@ -61,21 +67,58 @@ const posts = await client.list(app.bsky.feed.post, { limit: 10 })
61
67
  - [Making simple XRPC Requests](#making-simple-xrpc-requests)
62
68
  - [Client API](#client-api)
63
69
  - [Creating a Client](#creating-a-client)
70
+ - [Unauthenticated Client](#unauthenticated-client)
71
+ - [Authenticated Client with OAuth](#authenticated-client-with-oauth)
72
+ - [Authenticated Client with Password](#authenticated-client-with-password)
73
+ - [Client with Service Proxy (authenticated only)](#client-with-service-proxy-authenticated-only)
74
+ - [Validation and Strictness Options](#validation-and-strictness-options)
64
75
  - [Core Methods](#core-methods)
76
+ - [`client.call()`](#clientcall)
77
+ - [`client.create()`](#clientcreate)
78
+ - [`client.get()`](#clientget)
79
+ - [`client.put()`](#clientput)
80
+ - [`client.delete()`](#clientdelete)
81
+ - [`client.list()`](#clientlist)
82
+ - [`client.applyWrites()`](#clientapplywrites)
65
83
  - [Error Handling](#error-handling)
84
+ - [Safe Methods](#safe-methods)
85
+ - [XrpcFailure Type](#xrpcfailure-type)
66
86
  - [Authentication Methods](#authentication-methods)
87
+ - [`client.did`](#clientdid)
88
+ - [`client.assertAuthenticated()`](#clientassertauthenticated)
89
+ - [`client.assertDid`](#clientassertdid)
67
90
  - [Labeler Configuration](#labeler-configuration)
68
91
  - [Low-Level XRPC](#low-level-xrpc)
69
92
  - [Utilities](#utilities)
70
93
  - [Datetime Strings](#datetime-strings)
71
94
  - [Advanced Usage](#advanced-usage)
72
95
  - [Workflow Integration](#workflow-integration)
96
+ - [Development Workflow](#development-workflow)
73
97
  - [Tree-Shaking](#tree-shaking)
98
+ - [Namespace notation](#namespace-notation)
99
+ - [Explicit `.main` reference](#explicit-main-reference)
100
+ - [Direct named import from the schema file](#direct-named-import-from-the-schema-file)
101
+ - [Default import (recommended)](#default-import-recommended)
102
+ - [Drawbacks of the default export](#drawbacks-of-the-default-export)
103
+ - [Summary](#summary)
74
104
  - [Blob references](#blob-references)
105
+ - [TypedBlobRef: The Current Standard](#typedblobref-the-current-standard)
106
+ - [LegacyBlobRef: Historical Format](#legacyblobref-historical-format)
107
+ - [Working with Both Formats](#working-with-both-formats)
75
108
  - [Actions](#actions)
109
+ - [What are Actions?](#what-are-actions)
110
+ - [Using Actions](#using-actions)
111
+ - [Composing Multiple Operations](#composing-multiple-operations)
112
+ - [Higher-Order Actions](#higher-order-actions)
76
113
  - [Creating a Client from Another Client](#creating-a-client-from-another-client)
77
114
  - [Building Library-Style APIs with Actions](#building-library-style-apis-with-actions)
115
+ - [Creating Posts](#creating-posts)
116
+ - [Following Users](#following-users)
117
+ - [Updating Profile with Retry Logic](#updating-profile-with-retry-logic)
118
+ - [Packaging Actions as a Library](#packaging-actions-as-a-library)
119
+ - [Best Practices for Actions](#best-practices-for-actions)
78
120
  - [Standard Schema Compatibility](#standard-schema-compatibility)
121
+ - [Validating Generic Schemas with `$check`](#validating-generic-schemas-with-check)
79
122
  - [License](#license)
80
123
 
81
124
  <!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -189,14 +232,16 @@ Options:
189
232
  - `--clear` - Clear output directory before generating
190
233
  - `--override` - Override existing files (has no effect with --clear)
191
234
  - `--no-pretty` - Don't run prettier on generated files (prettier is enabled by default)
192
- - `--ignore-errors` - How to handle errors when processing input files
193
- - `--pure-annotations` - Add `/*#__PURE__*/` annotations for tree-shaking tools. Set this to true if you are using generated lexicons in a library
235
+ - `--ignore-errors` - Skip files that fail to parse or compile instead of aborting the build
236
+ - `--ignore-invalid-lexicons` - Skip lexicon files that fail validation instead of exiting with an error
194
237
  - `--exclude <patterns...>` - List of strings or regex patterns to exclude lexicon documents by their IDs
195
238
  - `--include <patterns...>` - List of strings or regex patterns to include lexicon documents by their IDs
196
239
  - `--lib <package>` - Package name of the library to import the lex schema utility "l" from (default: `@atproto/lex`)
197
- - `--importExt <ext>` - File extension to use for import statements in generated files (default: `.js`). Use `--importExt ""` to generate extension-less imports
198
- - `--fileExt <ext>` - File extension to use for generated files (default: `.ts`)
199
- - `--indexFile` - Generate an "index" file that re-exports all root-level namespaces (disabled by default)
240
+ - `--import-ext <ext>` - File extension to use for import statements in generated files (default: `.js`). Use `--import-ext ""` to generate extension-less imports
241
+ - `--file-ext <ext>` - File extension to use for generated files (default: `.ts`)
242
+ - `--index-file` - Generate an "index" file that re-exports all root-level namespaces (disabled by default)
243
+ - `--defs-export` - When some definitions conflict with child namespaces, export lexicon definitions under a separate `$defs` namespace (e.g. `com.example.foo.$defs`)
244
+ - `--no-default-export` - Disable generation of a `default` export of the `main` schema in each schema's namespace file (default exports are enabled by default; see [Tree-Shaking](#tree-shaking))
200
245
 
201
246
  ### Generated Schema Structure
202
247
 
@@ -241,11 +286,12 @@ app.bsky.feed.post.$validate(post)
241
286
 
242
287
  ### Validation Helpers
243
288
 
244
- Each schema provides multiple validation methods:
289
+ Generated namespaces expose a handful of `$`-prefixed helpers bound to the namespace's `main` schema. They come in two groups:
245
290
 
246
- #### `$nsid` - Namespace Identifier
291
+ - [**Universal validation helpers**](#universal-validation-helpers) are available on every schema's `main`: `$matches`, `$assert`, `$check`, `$parse`, `$safeParse`, `$validate`, `$safeValidate` (and `$cast` / `$ifMatches`). These work for records, typed objects, queries, procedures, and subscriptions.
292
+ - [**Record / typed-object helpers**](#record--typed-object-helpers) are only emitted for record and typed-object schemas: `$type`, `$build`, `$isTypeOf`.
247
293
 
248
- Returns the NSID of the schema:
294
+ In addition, every generated namespace file exports a top-level `$nsid` constant containing the NSID of the lexicon document:
249
295
 
250
296
  ```typescript
251
297
  import * as app from './lexicons/app.js'
@@ -253,9 +299,13 @@ import * as app from './lexicons/app.js'
253
299
  console.log(app.bsky.feed.defs.$nsid) // 'app.bsky.feed.defs'
254
300
  ```
255
301
 
256
- #### `$type` - Type Identifier
302
+ The Schema instance itself (for example `app.bsky.feed.post.main`) also exposes the underlying methods both with and without the `$` prefix (e.g. `main.parse()` and `main.$parse()`).
257
303
 
258
- Returns the `$type` string of the schema (for record and object schemas):
304
+ #### Record / typed-object helpers
305
+
306
+ ##### `$type` - Type Identifier
307
+
308
+ Returns the `$type` string of the schema (only available on record and typed-object schemas):
259
309
 
260
310
  ```typescript
261
311
  import * as app from './lexicons/app.js'
@@ -264,7 +314,52 @@ console.log(app.bsky.feed.post.$type) // 'app.bsky.feed.post'
264
314
  console.log(app.bsky.actor.defs.profileViewBasic.$type) // 'app.bsky.actor.defs#profileViewBasic'
265
315
  ```
266
316
 
267
- #### `$check(data)` - Type Guard
317
+ Prefer `$type` over hard-coding the equivalent string literal in your code. The constant is emitted exactly once per schema in the generated namespace file, so every reference reuses the same string instance. Inlining `'app.bsky.feed.post'` everywhere instead leaks the same string into every call site, increases bundle size, and creates a typo-prone source of drift between your code and the schema.
318
+
319
+ ##### `$build(data)` - Build with Defaults
320
+
321
+ Builds data by adding the `$type` property and properly types the result. This also allows to declare a variable with the correct type without having to explicitly specify it.
322
+
323
+ ```typescript
324
+ import { l } from '@atproto/lex'
325
+ import * as app from './lexicons/app.js'
326
+
327
+ // The type of the "like" variable will be "app.bsky.feed.like.Main" (no need to explicitly specify the type)
328
+ const like = app.bsky.feed.like.$build({
329
+ subject: {
330
+ uri: 'at://did:plc:abc/app.bsky.feed.post/123',
331
+ cid: 'bafyrei...',
332
+ },
333
+ createdAt: l.currentDatetimeString(),
334
+ })
335
+ ```
336
+
337
+ > [!NOTE]
338
+ >
339
+ > `$build()` does not perform validation, and expects properly typed input data - use `$parse()` if you need validation.
340
+
341
+ ##### `$isTypeOf(data)` - Type Discriminator
342
+
343
+ Discriminates (pre-validated) data based on its `$type` property, without re-validating. This is especially useful when working with union types:
344
+
345
+ ```typescript
346
+ import { l } from '@atproto/lex'
347
+ import * as app from './lexicons/app.js'
348
+
349
+ declare const data:
350
+ | app.bsky.feed.post.Main
351
+ | app.bsky.feed.like.Main
352
+ | l.Unknown$TypedObject
353
+
354
+ // Discriminate by $type without re-validating
355
+ if (app.bsky.feed.post.$isTypeOf(data)) {
356
+ // data is a post
357
+ }
358
+ ```
359
+
360
+ #### Universal validation helpers
361
+
362
+ ##### `$matches(data)` - Type Guard
268
363
 
269
364
  Returns `true` if data matches the schema, `false` otherwise. Acts as a TypeScript type guard:
270
365
 
@@ -272,19 +367,45 @@ Returns `true` if data matches the schema, `false` otherwise. Acts as a TypeScri
272
367
  import { l } from '@atproto/lex'
273
368
  import * as app from './lexicons/app.js'
274
369
 
275
- const data = {
370
+ const data: unknown = {
276
371
  $type: 'app.bsky.feed.post',
277
372
  text: 'Hello!',
278
373
  createdAt: l.currentDatetimeString(),
279
374
  }
280
375
 
281
- if (app.bsky.feed.post.$check(data)) {
376
+ if (app.bsky.feed.post.$matches(data)) {
282
377
  // TypeScript knows data is a Post here
283
378
  console.log(data.text)
284
379
  }
285
380
  ```
286
381
 
287
- #### `$parse(data)` - Parse and Validate
382
+ > [!NOTE]
383
+ >
384
+ > Performs validation so [`$isTypeOf`](#istypeofdata---type-discriminator) is preferred for pre-validated & properly typed data.
385
+
386
+ ##### `$assert(data)` - Type-Narrowing Assertion
387
+
388
+ Throws if `data` does not match the schema. When the schema is statically known (e.g. `app.bsky.feed.post`), TypeScript narrows the type of `data` after the call:
389
+
390
+ ```typescript
391
+ import { l } from '@atproto/lex'
392
+ import * as app from './lexicons/app.js'
393
+
394
+ const data: unknown = {
395
+ $type: 'app.bsky.feed.post',
396
+ text: 'Hello!',
397
+ createdAt: l.currentDatetimeString(),
398
+ }
399
+
400
+ app.bsky.feed.post.$assert(data)
401
+
402
+ // TypeScript now knows data is app.bsky.feed.post.Main
403
+ console.log(data.text)
404
+ ```
405
+
406
+ For library code that operates on a schema parameter whose type cannot be fully expressed, see [Validating Generic Schemas with `$check`](#validating-generic-schemas-with-check).
407
+
408
+ ##### `$parse(data)` - Parse and Validate
288
409
 
289
410
  Validates and returns typed data, throwing an error if validation fails:
290
411
 
@@ -293,11 +414,12 @@ import { l } from '@atproto/lex'
293
414
  import * as app from './lexicons/app.js'
294
415
 
295
416
  try {
296
- const post = app.bsky.feed.post.$main.$parse({
417
+ const post = app.bsky.feed.post.$parse({
297
418
  $type: 'app.bsky.feed.post',
298
419
  text: 'Hello!',
299
420
  createdAt: l.currentDatetimeString(),
300
421
  })
422
+
301
423
  // post is now typed and validated
302
424
  console.log(post.text)
303
425
  } catch (error) {
@@ -309,7 +431,7 @@ try {
309
431
  >
310
432
  > The `$parse` method will apply defaults defined in the schema for optional fields, as well as data coercion (e.g., CID strings to Cid types). This means that the returned value might be different from the input data if defaults were applied. Use `$validate()` for value validation.
311
433
 
312
- #### `$validate(data)` - Validate a value against the schema
434
+ ##### `$validate(data)` - Validate a value against the schema
313
435
 
314
436
  Validates an existing value against a schema, returning the value itself if, and only if, it already matches the schema (ie. without applying defaults or coercion).
315
437
 
@@ -329,7 +451,7 @@ const result = app.bsky.feed.post.$validate(value)
329
451
  value === result // true
330
452
  ```
331
453
 
332
- #### `$safeParse(data, options?)` - Parse a value against a schema and get the resulting value
454
+ ##### `$safeParse(data, options?)` - Parse a value against a schema and get the resulting value
333
455
 
334
456
  Returns a detailed validation result object without throwing:
335
457
 
@@ -360,43 +482,6 @@ app.bsky.feed.post.$safeParse(data) // { strict: true } is the default
360
482
  app.bsky.feed.post.$safeParse(data, { strict: false })
361
483
  ```
362
484
 
363
- #### `$build(data)` - Build with Defaults
364
-
365
- Builds data by adding the `$type` property and properly types the result. Note that `$build()` does not perform validation - use `$parse()` if you need validation:
366
-
367
- ```typescript
368
- import { l } from '@atproto/lex'
369
- import * as app from './lexicons/app.js'
370
-
371
- // The type of the "like" variable will be "app.bsky.feed.like.Main"
372
- const like = app.bsky.feed.like.$build({
373
- subject: {
374
- uri: 'at://did:plc:abc/app.bsky.feed.post/123',
375
- cid: 'bafyrei...',
376
- },
377
- createdAt: l.currentDatetimeString(),
378
- })
379
- ```
380
-
381
- #### `$isTypeOf(data)` - Type Discriminator
382
-
383
- Discriminates (pre-validated) data based on its `$type` property, without re-validating. This is especially useful when working with union types:
384
-
385
- ```typescript
386
- import { l } from '@atproto/lex'
387
- import * as app from './lexicons/app.js'
388
-
389
- declare const data:
390
- | app.bsky.feed.post.Main
391
- | app.bsky.feed.like.Main
392
- | l.Unknown$TypedObject
393
-
394
- // Discriminate by $type without re-validating
395
- if (app.bsky.feed.post.$isTypeOf(data)) {
396
- // data is a post
397
- }
398
- ```
399
-
400
485
  ## Data Model
401
486
 
402
487
  The AT Protocol uses a [data model](https://atproto.com/specs/data-model) that extends JSON with two additional data structures: **CIDs** (content-addressed links) and **bytes** (for raw data). This data model can be encoded either as JSON for XRPC (HTTP API) or as [CBOR](https://dasl.ing/drisl.html) for storage and authentication (see [`@atproto/lex-cbor`](../lex-cbor)).
@@ -665,7 +750,7 @@ console.log(result.cid)
665
750
  Options:
666
751
 
667
752
  - `rkey` - Custom record key (auto-generated if not provided)
668
- - `validate` - Asks the PDS to validate the record against schema when processing the request
753
+ - `validate` - Tri-state instruction to the PDS. `true` forces server-side schema validation, `false` explicitly disables it, and `undefined` (default) lets the PDS decide (it validates only collections whose schemas it knows)
669
754
  - `validateRequest` - Validate the record locally against schema before submitting the request
670
755
  - `swapCommit` - CID for optimistic concurrency control
671
756
 
@@ -707,8 +792,8 @@ await client.put(app.bsky.actor.profile, {
707
792
  Options:
708
793
 
709
794
  - `rkey` - Record key (required for non-literal keys)
710
- - `validate` - Validate record against schema before updating (falls back to `validateRequest` option if not specified)
711
- - `validateRequest` - Alternative way to enable validation (used if `validate` is not specified)
795
+ - `validate` - Tri-state instruction to the PDS. `true` forces server-side schema validation, `false` explicitly disables it, and `undefined` (default) lets the PDS decide (it validates only collections whose schemas it knows)
796
+ - `validateRequest` - Validate the record locally against schema before submitting the request
712
797
  - `swapCommit` - Expected repo commit CID
713
798
  - `swapRecord` - Expected record CID
714
799
 
@@ -740,6 +825,11 @@ for (const record of result.records) {
740
825
  console.log(record.uri, record.value.text)
741
826
  }
742
827
 
828
+ // Records that failed local schema validation are returned separately
829
+ for (const invalid of result.invalid) {
830
+ console.warn('Invalid record:', invalid)
831
+ }
832
+
743
833
  // Pagination
744
834
  if (result.cursor) {
745
835
  const nextPage = await client.list(app.bsky.feed.post, {
@@ -749,6 +839,12 @@ if (result.cursor) {
749
839
  }
750
840
  ```
751
841
 
842
+ The result includes:
843
+
844
+ - `records` - Records that successfully validated against the schema
845
+ - `invalid` - Records returned by the server that failed local schema validation (raw `LexMap` values)
846
+ - `cursor` - Pagination cursor (if more results are available)
847
+
752
848
  #### `client.applyWrites()`
753
849
 
754
850
  Perform an atomic batch of create, update, and delete operations in a single request.
@@ -785,7 +881,7 @@ for (const result of response.body.results) {
785
881
  Options:
786
882
 
787
883
  - `repo` - Repository identifier (defaults to authenticated user's DID)
788
- - `validate` - Asks the PDS to validate records against schema
884
+ - `validate` - Tri-state instruction to the PDS. `true` forces server-side schema validation, `false` explicitly disables it, and `undefined` (default) lets the PDS decide (it validates only collections whose schemas it knows)
789
885
  - `swapCommit` - CID for optimistic concurrency control
790
886
 
791
887
  > [!NOTE]
@@ -824,13 +920,23 @@ if (result.success) {
824
920
  // Handle success
825
921
  console.log(result.body)
826
922
  } else {
827
- // Handle failure - result is an XrpcFailure
923
+ // Handle failure - result is an XrpcFailure.
924
+ //
925
+ // All XrpcFailure subclasses inherit from XrpcError and share these members:
926
+ result.error // string error code (e.g. "HandleNotFound", "UpstreamFailure")
927
+ result.message // string
928
+ result.shouldRetry() // boolean - whether the error is transient
929
+
930
+ if (result.matchesSchemaErrors()) {
931
+ // Check if the error matches a declared error in the schema.
932
+ // TypeScript narrows `result.error` to one of the method's declared error codes.
933
+ result.error // "HandleNotFound"
934
+ }
935
+
936
+ // Branch on the specific error class to access additional members:
828
937
  if (result instanceof XrpcResponseError) {
829
938
  // The server responded with an error status code (4xx or 5xx).
830
939
  // This is used for all error responses, whether or not they have a valid XRPC error payload.
831
-
832
- result.error // string (e.g. "HandleNotFound", "AuthenticationRequired", "UpstreamFailure", etc.)
833
- result.message // string
834
940
  result.response.status // number
835
941
  result.response.headers // Headers
836
942
  result.payload // undefined | { body: unknown; encoding: string }
@@ -840,25 +946,11 @@ if (result.success) {
840
946
  } else if (result instanceof XrpcInvalidResponseError) {
841
947
  // The response was truly invalid (3xx redirect, malformed JSON, schema mismatch, etc.).
842
948
  // This is a more specific error for responses that are not processable.
843
-
844
- result.error // "UpstreamFailure"
845
- result.message // string
846
949
  result.response.status // number
847
950
  result.response.headers // Headers
848
951
  result.payload // undefined | { body: unknown; encoding: string }
849
952
  } else if (result instanceof XrpcInternalError) {
850
953
  // Something went wrong on the client side (network error, etc.)
851
- result.error // "InternalServerError"
852
- result.message // string
853
- }
854
-
855
- // All XrpcFailure types have these properties:
856
- result.shouldRetry() // boolean - whether the error is transient
857
-
858
- if (result.matchesSchemaErrors()) {
859
- // Check if the error matches a declared error in the schema.
860
- // TypeScript knows this is a declared error for the method.
861
- result.error // "HandleNotFound"
862
954
  }
863
955
  }
864
956
  ```
@@ -996,9 +1088,9 @@ import {
996
1088
  isLanguageString, // Validate language tags (e.g., 'en', 'pt-BR')
997
1089
 
998
1090
  // Low-level JSON encoding helpers
999
- parseLexLink, // { $link: string } → Cid
1091
+ parseLexLink, // { $link: string } → Cid | undefined
1000
1092
  encodeLexLink, // Cid → { $link: string }
1001
- parseLexBytes, // { $bytes: string } → Uint8Array
1093
+ parseLexBytes, // { $bytes: string } → Uint8Array | undefined
1002
1094
  encodeLexBytes, // Uint8Array → { $bytes: string }
1003
1095
  } from '@atproto/lex'
1004
1096
 
@@ -1064,24 +1156,104 @@ This ensures that:
1064
1156
 
1065
1157
  ### Tree-Shaking
1066
1158
 
1067
- The generated TypeScript is optimized for tree-shaking. Import only what you need:
1159
+ The generated TypeScript code is structured to be tree-shakeable, but the way you reference schemas has a meaningful impact on the final bundle size. There are several ways to refer to a generated schema, and each comes with different trade-offs.
1160
+
1161
+ #### Namespace notation
1162
+
1163
+ The most ergonomic style is to use a namespace import and reference schemas through dotted paths:
1068
1164
 
1069
1165
  ```typescript
1070
- // Import specific methods
1071
- import { post } from './lexicons/app/bsky/feed/post.js'
1072
- import { getProfile } from './lexicons/app/bsky/actor/getProfile.js'
1166
+ import * as com from './lexicons/com.js'
1073
1167
 
1074
- // Or use namespace imports (still tree-shakeable)
1075
- import * as app from './lexicons/app.js'
1168
+ await client.call(com.atproto.repo.getRecord, {
1169
+ /* ... */
1170
+ })
1076
1171
  ```
1077
1172
 
1078
- For library authors, use `--pure-annotations` when building:
1173
+ This style is convenient and reads naturally as it mirrors the NSID of the schema. However, it produces the largest bundles. From the bundler's point of view, `com.atproto.repo.getRecord` is the whole schema namespace (which contains the `main` schema as well as helpers, and any other definitions). The bundler cannot know that `client.call()` only consumes the `main` schema, so it has to keep the rest of the namespace alive in the bundle.
1174
+
1175
+ #### Explicit `.main` reference
1176
+
1177
+ You can mitigate the bundle-size cost by explicitly naming the `main` definition:
1178
+
1179
+ ```typescript
1180
+ import * as com from './lexicons/com.js'
1181
+
1182
+ await client.call(com.atproto.repo.getRecord.main, {
1183
+ /* ... */
1184
+ })
1185
+ ```
1186
+
1187
+ This lets the bundler drop the sibling definitions inside `getRecord` that aren't referenced. The drawback is that it leaks an implementation detail: the `main` segment of the path. In Lexicon, `main` is typically implicit:
1188
+
1189
+ - Records use a `$type` of `app.bsky.feed.post` (no `#main`)
1190
+ - XRPC endpoints are exposed as `/xrpc/com.atproto.repo.getRecord` (no `main`)
1191
+
1192
+ So writing `.main` in application code feels verbose compared to how Lexicons are normally referred to.
1193
+
1194
+ #### Direct named import from the schema file
1195
+
1196
+ You can also import the `main` schema directly from the file that defines it:
1197
+
1198
+ ```typescript
1199
+ import { main as getRecord } from './lexicons/com/atproto/repo/getRecord.js'
1200
+
1201
+ await client.call(getRecord, {
1202
+ /* ... */
1203
+ })
1204
+ ```
1205
+
1206
+ This produces equally small bundles as the explicit `.main` reference, but it still surfaces the `main` identifier: you have to know to import `main` and likely rename it.
1207
+
1208
+ #### Default import (recommended)
1209
+
1210
+ To make the small-bundle path also the ergonomic path, every namespace file generated by `lex build` re-exports the `main` schema as its `default` export:
1211
+
1212
+ ```typescript
1213
+ // generated file: ./lexicons/com/atproto/repo/getRecord.js
1214
+ export * from './getRecord.defs.js'
1215
+ export { main as default } from './getRecord.defs.js'
1216
+ ```
1217
+
1218
+ This means you can write:
1219
+
1220
+ ```typescript
1221
+ import getRecord from './lexicons/com/atproto/repo/getRecord.js'
1222
+ import post from './lexicons/app/bsky/feed/post.js'
1223
+
1224
+ await client.call(getRecord, {
1225
+ /* ... */
1226
+ })
1227
+ await client.create(post, {
1228
+ /* ... */
1229
+ })
1230
+ ```
1231
+
1232
+ This is the most bundle-friendly style: the bundler only pulls in the `main` schema, and the import name doesn't have to mention `main` at all. This helps keeping application code aligned with how Lexicons are usually identified.
1233
+
1234
+ #### Drawbacks of the default export
1235
+
1236
+ The `default` re-export is enabled by default but has two minor drawbacks:
1237
+
1238
+ 1. It is one additional property on the namespace module, which can very slightly increase bundle size if you also use the namespace in some places.
1239
+ 2. Any Lexicon document whose path segment is literally `default` (for example a hypothetical `com.example.records.default`) would conflict with the generated `default` export.
1240
+
1241
+ If either of these matters for your use case, you can disable the generation of `default` exports with the `--no-defaultExport` flag:
1079
1242
 
1080
1243
  ```bash
1081
- lex build --pure-annotations
1244
+ lex build --no-defaultExport
1082
1245
  ```
1083
1246
 
1084
- This will make the generated code more easily tree-shakeable from places that import your library.
1247
+ #### Summary
1248
+
1249
+ | Style | Bundle size | Ergonomics |
1250
+ | ------------------------------------------------------ | ----------- | ---------------------------- |
1251
+ | `com.atproto.repo.getRecord` (namespace) | Largest | Best: matches the NSID |
1252
+ | `com.atproto.repo.getRecord.main` | Small | Leaks the `main` identifier |
1253
+ | `import { main as getRecord } from '.../getRecord.js'` | Small | Verbose, leaks `main` |
1254
+ | `import getRecord from '.../getRecord.js'` | Small | Concise, no `main` in source |
1255
+
1256
+ For libraries and applications where bundle size matters (typically anything shipped to a browser), prefer the default-import style. For scripts, tests, and server-side code where the bundle size of generated schemas is not a concern, the namespace style is perfectly fine.
1085
1257
 
1086
1258
  ### Blob references
1087
1259
 
@@ -1188,7 +1360,7 @@ An `Action` is a function with this signature:
1188
1360
  type Action<Input, Output> = (
1189
1361
  client: Client,
1190
1362
  input: Input,
1191
- options: CallOptions,
1363
+ options: ActionOptions,
1192
1364
  ) => Output | Promise<Output>
1193
1365
  ```
1194
1366
 
@@ -1196,7 +1368,7 @@ Actions receive:
1196
1368
 
1197
1369
  - `client` - The Client instance (to make XRPC calls)
1198
1370
  - `input` - The input data for the action
1199
- - `options` - Call options (signal)
1371
+ - `options` - `ActionOptions` (currently just `{ signal?: AbortSignal }`)
1200
1372
 
1201
1373
  #### Using Actions
1202
1374
 
@@ -1511,10 +1683,10 @@ export const updateProfile: Action<ProfileUpdate, void> = async (
1511
1683
  },
1512
1684
  })
1513
1685
 
1514
- const current = app.bsky.actor.profile.main.validate(res.body.record)
1686
+ const current = app.bsky.actor.profile.$safeValidate(res.body.record)
1515
1687
 
1516
1688
  // Merge updates with current profile (if valid)
1517
- const updated = app.bsky.actor.profile.main.build({
1689
+ const updated = app.bsky.actor.profile.$build({
1518
1690
  ...(current.success ? current.value : undefined),
1519
1691
  ...updates,
1520
1692
  })
@@ -1619,6 +1791,26 @@ if ('value' in result) {
1619
1791
 
1620
1792
  When validated through the Standard Schema interface, schemas operate in "parse" mode, meaning transformations like defaults and coercions are applied to the output.
1621
1793
 
1794
+ ### Validating Generic Schemas with `$check`
1795
+
1796
+ `$check(data)` is the non-narrowing counterpart to [`$assert(data)`](#assertdata---type-narrowing-assertion): both throw when `data` does not match the schema, but `$check` does not refine the static type of its argument.
1797
+
1798
+ `$check` is rarely needed in application code — prefer `$assert`. It is intended for library-style code that takes a schema as a generic parameter, where TypeScript cannot satisfy the assertion-signature requirement and `$assert` produces the following error:
1799
+
1800
+ > 'schema' needs an explicit type annotation.
1801
+ > Assertions require every name in the call target to be declared with an explicit type annotation. `ts(2775)`
1802
+
1803
+ In that situation, switch to `$check`:
1804
+
1805
+ ```typescript
1806
+ import type { Schema } from '@atproto/lex'
1807
+
1808
+ function ensureMatches<S extends Schema>(schema: S, data: unknown) {
1809
+ // schema.$assert(data) // ❌ ts(2775): needs an explicit type annotation
1810
+ schema.$check(data) // ✅ throws on invalid, no type narrowing
1811
+ }
1812
+ ```
1813
+
1622
1814
  ## License
1623
1815
 
1624
1816
  MIT or Apache2
package/bin/lex CHANGED
@@ -51,12 +51,6 @@ yargs(hideBin(process.argv))
51
51
  default: false,
52
52
  describe: 'how to handle errors when processing input files',
53
53
  },
54
- 'pure-annotations': {
55
- type: 'boolean',
56
- default: false,
57
- describe:
58
- 'adds `/*#__PURE__*/` annotations for tree-shaking tools. Set this to true if you are using generated lexicons in a library.',
59
- },
60
54
  exclude: {
61
55
  array: true,
62
56
  type: 'string',
@@ -75,25 +69,37 @@ yargs(hideBin(process.argv))
75
69
  describe:
76
70
  'package name of the library to import the lex schema utility "l" from',
77
71
  },
78
- importExt: {
72
+ 'import-ext': {
79
73
  type: 'string',
80
74
  default: '.js',
81
75
  describe:
82
76
  'file extension to use for import statements in generated files (e.g. ".ts", ".mts", ".cts"). Use --import-ext "" to generate extension-less imports.',
83
77
  },
84
- fileExt: {
78
+ 'file-ext': {
85
79
  type: 'string',
86
80
  default: '.ts',
87
81
  describe:
88
82
  'file extension to use for generated files (e.g. ".ts", ".mts", ".cts")',
89
83
  },
90
- indexFile: {
84
+ 'index-file': {
91
85
  type: 'boolean',
92
86
  default: false,
93
87
  describe:
94
88
  'generate an "index.<fileExt>" file that exports all root-level namespaces',
95
89
  },
96
- ignoreInvalidLexicons: {
90
+ 'defs-export': {
91
+ type: 'boolean',
92
+ default: false,
93
+ describe:
94
+ 'when some definitions conflict with child namespaces, this option allows to export lexicon definitions under a separate $defs namespace (e.g. com.example.foo.$defs)',
95
+ },
96
+ 'default-export': {
97
+ type: 'boolean',
98
+ default: true,
99
+ describe:
100
+ 'whether to generate a default export for the "main" lexicon definition schema in the parent namespace file',
101
+ },
102
+ 'ignore-invalid-lexicons': {
97
103
  type: 'boolean',
98
104
  default: false,
99
105
  describe:
@@ -153,4 +159,6 @@ yargs(hideBin(process.argv))
153
159
  })
154
160
  },
155
161
  )
162
+ .strictCommands()
163
+ .demandCommand(1)
156
164
  .parseAsync()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lex",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -36,13 +36,13 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "tslib": "^2.8.1",
39
- "yargs": "^17.0.0",
40
- "@atproto/lex-builder": "^0.1.1",
41
- "@atproto/lex-client": "^0.1.1",
39
+ "yargs": "^18.0.0",
40
+ "@atproto/lex-builder": "^0.1.2",
41
+ "@atproto/lex-client": "^0.1.3",
42
42
  "@atproto/lex-data": "^0.1.1",
43
43
  "@atproto/lex-json": "^0.1.0",
44
44
  "@atproto/lex-installer": "^0.1.0",
45
- "@atproto/lex-schema": "^0.1.1"
45
+ "@atproto/lex-schema": "^0.1.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/yargs": "^17.0.33",