@atproto/lex-installer 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fs.d.ts +4 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +24 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/lex-installer.d.ts +2143 -0
- package/dist/lex-installer.d.ts.map +1 -0
- package/dist/lex-installer.js +249 -0
- package/dist/lex-installer.js.map +1 -0
- package/dist/lexicons-manifest.d.ts +31 -0
- package/dist/lexicons-manifest.d.ts.map +1 -0
- package/dist/lexicons-manifest.js +15 -0
- package/dist/lexicons-manifest.js.map +1 -0
- package/dist/nsid-map.d.ts +32 -0
- package/dist/nsid-map.d.ts.map +1 -0
- package/dist/nsid-map.js +69 -0
- package/dist/nsid-map.js.map +1 -0
- package/dist/nsid-set.d.ts +30 -0
- package/dist/nsid-set.d.ts.map +1 -0
- package/dist/nsid-set.js +66 -0
- package/dist/nsid-set.js.map +1 -0
- package/package.json +51 -0
- package/src/fs.ts +24 -0
- package/src/index.ts +41 -0
- package/src/lex-installer.ts +314 -0
- package/src/lexicons-manifest.test.ts +50 -0
- package/src/lexicons-manifest.ts +23 -0
- package/src/nsid-map.ts +83 -0
- package/src/nsid-set.ts +77 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto/lex-installer",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Lexicon document packet manager for AT Lexicons",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"atproto",
|
|
8
|
+
"lexicon",
|
|
9
|
+
"install",
|
|
10
|
+
"lex"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://atproto.com",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/bluesky-social/atproto",
|
|
16
|
+
"directory": "packages/lex/lex-installer"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"./src",
|
|
20
|
+
"./dist"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"type": "commonjs",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"browser": "./dist/index.js",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"require": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@atproto/lex-builder": "workspace:*",
|
|
36
|
+
"@atproto/lex-cbor": "workspace:*",
|
|
37
|
+
"@atproto/lex-data": "workspace:*",
|
|
38
|
+
"@atproto/lex-document": "workspace:*",
|
|
39
|
+
"@atproto/lex-resolver": "workspace:*",
|
|
40
|
+
"@atproto/lex-schema": "workspace:*",
|
|
41
|
+
"@atproto/syntax": "workspace:*",
|
|
42
|
+
"tslib": "^2.8.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"jest": "^28.1.2"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc --build tsconfig.build.json",
|
|
49
|
+
"test": "jest"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/fs.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export async function readJsonFile(path: string): Promise<unknown> {
|
|
5
|
+
const contents = await readFile(path, 'utf8')
|
|
6
|
+
return JSON.parse(contents)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function writeJsonFile(
|
|
10
|
+
path: string,
|
|
11
|
+
data: unknown,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
await mkdir(dirname(path), { recursive: true })
|
|
14
|
+
const contents = JSON.stringify(data, null, 2)
|
|
15
|
+
await writeFile(path, contents, {
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
mode: 0o644,
|
|
18
|
+
flag: 'w', // override
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isEnoentError(err: unknown): boolean {
|
|
23
|
+
return err instanceof Error && 'code' in err && err.code === 'ENOENT'
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isEnoentError, readJsonFile } from './fs.js'
|
|
2
|
+
import { LexInstaller, LexInstallerOptions } from './lex-installer.js'
|
|
3
|
+
import {
|
|
4
|
+
LexiconsManifest,
|
|
5
|
+
lexiconsManifestSchema,
|
|
6
|
+
} from './lexicons-manifest.js'
|
|
7
|
+
|
|
8
|
+
export type LexInstallOptions = LexInstallerOptions & {
|
|
9
|
+
add?: string[]
|
|
10
|
+
save?: boolean
|
|
11
|
+
ci?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function install(options: LexInstallOptions) {
|
|
15
|
+
const manifest: LexiconsManifest | undefined = await readJsonFile(
|
|
16
|
+
options.manifest,
|
|
17
|
+
).then(
|
|
18
|
+
(json) => lexiconsManifestSchema.parse(json),
|
|
19
|
+
(cause: unknown) => {
|
|
20
|
+
if (isEnoentError(cause)) return undefined
|
|
21
|
+
throw new Error('Failed to read lexicons manifest', { cause })
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const additions = new Set(options.add)
|
|
26
|
+
|
|
27
|
+
// Perform the installation using the existing manifest as "hint"
|
|
28
|
+
await using installer = new LexInstaller(options)
|
|
29
|
+
|
|
30
|
+
await installer.install({ additions, manifest })
|
|
31
|
+
|
|
32
|
+
// Verify lockfile
|
|
33
|
+
if (options.ci && (!manifest || !installer.matches(manifest))) {
|
|
34
|
+
throw new Error('Lexicons manifest is out of date')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Save changes if requested
|
|
38
|
+
if (options.save) {
|
|
39
|
+
await installer.save()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { LexiconDirectoryIndexer } from '@atproto/lex-builder'
|
|
3
|
+
import { cidForLex } from '@atproto/lex-cbor'
|
|
4
|
+
import { lexEquals } from '@atproto/lex-data'
|
|
5
|
+
import {
|
|
6
|
+
LexiconDocument,
|
|
7
|
+
LexiconParameters,
|
|
8
|
+
LexiconPermission,
|
|
9
|
+
LexiconRef,
|
|
10
|
+
LexiconRefUnion,
|
|
11
|
+
LexiconUnknown,
|
|
12
|
+
MainLexiconDefinition,
|
|
13
|
+
NamedLexiconDefinition,
|
|
14
|
+
} from '@atproto/lex-document'
|
|
15
|
+
import { LexResolver, LexResolverOptions } from '@atproto/lex-resolver'
|
|
16
|
+
import { AtUri as AtUriString, Nsid as NsidString } from '@atproto/lex-schema'
|
|
17
|
+
import { AtUri, NSID } from '@atproto/syntax'
|
|
18
|
+
import { isEnoentError, writeJsonFile } from './fs.js'
|
|
19
|
+
import { LexiconsManifest } from './lexicons-manifest.js'
|
|
20
|
+
import { NsidMap } from './nsid-map.js'
|
|
21
|
+
import { NsidSet } from './nsid-set.js'
|
|
22
|
+
|
|
23
|
+
export type LexInstallerOptions = LexResolverOptions & {
|
|
24
|
+
lexicons: string
|
|
25
|
+
manifest: string
|
|
26
|
+
update?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class LexInstaller implements AsyncDisposable {
|
|
30
|
+
protected readonly lexiconResolver: LexResolver
|
|
31
|
+
protected readonly indexer: LexiconDirectoryIndexer
|
|
32
|
+
protected readonly documents = new NsidMap<LexiconDocument>()
|
|
33
|
+
protected readonly manifest: LexiconsManifest = {
|
|
34
|
+
version: 1,
|
|
35
|
+
lexicons: [],
|
|
36
|
+
resolutions: {},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
constructor(protected readonly options: LexInstallerOptions) {
|
|
40
|
+
this.lexiconResolver = new LexResolver(options)
|
|
41
|
+
this.indexer = new LexiconDirectoryIndexer({
|
|
42
|
+
lexicons: options.lexicons,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
47
|
+
await this.indexer[Symbol.asyncDispose]()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
matches(manifest: LexiconsManifest): boolean {
|
|
51
|
+
return lexEquals(this.manifest, manifest)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async install({
|
|
55
|
+
additions,
|
|
56
|
+
manifest,
|
|
57
|
+
}: {
|
|
58
|
+
additions?: Iterable<string>
|
|
59
|
+
manifest?: LexiconsManifest
|
|
60
|
+
} = {}): Promise<void> {
|
|
61
|
+
const roots = new NsidMap<AtUri | null>()
|
|
62
|
+
|
|
63
|
+
// First, process explicit additions
|
|
64
|
+
for (const lexicon of new Set(additions)) {
|
|
65
|
+
const [nsid, uri]: [NSID, AtUri | null] = lexicon.startsWith('at://')
|
|
66
|
+
? ((uri) => [NSID.from(uri.rkey), uri])(new AtUri(lexicon))
|
|
67
|
+
: [NSID.from(lexicon), null]
|
|
68
|
+
|
|
69
|
+
if (roots.has(nsid)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Duplicate lexicon addition: ${nsid} (${roots.get(nsid) ?? lexicon})`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
roots.set(nsid, uri)
|
|
76
|
+
console.debug(`Adding new lexicon: ${nsid} (${uri ?? 'from NSID'})`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Next, restore previously existing manifest entries
|
|
80
|
+
if (manifest) {
|
|
81
|
+
for (const lexicon of manifest.lexicons) {
|
|
82
|
+
const nsid = NSID.from(lexicon)
|
|
83
|
+
|
|
84
|
+
// Skip entries already added explicitly
|
|
85
|
+
if (!roots.has(nsid)) {
|
|
86
|
+
const uri = manifest.resolutions[lexicon]
|
|
87
|
+
? new AtUri(manifest.resolutions[lexicon].uri)
|
|
88
|
+
: null
|
|
89
|
+
|
|
90
|
+
roots.set(nsid, uri)
|
|
91
|
+
|
|
92
|
+
console.debug(
|
|
93
|
+
`Adding lexicon from manifest: ${nsid} (${uri ?? 'from NSID'})`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Install all root lexicons (and store them in the manifest)
|
|
100
|
+
await Promise.all(
|
|
101
|
+
Array.from(roots, async ([nsid, sourceUri]) => {
|
|
102
|
+
console.debug(`Installing lexicon: ${nsid}`)
|
|
103
|
+
|
|
104
|
+
const { document } = sourceUri
|
|
105
|
+
? await this.installFromUri(sourceUri)
|
|
106
|
+
: await this.installFromNsid(nsid)
|
|
107
|
+
|
|
108
|
+
// Store the direct reference in the new manifest
|
|
109
|
+
this.manifest.lexicons.push(document.id)
|
|
110
|
+
this.manifest.lexicons.sort()
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// Then recursively install all referenced lexicons
|
|
115
|
+
let results: unknown[]
|
|
116
|
+
do {
|
|
117
|
+
results = await Promise.all(
|
|
118
|
+
Array.from(this.getMissingIds(), async (nsid) => {
|
|
119
|
+
console.debug(`Resolving dependency lexicon: ${nsid}`)
|
|
120
|
+
|
|
121
|
+
const nsidStr = nsid.toString() as NsidString
|
|
122
|
+
const resolvedUri = manifest?.resolutions[nsidStr]?.uri
|
|
123
|
+
? new AtUri(manifest.resolutions[nsidStr].uri)
|
|
124
|
+
: null
|
|
125
|
+
if (resolvedUri) {
|
|
126
|
+
await this.installFromUri(resolvedUri)
|
|
127
|
+
} else {
|
|
128
|
+
await this.installFromNsid(nsid)
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
} while (results.length > 0)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
protected getMissingIds(): NsidSet {
|
|
136
|
+
const missing = new NsidSet()
|
|
137
|
+
|
|
138
|
+
for (const document of this.documents.values()) {
|
|
139
|
+
for (const nsid of listDocumentNsidRefs(document)) {
|
|
140
|
+
if (!this.documents.has(nsid)) {
|
|
141
|
+
missing.add(nsid)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return missing
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected async installFromNsid(nsid: NSID) {
|
|
150
|
+
const uri = await this.lexiconResolver.resolve(nsid)
|
|
151
|
+
return this.installFromUri(uri)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected async installFromUri(uri: AtUri): Promise<{
|
|
155
|
+
document: LexiconDocument
|
|
156
|
+
uri: AtUri
|
|
157
|
+
}> {
|
|
158
|
+
const document = this.options.update
|
|
159
|
+
? await this.fetch(uri)
|
|
160
|
+
: await this.indexer.get(uri.rkey).then(
|
|
161
|
+
(document) => {
|
|
162
|
+
console.debug(`Re-using existing lexicon ${uri.rkey} from indexer`)
|
|
163
|
+
return document
|
|
164
|
+
},
|
|
165
|
+
(err) => {
|
|
166
|
+
if (isEnoentError(err)) return this.fetch(uri)
|
|
167
|
+
throw err
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
this.documents.set(NSID.from(document.id), document)
|
|
172
|
+
this.manifest.resolutions[document.id] = {
|
|
173
|
+
cid: (await cidForLex(document)).toString(),
|
|
174
|
+
uri: uri.toString() as AtUriString,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { document, uri }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async fetch(uri: AtUri): Promise<LexiconDocument> {
|
|
181
|
+
console.debug(`Fetching lexicon from ${uri}...`)
|
|
182
|
+
|
|
183
|
+
const document = await this.lexiconResolver.fetch(uri, {
|
|
184
|
+
noCache: this.options.update,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const basePath = join(this.options.lexicons, ...document.id.split('.'))
|
|
188
|
+
await writeJsonFile(`${basePath}.json`, document)
|
|
189
|
+
|
|
190
|
+
return document
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async save(): Promise<void> {
|
|
194
|
+
await writeJsonFile(this.options.manifest, this.manifest)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function* listDocumentNsidRefs(doc: LexiconDocument): Iterable<NSID> {
|
|
199
|
+
try {
|
|
200
|
+
for (const def of Object.values(doc.defs)) {
|
|
201
|
+
if (def) {
|
|
202
|
+
for (const ref of defRefs(def)) {
|
|
203
|
+
const [nsid] = ref.split('#', 1)
|
|
204
|
+
if (nsid) yield NSID.from(nsid)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (cause) {
|
|
209
|
+
throw new Error(`Failed to extract refs from lexicon ${doc.id}`, { cause })
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function* defRefs(
|
|
214
|
+
def:
|
|
215
|
+
| MainLexiconDefinition
|
|
216
|
+
| NamedLexiconDefinition
|
|
217
|
+
| LexiconPermission
|
|
218
|
+
| LexiconUnknown
|
|
219
|
+
| LexiconParameters
|
|
220
|
+
| LexiconRef
|
|
221
|
+
| LexiconRefUnion,
|
|
222
|
+
): Iterable<string> {
|
|
223
|
+
switch (def.type) {
|
|
224
|
+
case 'string':
|
|
225
|
+
if (def.knownValues) {
|
|
226
|
+
for (const val of def.knownValues) {
|
|
227
|
+
// Tokens ?
|
|
228
|
+
const { length, 0: nsid, 1: hash } = val.split('#')
|
|
229
|
+
if (length === 2 && hash) {
|
|
230
|
+
try {
|
|
231
|
+
NSID.from(nsid)
|
|
232
|
+
yield val
|
|
233
|
+
} catch {
|
|
234
|
+
// ignore invalid nsid
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return
|
|
240
|
+
case 'array':
|
|
241
|
+
return yield* defRefs(def.items)
|
|
242
|
+
case 'params':
|
|
243
|
+
case 'object':
|
|
244
|
+
for (const prop of Object.values(def.properties)) {
|
|
245
|
+
yield* defRefs(prop)
|
|
246
|
+
}
|
|
247
|
+
return
|
|
248
|
+
case 'union':
|
|
249
|
+
yield* def.refs
|
|
250
|
+
return
|
|
251
|
+
case 'ref': {
|
|
252
|
+
yield def.ref
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
case 'record':
|
|
256
|
+
yield* defRefs(def.record)
|
|
257
|
+
return
|
|
258
|
+
case 'procedure':
|
|
259
|
+
if (def.input?.schema) {
|
|
260
|
+
yield* defRefs(def.input.schema)
|
|
261
|
+
}
|
|
262
|
+
// fallthrough
|
|
263
|
+
case 'query':
|
|
264
|
+
if (def.output?.schema) {
|
|
265
|
+
yield* defRefs(def.output.schema)
|
|
266
|
+
}
|
|
267
|
+
// fallthrough
|
|
268
|
+
case 'subscription':
|
|
269
|
+
if (def.parameters) {
|
|
270
|
+
yield* defRefs(def.parameters)
|
|
271
|
+
}
|
|
272
|
+
if ('message' in def && def.message?.schema) {
|
|
273
|
+
yield* defRefs(def.message.schema)
|
|
274
|
+
}
|
|
275
|
+
return
|
|
276
|
+
case 'permission-set':
|
|
277
|
+
for (const permission of def.permissions) {
|
|
278
|
+
yield* defRefs(permission)
|
|
279
|
+
}
|
|
280
|
+
return
|
|
281
|
+
case 'permission':
|
|
282
|
+
if (def.resource === 'rpc') {
|
|
283
|
+
if (Array.isArray(def.lxm)) {
|
|
284
|
+
for (const lxm of def.lxm) {
|
|
285
|
+
if (typeof lxm === 'string') {
|
|
286
|
+
yield lxm
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} else if (def.resource === 'repo') {
|
|
291
|
+
if (Array.isArray(def.collection)) {
|
|
292
|
+
for (const lxm of def.collection) {
|
|
293
|
+
if (typeof lxm === 'string') {
|
|
294
|
+
yield lxm
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return
|
|
300
|
+
case 'boolean':
|
|
301
|
+
case 'cid-link':
|
|
302
|
+
case 'token':
|
|
303
|
+
case 'bytes':
|
|
304
|
+
case 'blob':
|
|
305
|
+
case 'integer':
|
|
306
|
+
case 'unknown':
|
|
307
|
+
// @NOTE We explicitly list all types here to ensure exhaustiveness
|
|
308
|
+
// causing TS to error if a new type is added without updating this switch
|
|
309
|
+
return
|
|
310
|
+
default:
|
|
311
|
+
// @ts-expect-error
|
|
312
|
+
throw new Error(`Unknown lexicon def type: ${def.type}`)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { lexiconsManifestSchema } from './lexicons-manifest.js'
|
|
2
|
+
|
|
3
|
+
describe('lexiconsManifestSchema', () => {
|
|
4
|
+
it('parses a valid manifest', () => {
|
|
5
|
+
expect(
|
|
6
|
+
lexiconsManifestSchema.parse({
|
|
7
|
+
version: 1,
|
|
8
|
+
lexicons: ['com.example.lexicon'],
|
|
9
|
+
resolutions: {
|
|
10
|
+
'com.example.lexicon': {
|
|
11
|
+
uri: 'at://did:plc:foobar/com.atproto.lexicon.schema/com.example.lexicon',
|
|
12
|
+
cid: 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
).toEqual({
|
|
17
|
+
version: 1,
|
|
18
|
+
lexicons: ['com.example.lexicon'],
|
|
19
|
+
resolutions: {
|
|
20
|
+
'com.example.lexicon': {
|
|
21
|
+
uri: 'at://did:plc:foobar/com.atproto.lexicon.schema/com.example.lexicon',
|
|
22
|
+
cid: 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects an invalid manifest', () => {
|
|
29
|
+
expect(() =>
|
|
30
|
+
lexiconsManifestSchema.parse({
|
|
31
|
+
version: 1,
|
|
32
|
+
lexicons: ['com.example.lexicon'],
|
|
33
|
+
resolutions: {
|
|
34
|
+
'com.example.lexicon': {
|
|
35
|
+
uri: 'invalid-uri',
|
|
36
|
+
cid: 'not-a-cid',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
).toThrow()
|
|
41
|
+
|
|
42
|
+
expect(() =>
|
|
43
|
+
lexiconsManifestSchema.parse({
|
|
44
|
+
version: 2,
|
|
45
|
+
lexicons: ['com.example.lexicon'],
|
|
46
|
+
resolutions: {},
|
|
47
|
+
}),
|
|
48
|
+
).toThrow()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { l } from '@atproto/lex-schema'
|
|
2
|
+
|
|
3
|
+
export const lexiconsManifestSchema = l.object(
|
|
4
|
+
{
|
|
5
|
+
version: l.literal(1),
|
|
6
|
+
lexicons: l.array(l.string({ format: 'nsid' })),
|
|
7
|
+
resolutions: l.dict(
|
|
8
|
+
l.string({ format: 'nsid' }),
|
|
9
|
+
l.object(
|
|
10
|
+
{
|
|
11
|
+
cid: l.string({ format: 'cid' }),
|
|
12
|
+
uri: l.string({ format: 'at-uri' }),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
required: ['cid', 'uri'],
|
|
16
|
+
},
|
|
17
|
+
),
|
|
18
|
+
),
|
|
19
|
+
},
|
|
20
|
+
{ required: ['version', 'lexicons', 'resolutions'] },
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export type LexiconsManifest = l.Infer<typeof lexiconsManifestSchema>
|
package/src/nsid-map.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { NSID } from '@atproto/syntax'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Map implementation that maps keys of type K to an internal representation
|
|
5
|
+
* I. Key identity is determined by the {@link Object.is} comparison of the
|
|
6
|
+
* encoded keys.
|
|
7
|
+
*
|
|
8
|
+
* This is typically useful for values that can be serialized to a unique string
|
|
9
|
+
* representation.
|
|
10
|
+
*/
|
|
11
|
+
class MappedMap<K, V, I = any> implements Map<K, V> {
|
|
12
|
+
private map = new Map<I, V>()
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly encodeKey: (key: K) => I,
|
|
16
|
+
private readonly decodeKey: (enc: I) => K,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
get size(): number {
|
|
20
|
+
return this.map.size
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clear(): void {
|
|
24
|
+
this.map.clear()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set(key: K, value: V): this {
|
|
28
|
+
this.map.set(this.encodeKey(key), value)
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(key: K): V | undefined {
|
|
33
|
+
return this.map.get(this.encodeKey(key))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
has(key: K): boolean {
|
|
37
|
+
return this.map.has(this.encodeKey(key))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
delete(key: K): boolean {
|
|
41
|
+
return this.map.delete(this.encodeKey(key))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
values(): IterableIterator<V> {
|
|
45
|
+
return this.map.values()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
*keys(): IterableIterator<K> {
|
|
49
|
+
for (const key of this.map.keys()) {
|
|
50
|
+
yield this.decodeKey(key)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
*entries(): IterableIterator<[K, V]> {
|
|
55
|
+
for (const [key, value] of this.map.entries()) {
|
|
56
|
+
yield [this.decodeKey(key), value]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
forEach(
|
|
61
|
+
callbackfn: (value: V, key: K, map: MappedMap<K, V>) => void,
|
|
62
|
+
thisArg?: any,
|
|
63
|
+
): void {
|
|
64
|
+
for (const [key, value] of this) {
|
|
65
|
+
callbackfn.call(thisArg, value, key, this)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[Symbol.iterator](): IterableIterator<[K, V]> {
|
|
70
|
+
return this.entries()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[Symbol.toStringTag]: string = 'MappedMap'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class NsidMap<T> extends MappedMap<NSID, T, string> {
|
|
77
|
+
constructor() {
|
|
78
|
+
super(
|
|
79
|
+
(key) => key.toString(),
|
|
80
|
+
(enc) => NSID.from(enc),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/nsid-set.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { NSID } from '@atproto/syntax'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Set implementation that maps values of type K to an internal representation
|
|
5
|
+
* I. Value identity is determined by the {@link Object.is} comparison of the
|
|
6
|
+
* encoded values.
|
|
7
|
+
*
|
|
8
|
+
* This is typically useful for values that can be serialized to a unique string
|
|
9
|
+
* representation.
|
|
10
|
+
*/
|
|
11
|
+
export class MappedSet<K, I = any> implements Set<K> {
|
|
12
|
+
private set = new Set<I>()
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly encodeValue: (val: K) => I,
|
|
16
|
+
private readonly decodeValue: (enc: I) => K,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
get size(): number {
|
|
20
|
+
return this.set.size
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clear(): void {
|
|
24
|
+
this.set.clear()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
add(val: K): this {
|
|
28
|
+
this.set.add(this.encodeValue(val))
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
has(val: K): boolean {
|
|
33
|
+
return this.set.has(this.encodeValue(val))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
delete(val: K): boolean {
|
|
37
|
+
return this.set.delete(this.encodeValue(val))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
*values(): IterableIterator<K> {
|
|
41
|
+
for (const val of this.set.values()) {
|
|
42
|
+
yield this.decodeValue(val)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
keys(): SetIterator<K> {
|
|
47
|
+
return this.values()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
*entries(): IterableIterator<[K, K]> {
|
|
51
|
+
for (const val of this) yield [val, val]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
forEach(
|
|
55
|
+
callbackfn: (value: K, value2: K, set: Set<K>) => void,
|
|
56
|
+
thisArg?: any,
|
|
57
|
+
): void {
|
|
58
|
+
for (const val of this) {
|
|
59
|
+
callbackfn.call(thisArg, val, val, this)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[Symbol.iterator](): IterableIterator<K> {
|
|
64
|
+
return this.values()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[Symbol.toStringTag]: string = 'MappedSet'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class NsidSet extends MappedSet<NSID, string> {
|
|
71
|
+
constructor() {
|
|
72
|
+
super(
|
|
73
|
+
(val) => val.toString(),
|
|
74
|
+
(enc) => NSID.from(enc),
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|