finish phase 4: drop @atproto/api dependency, lex-only errors, delete toLex and legacy validators

oxlint-suppressions.json gains a baseline entry for webpack.config.js: the
file was never previously staged/linted (lint script covers src+modules
only), and editing it exposed its pre-existing violations to lint-staged.
All suppressed counts are pre-existing; this change only removes a line.
This commit is contained in:
Samuel Newman
2026-07-17 13:28:29 +03:00
parent ca5b8b6346
commit 8ee62d67bd
20 changed files with 80 additions and 265 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import {RichText} from '@atproto/api'
import {RichText} from '@bsky.app/sdk/richtext'
import {i18n} from '@lingui/core'
import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
+11
View File
@@ -1813,5 +1813,16 @@
"typescript/no-unsafe-member-access": {
"count": 3
}
},
"webpack.config.js": {
"import/no-nodejs-modules": {
"count": 1
},
"typescript/no-unsafe-call": {
"count": 11
},
"typescript/no-unsafe-member-access": {
"count": 30
}
}
}
-1
View File
@@ -98,7 +98,6 @@
"prettier": "prettier --check ."
},
"dependencies": {
"@atproto/api": "0.20.34",
"@atproto/common-web": "0.5.6",
"@atproto/lex": "0.2.2",
"@atproto/lex-client": "0.2.4",
+1 -1
View File
@@ -7,7 +7,7 @@
* label simply drops the `https://` scheme for readability.
*
* Kept as a dependency-free leaf module (no #/lib/strings/url-helpers import)
* so its unit tests stay fast and isolated from the heavy @atproto/api graph.
* so its unit tests stay fast and isolated from the heavy SDK graph.
*/
function stripLeadingAt(handle: string): string {
+11 -24
View File
@@ -7,22 +7,20 @@
jest.unmock('multiformats/cid')
jest.unmock('multiformats/hashes/hasher')
import {BlobRef} from '@atproto/api'
import {CID} from 'multiformats/cid'
import {computeCid} from '#/lib/api/computeCid'
import {type app, type com} from '#/lexicons'
/*
* Golden-CID regression test for the composer post pipeline (design section F).
* Golden-CID regression test for the composer post pipeline.
*
* `computeCid` hashes a post record in the client so a thread's later posts can
* reference earlier posts by CID before the server assigns them. The hash is
* byte-sensitive: any drift in how records (especially blobs) are serialized to
* DAG-CBOR silently produces the wrong CID and breaks reply chains with NO type
* error. These golden values were captured from the PRE-migration `computeCid`
* (the `instanceof BlobRef` path) and MUST remain byte-identical after the guard
* is changed to the structural `isBlobRef` shape check.
* error. These golden values MUST remain byte-identical - they gate the
* structural `isBlobRef` shape check.
*
* The blob CID below is a fixed, deterministic CIDv1/raw/sha256 used purely as a
* stable fixture - it is not derived from any real upload.
@@ -30,10 +28,7 @@ import {type app, type com} from '#/lexicons'
const BLOB_CID = 'bafkreieq5jui4j25lacwomsqgjeswwl3y5zcdrresptwgmfylxo2depppq'
/**
* Build a post record with an image embed whose blob is the given value. Used to
* prove that a `@atproto/api` `BlobRef` class instance (the shape the not-yet
* -migrated video path still yields) and a plain-JSON lex blob (the shape lex
* `uploadBlob` returns) hash to the SAME CID.
* Build a post record with an image embed whose blob is the given value.
*/
function postWithImageBlob(blob: unknown): app.bsky.feed.post.Main {
return {
@@ -65,22 +60,14 @@ describe('computeCid', () => {
)
})
it('case 2: record whose embed carries a BlobRef class instance', async () => {
const blob = new BlobRef(CID.parse(BLOB_CID), 'image/jpeg', 12345)
expect(await computeCid(postWithImageBlob(blob))).toBe(
'bafyreiem7g6vja66nebr7he4fshfnlyndyldbvle2n265oixscmepjcbii',
)
})
it('case 2b: a plain-JSON blob object hashes identically to the class instance', async () => {
it('case 2: record whose embed carries a plain-JSON lex blob', async () => {
/*
* This is the post-migration shape: lex `uploadBlob` returns a plain object
* `{$type: 'blob', ref, mimeType, size}` (with `ref` a parsed CID), not a
* `BlobRef` class instance. The structural `isBlobRef` guard must treat it
* exactly like the class instance so the CID is unchanged. Under the
* pre-change `instanceof` code this case already matches because the plain
* object walks through `prepareForHashing` unchanged and DAG-CBOR encodes
* its CID `ref` the same way `.ipld()` does.
* The blob shape lex `uploadBlob` returns: a plain object
* `{$type: 'blob', ref, mimeType, size}` with `ref` a parsed CID. The
* structural `isBlobRef` guard passes it through `prepareForHashing`
* untouched and DAG-CBOR encodes its CID `ref` as a CID link. The golden
* CID below is the byte-identical value the pre-migration `BlobRef` class
* instance produced via `.ipld()`.
*/
const blob = {
$type: 'blob' as const,
+3 -32
View File
@@ -46,10 +46,9 @@ export async function computeCid(
}
/**
* True for a plain-JSON lexicon blob, the shape lex `uploadBlob` now returns
* (`{$type: 'blob', ref, mimeType, size}` with `ref` a parsed CID). Replaces
* the old `instanceof BlobRef` check, since lex blobs are plain objects, not
* class instances (design section F).
* True for a plain-JSON lexicon blob, the shape lex `uploadBlob` returns
* (`{$type: 'blob', ref, mimeType, size}` with `ref` a parsed CID). Lex blobs
* are plain objects, not class instances.
*/
function isBlobRef(v: unknown): boolean {
if (v == null || typeof v !== 'object') return false
@@ -57,24 +56,6 @@ function isBlobRef(v: unknown): boolean {
return o.$type === 'blob' && 'ref' in o && 'mimeType' in o
}
/**
* True for a legacy `@atproto/api` `BlobRef` class instance. During the
* migration the video embed path still yields these (its blob comes from the
* not-yet-migrated `app.bsky.video.getJobStatus` bridge call), so we must keep
* handling them here even though the composer's own uploads are now plain lex
* blobs. A class instance is duck-typed by its `ipld()` method plus the
* `ref`/`mimeType` fields; it has NO `$type` and a non-plain prototype, so it
* would otherwise slip past both `isBlobRef` and `isPlainObject` and be encoded
* wrong - silently breaking video reply-chain CIDs.
*/
function isBlobRefInstance(
v: unknown,
): v is {ipld: () => unknown; ref: unknown; mimeType: unknown} {
if (v == null || typeof v !== 'object') return false
const o = v as Record<string, unknown>
return typeof o.ipld === 'function' && 'ref' in o && 'mimeType' in o
}
// Returns a transformed version of the object for use in DAG-CBOR.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function prepareForHashing(v: any): any {
@@ -87,16 +68,6 @@ function prepareForHashing(v: any): any {
return v
}
/*
* A legacy `BlobRef` class instance must be converted via `ipld()` to the
* plain `{$type, ref, mimeType, size}` object; encoding the instance directly
* would emit its internal `original` field and omit `$type`, producing the
* wrong CID. `ipld()` returns exactly what `isBlobRef` accepts above.
*/
if (isBlobRefInstance(v)) {
return v.ipld()
}
// Walk through arrays
if (Array.isArray(v)) {
let pure = true
+3 -4
View File
@@ -402,10 +402,9 @@ async function resolveMedia(
return {
$type: 'app.bsky.embed.video',
/*
* The video blob is a legacy `@atproto/api` BlobRef from the not-yet
* -migrated video pipeline (getJobStatus, in composer state/video). Its
* structural shape matches the lexicon blob field; the CID hasher handles
* both class instances and plain lex blobs (see computeCid).
* The video blob is a plain lex blob from the video pipeline
* (getJobStatus, in composer state/video). Its structural shape matches
* the lexicon blob field and the CID hasher (see computeCid).
*/
video: videoDraft.pendingPublish.blobRef,
alt: videoDraft.altText || undefined,
+7 -35
View File
@@ -1,4 +1,3 @@
import {XRPCError} from '@atproto/api'
import {
type Procedure,
type Query,
@@ -15,21 +14,9 @@ import {
import {isErrorMaybeAppPasswordPermissions, isNetworkError} from '../errors'
/**
* Old-world fixture: `@atproto/api` XRPCError. `.headers` is a plain record,
* `.error` is the lexicon code string, `.status` a numeric ResponseType enum.
*/
function oldError(
status: number,
error: string,
headers?: Record<string, string>,
) {
return new XRPCError(status, error, undefined, headers)
}
/**
* New-world fixture: lex `XrpcResponseError`. Built from a WHATWG `Response`
* (so `.headers` is a `Headers` object and `.status` is the numeric HTTP
* status) plus a JSON error payload carrying `.error`.
* Lex `XrpcResponseError` fixture. Built from a WHATWG `Response` (so
* `.headers` is a `Headers` object and `.status` is the numeric HTTP status)
* plus a JSON error payload carrying `.error`.
*/
function lexError(
status: number,
@@ -45,8 +32,7 @@ function lexError(
}
describe('isXrpcError', () => {
it('matches both old XRPCError and lex XrpcResponseError', () => {
expect(isXrpcError(oldError(400, 'TokenInvalid'))).toBe(true)
it('matches a lex XrpcResponseError', () => {
expect(isXrpcError(lexError(400, 'TokenInvalid'))).toBe(true)
})
@@ -58,8 +44,7 @@ describe('isXrpcError', () => {
})
describe('getErrorStatus', () => {
it('reads status from both worlds', () => {
expect(typeof getErrorStatus(oldError(400, 'TokenInvalid'))).toBe('number')
it('reads status from a lex XrpcResponseError', () => {
expect(getErrorStatus(lexError(429, 'RateLimitExceeded'))).toBe(429)
})
@@ -69,8 +54,7 @@ describe('getErrorStatus', () => {
})
describe('getErrorName', () => {
it('reads the lexicon error code from both worlds', () => {
expect(getErrorName(oldError(400, 'TokenInvalid'))).toBe('TokenInvalid')
it('reads the lexicon error code', () => {
expect(getErrorName(lexError(400, 'TokenInvalid'))).toBe('TokenInvalid')
})
@@ -80,12 +64,6 @@ describe('getErrorName', () => {
})
describe('getErrorHeader', () => {
it('reads a header from the old record shape (case-insensitive)', () => {
const e = oldError(429, 'RateLimitExceeded', {'ratelimit-reset': '123'})
expect(getErrorHeader(e, 'ratelimit-reset')).toBe('123')
expect(getErrorHeader(e, 'RateLimit-Reset')).toBe('123')
})
it('reads a header from the lex Headers object', () => {
const e = lexError(429, 'RateLimitExceeded', {'ratelimit-reset': '123'})
expect(getErrorHeader(e, 'ratelimit-reset')).toBe('123')
@@ -99,10 +77,7 @@ describe('getErrorHeader', () => {
})
describe('isErrorMaybeAppPasswordPermissions', () => {
it('matches a TokenInvalid error from both worlds', () => {
expect(
isErrorMaybeAppPasswordPermissions(oldError(400, 'TokenInvalid')),
).toBe(true)
it('matches a TokenInvalid error', () => {
expect(
isErrorMaybeAppPasswordPermissions(lexError(400, 'TokenInvalid')),
).toBe(true)
@@ -118,9 +93,6 @@ describe('isErrorMaybeAppPasswordPermissions', () => {
})
it('does not match unrelated XRPC errors', () => {
expect(
isErrorMaybeAppPasswordPermissions(oldError(400, 'InvalidRequest')),
).toBe(false)
expect(
isErrorMaybeAppPasswordPermissions(lexError(400, 'InvalidRequest')),
).toBe(false)
+2 -4
View File
@@ -1,6 +1,6 @@
import {t} from '@lingui/core/macro'
import {getErrorName, getErrorStatus, isXrpcError} from '#/lib/xrpc-error'
import {getErrorName, isXrpcError} from '#/lib/xrpc-error'
export function cleanError(e: unknown): string {
if (!e) {
@@ -87,13 +87,11 @@ export function isCancelledError(e: unknown) {
return str.includes('cancel')
}
// TODO Replace this with error.shouldRetry() when available. -dsb
const RETRYABLE_ERRORS = [408, 425, 429, 500, 502, 503, 504, 522, 524]
export function isRetryableHttpStatus(status: number) {
return RETRYABLE_ERRORS.includes(status)
}
export function shouldRetryError(e: unknown) {
const status = getErrorStatus(e)
return status !== undefined && isRetryableHttpStatus(status)
return isXrpcError(e) && e.shouldRetry()
}
+14 -24
View File
@@ -1,44 +1,34 @@
import {XRPCError} from '@atproto/api'
import {XrpcError, XrpcResponseError} from '@atproto/lex-client'
/**
* True for an XRPC error from either the old bridge agent (`@atproto/api`
* `XRPCError`) or a lex `Client` (`@atproto/lex-client` `XrpcError`, the
* abstract base of `XrpcResponseError`/`XrpcInvalidResponseError`/
* `XrpcInternalError`). During the migration both worlds can throw, so matchers
* must accept both.
* True for an XRPC error from a lex `Client` (`@atproto/lex-client` `XrpcError`,
* the abstract base of `XrpcResponseError`/`XrpcInvalidResponseError`/
* `XrpcInternalError`).
*/
export function isXrpcError(e: unknown): e is XRPCError | XrpcError {
return e instanceof XRPCError || e instanceof XrpcError
export function isXrpcError(e: unknown): e is XrpcError {
return e instanceof XrpcError
}
/**
* HTTP status, or undefined if not an XRPC error / no response. Only lex
* HTTP status, or undefined if not an XRPC error / no response. Only
* `XrpcResponseError` (a server response) carries a status; the internal/fetch
* lex errors do not.
*/
export function getErrorStatus(e: unknown): number | undefined {
if (e instanceof XRPCError) return e.status
if (e instanceof XrpcResponseError) return e.status
return undefined
return e instanceof XrpcResponseError ? e.status : undefined
}
/** The lexicon error code (`err.error`), from either world. */
/** The lexicon error code (`err.error`). */
export function getErrorName(e: unknown): string | undefined {
if (isXrpcError(e)) return (e as {error?: string}).error
return undefined
return isXrpcError(e) ? (e as {error?: string}).error : undefined
}
/**
* Read a response header off an XRPC error, normalizing the shape change:
* old XRPCError.headers is a plain record; lex XrpcResponseError.headers is a
* WHATWG Headers object.
* Read a response header off an XRPC error. Only `XrpcResponseError` carries a
* response; its `.headers` is a WHATWG `Headers` object.
*/
export function getErrorHeader(e: unknown, name: string): string | undefined {
if (e instanceof XrpcResponseError) return e.headers.get(name) ?? undefined
if (e instanceof XRPCError) {
const h = (e as {headers?: Record<string, string>}).headers
return h?.[name.toLowerCase()]
}
return undefined
return e instanceof XrpcResponseError
? (e.headers.get(name) ?? undefined)
: undefined
}
@@ -16,7 +16,6 @@ import {ScreenTransition} from '#/components/ScreenTransition'
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard'
import {Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env'
import {type app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
function keyExtractor(item: bsky.profile.AnyProfileView) {
+2 -2
View File
@@ -66,8 +66,8 @@ function toSystemMessageView(
* The guard disjunction is kept inline in the `if` (rather than a separate
* boolean) so TS narrows `ev` to the union of system-message log types, all
* of which carry a `message: SystemMessageView`. The generated lexicon types
* are stricter than the old `@atproto/api` ones (no `[k: string]: unknown`
* index signature), so a separate boolean would not narrow the access.
* are strict (no `[k: string]: unknown` index signature), so a separate
* boolean would not narrow the access.
*/
if (
bsky.isType(chat.bsky.convo.defs.logAddMember, ev) ||
+2 -2
View File
@@ -205,7 +205,7 @@ export function useFeedPreviews(
if (item.isFallbackMarker) continue
const moderations = item.items.map(item =>
// @atproto/api and yields lex-typed slice items.
// the tuner yields lex-typed slice items.
moderatePost(item.post, moderationOpts!),
)
@@ -232,7 +232,7 @@ export function useFeedPreviews(
const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
uri: subItem.post.uri,
// off @atproto/api and yields lex-typed slice items.
// the tuner yields lex-typed slice items.
post: subItem.post,
record: subItem.record,
moderation: moderations[i],
+2 -2
View File
@@ -288,7 +288,7 @@ export function usePostFeedQuery(
.map(slice => {
const moderations = slice.items.map(item =>
moderatePost(
// off @atproto/api and yields lex-typed slice items.
// the tuner yields lex-typed slice items.
item.post,
moderationOpts!,
),
@@ -341,7 +341,7 @@ export function usePostFeedQuery(
const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
uri: item.post.uri,
// off @atproto/api and yields lex-typed slice items.
// the tuner yields lex-typed slice items.
post: item.post,
record: item.record,
moderation: moderations[i],
+2 -2
View File
@@ -289,8 +289,8 @@ export function buildSearchPostsV2Filters(
}
/**
* Consistent with timestamp parsing in @atproto/api. Only the date is used; the
* time is appended here since the lexicon expects a datetime value.
* Consistent with atproto timestamp parsing. Only the date is used; the time
* is appended here since the lexicon expects a datetime value.
*/
const parseTimestamp = (value: string | undefined): string | undefined => {
if (!value) return undefined
+3 -45
View File
@@ -1,5 +1,3 @@
import {AppBskyFeedPost} from '@atproto/api'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
@@ -44,8 +42,8 @@ describe('types/bsky new-world helpers (#/lexicons)', () => {
})
it('accepts an invalid body that has the right $type (dangerous semantics)', () => {
// Mirrors the old `dangerousIsType`: only the `$type` is checked, so a
// structurally invalid record still passes.
// Only the `$type` is checked, so a structurally invalid record still
// passes.
expect(bsky.isType(app.bsky.feed.post, invalidPost)).toBe(true)
})
@@ -55,8 +53,7 @@ describe('types/bsky new-world helpers (#/lexicons)', () => {
})
it('returns false (does not throw) for null and undefined', () => {
// Mirrors the old `dangerousIsType`/`is$typed` behavior - call sites
// pass e.g. `post.record` which may be undefined.
// Call sites pass e.g. `post.record` which may be undefined.
expect(bsky.isType(app.bsky.feed.post, null)).toBe(false)
expect(bsky.isType(app.bsky.feed.post, undefined)).toBe(false)
expect(bsky.isType(app.bsky.feed.post, 'string')).toBe(false)
@@ -122,42 +119,3 @@ describe('types/bsky new-world helpers (#/lexicons)', () => {
})
})
})
describe('types/bsky legacy helpers (@atproto/api) still work', () => {
it('dangerousIsType accepts the right $type without validating the body', () => {
expect(
bsky.dangerousIsType<AppBskyFeedPost.Record>(
validPost,
AppBskyFeedPost.isRecord,
),
).toBe(true)
// Right $type, invalid body - still passes the fast guard.
expect(
bsky.dangerousIsType<AppBskyFeedPost.Record>(
invalidPost,
AppBskyFeedPost.isRecord,
),
).toBe(true)
expect(
bsky.dangerousIsType<AppBskyFeedPost.Record>(
wrongType,
AppBskyFeedPost.isRecord,
),
).toBe(false)
})
it('validate fully validates the body', () => {
expect(
bsky.validate<AppBskyFeedPost.Record>(
validPost,
AppBskyFeedPost.validateRecord,
),
).toBe(true)
expect(
bsky.validate<AppBskyFeedPost.Record>(
invalidPost,
AppBskyFeedPost.validateRecord,
),
).toBe(false)
})
})
+10 -78
View File
@@ -10,86 +10,21 @@ import {
type ValidateOptions,
type ValidationResult,
} from '@atproto/lex'
import {type ValidationResult as LegacyValidationResult} from '@atproto/lexicon'
export * as post from '#/types/bsky/post'
export * as profile from '#/types/bsky/profile'
export * as starterPack from '#/types/bsky/starterPack'
/**
* Unsafe cast from an old `@atproto/api` view/record type to its `#/lexicons`
* equivalent. Only for mixed-world boundaries during the migration where a
* producer has not yet flipped. Structurally the shapes are identical modulo
* branded string types; this asserts the brand the compiler cannot prove.
* Every use is a migration debt marker - grep `toLex` at Phase 4 cleanup.
*/
export function toLex<T>(v: unknown): T {
return v as T
}
/**
* Fast type checking without full schema validation, for use with data we
* trust, or for non-critical path use cases. Why? Our SDK's `is*` identity
* utils do not assert the type of the entire object, only the `$type` string.
*
* For full validation of the object schema, use the `validate` export from
* this file.
*
* Usage:
* ```ts
* import * as bsky from '#/types/bsky'
*
* if (bsky.dangerousIsType<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) {
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
* }
* ```
*/
export function dangerousIsType<R extends {$type?: string}>(
record: unknown,
identity: <V>(v: V) => v is V & {$type: NonNullable<R['$type']>},
): record is R {
return identity(record)
}
/**
* Fully validates the object schema, which has a performance cost.
*
* For faster checks with data we trust, like that from our app view, use the
* `dangerousIsType` export from this same file.
*
* Usage:
* ```ts
* import * as bsky from '#/types/bsky'
*
* if (bsky.validate(item, AppBskyFeedPost.validateRecord)) {
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
* }
* ```
*/
export function validate<R extends {$type?: string}>(
record: unknown,
validator: (v: unknown) => LegacyValidationResult<R>,
): record is R {
return validator(record).success
}
/*
* New-world helpers (below) operate on the generated lexicon schema objects
* from '#/lexicons', replacing the `@atproto/api`-based helpers above.
*
* The old helpers take a record plus a standalone `is*`/`validate*` function
* (e.g. `AppBskyFeedPost.isRecord`). The new codegen instead attaches the
* validation surface directly to each schema, so these helpers take the schema
* object itself:
* These helpers operate on the generated lexicon schema objects from
* '#/lexicons'. The codegen attaches the validation surface directly to each
* schema, so these helpers take the schema object itself:
*
* ```ts
* import {app} from '#/lexicons'
* import * as bsky from '#/types/bsky'
*
* // old: bsky.dangerousIsType(v, AppBskyFeedPost.isRecord)
* bsky.isType(app.bsky.feed.post, v)
*
* // old: bsky.validate(v, AppBskyFeedPost.validateRecord)
* bsky.matches(app.bsky.feed.post, v)
* ```
*
@@ -113,9 +48,8 @@ type TypedSchema = RecordSchema | TypedObjectSchema
* string; it does NOT assert the rest of the object matches the schema. An
* invalid record with the right `$type` will pass.
*
* This is the '#/lexicons' equivalent of {@link dangerousIsType}. For full
* validation of the object schema, use {@link matches}, {@link parse}, or
* {@link safeParse} from this same file.
* For full validation of the object schema, use {@link matches}, {@link parse},
* or {@link safeParse} from this same file.
*
* Usage:
* ```ts
@@ -135,10 +69,9 @@ export function isType<S extends TypedSchema>(
* Deliberately NOT delegating to the schema's `isTypeOf`: the generated
* `TypedObjectSchema.isTypeOf` treats a MISSING `$type` as a match
* (maybe-typed semantics), which would let any plain object satisfy any
* def-schema check and break `$type`-discriminated unions. The old
* `dangerousIsType` required a present, matching `$type`, and so do we.
* The nullish/object guard also mirrors the old `is$typed` behavior of
* returning false (not throwing) for null/undefined input.
* def-schema check and break `$type`-discriminated unions. We require a
* present, matching `$type`. The nullish/object guard also returns false
* (not throws) for null/undefined input.
*/
return (
value != null &&
@@ -151,9 +84,8 @@ export function isType<S extends TypedSchema>(
* Fully validates the object against the schema (strict, no coercion), which
* has a performance cost, and narrows the value on success.
*
* This is the '#/lexicons' equivalent of {@link validate}. For faster checks
* with data we trust, like that from our app view, use {@link isType} from this
* same file.
* For faster checks with data we trust, like that from our app view, use
* {@link isType} from this same file.
*
* Usage:
* ```ts
+4 -5
View File
@@ -1,11 +1,10 @@
import {app} from '#/lexicons'
/*
* The generated `$type`-only guards. The old `@atproto/api`
* `AppBskyGraphDefs.isStarterPackView*` helpers matched on a present,
* matching `$type`; we reproduce that here against the `#/lexicons` schema's
* `$type` string rather than delegating to the schema's `isTypeOf` (which
* treats a missing `$type` as a match).
* The generated `$type`-only guards. These match on a present, matching
* `$type` against the `#/lexicons` schema's `$type` string rather than
* delegating to the schema's `isTypeOf` (which treats a missing `$type` as a
* match).
*/
export function isBasicView(
v: unknown,
+1 -1
View File
@@ -1,7 +1,7 @@
import {type app} from '#/lexicons'
/**
* Types for draft display and local media tracking.
* Server draft types come from @atproto/api.
* Server draft types come from the atproto lexicons.
*/
/**
+1 -1
View File
@@ -45,7 +45,7 @@ function patchSourceMapFilter(rules, pathPattern) {
module.exports = async function (env, argv) {
env.babel = {
dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo', '@atproto/api'],
dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo'],
}
let config = await createExpoWebpackConfigAsync(env, argv)
/*