Archive of https://github.com/w-social-eu/w-social-atproto (see also https://github.com/brw/w-social-atproto-archive)
0

Configure Feed

Select the types of activity you want to include in your feed.

lex SDK improvements (#4571)

* Improve code coverage of `@atproto/lex-data` tests

* Improve typing of `@atproto/syntax` type assertion utilities

* Improve performance of `@atproto/lex-schema ` string format checking

* Remove `assertX` string format assertion utilities

* tidy

* tidy

* Rename `isLanguageString` to `validateLanguage`

* add string format utils

* tidy

* Refactor uri validation to use `@atproto/syntax`

* More language validation to @atproto/syntax

+718 -604
+5
.changeset/better-experts-tap.md
··· 1 + --- 2 + '@atproto/lex-schema': patch 3 + --- 4 + 5 + Remove `asX` string format casting utilities
+5
.changeset/dirty-meals-throw.md
··· 1 + --- 2 + '@atproto/syntax': patch 3 + --- 4 + 5 + Add new `isValidAtUri` utility
+5
.changeset/free-beans-burn.md
··· 1 + --- 2 + '@atproto/syntax': patch 3 + --- 4 + 5 + Add `isValidUri` utility
+5
.changeset/hip-nights-mate.md
··· 1 + --- 2 + '@atproto/lexicon': patch 3 + --- 4 + 5 + Refactor uri validation to use `@atproto/syntax`
+5
.changeset/icy-pots-feel.md
··· 1 + --- 2 + '@atproto/syntax': patch 3 + --- 4 + 5 + Remove `assertX` string format assertion utilities
+5
.changeset/silly-dots-behave.md
··· 1 + --- 2 + '@atproto/syntax': patch 3 + --- 4 + 5 + Improve typing of type assertion utilities
+5
.changeset/silver-pans-say.md
··· 1 + --- 2 + '@atproto/lex-schema': patch 3 + --- 4 + 5 + Improve performance of string format checking
+5
.changeset/three-jobs-fly.md
··· 1 + --- 2 + '@atproto/lex-data': patch 3 + --- 4 + 5 + Rename `isLanguageString` to `validateLanguage`
+5
.changeset/wacky-geese-camp.md
··· 1 + --- 2 + '@atproto/syntax': patch 3 + --- 4 + 5 + Add language parsing utility
+5
.changeset/wide-suits-hear.md
··· 1 + --- 2 + '@atproto/lex-data': patch 3 + --- 4 + 5 + Improve code coverage of tests
+1
packages/common-web/package.json
··· 21 21 "dependencies": { 22 22 "@atproto/lex-data": "workspace:*", 23 23 "@atproto/lex-json": "workspace:*", 24 + "@atproto/syntax": "workspace:*", 24 25 "zod": "^3.23.8" 25 26 }, 26 27 "devDependencies": {
+7 -10
packages/common-web/src/strings.ts
··· 1 + import { fromBase64, graphemeLen, toBase64, utf8Len } from '@atproto/lex-data' 1 2 import { 2 3 LanguageTag, 3 - fromBase64, 4 - graphemeLen, 5 - isLanguageString, 4 + isValidLanguage, 6 5 parseLanguageString, 7 - toBase64, 8 - utf8Len, 9 - } from '@atproto/lex-data' 6 + } from '@atproto/syntax' 10 7 11 8 /** 12 9 * @deprecated Use {@link graphemeLen} from `@atproto/lex-data` instead. ··· 27 24 export type { LanguageTagLegacy as LanguageTag } 28 25 29 26 /** 30 - * @deprecated Use {@link parseLanguageString} from `@atproto/lex-data` instead. 27 + * @deprecated Use {@link parseLanguageString} from `@atproto/syntax` instead. 31 28 */ 32 - export const parseLanguageLegacy = parseLanguageString 29 + const parseLanguageLegacy = parseLanguageString 33 30 export { parseLanguageLegacy as parseLanguage } 34 31 35 32 /** 36 - * @deprecated Use {@link isLanguageString} from `@atproto/lex-data` instead. 33 + * @deprecated Use {@link isLanguageString} from `@atproto/syntax` instead. 37 34 */ 38 - export const validateLanguage = isLanguageString 35 + export const validateLanguage = isValidLanguage 39 36 40 37 /** 41 38 * @deprecated Use {@link toBase64} from `@atproto/lex-data` instead.
+1 -1
packages/lex/lex-client/tests/client.test.ts
··· 396 396 validateRequest: true, 397 397 }, 398 398 ) 399 - }).rejects.toThrow('Invalid DID format') 399 + }).rejects.toThrow('Invalid DID at $.did') 400 400 401 401 // validate performs schema validation before making the request 402 402 expect(fetchHandler).toHaveBeenCalledTimes(0)
-1
packages/lex/lex-data/package.json
··· 35 35 } 36 36 }, 37 37 "dependencies": { 38 - "@atproto/syntax": "workspace:*", 39 38 "multiformats": "^9.9.0", 40 39 "tslib": "^2.8.1", 41 40 "uint8arrays": "3.0.0",
+139 -32
packages/lex/lex-data/src/cid.test.ts
··· 14 14 decodeCid, 15 15 ensureValidCidString, 16 16 isCid, 17 + isCidForBytes, 17 18 parseCid, 18 19 parseCidSafe, 19 20 } from './cid.js' ··· 27 28 const rawCidStr = 'bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4' 28 29 const rawCid = parseCid(rawCidStr, { flavor: 'raw' }) 29 30 30 - const rawCidCustom: Cid = createCustomCid( 31 + const rawCidLike: Cid = createCustomCid( 31 32 1, 32 33 RAW_MULTICODEC, 33 34 SHA256_MULTIHASH, 34 35 rawCid.multihash.digest, 35 36 ) 36 - const rawCidCustomBytes = new BytesCid(rawCid.bytes) 37 + const rawBytesCid = new BytesCid(rawCid.bytes) 37 38 38 39 describe(isCid, () => { 39 40 describe('non-strict mode', () => { ··· 43 44 }) 44 45 45 46 it('returns true for custom compatible CID implementations', () => { 46 - expect(isCid(rawCidCustom)).toBe(true) 47 - expect(isCid(rawCidCustomBytes)).toBe(true) 47 + expect(isCid(rawCidLike)).toBe(true) 48 + expect(isCid(rawBytesCid)).toBe(true) 48 49 }) 49 50 50 51 it('returns true for CID v0 and v1', async () => { ··· 118 119 119 120 describe('alternative cid implementations', () => { 120 121 it('accepts compatible CID implementations', () => { 121 - expect(isCid(rawCidCustom)).toBe(true) 122 + expect(isCid(rawCidLike)).toBe(true) 122 123 }) 123 124 124 125 it('rejects non-matching version', () => { 125 - expect(isCid({ ...rawCidCustom, version: 0 })).toBe(false) 126 + expect(isCid({ ...rawCidLike, version: 0 })).toBe(false) 126 127 }) 127 128 128 129 it('rejects non-matching code', () => { 129 - expect(isCid({ ...rawCidCustom, code: 0 })).toBe(false) 130 + expect(isCid({ ...rawCidLike, code: -1 })).toBe(false) 131 + expect(isCid({ ...rawCidLike, code: 0 })).toBe(false) 132 + expect(isCid({ ...rawCidLike, code: 256 })).toBe(false) 130 133 }) 131 134 132 - it('rejects non-matching multihash code', () => { 135 + it('rejects invalid bytes property', () => { 136 + expect(isCid({ ...rawCidLike, bytes: undefined })).toBe(false) 137 + expect(isCid({ ...rawCidLike, bytes: 12 })).toBe(false) 138 + expect(isCid({ ...rawCidLike, bytes: {} })).toBe(false) 139 + expect(isCid({ ...rawCidLike, bytes: [] })).toBe(false) 140 + 133 141 expect( 134 142 isCid({ 135 - ...rawCidCustom, 136 - multihash: { ...rawCidCustom.multihash, code: 0 }, 143 + ...rawCidLike, 144 + bytes: rawCidLike.bytes.subarray(0, rawCidLike.bytes.length - 1), 137 145 }), 138 146 ).toBe(false) 147 + 148 + const bytes = new Uint8Array(rawCidLike.bytes.length) 149 + 150 + bytes.set(rawCidLike.bytes) 151 + expect(isCid({ ...rawCidLike, bytes })).toBe(true) 152 + 153 + bytes[0] = bytes[0] ^ 0xff 154 + expect(isCid({ ...rawCidLike, bytes })).toBe(false) 155 + bytes.set(rawCidLike.bytes) 156 + 157 + bytes[3] = bytes[3] ^ 0xff 158 + expect(isCid({ ...rawCidLike, bytes })).toBe(false) 159 + bytes.set(rawCidLike.bytes) 160 + 161 + bytes[6] = bytes[6] ^ 0xff 162 + expect(isCid({ ...rawCidLike, bytes })).toBe(false) 163 + bytes.set(rawCidLike.bytes) 139 164 }) 140 165 141 - it('rejects non-matching multihash digest', () => { 142 - const differentDigest = new Uint8Array(32) 143 - differentDigest[0] = 1 144 - expect( 145 - isCid({ 146 - ...rawCidCustom, 147 - multihash: { ...rawCidCustom.multihash, digest: differentDigest }, 148 - }), 149 - ).toBe(false) 166 + describe('multihash property', () => { 167 + it('rejects non-matching object', () => { 168 + expect(isCid({ ...rawCidLike, multihash: undefined })).toBe(false) 169 + expect(isCid({ ...rawCidLike, multihash: 12 })).toBe(false) 170 + expect(isCid({ ...rawCidLike, multihash: {} })).toBe(false) 171 + expect(isCid({ ...rawCidLike, multihash: [] })).toBe(false) 172 + }) 173 + 174 + it('rejects non-matching code', () => { 175 + expect( 176 + isCid({ 177 + ...rawCidLike, 178 + multihash: { ...rawCidLike.multihash, code: -1 }, 179 + }), 180 + ).toBe(false) 181 + expect( 182 + isCid({ 183 + ...rawCidLike, 184 + multihash: { ...rawCidLike.multihash, code: 0 }, 185 + }), 186 + ).toBe(false) 187 + expect( 188 + isCid({ 189 + ...rawCidLike, 190 + multihash: { ...rawCidLike.multihash, code: 256 }, 191 + }), 192 + ).toBe(false) 193 + }) 194 + 195 + it('rejects non Uint8Array digest', () => { 196 + expect( 197 + isCid({ 198 + ...rawCidLike, 199 + multihash: { ...rawCidLike.multihash, digest: new Array(32) }, 200 + }), 201 + ).toBe(false) 202 + }) 203 + 204 + it('rejects non Uint8Array digest', () => { 205 + expect( 206 + isCid({ 207 + ...rawCidLike, 208 + multihash: { ...rawCidLike.multihash, digest: new Array(32) }, 209 + }), 210 + ).toBe(false) 211 + }) 212 + 213 + it('rejects non-matching digest', () => { 214 + const differentDigest = new Uint8Array(32) 215 + differentDigest[0] = 1 216 + expect( 217 + isCid({ 218 + ...rawCidLike, 219 + multihash: { ...rawCidLike.multihash, digest: differentDigest }, 220 + }), 221 + ).toBe(false) 222 + }) 150 223 }) 151 224 152 - it('rejects objects without equals method', () => { 153 - expect(isCid({ ...rawCidCustom, equals: undefined })).toBe(false) 154 - }) 225 + describe('equals() method', () => { 226 + it('rejects objects without equals method', () => { 227 + expect(isCid({ ...rawCidLike, equals: undefined })).toBe(false) 228 + expect(isCid({ ...rawCidLike, equals: () => false })).toBe(false) 229 + }) 155 230 156 - it('rejects object with throwing equals method', () => { 157 - expect( 158 - isCid({ 159 - ...rawCidCustom, 160 - equals: () => { 161 - throw new Error('fail') 162 - }, 163 - }), 164 - ).toBe(false) 231 + it('rejects object with throwing equals method', () => { 232 + expect( 233 + isCid({ 234 + ...rawCidLike, 235 + equals: () => { 236 + throw new Error('fail') 237 + }, 238 + }), 239 + ).toBe(false) 240 + }) 165 241 }) 166 242 }) 167 243 }) ··· 186 262 }) 187 263 }) 188 264 265 + describe(isCidForBytes, () => { 266 + describe('raw', () => { 267 + it('returns true for valid raw CID bytes', async () => { 268 + for (const hasher of [sha256, sha512]) { 269 + const data = new TextEncoder().encode('hello world') 270 + const digest = await hasher.digest(data) 271 + const cid = CID.createV1(RAW_MULTICODEC, digest) 272 + expect(await isCidForBytes(cid, data)).toBe(true) 273 + 274 + data[0] = data[0] ^ 0xff 275 + expect(await isCidForBytes(cid, data)).toBe(false) 276 + } 277 + }) 278 + }) 279 + 280 + describe('cbor', () => { 281 + it('returns true for valid cbor CID bytes', async () => { 282 + for (const hasher of [sha256, sha512]) { 283 + // @NOTE this is not valid CBOR, but sufficient for testing the hash 284 + const data = new TextEncoder().encode('hello world') 285 + const digest = await hasher.digest(data) 286 + const cid = CID.createV1(DAG_CBOR_MULTICODEC, digest) 287 + expect(await isCidForBytes(cid, data)).toBe(true) 288 + 289 + data[0] = data[0] ^ 0xff 290 + expect(await isCidForBytes(cid, data)).toBe(false) 291 + } 292 + }) 293 + }) 294 + }) 295 + 189 296 describe(parseCidSafe, () => { 190 297 it('parses valid CIDs', () => { 191 298 expect(parseCidSafe(cborCidStr)?.toString()).toBe(cborCidStr) ··· 230 337 231 338 describe(asMultiformatsCID, () => { 232 339 it('converts compatible CID to multiformats CID', () => { 233 - for (const cid of [cborCid, rawCid, rawCidCustom, rawCidCustomBytes]) { 340 + for (const cid of [cborCid, rawCid, rawCidLike, rawBytesCid]) { 234 341 expect(asMultiformatsCID(cid)).toBeInstanceOf(CID) 235 342 expect(asMultiformatsCID(cid)).toMatchObject({ 236 343 version: cid.version,
-1
packages/lex/lex-data/src/index.ts
··· 1 1 export * from './blob.js' 2 2 export * from './cid.js' 3 - export * from './language.js' 4 3 export * from './lex-equals.js' 5 4 export * from './lex-error.js' 6 5 export * from './lex.js'
-88
packages/lex/lex-data/src/language.test.ts
··· 1 - import { describe, expect, it } from 'vitest' 2 - import { isLanguageString, parseLanguageString } from './language' 3 - 4 - describe('string', () => { 5 - describe('languages', () => { 6 - it('validates BCP 47', () => { 7 - // valid 8 - expect(isLanguageString('de')).toEqual(true) 9 - expect(isLanguageString('de-CH')).toEqual(true) 10 - expect(isLanguageString('de-DE-1901')).toEqual(true) 11 - expect(isLanguageString('es-419')).toEqual(true) 12 - expect(isLanguageString('sl-IT-nedis')).toEqual(true) 13 - expect(isLanguageString('mn-Cyrl-MN')).toEqual(true) 14 - expect(isLanguageString('x-fr-CH')).toEqual(true) 15 - expect( 16 - isLanguageString('en-GB-boont-r-extended-sequence-x-private'), 17 - ).toEqual(true) 18 - expect(isLanguageString('sr-Cyrl')).toEqual(true) 19 - expect(isLanguageString('hy-Latn-IT-arevela')).toEqual(true) 20 - expect(isLanguageString('i-klingon')).toEqual(true) 21 - // invalid 22 - expect(isLanguageString('')).toEqual(false) 23 - expect(isLanguageString('x')).toEqual(false) 24 - expect(isLanguageString('de-CH-')).toEqual(false) 25 - expect(isLanguageString('i-bad-grandfathered')).toEqual(false) 26 - }) 27 - 28 - it('parses BCP 47', () => { 29 - // valid 30 - expect(parseLanguageString('de')).toEqual({ 31 - language: 'de', 32 - }) 33 - expect(parseLanguageString('de-CH')).toEqual({ 34 - language: 'de', 35 - region: 'CH', 36 - }) 37 - expect(parseLanguageString('de-DE-1901')).toEqual({ 38 - language: 'de', 39 - region: 'DE', 40 - variant: '1901', 41 - }) 42 - expect(parseLanguageString('es-419')).toEqual({ 43 - language: 'es', 44 - region: '419', 45 - }) 46 - expect(parseLanguageString('sl-IT-nedis')).toEqual({ 47 - language: 'sl', 48 - region: 'IT', 49 - variant: 'nedis', 50 - }) 51 - expect(parseLanguageString('mn-Cyrl-MN')).toEqual({ 52 - language: 'mn', 53 - script: 'Cyrl', 54 - region: 'MN', 55 - }) 56 - expect(parseLanguageString('x-fr-CH')).toEqual({ 57 - privateUse: 'x-fr-CH', 58 - }) 59 - expect( 60 - parseLanguageString('en-GB-boont-r-extended-sequence-x-private'), 61 - ).toEqual({ 62 - language: 'en', 63 - region: 'GB', 64 - variant: 'boont', 65 - extension: 'r-extended-sequence', 66 - privateUse: 'x-private', 67 - }) 68 - expect(parseLanguageString('sr-Cyrl')).toEqual({ 69 - language: 'sr', 70 - script: 'Cyrl', 71 - }) 72 - expect(parseLanguageString('hy-Latn-IT-arevela')).toEqual({ 73 - language: 'hy', 74 - script: 'Latn', 75 - region: 'IT', 76 - variant: 'arevela', 77 - }) 78 - expect(parseLanguageString('i-klingon')).toEqual({ 79 - grandfathered: 'i-klingon', 80 - }) 81 - // invalid 82 - expect(parseLanguageString('')).toEqual(null) 83 - expect(parseLanguageString('x')).toEqual(null) 84 - expect(parseLanguageString('de-CH-')).toEqual(null) 85 - expect(parseLanguageString('i-bad-grandfathered')).toEqual(null) 86 - }) 87 - }) 88 - })
+1 -1
packages/lex/lex-data/src/language.ts packages/syntax/src/language.ts
··· 34 34 * 35 35 * @see {@link https://www.rfc-editor.org/rfc/rfc5646.html#section-2.1} 36 36 */ 37 - export function isLanguageString(input: string): boolean { 37 + export function isValidLanguage(input: string): boolean { 38 38 return BCP47_REGEXP.test(input) 39 39 }
+24 -5
packages/lex/lex-data/src/uint8array.test.ts
··· 5 5 import { 6 6 asUint8Array, 7 7 fromBase64, 8 + ifUint8Array, 8 9 toBase64, 9 10 ui8Concat, 10 11 ui8Equals, 11 12 } from './uint8array.js' 12 13 13 - describe('toBase64', () => { 14 + describe(toBase64, () => { 14 15 it('encodes empty Uint8Array', () => { 15 16 const encoded = toBase64(new Uint8Array(0)) 16 17 expect(typeof encoded).toBe('string') ··· 49 50 }) 50 51 }) 51 52 52 - describe('fromBase64', () => { 53 + describe(fromBase64, () => { 53 54 it('decodes empty string', () => { 54 55 const decoded = fromBase64('') 55 56 expect(decoded).toBeInstanceOf(Uint8Array) ··· 133 134 }) 134 135 }) 135 136 136 - describe('asUint8Array', () => { 137 + describe(asUint8Array, () => { 137 138 describe('Uint8Array input', () => { 138 139 it('returns same Uint8Array instance', () => { 139 140 const input = new Uint8Array([1, 2, 3]) ··· 294 295 }) 295 296 }) 296 297 297 - describe('ui8Equals', () => { 298 + describe(ui8Equals, () => { 298 299 describe('equal arrays', () => { 299 300 it('returns true for identical arrays', () => { 300 301 const a = new Uint8Array([1, 2, 3]) ··· 434 435 }) 435 436 }) 436 437 437 - describe('ui8Concat', () => { 438 + describe(ifUint8Array, () => { 439 + it('returns the input if it is a Uint8Array', () => { 440 + const input = new Uint8Array([1, 2, 3]) 441 + const result = ifUint8Array(input) 442 + expect(result).toBe(input) 443 + }) 444 + 445 + it('returns undefined for non-Uint8Array inputs', () => { 446 + expect(ifUint8Array(null)).toBeUndefined() 447 + expect(ifUint8Array(undefined)).toBeUndefined() 448 + expect(ifUint8Array({})).toBeUndefined() 449 + expect(ifUint8Array([])).toBeUndefined() 450 + expect(ifUint8Array('string')).toBeUndefined() 451 + expect(ifUint8Array(123)).toBeUndefined() 452 + expect(ifUint8Array(true)).toBeUndefined() 453 + }) 454 + }) 455 + 456 + describe(ui8Concat, () => { 438 457 it('concatenates empty array', () => { 439 458 const result = ui8Concat([]) 440 459 expect(result).toBeInstanceOf(Uint8Array)
+5 -6
packages/lex/lex-data/src/uint8array.ts
··· 28 28 alphabet?: Base64Alphabet, 29 29 ) => string = 30 30 /* v8 ignore next -- @preserve */ toBase64Native ?? 31 - /* v8 ignore next -- @preserve */ toBase64Node ?? 32 - /* v8 ignore next -- @preserve */ toBase64Ponyfill 31 + toBase64Node ?? 32 + toBase64Ponyfill 33 33 34 34 /** 35 35 * Decodes a base64 string into a Uint8Array. This function supports both padded ··· 43 43 alphabet?: Base64Alphabet, 44 44 ) => Uint8Array = 45 45 /* v8 ignore next -- @preserve */ fromBase64Native ?? 46 - /* v8 ignore next -- @preserve */ fromBase64Node ?? 47 - /* v8 ignore next -- @preserve */ fromBase64Ponyfill 46 + fromBase64Node ?? 47 + fromBase64Ponyfill 48 48 49 49 /* v8 ignore next -- @preserve */ 50 50 if (toBase64 === toBase64Ponyfill || fromBase64 === fromBase64Ponyfill) { ··· 102 102 } 103 103 104 104 export const ui8Concat = 105 - /* v8 ignore next -- @preserve */ ui8ConcatNode ?? 106 - /* v8 ignore next -- @preserve */ ui8ConcatPonyfill 105 + /* v8 ignore next -- @preserve */ ui8ConcatNode ?? ui8ConcatPonyfill
+19 -28
packages/lex/lex-data/src/utf8-len.test.ts
··· 1 1 import { describe, expect, it } from 'vitest' 2 2 import { utf8LenCompute, utf8LenNode } from './utf8-len.js' 3 3 4 - describe(utf8LenNode!, () => { 5 - it('computes utf8 string length', () => { 6 - expect(utf8LenNode!('a')).toBe(1) 7 - expect(utf8LenNode!('~')).toBe(1) 8 - expect(utf8LenNode!('ö')).toBe(2) 9 - expect(utf8LenNode!('ñ')).toBe(2) 10 - expect(utf8LenNode!('©')).toBe(2) 11 - expect(utf8LenNode!('⽘')).toBe(3) 12 - expect(utf8LenNode!('☎')).toBe(3) 13 - expect(utf8LenNode!('𓋓')).toBe(4) 14 - expect(utf8LenNode!('😀')).toBe(4) 15 - expect(utf8LenNode!('👨‍👩‍👧‍👧')).toBe(25) 4 + for (const utf8Len of [utf8LenNode!, utf8LenCompute!] as const) { 5 + describe(utf8Len, () => { 6 + it('computes utf8 string length', () => { 7 + expect(utf8Len('a')).toBe(1) 8 + expect(utf8Len('~')).toBe(1) 9 + expect(utf8Len('ö')).toBe(2) 10 + expect(utf8Len('ñ')).toBe(2) 11 + expect(utf8Len('©')).toBe(2) 12 + expect(utf8Len('⽘')).toBe(3) 13 + expect(utf8Len('☎')).toBe(3) 14 + expect(utf8Len('𓋓')).toBe(4) 15 + expect(utf8Len('😀')).toBe(4) 16 + expect(utf8Len('👨‍👩‍👧‍👧')).toBe(25) 17 + // high surrogate with no low surrogate 18 + expect(utf8Len('\uD83D')).toBe(3) 19 + // low surrogate with no high surrogate 20 + expect(utf8Len('\uDC00')).toBe(3) 21 + }) 16 22 }) 17 - }) 18 - 19 - describe(utf8LenCompute, () => { 20 - it('computes utf8 string length', () => { 21 - expect(utf8LenCompute('a')).toBe(1) 22 - expect(utf8LenCompute('~')).toBe(1) 23 - expect(utf8LenCompute('ö')).toBe(2) 24 - expect(utf8LenCompute('ñ')).toBe(2) 25 - expect(utf8LenCompute('©')).toBe(2) 26 - expect(utf8LenCompute('⽘')).toBe(3) 27 - expect(utf8LenCompute('☎')).toBe(3) 28 - expect(utf8LenCompute('𓋓')).toBe(4) 29 - expect(utf8LenCompute('😀')).toBe(4) 30 - expect(utf8LenCompute('👨‍👩‍👧‍👧')).toBe(25) 31 - }) 32 - }) 23 + }
+4 -8
packages/lex/lex-data/src/utf8.ts
··· 8 8 import { utf8ToBase64Node, utf8ToBase64Ponyfill } from './utf8-to-base64.js' 9 9 10 10 export const graphemeLen: (str: string) => number = 11 - /* v8 ignore next -- @preserve */ graphemeLenNative ?? 12 - /* v8 ignore next -- @preserve */ graphemeLenPonyfill 11 + /* v8 ignore next -- @preserve */ graphemeLenNative ?? graphemeLenPonyfill 13 12 14 13 /* v8 ignore next -- @preserve */ 15 14 if (graphemeLen === graphemeLenPonyfill) { ··· 20 19 } 21 20 22 21 export const utf8Len: (string: string) => number = 23 - /* v8 ignore next -- @preserve */ utf8LenNode ?? 24 - /* v8 ignore next -- @preserve */ utf8LenCompute 22 + /* v8 ignore next -- @preserve */ utf8LenNode ?? utf8LenCompute 25 23 26 24 export const utf8ToBase64: (str: string, alphabet?: Base64Alphabet) => string = 27 - /* v8 ignore next -- @preserve */ utf8ToBase64Node ?? 28 - /* v8 ignore next -- @preserve */ utf8ToBase64Ponyfill 25 + /* v8 ignore next -- @preserve */ utf8ToBase64Node ?? utf8ToBase64Ponyfill 29 26 30 27 export const utf8FromBase64: ( 31 28 b64: string, 32 29 alphabet?: Base64Alphabet, 33 30 ) => string = 34 - /* v8 ignore next -- @preserve */ utf8FromBase64Node ?? 35 - /* v8 ignore next -- @preserve */ utf8FromBase64Ponyfill 31 + /* v8 ignore next -- @preserve */ utf8FromBase64Node ?? utf8FromBase64Ponyfill
+112 -151
packages/lex/lex-schema/src/core/string-format.ts
··· 1 - import { ensureValidCidString, isLanguageString } from '@atproto/lex-data' 1 + import { validateCidString } from '@atproto/lex-data' 2 2 import { 3 3 AtIdentifierString, 4 4 AtUriString, ··· 8 8 NsidString, 9 9 RecordKeyString, 10 10 TidString, 11 - ensureValidAtIdentifier, 12 - ensureValidAtUri, 13 - ensureValidDatetime, 14 - ensureValidDid, 15 - ensureValidHandle, 16 - ensureValidNsid, 17 - ensureValidRecordKey, 18 - ensureValidTid, 11 + UriString, 12 + isValidAtIdentifier as isValidAtId, 13 + isValidAtUri, 14 + isValidDatetime, 15 + isValidDid, 16 + isValidHandle, 17 + isValidLanguage, 18 + isValidNsid, 19 + isValidRecordKey, 20 + isValidTid, 21 + isValidUri, 19 22 } from '@atproto/syntax' 20 - import { 21 - AssertFn, 22 - CastFn, 23 - CheckFn, 24 - createAssertFunction, 25 - createCastFunction, 26 - createCheckFunction, 27 - } from '../util/assertion-util.js' 23 + import { CheckFn } from '../util/assertion-util.js' 28 24 29 - // Format utilities missing from @atproto/syntax 25 + // Expose all individual string format types and type guards 26 + 27 + export type { AtIdentifierString } 28 + export const isAtIdentifierString: CheckFn<AtIdentifierString> = isValidAtId 29 + 30 + export type { AtUriString } 31 + export const isAtUriString: CheckFn<AtUriString> = isValidAtUri 32 + 30 33 export type CidString = string 34 + export const isCidString = ((v) => validateCidString(v)) as CheckFn<CidString> 35 + 36 + export type { DatetimeString } 37 + export const isDatetimeString: CheckFn<DatetimeString> = isValidDatetime 38 + 39 + export type { DidString } 40 + export const isDidString: CheckFn<DidString> = isValidDid 41 + 42 + export type { HandleString } 43 + export const isHandleString: CheckFn<HandleString> = isValidHandle 44 + 31 45 export type LanguageString = string 32 - export type UriString = `${string}:${string}` 46 + export const isLanguageString = isValidLanguage as CheckFn<LanguageString> 33 47 34 - /*@__NO_SIDE_EFFECTS__*/ 35 - export function isUriString<T extends string>( 36 - input: T, 37 - ): input is T & UriString { 38 - return /^\w+:(?:\/\/)?[^\s/][^\s]*$/.test(input) 39 - } 48 + export type { NsidString } 49 + export const isNsidString: CheckFn<NsidString> = isValidNsid 40 50 41 - // Re-export utility typed as assertion functions so that TypeScript can 42 - // infer the narrowed type after calling them. 51 + export type { RecordKeyString } 52 + export const isRecordKeyString: CheckFn<RecordKeyString> = isValidRecordKey 43 53 44 - export type { 45 - AtIdentifierString, 46 - AtUriString, 47 - DatetimeString, 48 - DidString, 49 - HandleString, 50 - NsidString, 51 - RecordKeyString, 52 - TidString, 53 - } from '@atproto/syntax' 54 + export type { TidString } 55 + export const isTidString: CheckFn<TidString> = isValidTid 54 56 55 - // Export utilities for formats missing from @atproto/syntax 57 + export type { UriString } 58 + export const isUriString: CheckFn<UriString> = isValidUri 56 59 57 - export const assertAtIdentifierString: AssertFn<AtIdentifierString> = 58 - ensureValidAtIdentifier 59 - export const assertAtUriString: AssertFn<AtUriString> = ensureValidAtUri 60 - export const assertCidString: AssertFn<CidString> = ensureValidCidString 61 - export const assertDatetimeString: AssertFn<DatetimeString> = 62 - ensureValidDatetime 63 - export const assertDidString: AssertFn<DidString> = ensureValidDid 64 - export const assertHandleString: AssertFn<HandleString> = ensureValidHandle 65 - export const assertLanguageString: AssertFn<LanguageString> = 66 - createAssertFunction<LanguageString>( 67 - isLanguageString, 68 - 'Invalid BCP 47 string', 69 - ) 70 - export const assertNsidString: AssertFn<NsidString> = ensureValidNsid 71 - export const assertRecordKeyString: AssertFn<RecordKeyString> = 72 - ensureValidRecordKey 73 - export const assertTidString: AssertFn<TidString> = ensureValidTid 74 - export const assertUriString: AssertFn<UriString> = 75 - createAssertFunction<UriString>(isUriString, 'Invalid URI') 60 + // String format registry (maps format names to their types and type guards) 76 61 77 - export const asAtIdentifierString: CastFn<AtIdentifierString> = 78 - /*#__PURE__*/ createCastFunction(assertAtIdentifierString) 79 - export const asAtUriString: CastFn<AtUriString> = 80 - /*#__PURE__*/ createCastFunction(ensureValidAtUri) 81 - export const asCidString: CastFn<CidString> = 82 - /*#__PURE__*/ createCastFunction(ensureValidCidString) 83 - export const asDatetimeString: CastFn<DatetimeString> = 84 - /*#__PURE__*/ createCastFunction(ensureValidDatetime) 85 - export const asDidString: CastFn<DidString> = 86 - /*#__PURE__*/ createCastFunction(ensureValidDid) 87 - export const asHandleString: CastFn<HandleString> = 88 - /*#__PURE__*/ createCastFunction(ensureValidHandle) 89 - export const asLanguageString: CastFn<LanguageString> = 90 - /*#__PURE__*/ createCastFunction(assertLanguageString) 91 - export const asNsidString: CastFn<NsidString> = 92 - /*#__PURE__*/ createCastFunction(ensureValidNsid) 93 - export const asRecordKeyString: CastFn<RecordKeyString> = 94 - /*#__PURE__*/ createCastFunction(ensureValidRecordKey) 95 - export const asTidString: CastFn<TidString> = 96 - /*#__PURE__*/ createCastFunction(ensureValidTid) 97 - export const asUriString: CastFn<UriString> = 98 - /*#__PURE__*/ createCastFunction(assertUriString) 62 + type StringFormats = { 63 + 'at-identifier': AtIdentifierString 64 + 'at-uri': AtUriString 65 + cid: CidString 66 + datetime: DatetimeString 67 + did: DidString 68 + handle: HandleString 69 + language: LanguageString 70 + nsid: NsidString 71 + 'record-key': RecordKeyString 72 + tid: TidString 73 + uri: UriString 74 + } 75 + 76 + export type StringFormat = Extract<keyof StringFormats, string> 77 + 78 + const stringFormatVerifiers: { 79 + readonly [K in StringFormat]: CheckFn<StringFormats[K]> 80 + } = /*#__PURE__*/ Object.freeze({ 81 + __proto__: null, 99 82 100 - export { isLanguageString } 101 - export const isAtIdentifierString: CheckFn<AtIdentifierString> = 102 - /*#__PURE__*/ createCheckFunction(assertAtIdentifierString) 103 - export const isAtUriString: CheckFn<AtUriString> = 104 - /*#__PURE__*/ createCheckFunction(assertAtIdentifierString) 105 - export const isCidString: CheckFn<CidString> = 106 - /*#__PURE__*/ createCheckFunction(assertCidString) 107 - export const isDatetimeString: CheckFn<DatetimeString> = 108 - /*#__PURE__*/ createCheckFunction(assertDatetimeString) 109 - export const isDidString: CheckFn<DidString> = 110 - /*#__PURE__*/ createCheckFunction(assertDidString) 111 - export const isHandleString: CheckFn<HandleString> = 112 - /*#__PURE__*/ createCheckFunction(assertHandleString) 113 - export const isNsidString: CheckFn<NsidString> = 114 - /*#__PURE__*/ createCheckFunction(assertNsidString) 115 - export const isRecordKeyString: CheckFn<RecordKeyString> = 116 - /*#__PURE__*/ createCheckFunction(assertRecordKeyString) 117 - export const isTidString: CheckFn<TidString> = 118 - /*#__PURE__*/ createCheckFunction(assertTidString) 83 + 'at-identifier': isAtIdentifierString, 84 + 'at-uri': isAtUriString, 85 + cid: isCidString, 86 + datetime: isDatetimeString, 87 + did: isDidString, 88 + handle: isHandleString, 89 + language: isLanguageString, 90 + nsid: isNsidString, 91 + 'record-key': isRecordKeyString, 92 + tid: isTidString, 93 + uri: isUriString, 94 + }) 119 95 120 - // String formatting types and utilities 96 + export type InferStringFormat<F extends StringFormat> = F extends StringFormat 97 + ? StringFormats[F] 98 + : never 121 99 122 - export const STRING_FORMATS = Object.freeze([ 123 - 'datetime', 124 - 'uri', 125 - 'at-uri', 126 - 'did', 127 - 'handle', 128 - 'at-identifier', 129 - 'nsid', 130 - 'cid', 131 - 'language', 132 - 'tid', 133 - 'record-key', 134 - ] as const) 100 + /*@__NO_SIDE_EFFECTS__*/ 101 + export function isStringFormat<I extends string, F extends StringFormat>( 102 + input: I, 103 + format: F, 104 + ): input is I & StringFormats[F] { 105 + const formatVerifier = stringFormatVerifiers[format] 106 + // Fool-proof 107 + if (!formatVerifier) throw new TypeError(`Unknown string format: ${format}`) 135 108 136 - export type StringFormat = (typeof STRING_FORMATS)[number] 109 + return formatVerifier(input) 110 + } 137 111 138 - export type InferStringFormat<F> = 139 - // 140 - F extends 'datetime' 141 - ? DatetimeString 142 - : F extends 'uri' 143 - ? UriString 144 - : F extends 'at-uri' 145 - ? AtUriString 146 - : F extends 'did' 147 - ? DidString 148 - : F extends 'handle' 149 - ? HandleString 150 - : F extends 'at-identifier' 151 - ? AtIdentifierString 152 - : F extends 'nsid' 153 - ? NsidString 154 - : // LanguageString | CidString | TidString | RecordKeyString 155 - string 112 + /*@__NO_SIDE_EFFECTS__*/ 113 + export function assertStringFormat<I extends string, F extends StringFormat>( 114 + input: I, 115 + format: F, 116 + ): asserts input is I & StringFormats[F] { 117 + if (!isStringFormat(input, format)) { 118 + throw new TypeError(`Invalid string format (${format}): ${input}`) 119 + } 120 + } 156 121 157 - const formatters = /*#__PURE__*/ new Map<StringFormat, (str: string) => void>([ 158 - ['at-identifier', assertAtIdentifierString], 159 - ['at-uri', assertAtUriString], 160 - ['cid', assertCidString], 161 - ['datetime', assertDatetimeString], 162 - ['did', assertDidString], 163 - ['handle', assertHandleString], 164 - ['language', assertLanguageString], 165 - ['nsid', assertNsidString], 166 - ['record-key', assertRecordKeyString], 167 - ['tid', assertTidString], 168 - ['uri', assertUriString], 169 - ] as const) 122 + /*@__NO_SIDE_EFFECTS__*/ 123 + export function asStringFormat<I extends string, F extends StringFormat>( 124 + input: I, 125 + format: F, 126 + ): I & StringFormats[F] { 127 + assertStringFormat(input, format) 128 + return input 129 + } 170 130 171 131 /*@__NO_SIDE_EFFECTS__*/ 172 - export function assertStringFormat<F extends StringFormat>( 173 - input: string, 132 + export function ifStringFormat<I extends string, F extends StringFormat>( 133 + input: I, 174 134 format: F, 175 - ): asserts input is InferStringFormat<F> { 176 - const assertFn = formatters.get(format) 177 - if (assertFn) assertFn(input) 178 - // Fool-proof 179 - else throw new Error(`Unknown string format: ${format}`) 135 + ): undefined | (I & StringFormats[F]) { 136 + return isStringFormat(input, format) ? input : undefined 180 137 } 138 + 139 + export const STRING_FORMATS = /*#__PURE__*/ Object.freeze( 140 + /*#__PURE__*/ Object.keys(stringFormatVerifiers), 141 + ) as readonly StringFormat[]
+1 -1
packages/lex/lex-schema/src/core/validation-issue.ts
··· 44 44 } 45 45 46 46 toString() { 47 - return `Invalid ${this.formatDescription} format${this.message ? ` (${this.message})` : ''}${stringifyPath(this.path)} (got ${stringifyValue(this.input)})` 47 + return `Invalid ${this.formatDescription}${this.message ? ` (${this.message})` : ''}${stringifyPath(this.path)} (got ${stringifyValue(this.input)})` 48 48 } 49 49 50 50 toJSON() {
+36 -38
packages/lex/lex-schema/src/schema/permission-set.test.ts
··· 1 1 import { describe, expect, it } from 'vitest' 2 - import { asNsidString } from '../core.js' 3 2 import { PermissionSet, permissionSet } from './permission-set.js' 4 3 import { Permission, permission } from './permission.js' 5 4 6 5 describe('PermissionSet', () => { 7 6 describe('constructor', () => { 8 7 it('creates a PermissionSet instance with all parameters', () => { 9 - const nsid = asNsidString('app.bsky.oauth.permissions') 8 + const nsid = 'app.bsky.oauth.permissions' 10 9 const permissions = [ 11 10 permission('app.bsky.feed.post:read', {}), 12 11 permission('app.bsky.feed.post:write', {}), ··· 25 24 }) 26 25 27 26 it('creates a PermissionSet instance with minimal options', () => { 28 - const nsid = asNsidString('app.bsky.oauth.permissions') 27 + const nsid = 'app.bsky.oauth.permissions' 29 28 const permissions = [permission('app.bsky.feed.post:read', {})] as const 30 29 const options = {} 31 30 ··· 38 37 }) 39 38 40 39 it('creates a PermissionSet instance with empty permissions array', () => { 41 - const nsid = asNsidString('app.bsky.oauth.permissions') 40 + const nsid = 'app.bsky.oauth.permissions' 42 41 const permissions = [] as const 43 42 const options = {} 44 43 ··· 49 48 }) 50 49 51 50 it('creates a PermissionSet instance with title only', () => { 52 - const nsid = asNsidString('app.bsky.oauth.permissions') 51 + const nsid = 'app.bsky.oauth.permissions' 53 52 const permissions = [permission('app.bsky.feed.like:read', {})] as const 54 53 const options = { 55 54 title: 'Like Management', ··· 62 61 }) 63 62 64 63 it('creates a PermissionSet instance with detail only', () => { 65 - const nsid = asNsidString('app.bsky.oauth.permissions') 64 + const nsid = 'app.bsky.oauth.permissions' 66 65 const permissions = [permission('app.bsky.feed.like:read', {})] as const 67 66 const options = { 68 67 detail: 'Allows reading likes on posts', ··· 75 74 }) 76 75 77 76 it('creates a PermissionSet instance with localized titles', () => { 78 - const nsid = asNsidString('app.bsky.oauth.permissions') 77 + const nsid = 'app.bsky.oauth.permissions' 79 78 const permissions = [permission('app.bsky.feed.post:read', {})] as const 80 79 const options = { 81 80 title: 'Post Management', ··· 95 94 }) 96 95 97 96 it('creates a PermissionSet instance with localized details', () => { 98 - const nsid = asNsidString('app.bsky.oauth.permissions') 97 + const nsid = 'app.bsky.oauth.permissions' 99 98 const permissions = [permission('app.bsky.feed.post:read', {})] as const 100 99 const options = { 101 100 detail: 'Allows reading posts', ··· 115 114 }) 116 115 117 116 it('creates a PermissionSet instance with all options including localization', () => { 118 - const nsid = asNsidString('app.bsky.oauth.permissions') 117 + const nsid = 'app.bsky.oauth.permissions' 119 118 const permissions = [ 120 119 permission('app.bsky.feed.post:read', {}), 121 120 permission('app.bsky.feed.post:write', {}), ··· 154 153 155 154 describe('property immutability', () => { 156 155 it('options object itself is mutable', () => { 157 - const nsid = asNsidString('app.bsky.oauth.permissions') 156 + const nsid = 'app.bsky.oauth.permissions' 158 157 const permissions = [permission('app.bsky.feed.post:read', {})] as const 159 158 const options = { title: 'Test' } 160 159 ··· 168 167 169 168 describe('with multiple permissions', () => { 170 169 it('creates a PermissionSet with multiple read permissions', () => { 171 - const nsid = asNsidString('app.bsky.oauth.read') 170 + const nsid = 'app.bsky.oauth.read' 172 171 const permissions = [ 173 172 permission('app.bsky.feed.post:read', {}), 174 173 permission('app.bsky.feed.like:read', {}), ··· 190 189 }) 191 190 192 191 it('creates a PermissionSet with mixed read/write permissions', () => { 193 - const nsid = asNsidString('app.bsky.oauth.full') 192 + const nsid = 'app.bsky.oauth.full' 194 193 const permissions = [ 195 194 permission('app.bsky.feed.post:read', {}), 196 195 permission('app.bsky.feed.post:write', {}), ··· 208 207 }) 209 208 210 209 it('creates a PermissionSet with a single permission', () => { 211 - const nsid = asNsidString('app.bsky.oauth.limited') 210 + const nsid = 'app.bsky.oauth.limited' 212 211 const permissions = [ 213 212 permission('app.bsky.actor.profile:read', {}), 214 213 ] as const ··· 226 225 227 226 describe('edge cases', () => { 228 227 it('handles very long NSID', () => { 229 - const nsid = asNsidString( 230 - 'com.example.very.long.namespace.identifier.oauth.permissions', 231 - ) 228 + const nsid = 229 + 'com.example.very.long.namespace.identifier.oauth.permissions' 232 230 const permissions = [permission('resource:action', {})] as const 233 231 const options = {} 234 232 ··· 238 236 }) 239 237 240 238 it('handles long title strings', () => { 241 - const nsid = asNsidString('app.bsky.oauth.permissions') 239 + const nsid = 'app.bsky.oauth.permissions' 242 240 const permissions = [permission('app.bsky.feed.post:read', {})] as const 243 241 const longTitle = 'A'.repeat(500) 244 242 const options = { ··· 252 250 }) 253 251 254 252 it('handles long detail strings', () => { 255 - const nsid = asNsidString('app.bsky.oauth.permissions') 253 + const nsid = 'app.bsky.oauth.permissions' 256 254 const permissions = [permission('app.bsky.feed.post:read', {})] as const 257 255 const longDetail = 'B'.repeat(1000) 258 256 const options = { ··· 266 264 }) 267 265 268 266 it('handles multiple language codes in title:lang', () => { 269 - const nsid = asNsidString('app.bsky.oauth.permissions') 267 + const nsid = 'app.bsky.oauth.permissions' 270 268 const permissions = [permission('app.bsky.feed.post:read', {})] as const 271 269 const options = { 272 270 title: 'Post Management', ··· 288 286 }) 289 287 290 288 it('handles undefined values in title:lang', () => { 291 - const nsid = asNsidString('app.bsky.oauth.permissions') 289 + const nsid = 'app.bsky.oauth.permissions' 292 290 const permissions = [permission('app.bsky.feed.post:read', {})] as const 293 291 const options = { 294 292 title: 'Post Management', ··· 307 305 }) 308 306 309 307 it('handles undefined values in detail:lang', () => { 310 - const nsid = asNsidString('app.bsky.oauth.permissions') 308 + const nsid = 'app.bsky.oauth.permissions' 311 309 const permissions = [permission('app.bsky.feed.post:read', {})] as const 312 310 const options = { 313 311 detail: 'Allows reading posts', ··· 326 324 }) 327 325 328 326 it('handles special characters in title', () => { 329 - const nsid = asNsidString('app.bsky.oauth.permissions') 327 + const nsid = 'app.bsky.oauth.permissions' 330 328 const permissions = [permission('app.bsky.feed.post:read', {})] as const 331 329 const options = { 332 330 title: 'Post Management: Read & Write (Full Access)', ··· 340 338 }) 341 339 342 340 it('handles special characters in detail', () => { 343 - const nsid = asNsidString('app.bsky.oauth.permissions') 341 + const nsid = 'app.bsky.oauth.permissions' 344 342 const permissions = [permission('app.bsky.feed.post:read', {})] as const 345 343 const options = { 346 344 detail: ··· 355 353 }) 356 354 357 355 it('handles unicode characters in title', () => { 358 - const nsid = asNsidString('app.bsky.oauth.permissions') 356 + const nsid = 'app.bsky.oauth.permissions' 359 357 const permissions = [permission('app.bsky.feed.post:read', {})] as const 360 358 const options = { 361 359 title: '投稿管理 📝', ··· 367 365 }) 368 366 369 367 it('handles empty strings in title', () => { 370 - const nsid = asNsidString('app.bsky.oauth.permissions') 368 + const nsid = 'app.bsky.oauth.permissions' 371 369 const permissions = [permission('app.bsky.feed.post:read', {})] as const 372 370 const options = { 373 371 title: '', ··· 379 377 }) 380 378 381 379 it('handles empty strings in detail', () => { 382 - const nsid = asNsidString('app.bsky.oauth.permissions') 380 + const nsid = 'app.bsky.oauth.permissions' 383 381 const permissions = [permission('app.bsky.feed.post:read', {})] as const 384 382 const options = { 385 383 detail: '', ··· 391 389 }) 392 390 393 391 it('handles large number of permissions', () => { 394 - const nsid = asNsidString('app.bsky.oauth.permissions') 392 + const nsid = 'app.bsky.oauth.permissions' 395 393 const permissions = Array.from({ length: 100 }, (_, i) => 396 394 permission(`resource${i}:action`, {}), 397 395 ) as any ··· 405 403 406 404 describe('real-world permission set examples', () => { 407 405 it('creates a feed management permission set', () => { 408 - const nsid = asNsidString('app.bsky.oauth.feed') 406 + const nsid = 'app.bsky.oauth.feed' 409 407 const permissions = [ 410 408 permission('app.bsky.feed.post:read', {}), 411 409 permission('app.bsky.feed.post:write', {}), ··· 435 433 }) 436 434 437 435 it('creates a read-only permission set', () => { 438 - const nsid = asNsidString('app.bsky.oauth.readonly') 436 + const nsid = 'app.bsky.oauth.readonly' 439 437 const permissions = [ 440 438 permission('app.bsky.feed.post:read', {}), 441 439 permission('app.bsky.feed.like:read', {}), ··· 456 454 }) 457 455 458 456 it('creates a profile management permission set', () => { 459 - const nsid = asNsidString('app.bsky.oauth.profile') 457 + const nsid = 'app.bsky.oauth.profile' 460 458 const permissions = [ 461 459 permission('app.bsky.actor.profile:read', {}), 462 460 permission('app.bsky.actor.profile:write', {}), ··· 483 481 }) 484 482 485 483 it('creates a minimal permission set', () => { 486 - const nsid = asNsidString('app.bsky.oauth.minimal') 484 + const nsid = 'app.bsky.oauth.minimal' 487 485 const permissions = [ 488 486 permission('app.bsky.actor.profile:read', {}), 489 487 ] as const ··· 501 499 502 500 describe('permission validation', () => { 503 501 it('validates that permissions are Permission instances', () => { 504 - const nsid = asNsidString('app.bsky.oauth.permissions') 502 + const nsid = 'app.bsky.oauth.permissions' 505 503 const permission1 = permission('app.bsky.feed.post:read', {}) 506 504 const permission2 = permission('app.bsky.feed.post:write', {}) 507 505 const permissions = [permission1, permission2] as const ··· 514 512 }) 515 513 516 514 it('preserves permission resource strings', () => { 517 - const nsid = asNsidString('app.bsky.oauth.permissions') 515 + const nsid = 'app.bsky.oauth.permissions' 518 516 const permissions = [ 519 517 permission('app.bsky.feed.post:read', {}), 520 518 permission('app.bsky.feed.like:write', {}), ··· 530 528 }) 531 529 532 530 it('preserves permission options', () => { 533 - const nsid = asNsidString('app.bsky.oauth.permissions') 531 + const nsid = 'app.bsky.oauth.permissions' 534 532 const permissionOptions = { custom: 'value' } 535 533 const permissions = [ 536 534 permission('app.bsky.feed.post:read', permissionOptions), ··· 545 543 546 544 describe('option variations', () => { 547 545 it('accepts title without detail', () => { 548 - const nsid = asNsidString('app.bsky.oauth.permissions') 546 + const nsid = 'app.bsky.oauth.permissions' 549 547 const permissions = [permission('app.bsky.feed.post:read', {})] as const 550 548 const options = { 551 549 title: 'Post Reading', ··· 560 558 }) 561 559 562 560 it('accepts detail without title', () => { 563 - const nsid = asNsidString('app.bsky.oauth.permissions') 561 + const nsid = 'app.bsky.oauth.permissions' 564 562 const permissions = [permission('app.bsky.feed.post:read', {})] as const 565 563 const options = { 566 564 detail: 'Allows reading posts from the feed', ··· 575 573 }) 576 574 577 575 it('accepts title:lang without title', () => { 578 - const nsid = asNsidString('app.bsky.oauth.permissions') 576 + const nsid = 'app.bsky.oauth.permissions' 579 577 const permissions = [permission('app.bsky.feed.post:read', {})] as const 580 578 const options = { 581 579 'title:lang': { ··· 594 592 }) 595 593 596 594 it('accepts detail:lang without detail', () => { 597 - const nsid = asNsidString('app.bsky.oauth.permissions') 595 + const nsid = 'app.bsky.oauth.permissions' 598 596 const permissions = [permission('app.bsky.feed.post:read', {})] as const 599 597 const options = { 600 598 'detail:lang': {
+20 -23
packages/lex/lex-schema/src/schema/query.test.ts
··· 1 1 import { describe, expect, it } from 'vitest' 2 - import { asNsidString } from '../core.js' 3 2 import { integer } from './integer.js' 4 3 import { ObjectSchema, object } from './object.js' 5 4 import { optional } from './optional.js' ··· 12 11 describe('Query', () => { 13 12 describe('constructor', () => { 14 13 it('creates a Query instance with all parameters', () => { 15 - const nsid = asNsidString('app.bsky.feed.getFeedSkeleton') 14 + const nsid = 'app.bsky.feed.getFeedSkeleton' 16 15 const parameters = params({ 17 16 feed: string({ format: 'at-uri' }), 18 17 limit: optional(integer({ minimum: 1, maximum: 100 })), ··· 35 34 }) 36 35 37 36 it('creates a Query instance without errors', () => { 38 - const nsid = asNsidString('app.bsky.feed.getFeedSkeleton') 37 + const nsid = 'app.bsky.feed.getFeedSkeleton' 39 38 const parameters = params({ 40 39 feed: string({ format: 'at-uri' }), 41 40 }) ··· 56 55 }) 57 56 58 57 it('creates a Query instance with empty parameters', () => { 59 - const nsid = asNsidString('app.bsky.actor.getProfile') 58 + const nsid = 'app.bsky.actor.getProfile' 60 59 const parameters = params() 61 60 const output = payload( 62 61 'application/json', ··· 74 73 75 74 describe('properties', () => { 76 75 it('has nsid property', () => { 77 - const nsid = asNsidString('app.bsky.feed.getFeedSkeleton') 76 + const nsid = 'app.bsky.feed.getFeedSkeleton' 78 77 const parameters = params() 79 78 const output = payload('application/json', undefined) 80 79 ··· 85 84 }) 86 85 87 86 it('has parameters property', () => { 88 - const nsid = asNsidString('app.bsky.feed.getFeedSkeleton') 87 + const nsid = 'app.bsky.feed.getFeedSkeleton' 89 88 const parameters = params({ 90 89 feed: string({ format: 'at-uri' }), 91 90 }) ··· 98 97 }) 99 98 100 99 it('has output property', () => { 101 - const nsid = asNsidString('app.bsky.feed.getFeedSkeleton') 100 + const nsid = 'app.bsky.feed.getFeedSkeleton' 102 101 const parameters = params() 103 102 const output = payload('application/json', undefined) 104 103 ··· 109 108 }) 110 109 111 110 it('has errors property', () => { 112 - const nsid = asNsidString('app.bsky.feed.getFeedSkeleton') 111 + const nsid = 'app.bsky.feed.getFeedSkeleton' 113 112 const parameters = params() 114 113 const output = payload('application/json', undefined) 115 114 const errors = ['NotFound', 'RateLimitExceeded'] as const ··· 123 122 124 123 describe('with complex parameters', () => { 125 124 it('creates a Query with multiple parameter types', () => { 126 - const nsid = asNsidString('app.bsky.feed.searchPosts') 125 + const nsid = 'app.bsky.feed.searchPosts' 127 126 const parameters = params({ 128 127 q: string({ minLength: 1 }), 129 128 limit: optional(integer({ minimum: 1, maximum: 100 })), ··· 149 148 150 149 describe('with various output payloads', () => { 151 150 it('creates a Query with undefined output payload', () => { 152 - const nsid = asNsidString('app.bsky.actor.getProfile') 151 + const nsid = 'app.bsky.actor.getProfile' 153 152 const parameters = params({ 154 153 actor: string({ format: 'at-identifier' }), 155 154 }) ··· 163 162 }) 164 163 165 164 it('creates a Query with JSON payload', () => { 166 - const nsid = asNsidString('app.bsky.actor.getProfile') 165 + const nsid = 'app.bsky.actor.getProfile' 167 166 const parameters = params({ 168 167 actor: string({ format: 'at-identifier' }), 169 168 }) ··· 183 182 }) 184 183 185 184 it('creates a Query with text payload', () => { 186 - const nsid = asNsidString('app.bsky.feed.getPost') 185 + const nsid = 'app.bsky.feed.getPost' 187 186 const parameters = params({ 188 187 uri: string({ format: 'at-uri' }), 189 188 }) ··· 198 197 199 198 describe('with different error configurations', () => { 200 199 it('creates a Query with a single error', () => { 201 - const nsid = asNsidString('app.bsky.feed.getPost') 200 + const nsid = 'app.bsky.feed.getPost' 202 201 const parameters = params() 203 202 const output = payload('application/json', undefined) 204 203 const errors = ['NotFound'] as const ··· 209 208 }) 210 209 211 210 it('creates a Query with multiple errors', () => { 212 - const nsid = asNsidString('app.bsky.feed.getPost') 211 + const nsid = 'app.bsky.feed.getPost' 213 212 const parameters = params() 214 213 const output = payload('application/json', undefined) 215 214 const errors = ['NotFound', 'Unauthorized', 'RateLimitExceeded'] as const ··· 224 223 }) 225 224 226 225 it('creates a Query with empty errors array', () => { 227 - const nsid = asNsidString('app.bsky.feed.getPost') 226 + const nsid = 'app.bsky.feed.getPost' 228 227 const parameters = params() 229 228 const output = payload('application/json', undefined) 230 229 const errors = [] as const ··· 237 236 238 237 describe('edge cases', () => { 239 238 it('handles very long NSID', () => { 240 - const nsid = asNsidString( 241 - 'com.example.very.long.namespace.identifier.method.name', 242 - ) 239 + const nsid = 'com.example.very.long.namespace.identifier.method.name' 243 240 const parameters = params() 244 241 const output = payload('application/json', undefined) 245 242 ··· 249 246 }) 250 247 251 248 it('handles myQuery with all optional parameters', () => { 252 - const nsid = asNsidString('app.bsky.feed.search') 249 + const nsid = 'app.bsky.feed.search' 253 250 const parameters = params({ 254 251 q: optional(string()), 255 252 limit: optional(integer()), ··· 263 260 }) 264 261 265 262 it('handles myQuery with complex nested output schema', () => { 266 - const nsid = asNsidString('app.bsky.feed.getTimeline') 263 + const nsid = 'app.bsky.feed.getTimeline' 267 264 const parameters = params() 268 265 const output = payload( 269 266 'application/json', ··· 286 283 287 284 describe('real-world myQuery examples', () => { 288 285 it('creates a getFeedSkeleton myQuery', () => { 289 - const nsid = asNsidString('app.bsky.feed.getFeedSkeleton') 286 + const nsid = 'app.bsky.feed.getFeedSkeleton' 290 287 const parameters = params({ 291 288 feed: string({ format: 'at-uri' }), 292 289 limit: optional(withDefault(integer({ minimum: 1, maximum: 100 }), 50)), ··· 306 303 }) 307 304 308 305 it('creates a searchPosts myQuery', () => { 309 - const nsid = asNsidString('app.bsky.feed.searchPosts') 306 + const nsid = 'app.bsky.feed.searchPosts' 310 307 const parameters = params({ 311 308 q: string({ minLength: 1, maxLength: 300 }), 312 309 limit: optional(withDefault(integer({ minimum: 1, maximum: 100 }), 25)), ··· 329 326 }) 330 327 331 328 it('creates a getProfile myQuery', () => { 332 - const nsid = asNsidString('app.bsky.actor.getProfile') 329 + const nsid = 'app.bsky.actor.getProfile' 333 330 const parameters = params({ 334 331 actor: string({ format: 'at-identifier' }), 335 332 })
+3 -10
packages/lex/lex-schema/src/schema/string.ts
··· 4 4 Schema, 5 5 StringFormat, 6 6 ValidationContext, 7 - assertStringFormat, 7 + isStringFormat, 8 8 } from '../core.js' 9 9 import { memoizedOptions } from '../util/memoize.js' 10 10 import { TokenSchema } from './token.js' ··· 75 75 } 76 76 77 77 const format = this.options?.format 78 - if (format != null) { 79 - try { 80 - // @TODO optimize to avoid throw cost (requires re-writing utilities 81 - // from @atproto/syntax) 82 - assertStringFormat(str, format) 83 - } catch (err) { 84 - const message = err instanceof Error ? err.message : undefined 85 - return ctx.issueInvalidFormat(str, format, message) 86 - } 78 + if (format != null && !isStringFormat(str, format)) { 79 + return ctx.issueInvalidFormat(str, format) 87 80 } 88 81 89 82 return ctx.success(str)
+24 -26
packages/lex/lex-schema/src/schema/subscription.test.ts
··· 1 1 import { describe, expect, expectTypeOf, it } from 'vitest' 2 - import { asNsidString } from '../core.js' 3 2 import { integer } from './integer.js' 4 3 import { ObjectSchema, object } from './object.js' 5 4 import { optional } from './optional.js' ··· 16 15 describe('Subscription', () => { 17 16 describe('constructor', () => { 18 17 it('creates a Subscription instance with all parameters', () => { 19 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 18 + const nsid = 'com.atproto.sync.subscribeRepos' 20 19 const parameters = params({ 21 20 cursor: optional(integer()), 22 21 }) ··· 36 35 }) 37 36 38 37 it('creates a Subscription instance without errors', () => { 39 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 38 + const nsid = 'com.atproto.sync.subscribeRepos' 40 39 const parameters = params({ 41 40 cursor: optional(integer()), 42 41 }) ··· 54 53 }) 55 54 56 55 it('creates a Subscription instance with empty parameters', () => { 57 - const nsid = asNsidString('app.bsky.notification.subscribe') 56 + const nsid = 'app.bsky.notification.subscribe' 58 57 const parameters = params() 59 58 const message = object({ 60 59 type: string(), ··· 69 68 70 69 describe('type property', () => { 71 70 it('has type set to "subscription"', () => { 72 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 71 + const nsid = 'com.atproto.sync.subscribeRepos' 73 72 const parameters = params() 74 73 const message = object({ 75 74 seq: integer(), ··· 81 80 }) 82 81 83 82 it('type is a constant value', () => { 84 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 83 + const nsid = 'com.atproto.sync.subscribeRepos' 85 84 const parameters = params() 86 85 const message = object({ 87 86 seq: integer(), ··· 97 96 98 97 describe('properties', () => { 99 98 it('has nsid property', () => { 100 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 99 + const nsid = 'com.atproto.sync.subscribeRepos' 101 100 const parameters = params() 102 101 const message = object({ 103 102 seq: integer(), ··· 110 109 }) 111 110 112 111 it('has parameters property', () => { 113 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 112 + const nsid = 'com.atproto.sync.subscribeRepos' 114 113 const parameters = params() 115 114 const message = object({ 116 115 seq: integer(), ··· 123 122 }) 124 123 125 124 it('has message property', () => { 126 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 125 + const nsid = 'com.atproto.sync.subscribeRepos' 127 126 const parameters = params() 128 127 const message = object({ 129 128 seq: integer(), ··· 136 135 }) 137 136 138 137 it('has errors property', () => { 139 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 138 + const nsid = 'com.atproto.sync.subscribeRepos' 140 139 const parameters = params() 141 140 const message = object({ 142 141 seq: integer(), ··· 152 151 153 152 describe('with complex parameters', () => { 154 153 it('creates a Subscription with multiple parameter types', () => { 155 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 154 + const nsid = 'com.atproto.sync.subscribeRepos' 156 155 const parameters = params({ 157 156 cursor: optional(integer({ minimum: 0 })), 158 157 includeDeletes: optional(integer({ minimum: 0, maximum: 1 })), ··· 264 263 }) 265 264 266 265 it('InferSubscriptionMessage correctly infers message type', () => { 267 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 266 + const nsid = 'com.atproto.sync.subscribeRepos' 268 267 const parameters = params() 269 268 const message = object({ 270 269 seq: integer(), ··· 284 283 285 284 describe('edge cases', () => { 286 285 it('handles very long NSID', () => { 287 - const nsid = asNsidString( 288 - 'com.example.very.long.namespace.identifier.subscription.name', 289 - ) 286 + const nsid = 287 + 'com.example.very.long.namespace.identifier.subscription.name' 290 288 const parameters = params() 291 289 const message = object({}) 292 290 ··· 296 294 }) 297 295 298 296 it('handles subscription with all optional parameters', () => { 299 - const nsid = asNsidString('app.bsky.feed.subscribe') 297 + const nsid = 'app.bsky.feed.subscribe' 300 298 const parameters = params({ 301 299 cursor: optional(integer()), 302 300 includeDeletes: optional(integer()), ··· 310 308 }) 311 309 312 310 it('handles subscription with complex nested message schema', () => { 313 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 311 + const nsid = 'com.atproto.sync.subscribeRepos' 314 312 const parameters = params() 315 313 const message = object({ 316 314 seq: integer(), ··· 331 329 332 330 describe('real-world subscription examples', () => { 333 331 it('creates a subscribeLabels subscription', () => { 334 - const nsid = asNsidString('com.atproto.label.subscribeLabels') 332 + const nsid = 'com.atproto.label.subscribeLabels' 335 333 const parameters = params({ 336 334 cursor: optional(integer({ minimum: 0 })), 337 335 }) ··· 364 362 }) 365 363 366 364 it('creates a notification subscription', () => { 367 - const nsid = asNsidString('app.bsky.notification.subscribe') 365 + const nsid = 'app.bsky.notification.subscribe' 368 366 const parameters = params({ 369 367 cursor: optional(string()), 370 368 }) ··· 383 381 384 382 describe('with mixed parameter and message types', () => { 385 383 it('handles required and optional parameters with complex message', () => { 386 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 384 + const nsid = 'com.atproto.sync.subscribeRepos' 387 385 const parameters = params({ 388 386 cursor: optional(integer({ minimum: 0 })), 389 387 includeDeletes: optional(integer({ minimum: 0, maximum: 1 })), ··· 408 406 409 407 describe('validation through nested schemas', () => { 410 408 it('parameters can validate input through ParamsSchema', () => { 411 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 409 + const nsid = 'com.atproto.sync.subscribeRepos' 412 410 const parameters = params({ 413 411 cursor: integer({ minimum: 0 }), 414 412 }) ··· 425 423 }) 426 424 427 425 it('message can validate input through ObjectSchema', () => { 428 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 426 + const nsid = 'com.atproto.sync.subscribeRepos' 429 427 const parameters = params() 430 428 const message = object({ 431 429 seq: integer({ minimum: 0 }), ··· 451 449 452 450 describe('property access', () => { 453 451 it('can access nsid after construction', () => { 454 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 452 + const nsid = 'com.atproto.sync.subscribeRepos' 455 453 const parameters = params() 456 454 const message = object({}) 457 455 ··· 462 460 }) 463 461 464 462 it('can access type after construction', () => { 465 - const nsid = asNsidString('com.atproto.sync.subscribeRepos') 463 + const nsid = 'com.atproto.sync.subscribeRepos' 466 464 const parameters = params() 467 465 const message = object({}) 468 466 ··· 475 473 476 474 describe('different message schema types', () => { 477 475 it('constructs with ObjectSchema message', () => { 478 - const nsid = asNsidString('app.bsky.test') 476 + const nsid = 'app.bsky.test' 479 477 const parameters = params() 480 478 const message = object({ 481 479 field: string(), ··· 488 486 }) 489 487 490 488 it('constructs with RefSchema message', () => { 491 - const nsid = asNsidString('app.bsky.test') 489 + const nsid = 'app.bsky.test' 492 490 const parameters = params() 493 491 const message = ref(() => object({})) 494 492
-39
packages/lex/lex-schema/src/util/assertion-util.ts
··· 1 - export type AssertFn<T> = <I extends string>(input: I) => asserts input is I & T 2 - export type CastFn<T> = <I extends string>(input: I) => I & T 3 1 export type CheckFn<T> = <I extends string>(input: I) => input is I & T 4 - 5 - export function createAssertFunction<T extends string>( 6 - checkFn: (input: string) => input is T, 7 - errorMessage?: string, 8 - ): AssertFn<T> 9 - export function createAssertFunction<T extends string>( 10 - checkFn: (input: string) => boolean, 11 - errorMessage?: string, 12 - ): AssertFn<T> 13 - export function createAssertFunction<T extends string>( 14 - checkFn: (input: string) => boolean, 15 - errorMessage = 'Invalid format', 16 - ): AssertFn<T> { 17 - return (input: string) => { 18 - if (!checkFn(input)) throw new Error(errorMessage) 19 - } 20 - } 21 - 22 - /*@__NO_SIDE_EFFECTS__*/ 23 - export function createCastFunction<T>(assertFn: AssertFn<T>): CastFn<T> { 24 - return <I extends string>(input: I) => { 25 - assertFn(input) 26 - return input as I & T 27 - } 28 - } 29 - 30 - /*@__NO_SIDE_EFFECTS__ */ 31 - export function createCheckFunction<T>(assertFn: AssertFn<T>): CheckFn<T> { 32 - return <I extends string>(input: I): input is I & T => { 33 - try { 34 - assertFn(input) 35 - return true 36 - } catch { 37 - return false 38 - } 39 - } 40 - }
+13 -35
packages/lex/lex/tests/string.test.ts
··· 273 273 $type: 'com.example.datetime', 274 274 datetime: 'bad date', 275 275 }), 276 - ).toThrow( 277 - 'Invalid datetime format (datetime did not parse as ISO 8601) at $.datetime (got "bad date")', 278 - ) 276 + ).toThrow('Invalid datetime at $.datetime (got "bad date")') 279 277 }) 280 278 }) 281 279 describe('com.example.uri', () => { ··· 297 295 $type: 'com.example.uri', 298 296 uri: 'not a uri', 299 297 }), 300 - ).toThrow('Invalid uri format (Invalid URI) at $.uri (got "not a uri")') 298 + ).toThrow('Invalid uri at $.uri (got "not a uri")') 301 299 }) 302 300 }) 303 301 describe('com.example.atUri', () => { ··· 311 309 $type: 'com.example.atUri', 312 310 atUri: 'http://not-atproto.com', 313 311 }), 314 - ).toThrow( 315 - 'Invalid at-uri format (ATURI must start with "at://") at $.atUri (got "http://not-atproto.com")', 316 - ) 312 + ).toThrow('Invalid at-uri at $.atUri (got "http://not-atproto.com")') 317 313 }) 318 314 }) 319 315 describe('com.example.did', () => { ··· 332 328 $type: 'com.example.did', 333 329 did: 'bad did', 334 330 }), 335 - ).toThrow( 336 - 'Invalid DID format (DID requires "did:" prefix) at $.did (got "bad did")', 337 - ) 331 + ).toThrow('Invalid DID at $.did (got "bad did")') 338 332 expect(() => 339 333 com.example.did.$parse({ 340 334 $type: 'com.example.did', 341 335 did: 'did:short', 342 336 }), 343 - ).toThrow( 344 - 'Invalid DID format (DID requires prefix, method, and method-specific content) at $.did (got "did:short")', 345 - ) 337 + ).toThrow('Invalid DID at $.did (got "did:short")') 346 338 }) 347 339 }) 348 340 describe('com.example.handle', () => { ··· 361 353 $type: 'com.example.handle', 362 354 handle: 'bad handle', 363 355 }), 364 - ).toThrow( 365 - 'Invalid handle format (Disallowed characters in handle (ASCII letters, digits, dashes, periods only)) at $.handle (got "bad handle")', 366 - ) 356 + ).toThrow('Invalid handle at $.handle (got "bad handle")') 367 357 expect(() => 368 358 com.example.handle.$parse({ 369 359 $type: 'com.example.handle', 370 360 handle: '-bad-.test', 371 361 }), 372 - ).toThrow( 373 - 'Invalid handle format (Handle parts can not start or end with hyphens) at $.handle (got "-bad-.test")', 374 - ) 362 + ).toThrow('Invalid handle at $.handle (got "-bad-.test")') 375 363 }) 376 364 }) 377 365 describe('com.example.atIdentifier', () => { ··· 390 378 $type: 'com.example.atIdentifier', 391 379 atIdentifier: 'bad id', 392 380 }), 393 - ).toThrow( 394 - 'Invalid AT identifier format (Invalid DID or handle) at $.atIdentifier (got "bad id")', 395 - ) 381 + ).toThrow('Invalid AT identifier at $.atIdentifier (got "bad id")') 396 382 expect(() => 397 383 com.example.atIdentifier.$parse({ 398 384 $type: 'com.example.atIdentifier', 399 385 atIdentifier: '-bad-.test', 400 386 }), 401 - ).toThrow( 402 - 'Invalid AT identifier format (Invalid DID or handle) at $.atIdentifier (got "-bad-.test")', 403 - ) 387 + ).toThrow('Invalid AT identifier at $.atIdentifier (got "-bad-.test")') 404 388 }) 405 389 }) 406 390 describe('com.example.nsid', () => { ··· 419 403 $type: 'com.example.nsid', 420 404 nsid: 'bad nsid', 421 405 }), 422 - ).toThrow( 423 - 'Invalid NSID format (Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)) at $.nsid (got "bad nsid")', 424 - ) 406 + ).toThrow('Invalid NSID at $.nsid (got "bad nsid")') 425 407 expect(() => 426 408 com.example.nsid.$parse({ 427 409 $type: 'com.example.nsid', 428 410 nsid: 'com.bad-.foo', 429 411 }), 430 - ).toThrow( 431 - 'Invalid NSID format (NSID parts can not start or end with hyphen) at $.nsid (got "com.bad-.foo")', 432 - ) 412 + ).toThrow('Invalid NSID at $.nsid (got "com.bad-.foo")') 433 413 }) 434 414 }) 435 415 describe('com.example.cid', () => { ··· 444 424 cid: 'abapsdofiuwrpoiasdfuaspdfoiu', 445 425 }), 446 426 ).toThrow( 447 - 'Invalid CID string format (Invalid CID string) at $.cid (got "abapsdofiuwrpoiasdfuaspdfoiu")', 427 + 'Invalid CID string at $.cid (got "abapsdofiuwrpoiasdfuaspdfoiu")', 448 428 ) 449 429 }) 450 430 }) ··· 459 439 $type: 'com.example.language', 460 440 language: 'not-a-language-', 461 441 }), 462 - ).toThrow( 463 - 'Invalid language format (Invalid BCP 47 string) at $.language (got "not-a-language-")', 464 - ) 442 + ).toThrow('Invalid language at $.language (got "not-a-language-")') 465 443 }) 466 444 })
+2 -2
packages/lexicon/src/validators/formats.ts
··· 10 10 ensureValidRecordKey, 11 11 isValidNsid, 12 12 isValidTid, 13 + isValidUri, 13 14 } from '@atproto/syntax' 14 15 import { ValidationError, ValidationResult } from '../types' 15 16 ··· 30 31 } 31 32 32 33 export function uri(path: string, value: string): ValidationResult { 33 - const isUri = value.match(/^\w+:(?:\/\/)?[^\s/][^\s]*$/) !== null 34 - if (!isUri) { 34 + if (!isValidUri(value)) { 35 35 return { 36 36 success: false, 37 37 error: new ValidationError(`${path} must be a uri`),
-7
packages/syntax/jest.config.js
··· 1 - /** @type {import('jest').Config} */ 2 - module.exports = { 3 - displayName: 'Syntax', 4 - transform: { '^.+\\.(t|j)s$': '@swc/jest' }, 5 - setupFiles: ['<rootDir>/../../jest.setup.ts'], 6 - moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': ['$1.ts', '$1.js'] }, 7 - }
+6 -3
packages/syntax/package.json
··· 18 18 "main": "dist/index.js", 19 19 "types": "dist/index.d.ts", 20 20 "scripts": { 21 - "test": "jest", 21 + "test": "vitest run", 22 22 "build": "tsc --build tsconfig.build.json" 23 + }, 24 + "dependencies": { 25 + "tslib": "^2.8.1" 23 26 }, 24 27 "devDependencies": { 25 - "jest": "^28.1.2", 26 - "typescript": "^5.6.3" 28 + "typescript": "^5.6.3", 29 + "vitest": "^4.0.16" 27 30 }, 28 31 "browser": { 29 32 "dns/promises": false
+12 -1
packages/syntax/src/at-identifier.ts
··· 1 - import { DidString, ensureValidDidRegex } from './did.js' 1 + import { DidString, ensureValidDidRegex, isValidDid } from './did.js' 2 2 import { 3 3 HandleString, 4 4 InvalidHandleError, 5 5 ensureValidHandleRegex, 6 + isValidHandle, 6 7 } from './handle.js' 7 8 8 9 export type AtIdentifierString = DidString | HandleString ··· 20 21 throw new InvalidHandleError('Invalid DID or handle', { cause }) 21 22 } 22 23 } 24 + 25 + export function isValidAtIdentifier<I extends string>( 26 + input: I, 27 + ): input is I & AtIdentifierString { 28 + if (input.startsWith('did:')) { 29 + return isValidDid(input) 30 + } else { 31 + return isValidHandle(input) 32 + } 33 + }
+20 -4
packages/syntax/src/aturi_validation.ts
··· 22 22 // - rkey must have at least one char 23 23 // - regardless of path component, a fragment can follow as "#" and then a JSON pointer (RFC-6901) 24 24 25 - export function ensureValidAtUri(input: string): asserts input is AtUriString { 25 + export function ensureValidAtUri<I extends string>( 26 + input: I, 27 + ): asserts input is I & AtUriString { 26 28 const fragmentIndex = input.indexOf('#') 27 29 if (fragmentIndex !== -1) { 28 30 if (input.charCodeAt(fragmentIndex + 1) !== 47) { ··· 105 107 } 106 108 } 107 109 108 - export function ensureValidAtUriRegex(uri: string): asserts uri is AtUriString { 110 + export function ensureValidAtUriRegex<I extends string>( 111 + input: I, 112 + ): asserts input is I & AtUriString { 109 113 // simple regex to enforce most constraints via just regex and length. 110 114 // hand wrote this regex based on above constraints. whew! 111 115 const aturiRegex = 112 116 /^at:\/\/(?<authority>[a-zA-Z0-9._:%-]+)(\/(?<collection>[a-zA-Z0-9-.]+)(\/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/ 113 - const rm = uri.match(aturiRegex) 117 + const rm = input.match(aturiRegex) 114 118 if (!rm || !rm.groups) { 115 119 throw new Error("ATURI didn't validate via regex") 116 120 } ··· 130 134 throw new Error('ATURI collection path segment must be a valid NSID') 131 135 } 132 136 133 - if (uri.length > 8 * 1024) { 137 + if (input.length > 8 * 1024) { 134 138 throw new Error('ATURI is far too long') 135 139 } 136 140 } 141 + 142 + export function isValidAtUri<I extends string>( 143 + input: I, 144 + ): input is I & AtUriString { 145 + try { 146 + ensureValidAtUriRegex(input) 147 + } catch { 148 + return false 149 + } 150 + 151 + return true 152 + }
+13 -14
packages/syntax/src/datetime.ts
··· 12 12 /* Validates datetime string against atproto Lexicon 'datetime' format. 13 13 * Syntax is described at: https://atproto.com/specs/lexicon#datetime 14 14 */ 15 - export function ensureValidDatetime( 16 - dtStr: string, 17 - ): asserts dtStr is DatetimeString { 18 - const date = new Date(dtStr) 15 + export function ensureValidDatetime<I extends string>( 16 + input: I, 17 + ): asserts input is I & DatetimeString { 18 + const date = new Date(input) 19 19 // must parse as ISO 8601; this also verifies semantics like month is not 13 or 00 20 20 if (isNaN(date.getTime())) { 21 21 throw new InvalidDatetimeError('datetime did not parse as ISO 8601') ··· 26 26 // regex and other checks for RFC-3339 27 27 if ( 28 28 !/^[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-6][0-9]:[0-6][0-9](.[0-9]{1,20})?(Z|([+-][0-2][0-9]:[0-5][0-9]))$/.test( 29 - dtStr, 29 + input, 30 30 ) 31 31 ) { 32 32 throw new InvalidDatetimeError("datetime didn't validate via regex") 33 33 } 34 - if (dtStr.length > 64) { 34 + if (input.length > 64) { 35 35 throw new InvalidDatetimeError('datetime is too long (64 chars max)') 36 36 } 37 - if (dtStr.endsWith('-00:00')) { 37 + if (input.endsWith('-00:00')) { 38 38 throw new InvalidDatetimeError( 39 39 'datetime can not use "-00:00" for UTC timezone', 40 40 ) 41 41 } 42 - if (dtStr.startsWith('000')) { 42 + if (input.startsWith('000')) { 43 43 throw new InvalidDatetimeError('datetime so close to year zero not allowed') 44 44 } 45 45 } 46 46 47 47 /* Same logic as ensureValidDatetime(), but returns a boolean instead of throwing an exception. 48 48 */ 49 - export function isValidDatetime(dtStr: string): dtStr is DatetimeString { 49 + export function isValidDatetime<I extends string>( 50 + input: I, 51 + ): input is I & DatetimeString { 50 52 try { 51 - ensureValidDatetime(dtStr) 53 + ensureValidDatetime(input) 52 54 } catch (err) { 53 - if (err instanceof InvalidDatetimeError) { 54 - return false 55 - } 56 - throw err 55 + return false 57 56 } 58 57 59 58 return true
+25 -15
packages/syntax/src/did.ts
··· 14 14 15 15 export type DidString<M extends string = string> = `did:${M}:${string}` 16 16 17 - export function ensureValidDid(did: string): asserts did is DidString { 18 - if (!did.startsWith('did:')) { 17 + export function ensureValidDid<I extends string>( 18 + input: I, 19 + ): asserts input is I & DidString { 20 + if (!input.startsWith('did:')) { 19 21 throw new InvalidDidError('DID requires "did:" prefix') 20 22 } 21 23 24 + if (input.length > 2048) { 25 + throw new InvalidDidError('DID is too long (2048 chars max)') 26 + } 27 + 28 + if (input.endsWith(':') || input.endsWith('%')) { 29 + throw new InvalidDidError('DID can not end with ":" or "%"') 30 + } 31 + 22 32 // check that all chars are boring ASCII 23 - if (!/^[a-zA-Z0-9._:%-]*$/.test(did)) { 33 + if (!/^[a-zA-Z0-9._:%-]*$/.test(input)) { 24 34 throw new InvalidDidError( 25 35 'Disallowed characters in DID (ASCII letters, digits, and a couple other characters only)', 26 36 ) 27 37 } 28 38 29 - const { length, 1: method } = did.split(':') 39 + const { length, 1: method } = input.split(':') 30 40 if (length < 3) { 31 41 throw new InvalidDidError( 32 42 'DID requires prefix, method, and method-specific content', ··· 36 46 if (!/^[a-z]+$/.test(method)) { 37 47 throw new InvalidDidError('DID method must be lower-case letters') 38 48 } 39 - 40 - if (did.endsWith(':') || did.endsWith('%')) { 41 - throw new InvalidDidError('DID can not end with ":" or "%"') 42 - } 43 - 44 - if (did.length > 2 * 1024) { 45 - throw new InvalidDidError('DID is too long (2048 chars max)') 46 - } 47 49 } 48 50 49 - export function ensureValidDidRegex(did: string): asserts did is DidString { 51 + const DID_REGEX = /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/ 52 + 53 + export function ensureValidDidRegex<I extends string>( 54 + input: I, 55 + ): asserts input is I & DidString { 50 56 // simple regex to enforce most constraints via just regex and length. 51 57 // hand wrote this regex based on above constraints 52 - if (!/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/.test(did)) { 58 + if (!DID_REGEX.test(input)) { 53 59 throw new InvalidDidError("DID didn't validate via regex") 54 60 } 55 61 56 - if (did.length > 2 * 1024) { 62 + if (input.length > 2048) { 57 63 throw new InvalidDidError('DID is too long (2048 chars max)') 58 64 } 65 + } 66 + 67 + export function isValidDid<I extends string>(input: I): input is I & DidString { 68 + return input.length <= 2048 && DID_REGEX.test(input) 59 69 } 60 70 61 71 export class InvalidDidError extends Error {}
+15 -13
packages/syntax/src/handle.ts
··· 39 39 // - does not validate whether domain or TLD exists, or is a reserved or 40 40 // special TLD (eg, .onion or .local) 41 41 // - does not validate punycode 42 - export function ensureValidHandle( 43 - handle: string, 44 - ): asserts handle is HandleString { 42 + export function ensureValidHandle<I extends string>( 43 + input: I, 44 + ): asserts input is I & HandleString { 45 45 // check that all chars are boring ASCII 46 - if (!/^[a-zA-Z0-9.-]*$/.test(handle)) { 46 + if (!/^[a-zA-Z0-9.-]*$/.test(input)) { 47 47 throw new InvalidHandleError( 48 48 'Disallowed characters in handle (ASCII letters, digits, dashes, periods only)', 49 49 ) 50 50 } 51 51 52 - if (handle.length > 253) { 52 + if (input.length > 253) { 53 53 throw new InvalidHandleError('Handle is too long (253 chars max)') 54 54 } 55 - const labels = handle.split('.') 55 + const labels = input.split('.') 56 56 if (labels.length < 2) { 57 57 throw new InvalidHandleError('Handle domain needs at least two parts') 58 58 } ··· 81 81 const HANDLE_REGEX = 82 82 /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/ 83 83 84 - export function ensureValidHandleRegex( 85 - handle: string, 86 - ): asserts handle is HandleString { 87 - if (handle.length > 253) { 84 + export function ensureValidHandleRegex<I extends string>( 85 + input: I, 86 + ): asserts input is I & HandleString { 87 + if (input.length > 253) { 88 88 throw new InvalidHandleError('Handle is too long (253 chars max)') 89 89 } 90 - if (!HANDLE_REGEX.test(handle)) { 90 + if (!HANDLE_REGEX.test(input)) { 91 91 throw new InvalidHandleError("Handle didn't validate via regex") 92 92 } 93 93 } ··· 102 102 return normalized 103 103 } 104 104 105 - export function isValidHandle(handle: string): handle is HandleString { 106 - return handle.length <= 253 && HANDLE_REGEX.test(handle) 105 + export function isValidHandle<I extends string>( 106 + input: I, 107 + ): input is I & HandleString { 108 + return input.length <= 253 && HANDLE_REGEX.test(input) 107 109 } 108 110 109 111 export function isValidTld(handle: string): boolean {
+7 -5
packages/syntax/src/index.ts
··· 1 - export * from './handle.js' 1 + export * from './at-identifier.js' 2 + export * from './aturi.js' 3 + export * from './datetime.js' 2 4 export * from './did.js' 5 + export * from './handle.js' 3 6 export * from './nsid.js' 4 - export * from './aturi.js' 5 - export * from './at-identifier.js' 6 - export * from './tid.js' 7 + export * from './language.js' 7 8 export * from './recordkey.js' 8 - export * from './datetime.js' 9 + export * from './tid.js' 10 + export * from './uri.js'
+8 -4
packages/syntax/src/nsid.ts
··· 60 60 } 61 61 } 62 62 63 - export function ensureValidNsid(nsid: string): asserts nsid is NsidString { 64 - const result = validateNsid(nsid) 63 + export function ensureValidNsid<I extends string>( 64 + input: I, 65 + ): asserts input is I & NsidString { 66 + const result = validateNsid(input) 65 67 if (!result.success) throw new InvalidNsidError(result.message) 66 68 } 67 69 ··· 71 73 return result.value 72 74 } 73 75 74 - export function isValidNsid(nsid: string): nsid is NsidString { 76 + export function isValidNsid<I extends string>( 77 + input: I, 78 + ): input is I & NsidString { 75 79 // Since the regex version is more performant for valid NSIDs, we use it when 76 80 // we don't care about error details. 77 - return validateNsidRegex(nsid).success 81 + return validateNsidRegex(input).success 78 82 } 79 83 80 84 type ValidateResult<T> =
+14 -12
packages/syntax/src/recordkey.ts
··· 17 17 // RFC-3986, section 3.3). The above constraints satisfy this condition, by 18 18 // matching the "unreserved" characters allowed in generic URI paths. 19 19 20 - export function ensureValidRecordKey( 21 - rkey: string, 22 - ): asserts rkey is RecordKeyString { 20 + export function ensureValidRecordKey<I extends string>( 21 + input: I, 22 + ): asserts input is I & RecordKeyString { 23 23 if ( 24 - rkey.length > RECORD_KEY_MAX_LENGTH || 25 - rkey.length < RECORD_KEY_MIN_LENGTH 24 + input.length > RECORD_KEY_MAX_LENGTH || 25 + input.length < RECORD_KEY_MIN_LENGTH 26 26 ) { 27 27 throw new InvalidRecordKeyError( 28 28 `record key must be ${RECORD_KEY_MIN_LENGTH} to ${RECORD_KEY_MAX_LENGTH} characters`, 29 29 ) 30 30 } 31 - if (RECORD_KEY_INVALID_VALUES.has(rkey)) { 31 + if (RECORD_KEY_INVALID_VALUES.has(input)) { 32 32 throw new InvalidRecordKeyError('record key can not be "." or ".."') 33 33 } 34 34 // simple regex to enforce most constraints via just regex and length. 35 - if (!RECORD_KEY_REGEX.test(rkey)) { 35 + if (!RECORD_KEY_REGEX.test(input)) { 36 36 throw new InvalidRecordKeyError('record key syntax not valid (regex)') 37 37 } 38 38 } 39 39 40 - export function isValidRecordKey(rkey: string): rkey is RecordKeyString { 40 + export function isValidRecordKey<I extends string>( 41 + input: I, 42 + ): input is I & RecordKeyString { 41 43 return ( 42 - rkey.length >= RECORD_KEY_MIN_LENGTH && 43 - rkey.length <= RECORD_KEY_MAX_LENGTH && 44 - RECORD_KEY_REGEX.test(rkey) && 45 - !RECORD_KEY_INVALID_VALUES.has(rkey) 44 + input.length >= RECORD_KEY_MIN_LENGTH && 45 + input.length <= RECORD_KEY_MAX_LENGTH && 46 + RECORD_KEY_REGEX.test(input) && 47 + !RECORD_KEY_INVALID_VALUES.has(input) 46 48 ) 47 49 } 48 50
+7 -5
packages/syntax/src/tid.ts
··· 3 3 const TID_LENGTH = 13 4 4 const TID_REGEX = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/ 5 5 6 - export function ensureValidTid(tid: string): asserts tid is TidString { 7 - if (tid.length !== TID_LENGTH) { 6 + export function ensureValidTid<I extends string>( 7 + input: I, 8 + ): asserts input is I & TidString { 9 + if (input.length !== TID_LENGTH) { 8 10 throw new InvalidTidError(`TID must be ${TID_LENGTH} characters`) 9 11 } 10 12 // simple regex to enforce most constraints via just regex and length. 11 - if (!TID_REGEX.test(tid)) { 13 + if (!TID_REGEX.test(input)) { 12 14 throw new InvalidTidError('TID syntax not valid (regex)') 13 15 } 14 16 } 15 17 16 - export function isValidTid(tid: string): tid is TidString { 17 - return tid.length === TID_LENGTH && TID_REGEX.test(tid) 18 + export function isValidTid<I extends string>(input: I): input is I & TidString { 19 + return input.length === TID_LENGTH && TID_REGEX.test(input) 18 20 } 19 21 20 22 export class InvalidTidError extends Error {}
+5
packages/syntax/src/uri.ts
··· 1 + export type UriString = `${string}:${string}` 2 + 3 + export function isValidUri<I extends string>(input: I): input is I & UriString { 4 + return /^\w+:(?:\/\/)?[^\s/][^\s]*$/.test(input) 5 + }
+3 -2
packages/syntax/tests/aturi.test.ts
··· 1 1 import * as fs from 'node:fs' 2 2 import * as readline from 'node:readline' 3 - import { AtUri, ensureValidAtUri, ensureValidAtUriRegex } from '../src/index' 3 + import { describe, expect, it } from 'vitest' 4 + import { AtUri, ensureValidAtUri, ensureValidAtUriRegex } from '../src' 4 5 5 - describe('At Uris', () => { 6 + describe(AtUri, () => { 6 7 it('parses valid at uris', () => { 7 8 // input host path query hash 8 9 type AtUriTest = [string, string, string, string, string]
+1
packages/syntax/tests/datetime.test.ts
··· 1 1 import * as fs from 'node:fs' 2 2 import * as readline from 'node:readline' 3 + import { describe, expect, it } from 'vitest' 3 4 import { 4 5 InvalidDatetimeError, 5 6 ensureValidDatetime,
+1
packages/syntax/tests/did.test.ts
··· 1 1 import * as fs from 'node:fs' 2 2 import * as readline from 'node:readline' 3 + import { describe, expect, it } from 'vitest' 3 4 import { InvalidDidError, ensureValidDid, ensureValidDidRegex } from '../src' 4 5 5 6 describe('DID permissive validation', () => {
+1
packages/syntax/tests/handle.test.ts
··· 1 1 import * as fs from 'node:fs' 2 2 import * as readline from 'node:readline' 3 + import { describe, expect, it } from 'vitest' 3 4 import { 4 5 InvalidHandleError, 5 6 ensureValidHandle,
+88
packages/syntax/tests/language.test.ts
··· 1 + import { describe, expect, it } from 'vitest' 2 + import { isValidLanguage, parseLanguageString } from '../src' 3 + 4 + describe(isValidLanguage, () => { 5 + it('validates BCP 47', () => { 6 + // valid 7 + expect(isValidLanguage('de')).toEqual(true) 8 + expect(isValidLanguage('de-CH')).toEqual(true) 9 + expect(isValidLanguage('de-DE-1901')).toEqual(true) 10 + expect(isValidLanguage('es-419')).toEqual(true) 11 + expect(isValidLanguage('sl-IT-nedis')).toEqual(true) 12 + expect(isValidLanguage('mn-Cyrl-MN')).toEqual(true) 13 + expect(isValidLanguage('x-fr-CH')).toEqual(true) 14 + expect( 15 + isValidLanguage('en-GB-boont-r-extended-sequence-x-private'), 16 + ).toEqual(true) 17 + expect(isValidLanguage('sr-Cyrl')).toEqual(true) 18 + expect(isValidLanguage('hy-Latn-IT-arevela')).toEqual(true) 19 + expect(isValidLanguage('i-klingon')).toEqual(true) 20 + // invalid 21 + expect(isValidLanguage('')).toEqual(false) 22 + expect(isValidLanguage('x')).toEqual(false) 23 + expect(isValidLanguage('de-CH-')).toEqual(false) 24 + expect(isValidLanguage('i-bad-grandfathered')).toEqual(false) 25 + }) 26 + }) 27 + 28 + describe(parseLanguageString, () => { 29 + it('parses BCP 47', () => { 30 + // valid 31 + expect(parseLanguageString('de')).toEqual({ 32 + language: 'de', 33 + }) 34 + expect(parseLanguageString('de-CH')).toEqual({ 35 + language: 'de', 36 + region: 'CH', 37 + }) 38 + expect(parseLanguageString('de-DE-1901')).toEqual({ 39 + language: 'de', 40 + region: 'DE', 41 + variant: '1901', 42 + }) 43 + expect(parseLanguageString('es-419')).toEqual({ 44 + language: 'es', 45 + region: '419', 46 + }) 47 + expect(parseLanguageString('sl-IT-nedis')).toEqual({ 48 + language: 'sl', 49 + region: 'IT', 50 + variant: 'nedis', 51 + }) 52 + expect(parseLanguageString('mn-Cyrl-MN')).toEqual({ 53 + language: 'mn', 54 + script: 'Cyrl', 55 + region: 'MN', 56 + }) 57 + expect(parseLanguageString('x-fr-CH')).toEqual({ 58 + privateUse: 'x-fr-CH', 59 + }) 60 + expect( 61 + parseLanguageString('en-GB-boont-r-extended-sequence-x-private'), 62 + ).toEqual({ 63 + language: 'en', 64 + region: 'GB', 65 + variant: 'boont', 66 + extension: 'r-extended-sequence', 67 + privateUse: 'x-private', 68 + }) 69 + expect(parseLanguageString('sr-Cyrl')).toEqual({ 70 + language: 'sr', 71 + script: 'Cyrl', 72 + }) 73 + expect(parseLanguageString('hy-Latn-IT-arevela')).toEqual({ 74 + language: 'hy', 75 + script: 'Latn', 76 + region: 'IT', 77 + variant: 'arevela', 78 + }) 79 + expect(parseLanguageString('i-klingon')).toEqual({ 80 + grandfathered: 'i-klingon', 81 + }) 82 + // invalid 83 + expect(parseLanguageString('')).toEqual(null) 84 + expect(parseLanguageString('x')).toEqual(null) 85 + expect(parseLanguageString('de-CH-')).toEqual(null) 86 + expect(parseLanguageString('i-bad-grandfathered')).toEqual(null) 87 + }) 88 + })
+1
packages/syntax/tests/nsid.test.ts
··· 1 1 import * as fs from 'node:fs' 2 + import { describe, expect, it } from 'vitest' 2 3 import { 3 4 InvalidNsidError, 4 5 NSID,
+1
packages/syntax/tests/recordkey.test.ts
··· 1 1 import * as fs from 'node:fs' 2 2 import * as readline from 'node:readline' 3 + import { describe, expect, it } from 'vitest' 3 4 import { InvalidRecordKeyError, ensureValidRecordKey } from '../src' 4 5 5 6 describe('recordkey validation', () => {
+1
packages/syntax/tests/tid.test.ts
··· 1 1 import * as fs from 'node:fs' 2 2 import * as readline from 'node:readline' 3 + import { describe, expect, it } from 'vitest' 3 4 import { InvalidTidError, ensureValidTid } from '../src' 4 5 5 6 describe('tid validation', () => {
+5 -2
packages/syntax/tsconfig.build.json
··· 1 1 { 2 2 "extends": "../../tsconfig/isomorphic.json", 3 + "include": ["./src"], 4 + "exclude": ["**/*.test.ts"], 3 5 "compilerOptions": { 4 6 "noImplicitAny": true, 7 + "importHelpers": true, 8 + "target": "ES2023", 5 9 "rootDir": "./src", 6 10 "outDir": "./dist" 7 - }, 8 - "include": ["./src"] 11 + } 9 12 }
+6 -4
packages/syntax/tsconfig.tests.json
··· 1 1 { 2 - "extends": "../../tsconfig/tests.json", 2 + "extends": "../../tsconfig/vitest.json", 3 + "include": ["./tests", "./src/**/*.test.ts"], 3 4 "compilerOptions": { 4 - "rootDir": "." 5 - }, 6 - "include": ["./tests"] 5 + "noImplicitAny": true, 6 + "rootDir": "./", 7 + "baseUrl": "./" 8 + } 7 9 }
+5
packages/syntax/vitest.config.ts
··· 1 + import { defineProject } from 'vitest/config' 2 + 3 + export default defineProject({ 4 + test: {}, 5 + })
+10 -6
pnpm-lock.yaml
··· 454 454 '@atproto/lex-json': 455 455 specifier: workspace:* 456 456 version: link:../lex/lex-json 457 + '@atproto/syntax': 458 + specifier: workspace:* 459 + version: link:../syntax 457 460 zod: 458 461 specifier: ^3.23.8 459 462 version: 3.24.2 ··· 909 912 910 913 packages/lex/lex-data: 911 914 dependencies: 912 - '@atproto/syntax': 913 - specifier: workspace:* 914 - version: link:../../syntax 915 915 multiformats: 916 916 specifier: ^9.9.0 917 917 version: 9.9.0 ··· 2113 2113 version: 5.8.2 2114 2114 2115 2115 packages/syntax: 2116 + dependencies: 2117 + tslib: 2118 + specifier: ^2.8.1 2119 + version: 2.8.1 2116 2120 devDependencies: 2117 - jest: 2118 - specifier: ^28.1.2 2119 - version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2) 2120 2121 typescript: 2121 2122 specifier: ^5.6.3 2122 2123 version: 5.8.2 2124 + vitest: 2125 + specifier: ^4.0.16 2126 + version: 4.0.16(@types/node@18.19.67) 2123 2127 2124 2128 packages/tap: 2125 2129 dependencies:
+1 -1
vitest.config.ts
··· 11 11 '**/tests/**', 12 12 ], 13 13 }, 14 - projects: ['packages/lex/*', 'packages/tap'], 14 + projects: ['packages/lex/*', 'packages/syntax', 'packages/tap'], 15 15 }, 16 16 })