@atproto/lex-installer 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -2
- package/dist/index.js.map +1 -1
- package/dist/lex-installer.d.ts +5 -4
- package/dist/lex-installer.d.ts.map +1 -1
- package/dist/lex-installer.js +1 -1
- package/dist/lex-installer.js.map +1 -1
- package/dist/nsid-map.d.ts +6 -7
- package/dist/nsid-map.d.ts.map +1 -1
- package/dist/nsid-map.js +20 -4
- package/dist/nsid-map.js.map +1 -1
- package/dist/nsid-set.d.ts +10 -3
- package/dist/nsid-set.d.ts.map +1 -1
- package/dist/nsid-set.js +66 -0
- package/dist/nsid-set.js.map +1 -1
- package/package.json +11 -15
- package/src/fs.ts +0 -122
- package/src/index.ts +0 -126
- package/src/lex-installer.ts +0 -460
- package/src/lexicons-manifest.test.ts +0 -51
- package/src/lexicons-manifest.ts +0 -66
- package/src/nsid-map.ts +0 -124
- package/src/nsid-set.ts +0 -113
- package/tsconfig.build.json +0 -13
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
package/src/fs.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { dirname } from 'node:path'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Reads and parses a JSON file from the filesystem.
|
|
6
|
-
*
|
|
7
|
-
* @param path - Absolute or relative path to the JSON file
|
|
8
|
-
* @returns The parsed JSON content
|
|
9
|
-
* @throws {Error} When the file cannot be read (e.g., ENOENT, EACCES)
|
|
10
|
-
* @throws {SyntaxError} When the file contains invalid JSON
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* import { readJsonFile } from '@atproto/lex-installer'
|
|
15
|
-
*
|
|
16
|
-
* const manifest = await readJsonFile('./lexicons.manifest.json')
|
|
17
|
-
* ```
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* Handle missing file:
|
|
21
|
-
* ```typescript
|
|
22
|
-
* import { readJsonFile, isEnoentError } from '@atproto/lex-installer'
|
|
23
|
-
*
|
|
24
|
-
* try {
|
|
25
|
-
* const data = await readJsonFile('./config.json')
|
|
26
|
-
* } catch (err) {
|
|
27
|
-
* if (isEnoentError(err)) {
|
|
28
|
-
* console.log('File does not exist, using defaults')
|
|
29
|
-
* } else {
|
|
30
|
-
* throw err
|
|
31
|
-
* }
|
|
32
|
-
* }
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export async function readJsonFile(path: string): Promise<unknown> {
|
|
36
|
-
const contents = await readFile(path, 'utf8')
|
|
37
|
-
return JSON.parse(contents)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Writes data as formatted JSON to a file.
|
|
42
|
-
*
|
|
43
|
-
* The function:
|
|
44
|
-
* - Creates parent directories if they don't exist
|
|
45
|
-
* - Formats JSON with 2-space indentation
|
|
46
|
-
* - Overwrites existing files
|
|
47
|
-
* - Sets file permissions to 0o644 (rw-r--r--)
|
|
48
|
-
*
|
|
49
|
-
* @param path - Absolute or relative path for the output file
|
|
50
|
-
* @param data - Data to serialize as JSON
|
|
51
|
-
* @throws {Error} When the file cannot be written
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```typescript
|
|
55
|
-
* import { writeJsonFile } from '@atproto/lex-installer'
|
|
56
|
-
*
|
|
57
|
-
* await writeJsonFile('./output/data.json', {
|
|
58
|
-
* name: 'example',
|
|
59
|
-
* values: [1, 2, 3],
|
|
60
|
-
* })
|
|
61
|
-
* ```
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* Write a lexicon document:
|
|
65
|
-
* ```typescript
|
|
66
|
-
* import { writeJsonFile } from '@atproto/lex-installer'
|
|
67
|
-
*
|
|
68
|
-
* await writeJsonFile('./lexicons/app/bsky/feed/post.json', lexiconDocument)
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
export async function writeJsonFile(
|
|
72
|
-
path: string,
|
|
73
|
-
data: unknown,
|
|
74
|
-
): Promise<void> {
|
|
75
|
-
await mkdir(dirname(path), { recursive: true })
|
|
76
|
-
const contents = JSON.stringify(data, null, 2)
|
|
77
|
-
await writeFile(path, contents, {
|
|
78
|
-
encoding: 'utf8',
|
|
79
|
-
mode: 0o644,
|
|
80
|
-
flag: 'w', // override
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Checks if an error is an ENOENT (file not found) error.
|
|
86
|
-
*
|
|
87
|
-
* Useful for handling cases where a file may or may not exist,
|
|
88
|
-
* such as reading an optional configuration file.
|
|
89
|
-
*
|
|
90
|
-
* @param err - The error to check
|
|
91
|
-
* @returns `true` if the error is an ENOENT error, `false` otherwise
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* ```typescript
|
|
95
|
-
* import { readFile } from 'node:fs/promises'
|
|
96
|
-
* import { isEnoentError } from '@atproto/lex-installer'
|
|
97
|
-
*
|
|
98
|
-
* const config = await readFile('./config.json').catch((err) => {
|
|
99
|
-
* if (isEnoentError(err)) {
|
|
100
|
-
* return { defaults: true }
|
|
101
|
-
* }
|
|
102
|
-
* throw err
|
|
103
|
-
* })
|
|
104
|
-
* ```
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* In try/catch:
|
|
108
|
-
* ```typescript
|
|
109
|
-
* try {
|
|
110
|
-
* const manifest = await readFile('./lexicons.manifest.json', 'utf8')
|
|
111
|
-
* } catch (err) {
|
|
112
|
-
* if (isEnoentError(err)) {
|
|
113
|
-
* // File doesn't exist, create a new manifest
|
|
114
|
-
* return { version: 1, lexicons: [], resolutions: {} }
|
|
115
|
-
* }
|
|
116
|
-
* throw err
|
|
117
|
-
* }
|
|
118
|
-
* ```
|
|
119
|
-
*/
|
|
120
|
-
export function isEnoentError(err: unknown): boolean {
|
|
121
|
-
return err instanceof Error && 'code' in err && err.code === 'ENOENT'
|
|
122
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
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
|
-
/**
|
|
9
|
-
* Options for the {@link install} function.
|
|
10
|
-
*
|
|
11
|
-
* Extends {@link LexInstallerOptions} with additional options for controlling
|
|
12
|
-
* the installation behavior.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* const options: LexInstallOptions = {
|
|
17
|
-
* lexicons: './lexicons',
|
|
18
|
-
* manifest: './lexicons.manifest.json',
|
|
19
|
-
* add: ['com.example.myLexicon', 'at://did:plc:xyz/com.example.otherLexicon'],
|
|
20
|
-
* save: true,
|
|
21
|
-
* ci: false,
|
|
22
|
-
* }
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
export type LexInstallOptions = LexInstallerOptions & {
|
|
26
|
-
/**
|
|
27
|
-
* Array of lexicons to add to the installation. Can be NSID strings
|
|
28
|
-
* (e.g., 'com.example.myLexicon') or AT URIs
|
|
29
|
-
* (e.g., 'at://did:plc:xyz/com.example.myLexicon').
|
|
30
|
-
*/
|
|
31
|
-
add?: string[]
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Whether to save the updated manifest after installation.
|
|
35
|
-
* When `true`, the manifest file will be written with any new lexicons.
|
|
36
|
-
* @default false
|
|
37
|
-
*/
|
|
38
|
-
save?: boolean
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Enable CI mode for strict manifest verification.
|
|
42
|
-
* When `true`, throws an error if the manifest is out of date,
|
|
43
|
-
* useful for continuous integration pipelines.
|
|
44
|
-
* @default false
|
|
45
|
-
*/
|
|
46
|
-
ci?: boolean
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Installs lexicons from the network based on the provided options.
|
|
51
|
-
*
|
|
52
|
-
* This is the main entry point for programmatic lexicon installation.
|
|
53
|
-
* It reads an existing manifest (if present), installs any new lexicons,
|
|
54
|
-
* and optionally saves the updated manifest.
|
|
55
|
-
*
|
|
56
|
-
* @param options - Configuration options for the installation
|
|
57
|
-
* @throws {Error} When the manifest file cannot be read (unless it doesn't exist)
|
|
58
|
-
* @throws {Error} When in CI mode and the manifest is out of date
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* Install lexicons and save the manifest:
|
|
62
|
-
* ```typescript
|
|
63
|
-
* import { install } from '@atproto/lex-installer'
|
|
64
|
-
*
|
|
65
|
-
* await install({
|
|
66
|
-
* lexicons: './lexicons',
|
|
67
|
-
* manifest: './lexicons.manifest.json',
|
|
68
|
-
* add: ['app.bsky.feed.post', 'app.bsky.actor.profile'],
|
|
69
|
-
* save: true,
|
|
70
|
-
* })
|
|
71
|
-
* ```
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* Verify manifest in CI pipeline:
|
|
75
|
-
* ```typescript
|
|
76
|
-
* import { install } from '@atproto/lex-installer'
|
|
77
|
-
*
|
|
78
|
-
* // Throws if manifest is out of date
|
|
79
|
-
* await install({
|
|
80
|
-
* lexicons: './lexicons',
|
|
81
|
-
* manifest: './lexicons.manifest.json',
|
|
82
|
-
* ci: true,
|
|
83
|
-
* })
|
|
84
|
-
* ```
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* Install from specific AT URIs:
|
|
88
|
-
* ```typescript
|
|
89
|
-
* import { install } from '@atproto/lex-installer'
|
|
90
|
-
*
|
|
91
|
-
* await install({
|
|
92
|
-
* lexicons: './lexicons',
|
|
93
|
-
* manifest: './lexicons.manifest.json',
|
|
94
|
-
* add: ['at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post'],
|
|
95
|
-
* save: true,
|
|
96
|
-
* })
|
|
97
|
-
* ```
|
|
98
|
-
*/
|
|
99
|
-
export async function install(options: LexInstallOptions) {
|
|
100
|
-
const manifest: LexiconsManifest | undefined = await readJsonFile(
|
|
101
|
-
options.manifest,
|
|
102
|
-
).then(
|
|
103
|
-
(json) => lexiconsManifestSchema.parse(json),
|
|
104
|
-
(cause: unknown) => {
|
|
105
|
-
if (isEnoentError(cause)) return undefined
|
|
106
|
-
throw new Error('Failed to read lexicons manifest', { cause })
|
|
107
|
-
},
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
const additions = new Set(options.add)
|
|
111
|
-
|
|
112
|
-
// Perform the installation using the existing manifest as "hint"
|
|
113
|
-
await using installer = new LexInstaller(options)
|
|
114
|
-
|
|
115
|
-
await installer.install({ additions, manifest })
|
|
116
|
-
|
|
117
|
-
// Verify lockfile
|
|
118
|
-
if (options.ci && (!manifest || !installer.equals(manifest))) {
|
|
119
|
-
throw new Error('Lexicons manifest is out of date')
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Save changes if requested
|
|
123
|
-
if (options.save) {
|
|
124
|
-
await installer.save()
|
|
125
|
-
}
|
|
126
|
-
}
|
package/src/lex-installer.ts
DELETED
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { LexiconDirectoryIndexer } from '@atproto/lex-builder'
|
|
3
|
-
import { cidForLex } from '@atproto/lex-cbor'
|
|
4
|
-
import { Cid, 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 { AtUriString, NsidString } from '@atproto/lex-schema'
|
|
17
|
-
import { AtUri, NSID } from '@atproto/syntax'
|
|
18
|
-
import { isEnoentError, writeJsonFile } from './fs.js'
|
|
19
|
-
import {
|
|
20
|
-
LexiconsManifest,
|
|
21
|
-
normalizeLexiconsManifest,
|
|
22
|
-
} from './lexicons-manifest.js'
|
|
23
|
-
import { NsidMap } from './nsid-map.js'
|
|
24
|
-
import { NsidSet } from './nsid-set.js'
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Configuration options for the {@link LexInstaller} class.
|
|
28
|
-
*
|
|
29
|
-
* Extends {@link LexResolverOptions} with paths for lexicon storage
|
|
30
|
-
* and manifest management.
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```typescript
|
|
34
|
-
* const options: LexInstallerOptions = {
|
|
35
|
-
* lexicons: './lexicons',
|
|
36
|
-
* manifest: './lexicons.manifest.json',
|
|
37
|
-
* update: false,
|
|
38
|
-
* }
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
export type LexInstallerOptions = LexResolverOptions & {
|
|
42
|
-
/**
|
|
43
|
-
* Path to the directory where lexicon JSON files will be stored.
|
|
44
|
-
* The directory structure mirrors the NSID hierarchy
|
|
45
|
-
* (e.g., 'app.bsky.feed.post' becomes 'app/bsky/feed/post.json').
|
|
46
|
-
*/
|
|
47
|
-
lexicons: string
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Path to the manifest file that tracks installed lexicons and their resolutions.
|
|
51
|
-
*/
|
|
52
|
-
manifest: string
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* When `true`, forces re-fetching of lexicons from the network even if they
|
|
56
|
-
* already exist locally. Useful for updating to newer versions.
|
|
57
|
-
* @default false
|
|
58
|
-
*/
|
|
59
|
-
update?: boolean
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Manages the installation of Lexicon schemas from the AT Protocol network.
|
|
64
|
-
*
|
|
65
|
-
* The `LexInstaller` class handles fetching, caching, and organizing lexicon
|
|
66
|
-
* documents. It tracks dependencies between lexicons and ensures all referenced
|
|
67
|
-
* schemas are installed. The class implements `AsyncDisposable` for proper
|
|
68
|
-
* resource cleanup.
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* Basic usage with async disposal:
|
|
72
|
-
* ```typescript
|
|
73
|
-
* import { LexInstaller } from '@atproto/lex-installer'
|
|
74
|
-
*
|
|
75
|
-
* await using installer = new LexInstaller({
|
|
76
|
-
* lexicons: './lexicons',
|
|
77
|
-
* manifest: './lexicons.manifest.json',
|
|
78
|
-
* })
|
|
79
|
-
*
|
|
80
|
-
* await installer.install({
|
|
81
|
-
* additions: ['app.bsky.feed.post'],
|
|
82
|
-
* })
|
|
83
|
-
*
|
|
84
|
-
* await installer.save()
|
|
85
|
-
* // Resources automatically cleaned up when block exits
|
|
86
|
-
* ```
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* Manual disposal:
|
|
90
|
-
* ```typescript
|
|
91
|
-
* const installer = new LexInstaller({
|
|
92
|
-
* lexicons: './lexicons',
|
|
93
|
-
* manifest: './lexicons.manifest.json',
|
|
94
|
-
* })
|
|
95
|
-
*
|
|
96
|
-
* try {
|
|
97
|
-
* await installer.install({ additions: ['app.bsky.actor.profile'] })
|
|
98
|
-
* await installer.save()
|
|
99
|
-
* } finally {
|
|
100
|
-
* await installer[Symbol.asyncDispose]()
|
|
101
|
-
* }
|
|
102
|
-
* ```
|
|
103
|
-
*/
|
|
104
|
-
export class LexInstaller implements AsyncDisposable {
|
|
105
|
-
protected readonly lexiconResolver: LexResolver
|
|
106
|
-
protected readonly indexer: LexiconDirectoryIndexer
|
|
107
|
-
protected readonly documents = new NsidMap<LexiconDocument>()
|
|
108
|
-
protected readonly manifest: LexiconsManifest = {
|
|
109
|
-
version: 1,
|
|
110
|
-
lexicons: [],
|
|
111
|
-
resolutions: {},
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
constructor(protected readonly options: LexInstallerOptions) {
|
|
115
|
-
this.lexiconResolver = new LexResolver(options)
|
|
116
|
-
this.indexer = new LexiconDirectoryIndexer({
|
|
117
|
-
lexicons: options.lexicons,
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async [Symbol.asyncDispose](): Promise<void> {
|
|
122
|
-
await this.indexer[Symbol.asyncDispose]()
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Compares the current manifest state with another manifest for equality.
|
|
127
|
-
*
|
|
128
|
-
* Both manifests are normalized before comparison to ensure consistent
|
|
129
|
-
* ordering of entries. Useful for detecting changes during CI verification.
|
|
130
|
-
*
|
|
131
|
-
* @param manifest - The manifest to compare against
|
|
132
|
-
* @returns `true` if the manifests are equivalent, `false` otherwise
|
|
133
|
-
*/
|
|
134
|
-
equals(manifest: LexiconsManifest): boolean {
|
|
135
|
-
return lexEquals(
|
|
136
|
-
normalizeLexiconsManifest(manifest),
|
|
137
|
-
normalizeLexiconsManifest(this.manifest),
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Installs lexicons and their dependencies.
|
|
143
|
-
*
|
|
144
|
-
* This method processes explicit additions and restores entries from an
|
|
145
|
-
* existing manifest. It recursively resolves and installs all referenced
|
|
146
|
-
* lexicons to ensure complete dependency trees.
|
|
147
|
-
*
|
|
148
|
-
* @param options - Installation options
|
|
149
|
-
* @param options.additions - Iterable of lexicon identifiers to add.
|
|
150
|
-
* Can be NSID strings or AT URIs.
|
|
151
|
-
* @param options.manifest - Existing manifest to use as a baseline.
|
|
152
|
-
* Previously resolved URIs are preserved unless explicitly overridden.
|
|
153
|
-
*
|
|
154
|
-
* @example
|
|
155
|
-
* Install new lexicons:
|
|
156
|
-
* ```typescript
|
|
157
|
-
* await installer.install({
|
|
158
|
-
* additions: ['app.bsky.feed.post', 'app.bsky.actor.profile'],
|
|
159
|
-
* })
|
|
160
|
-
* ```
|
|
161
|
-
*
|
|
162
|
-
* @example
|
|
163
|
-
* Install with existing manifest as hint:
|
|
164
|
-
* ```typescript
|
|
165
|
-
* const existingManifest = await readJsonFile('./lexicons.manifest.json')
|
|
166
|
-
* await installer.install({
|
|
167
|
-
* additions: ['com.example.newLexicon'],
|
|
168
|
-
* manifest: existingManifest,
|
|
169
|
-
* })
|
|
170
|
-
* ```
|
|
171
|
-
*
|
|
172
|
-
* @example
|
|
173
|
-
* Install from specific AT URIs:
|
|
174
|
-
* ```typescript
|
|
175
|
-
* await installer.install({
|
|
176
|
-
* additions: [
|
|
177
|
-
* 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post',
|
|
178
|
-
* ],
|
|
179
|
-
* })
|
|
180
|
-
* ```
|
|
181
|
-
*/
|
|
182
|
-
async install({
|
|
183
|
-
additions,
|
|
184
|
-
manifest,
|
|
185
|
-
}: {
|
|
186
|
-
additions?: Iterable<string>
|
|
187
|
-
manifest?: LexiconsManifest
|
|
188
|
-
} = {}): Promise<void> {
|
|
189
|
-
const roots = new NsidMap<AtUri | null>()
|
|
190
|
-
|
|
191
|
-
// First, process explicit additions
|
|
192
|
-
for (const lexicon of new Set(additions)) {
|
|
193
|
-
const [nsid, uri]: [NSID, AtUri | null] = lexicon.startsWith('at://')
|
|
194
|
-
? ((uri) => [NSID.from(uri.rkey), uri])(new AtUri(lexicon))
|
|
195
|
-
: [NSID.from(lexicon), null]
|
|
196
|
-
|
|
197
|
-
if (roots.has(nsid)) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
`Duplicate lexicon addition: ${nsid} (${roots.get(nsid) ?? lexicon})`,
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
roots.set(nsid, uri)
|
|
204
|
-
console.debug(`Adding new lexicon: ${nsid} (${uri ?? 'from NSID'})`)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Next, restore previously existing manifest entries
|
|
208
|
-
if (manifest) {
|
|
209
|
-
for (const lexicon of manifest.lexicons) {
|
|
210
|
-
const nsid = NSID.from(lexicon)
|
|
211
|
-
|
|
212
|
-
// Skip entries already added explicitly
|
|
213
|
-
if (!roots.has(nsid)) {
|
|
214
|
-
const uri = manifest.resolutions[lexicon]
|
|
215
|
-
? new AtUri(manifest.resolutions[lexicon].uri)
|
|
216
|
-
: null
|
|
217
|
-
|
|
218
|
-
roots.set(nsid, uri)
|
|
219
|
-
|
|
220
|
-
console.debug(
|
|
221
|
-
`Adding lexicon from manifest: ${nsid} (${uri ?? 'from NSID'})`,
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Install all root lexicons (and store them in the manifest)
|
|
228
|
-
await Promise.all(
|
|
229
|
-
Array.from(roots, async ([nsid, sourceUri]) => {
|
|
230
|
-
console.debug(`Installing lexicon: ${nsid}`)
|
|
231
|
-
|
|
232
|
-
const { lexicon: document } = sourceUri
|
|
233
|
-
? await this.installFromUri(sourceUri)
|
|
234
|
-
: await this.installFromNsid(nsid)
|
|
235
|
-
|
|
236
|
-
// Store the direct reference in the new manifest
|
|
237
|
-
this.manifest.lexicons.push(document.id)
|
|
238
|
-
}),
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
// Then recursively install all referenced lexicons
|
|
242
|
-
let results: unknown[]
|
|
243
|
-
do {
|
|
244
|
-
results = await Promise.all(
|
|
245
|
-
Array.from(this.getMissingIds(), async (nsid) => {
|
|
246
|
-
console.debug(`Resolving dependency lexicon: ${nsid}`)
|
|
247
|
-
|
|
248
|
-
const nsidStr = nsid.toString() as NsidString
|
|
249
|
-
const resolvedUri = manifest?.resolutions[nsidStr]?.uri
|
|
250
|
-
? new AtUri(manifest.resolutions[nsidStr].uri)
|
|
251
|
-
: null
|
|
252
|
-
if (resolvedUri) {
|
|
253
|
-
await this.installFromUri(resolvedUri)
|
|
254
|
-
} else {
|
|
255
|
-
await this.installFromNsid(nsid)
|
|
256
|
-
}
|
|
257
|
-
}),
|
|
258
|
-
)
|
|
259
|
-
} while (results.length > 0)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
protected getMissingIds(): NsidSet {
|
|
263
|
-
const missing = new NsidSet()
|
|
264
|
-
|
|
265
|
-
for (const document of this.documents.values()) {
|
|
266
|
-
for (const nsid of listDocumentNsidRefs(document)) {
|
|
267
|
-
if (!this.documents.has(nsid)) {
|
|
268
|
-
missing.add(nsid)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return missing
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
protected async installFromNsid(nsid: NSID) {
|
|
277
|
-
const uri = await this.lexiconResolver.resolve(nsid)
|
|
278
|
-
return this.installFromUri(uri)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
protected async installFromUri(uri: AtUri): Promise<{
|
|
282
|
-
lexicon: LexiconDocument
|
|
283
|
-
uri: AtUri
|
|
284
|
-
}> {
|
|
285
|
-
const { lexicon, cid } = this.options.update
|
|
286
|
-
? await this.fetch(uri)
|
|
287
|
-
: await this.indexer.get(uri.rkey).then(
|
|
288
|
-
async (lexicon) => {
|
|
289
|
-
console.debug(`Re-using existing lexicon ${uri.rkey} from indexer`)
|
|
290
|
-
const cid = await cidForLex(lexicon)
|
|
291
|
-
return { cid, lexicon }
|
|
292
|
-
},
|
|
293
|
-
(err) => {
|
|
294
|
-
if (isEnoentError(err)) return this.fetch(uri)
|
|
295
|
-
throw err
|
|
296
|
-
},
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
this.documents.set(NSID.from(lexicon.id), lexicon)
|
|
300
|
-
this.manifest.resolutions[lexicon.id] = {
|
|
301
|
-
cid: cid.toString(),
|
|
302
|
-
uri: uri.toString() as AtUriString,
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return { lexicon, uri }
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Fetches a lexicon document from the network and saves it locally.
|
|
310
|
-
*
|
|
311
|
-
* The lexicon is retrieved from the specified AT URI, written to the
|
|
312
|
-
* local lexicons directory, and its metadata is recorded for the manifest.
|
|
313
|
-
*
|
|
314
|
-
* @param uri - The AT URI pointing to the lexicon document
|
|
315
|
-
* @returns An object containing the fetched lexicon document and its CID
|
|
316
|
-
*/
|
|
317
|
-
async fetch(uri: AtUri): Promise<{ lexicon: LexiconDocument; cid: Cid }> {
|
|
318
|
-
console.debug(`Fetching lexicon from ${uri}...`)
|
|
319
|
-
|
|
320
|
-
const { lexicon, cid } = await this.lexiconResolver.fetch(uri, {
|
|
321
|
-
noCache: this.options.update,
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
const basePath = join(this.options.lexicons, ...lexicon.id.split('.'))
|
|
325
|
-
await writeJsonFile(`${basePath}.json`, lexicon)
|
|
326
|
-
|
|
327
|
-
return { lexicon, cid }
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Saves the current manifest to disk.
|
|
332
|
-
*
|
|
333
|
-
* The manifest is normalized before saving to ensure consistent ordering
|
|
334
|
-
* of entries, making it suitable for version control.
|
|
335
|
-
*/
|
|
336
|
-
async save(): Promise<void> {
|
|
337
|
-
await writeJsonFile(
|
|
338
|
-
this.options.manifest,
|
|
339
|
-
normalizeLexiconsManifest(this.manifest),
|
|
340
|
-
)
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function* listDocumentNsidRefs(doc: LexiconDocument): Iterable<NSID> {
|
|
345
|
-
try {
|
|
346
|
-
for (const def of Object.values(doc.defs)) {
|
|
347
|
-
if (def) {
|
|
348
|
-
for (const ref of defRefs(def)) {
|
|
349
|
-
const [nsid] = ref.split('#', 1)
|
|
350
|
-
if (nsid) yield NSID.from(nsid)
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
} catch (cause) {
|
|
355
|
-
throw new Error(`Failed to extract refs from lexicon ${doc.id}`, { cause })
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function* defRefs(
|
|
360
|
-
def:
|
|
361
|
-
| MainLexiconDefinition
|
|
362
|
-
| NamedLexiconDefinition
|
|
363
|
-
| LexiconPermission
|
|
364
|
-
| LexiconUnknown
|
|
365
|
-
| LexiconParameters
|
|
366
|
-
| LexiconRef
|
|
367
|
-
| LexiconRefUnion,
|
|
368
|
-
): Iterable<string> {
|
|
369
|
-
switch (def.type) {
|
|
370
|
-
case 'string':
|
|
371
|
-
if (def.knownValues) {
|
|
372
|
-
for (const val of def.knownValues) {
|
|
373
|
-
// Tokens ?
|
|
374
|
-
const { length, 0: nsid, 1: hash } = val.split('#')
|
|
375
|
-
if (length === 2 && hash) {
|
|
376
|
-
try {
|
|
377
|
-
NSID.from(nsid)
|
|
378
|
-
yield val
|
|
379
|
-
} catch {
|
|
380
|
-
// ignore invalid nsid
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return
|
|
386
|
-
case 'array':
|
|
387
|
-
return yield* defRefs(def.items)
|
|
388
|
-
case 'params':
|
|
389
|
-
case 'object':
|
|
390
|
-
for (const prop of Object.values(def.properties)) {
|
|
391
|
-
yield* defRefs(prop)
|
|
392
|
-
}
|
|
393
|
-
return
|
|
394
|
-
case 'union':
|
|
395
|
-
yield* def.refs
|
|
396
|
-
return
|
|
397
|
-
case 'ref': {
|
|
398
|
-
yield def.ref
|
|
399
|
-
return
|
|
400
|
-
}
|
|
401
|
-
case 'record':
|
|
402
|
-
yield* defRefs(def.record)
|
|
403
|
-
return
|
|
404
|
-
case 'procedure':
|
|
405
|
-
if (def.input?.schema) {
|
|
406
|
-
yield* defRefs(def.input.schema)
|
|
407
|
-
}
|
|
408
|
-
// fallthrough
|
|
409
|
-
case 'query':
|
|
410
|
-
if (def.output?.schema) {
|
|
411
|
-
yield* defRefs(def.output.schema)
|
|
412
|
-
}
|
|
413
|
-
// fallthrough
|
|
414
|
-
case 'subscription':
|
|
415
|
-
if (def.parameters) {
|
|
416
|
-
yield* defRefs(def.parameters)
|
|
417
|
-
}
|
|
418
|
-
if ('message' in def && def.message?.schema) {
|
|
419
|
-
yield* defRefs(def.message.schema)
|
|
420
|
-
}
|
|
421
|
-
return
|
|
422
|
-
case 'permission-set':
|
|
423
|
-
for (const permission of def.permissions) {
|
|
424
|
-
yield* defRefs(permission)
|
|
425
|
-
}
|
|
426
|
-
return
|
|
427
|
-
case 'permission':
|
|
428
|
-
if (def.resource === 'rpc') {
|
|
429
|
-
if (Array.isArray(def.lxm)) {
|
|
430
|
-
for (const lxm of def.lxm) {
|
|
431
|
-
if (typeof lxm === 'string') {
|
|
432
|
-
yield lxm
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
} else if (def.resource === 'repo') {
|
|
437
|
-
if (Array.isArray(def.collection)) {
|
|
438
|
-
for (const lxm of def.collection) {
|
|
439
|
-
if (typeof lxm === 'string') {
|
|
440
|
-
yield lxm
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return
|
|
446
|
-
case 'boolean':
|
|
447
|
-
case 'cid-link':
|
|
448
|
-
case 'token':
|
|
449
|
-
case 'bytes':
|
|
450
|
-
case 'blob':
|
|
451
|
-
case 'integer':
|
|
452
|
-
case 'unknown':
|
|
453
|
-
// @NOTE We explicitly list all types here to ensure exhaustiveness
|
|
454
|
-
// causing TS to error if a new type is added without updating this switch
|
|
455
|
-
return
|
|
456
|
-
default:
|
|
457
|
-
// @ts-expect-error
|
|
458
|
-
throw new Error(`Unknown lexicon def type: ${def.type}`)
|
|
459
|
-
}
|
|
460
|
-
}
|