@atproto/lex-installer 0.0.14 → 0.0.16
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 +24 -0
- package/dist/fs.d.ts +98 -0
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +98 -0
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -1
- package/dist/lex-installer.d.ts +135 -0
- package/dist/lex-installer.d.ts.map +1 -1
- package/dist/lex-installer.js +107 -0
- package/dist/lex-installer.js.map +1 -1
- package/dist/lexicons-manifest.d.ts +28 -0
- package/dist/lexicons-manifest.d.ts.map +1 -1
- package/dist/lexicons-manifest.js +25 -0
- package/dist/lexicons-manifest.js.map +1 -1
- package/dist/nsid-map.d.ts +46 -5
- package/dist/nsid-map.d.ts.map +1 -1
- package/dist/nsid-map.js +46 -5
- package/dist/nsid-map.js.map +1 -1
- package/dist/nsid-set.d.ts +41 -5
- package/dist/nsid-set.d.ts.map +1 -1
- package/dist/nsid-set.js +41 -5
- package/dist/nsid-set.js.map +1 -1
- package/package.json +8 -8
- package/src/fs.ts +98 -0
- package/src/index.ts +85 -0
- package/src/lex-installer.ts +137 -0
- package/src/lexicons-manifest.ts +28 -0
- package/src/nsid-map.ts +46 -5
- package/src/nsid-set.ts +41 -5
package/dist/nsid-set.js
CHANGED
|
@@ -3,17 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.NsidSet = exports.MappedSet = void 0;
|
|
4
4
|
const syntax_1 = require("@atproto/syntax");
|
|
5
5
|
/**
|
|
6
|
-
* A Set implementation that maps values of type K to an internal representation
|
|
7
|
-
* I. Value identity is determined by the {@link Object.is} comparison of the
|
|
8
|
-
* encoded values.
|
|
6
|
+
* A Set implementation that maps values of type K to an internal representation I.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Value identity is determined by the {@link Object.is} comparison of the
|
|
9
|
+
* encoded values. This is useful for complex types that can be serialized
|
|
10
|
+
* to a unique primitive representation (typically strings).
|
|
11
|
+
*
|
|
12
|
+
* @typeParam K - The external value type stored in the set
|
|
13
|
+
* @typeParam I - The internal encoded type used for identity comparison
|
|
12
14
|
*/
|
|
13
15
|
class MappedSet {
|
|
14
16
|
encodeValue;
|
|
15
17
|
decodeValue;
|
|
16
18
|
set = new Set();
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new MappedSet with custom encoding/decoding functions.
|
|
21
|
+
*
|
|
22
|
+
* @param encodeValue - Function to convert external values to internal representation
|
|
23
|
+
* @param decodeValue - Function to convert internal representation back to external values
|
|
24
|
+
*/
|
|
17
25
|
constructor(encodeValue, decodeValue) {
|
|
18
26
|
this.encodeValue = encodeValue;
|
|
19
27
|
this.decodeValue = decodeValue;
|
|
@@ -57,7 +65,35 @@ class MappedSet {
|
|
|
57
65
|
[Symbol.toStringTag] = 'MappedSet';
|
|
58
66
|
}
|
|
59
67
|
exports.MappedSet = MappedSet;
|
|
68
|
+
/**
|
|
69
|
+
* A Set specialized for storing NSID (Namespaced Identifier) values.
|
|
70
|
+
*
|
|
71
|
+
* NSIDs are compared by their string representation, allowing different
|
|
72
|
+
* NSID object instances with the same value to be treated as equal.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* import { NsidSet } from '@atproto/lex-installer'
|
|
77
|
+
* import { NSID } from '@atproto/syntax'
|
|
78
|
+
*
|
|
79
|
+
* const nsidSet = new NsidSet()
|
|
80
|
+
*
|
|
81
|
+
* nsidSet.add(NSID.from('app.bsky.feed.post'))
|
|
82
|
+
* nsidSet.add(NSID.from('app.bsky.actor.profile'))
|
|
83
|
+
*
|
|
84
|
+
* // Check membership
|
|
85
|
+
* nsidSet.has(NSID.from('app.bsky.feed.post')) // true
|
|
86
|
+
*
|
|
87
|
+
* // Iterate over NSIDs
|
|
88
|
+
* for (const nsid of nsidSet) {
|
|
89
|
+
* console.log(nsid.toString())
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
60
93
|
class NsidSet extends MappedSet {
|
|
94
|
+
/**
|
|
95
|
+
* Creates a new empty NsidSet.
|
|
96
|
+
*/
|
|
61
97
|
constructor() {
|
|
62
98
|
super((val) => val.toString(), (enc) => syntax_1.NSID.from(enc));
|
|
63
99
|
}
|
package/dist/nsid-set.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nsid-set.js","sourceRoot":"","sources":["../src/nsid-set.ts"],"names":[],"mappings":";;;AAAA,4CAAsC;AAEtC
|
|
1
|
+
{"version":3,"file":"nsid-set.js","sourceRoot":"","sources":["../src/nsid-set.ts"],"names":[],"mappings":";;;AAAA,4CAAsC;AAEtC;;;;;;;;;GASG;AACH,MAAa,SAAS;IAUD;IACA;IAVX,GAAG,GAAG,IAAI,GAAG,EAAK,CAAA;IAE1B;;;;;OAKG;IACH,YACmB,WAA0B,EAC1B,WAA0B;QAD1B,gBAAW,GAAX,WAAW,CAAe;QAC1B,gBAAW,GAAX,WAAW,CAAe;IAC1C,CAAC;IAEJ,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAA;IACtB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,GAAG,CAAC,GAAM;QACR,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;QACnC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,GAAG,CAAC,GAAM;QACR,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;IAC5C,CAAC;IAED,MAAM,CAAC,GAAM;QACX,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;IAC/C,CAAC;IAED,CAAC,MAAM;QACL,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;YACpC,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,MAAM,EAAE,CAAA;IACtB,CAAC;IAED,CAAC,OAAO;QACN,KAAK,MAAM,GAAG,IAAI,IAAI;YAAE,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAC1C,CAAC;IAED,OAAO,CACL,UAAsD,EACtD,OAAa;QAEb,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;QAC1C,CAAC;IACH,CAAC;IAED,CAAC,MAAM,CAAC,QAAQ,CAAC;QACf,OAAO,IAAI,CAAC,MAAM,EAAE,CAAA;IACtB,CAAC;IAED,CAAC,MAAM,CAAC,WAAW,CAAC,GAAW,WAAW,CAAA;CAC3C;AA/DD,8BA+DC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAa,OAAQ,SAAQ,SAAuB;IAClD;;OAEG;IACH;QACE,KAAK,CACH,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,EACvB,CAAC,GAAG,EAAE,EAAE,CAAC,aAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CACxB,CAAA;IACH,CAAC;CACF;AAVD,0BAUC","sourcesContent":["import { NSID } from '@atproto/syntax'\n\n/**\n * A Set implementation that maps values of type K to an internal representation I.\n *\n * Value identity is determined by the {@link Object.is} comparison of the\n * encoded values. This is useful for complex types that can be serialized\n * to a unique primitive representation (typically strings).\n *\n * @typeParam K - The external value type stored in the set\n * @typeParam I - The internal encoded type used for identity comparison\n */\nexport class MappedSet<K, I = any> implements Set<K> {\n private set = new Set<I>()\n\n /**\n * Creates a new MappedSet with custom encoding/decoding functions.\n *\n * @param encodeValue - Function to convert external values to internal representation\n * @param decodeValue - Function to convert internal representation back to external values\n */\n constructor(\n private readonly encodeValue: (val: K) => I,\n private readonly decodeValue: (enc: I) => K,\n ) {}\n\n get size(): number {\n return this.set.size\n }\n\n clear(): void {\n this.set.clear()\n }\n\n add(val: K): this {\n this.set.add(this.encodeValue(val))\n return this\n }\n\n has(val: K): boolean {\n return this.set.has(this.encodeValue(val))\n }\n\n delete(val: K): boolean {\n return this.set.delete(this.encodeValue(val))\n }\n\n *values(): IterableIterator<K> {\n for (const val of this.set.values()) {\n yield this.decodeValue(val)\n }\n }\n\n keys(): SetIterator<K> {\n return this.values()\n }\n\n *entries(): IterableIterator<[K, K]> {\n for (const val of this) yield [val, val]\n }\n\n forEach(\n callbackfn: (value: K, value2: K, set: Set<K>) => void,\n thisArg?: any,\n ): void {\n for (const val of this) {\n callbackfn.call(thisArg, val, val, this)\n }\n }\n\n [Symbol.iterator](): IterableIterator<K> {\n return this.values()\n }\n\n [Symbol.toStringTag]: string = 'MappedSet'\n}\n\n/**\n * A Set specialized for storing NSID (Namespaced Identifier) values.\n *\n * NSIDs are compared by their string representation, allowing different\n * NSID object instances with the same value to be treated as equal.\n *\n * @example\n * ```typescript\n * import { NsidSet } from '@atproto/lex-installer'\n * import { NSID } from '@atproto/syntax'\n *\n * const nsidSet = new NsidSet()\n *\n * nsidSet.add(NSID.from('app.bsky.feed.post'))\n * nsidSet.add(NSID.from('app.bsky.actor.profile'))\n *\n * // Check membership\n * nsidSet.has(NSID.from('app.bsky.feed.post')) // true\n *\n * // Iterate over NSIDs\n * for (const nsid of nsidSet) {\n * console.log(nsid.toString())\n * }\n * ```\n */\nexport class NsidSet extends MappedSet<NSID, string> {\n /**\n * Creates a new empty NsidSet.\n */\n constructor() {\n super(\n (val) => val.toString(),\n (enc) => NSID.from(enc),\n )\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/lex-installer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Lexicon document packet manager for AT Lexicons",
|
|
6
6
|
"keywords": [
|
|
@@ -32,17 +32,17 @@
|
|
|
32
32
|
"types": "./dist/index.d.ts",
|
|
33
33
|
"browser": "./dist/index.js",
|
|
34
34
|
"import": "./dist/index.js",
|
|
35
|
-
"
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"tslib": "^2.8.1",
|
|
40
|
-
"@atproto/lex-builder": "^0.0.
|
|
41
|
-
"@atproto/lex-cbor": "^0.0.
|
|
42
|
-
"@atproto/lex-data": "^0.0.
|
|
43
|
-
"@atproto/lex-document": "^0.0.
|
|
44
|
-
"@atproto/lex-resolver": "^0.0.
|
|
45
|
-
"@atproto/lex-schema": "^0.0.
|
|
40
|
+
"@atproto/lex-builder": "^0.0.15",
|
|
41
|
+
"@atproto/lex-cbor": "^0.0.11",
|
|
42
|
+
"@atproto/lex-data": "^0.0.11",
|
|
43
|
+
"@atproto/lex-document": "^0.0.13",
|
|
44
|
+
"@atproto/lex-resolver": "^0.0.14",
|
|
45
|
+
"@atproto/lex-schema": "^0.0.12",
|
|
46
46
|
"@atproto/syntax": "^0.4.3"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
package/src/fs.ts
CHANGED
|
@@ -1,11 +1,73 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
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
|
+
*/
|
|
4
35
|
export async function readJsonFile(path: string): Promise<unknown> {
|
|
5
36
|
const contents = await readFile(path, 'utf8')
|
|
6
37
|
return JSON.parse(contents)
|
|
7
38
|
}
|
|
8
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
|
+
*/
|
|
9
71
|
export async function writeJsonFile(
|
|
10
72
|
path: string,
|
|
11
73
|
data: unknown,
|
|
@@ -19,6 +81,42 @@ export async function writeJsonFile(
|
|
|
19
81
|
})
|
|
20
82
|
}
|
|
21
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
|
+
*/
|
|
22
120
|
export function isEnoentError(err: unknown): boolean {
|
|
23
121
|
return err instanceof Error && 'code' in err && err.code === 'ENOENT'
|
|
24
122
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,12 +5,97 @@ import {
|
|
|
5
5
|
lexiconsManifestSchema,
|
|
6
6
|
} from './lexicons-manifest.js'
|
|
7
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
|
+
*/
|
|
8
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
|
+
*/
|
|
9
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
|
+
*/
|
|
10
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
|
+
*/
|
|
11
46
|
ci?: boolean
|
|
12
47
|
}
|
|
13
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
|
+
*/
|
|
14
99
|
export async function install(options: LexInstallOptions) {
|
|
15
100
|
const manifest: LexiconsManifest | undefined = await readJsonFile(
|
|
16
101
|
options.manifest,
|
package/src/lex-installer.ts
CHANGED
|
@@ -23,12 +23,84 @@ import {
|
|
|
23
23
|
import { NsidMap } from './nsid-map.js'
|
|
24
24
|
import { NsidSet } from './nsid-set.js'
|
|
25
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
|
+
*/
|
|
26
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
|
+
*/
|
|
27
47
|
lexicons: string
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Path to the manifest file that tracks installed lexicons and their resolutions.
|
|
51
|
+
*/
|
|
28
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
|
+
*/
|
|
29
59
|
update?: boolean
|
|
30
60
|
}
|
|
31
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
|
+
*/
|
|
32
104
|
export class LexInstaller implements AsyncDisposable {
|
|
33
105
|
protected readonly lexiconResolver: LexResolver
|
|
34
106
|
protected readonly indexer: LexiconDirectoryIndexer
|
|
@@ -50,6 +122,15 @@ export class LexInstaller implements AsyncDisposable {
|
|
|
50
122
|
await this.indexer[Symbol.asyncDispose]()
|
|
51
123
|
}
|
|
52
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
|
+
*/
|
|
53
134
|
equals(manifest: LexiconsManifest): boolean {
|
|
54
135
|
return lexEquals(
|
|
55
136
|
normalizeLexiconsManifest(manifest),
|
|
@@ -57,6 +138,47 @@ export class LexInstaller implements AsyncDisposable {
|
|
|
57
138
|
)
|
|
58
139
|
}
|
|
59
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
|
+
*/
|
|
60
182
|
async install({
|
|
61
183
|
additions,
|
|
62
184
|
manifest,
|
|
@@ -183,6 +305,15 @@ export class LexInstaller implements AsyncDisposable {
|
|
|
183
305
|
return { lexicon, uri }
|
|
184
306
|
}
|
|
185
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
|
+
*/
|
|
186
317
|
async fetch(uri: AtUri): Promise<{ lexicon: LexiconDocument; cid: Cid }> {
|
|
187
318
|
console.debug(`Fetching lexicon from ${uri}...`)
|
|
188
319
|
|
|
@@ -196,6 +327,12 @@ export class LexInstaller implements AsyncDisposable {
|
|
|
196
327
|
return { lexicon, cid }
|
|
197
328
|
}
|
|
198
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
|
+
*/
|
|
199
336
|
async save(): Promise<void> {
|
|
200
337
|
await writeJsonFile(
|
|
201
338
|
this.options.manifest,
|
package/src/lexicons-manifest.ts
CHANGED
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
import { l } from '@atproto/lex-schema'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Schema for validating and parsing lexicons manifest files.
|
|
5
|
+
*
|
|
6
|
+
* The manifest tracks which lexicons are installed and how they were resolved.
|
|
7
|
+
* This schema ensures the manifest file conforms to the expected structure.
|
|
8
|
+
*/
|
|
3
9
|
export const lexiconsManifestSchema = l.object({
|
|
10
|
+
/** Schema version, currently always 1 */
|
|
4
11
|
version: l.literal(1),
|
|
12
|
+
/** Array of NSID strings for directly requested lexicons */
|
|
5
13
|
lexicons: l.array(l.string({ format: 'nsid' })),
|
|
14
|
+
/** Map of NSID to resolution info (AT URI and CID) for all installed lexicons */
|
|
6
15
|
resolutions: l.dict(
|
|
7
16
|
l.string({ format: 'nsid' }),
|
|
8
17
|
l.object({
|
|
18
|
+
/** AT URI where the lexicon was fetched from */
|
|
9
19
|
uri: l.string({ format: 'at-uri' }),
|
|
20
|
+
/** Content identifier (CID) of the lexicon document */
|
|
10
21
|
cid: l.string({ format: 'cid' }),
|
|
11
22
|
}),
|
|
12
23
|
),
|
|
13
24
|
})
|
|
14
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Type representing a parsed lexicons manifest.
|
|
28
|
+
*/
|
|
15
29
|
export type LexiconsManifest = l.Infer<typeof lexiconsManifestSchema>
|
|
16
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Normalizes a lexicons manifest for consistent storage and comparison.
|
|
33
|
+
*
|
|
34
|
+
* This function:
|
|
35
|
+
* - Sorts the `lexicons` array alphabetically
|
|
36
|
+
* - Sorts the `resolutions` object entries by key
|
|
37
|
+
* - Validates the result against the schema
|
|
38
|
+
*
|
|
39
|
+
* Normalization ensures that manifests with the same content produce identical
|
|
40
|
+
* JSON output, making them suitable for version control and comparison.
|
|
41
|
+
*
|
|
42
|
+
* @param manifest - The manifest to normalize
|
|
43
|
+
* @returns A new normalized manifest object
|
|
44
|
+
*/
|
|
17
45
|
export function normalizeLexiconsManifest(
|
|
18
46
|
manifest: LexiconsManifest,
|
|
19
47
|
): LexiconsManifest {
|
package/src/nsid-map.ts
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { NSID } from '@atproto/syntax'
|
|
2
2
|
|
|
3
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.
|
|
4
|
+
* A Map implementation that maps keys of type K to an internal representation I.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Key identity is determined by the {@link Object.is} comparison of the
|
|
7
|
+
* encoded keys. This is useful for complex key types that can be serialized
|
|
8
|
+
* to a unique primitive representation (typically strings).
|
|
9
|
+
*
|
|
10
|
+
* @typeParam K - The external key type
|
|
11
|
+
* @typeParam V - The value type stored in the map
|
|
12
|
+
* @typeParam I - The internal encoded key type used for identity comparison
|
|
10
13
|
*/
|
|
11
14
|
class MappedMap<K, V, I = any> implements Map<K, V> {
|
|
12
15
|
private map = new Map<I, V>()
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new MappedMap with custom key encoding/decoding functions.
|
|
19
|
+
*
|
|
20
|
+
* @param encodeKey - Function to convert external keys to internal representation
|
|
21
|
+
* @param decodeKey - Function to convert internal representation back to external keys
|
|
22
|
+
*/
|
|
14
23
|
constructor(
|
|
15
24
|
private readonly encodeKey: (key: K) => I,
|
|
16
25
|
private readonly decodeKey: (enc: I) => K,
|
|
@@ -73,7 +82,39 @@ class MappedMap<K, V, I = any> implements Map<K, V> {
|
|
|
73
82
|
[Symbol.toStringTag]: string = 'MappedMap'
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A Map specialized for using NSID (Namespaced Identifier) values as keys.
|
|
87
|
+
*
|
|
88
|
+
* NSIDs are compared by their string representation, allowing different
|
|
89
|
+
* NSID object instances with the same value to be treated as the same key.
|
|
90
|
+
*
|
|
91
|
+
* @typeParam T - The value type stored in the map
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* import { NsidMap } from '@atproto/lex-installer'
|
|
96
|
+
* import { NSID } from '@atproto/syntax'
|
|
97
|
+
* import { LexiconDocument } from '@atproto/lex-document'
|
|
98
|
+
*
|
|
99
|
+
* const lexicons = new NsidMap<LexiconDocument>()
|
|
100
|
+
*
|
|
101
|
+
* // Store lexicon documents by NSID
|
|
102
|
+
* lexicons.set(NSID.from('app.bsky.feed.post'), postLexicon)
|
|
103
|
+
* lexicons.set(NSID.from('app.bsky.actor.profile'), profileLexicon)
|
|
104
|
+
*
|
|
105
|
+
* // Retrieve by NSID (different object instance works)
|
|
106
|
+
* const doc = lexicons.get(NSID.from('app.bsky.feed.post'))
|
|
107
|
+
*
|
|
108
|
+
* // Iterate over entries
|
|
109
|
+
* for (const [nsid, lexicon] of lexicons) {
|
|
110
|
+
* console.log(`${nsid}: ${lexicon.description}`)
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
76
114
|
export class NsidMap<T> extends MappedMap<NSID, T, string> {
|
|
115
|
+
/**
|
|
116
|
+
* Creates a new empty NsidMap.
|
|
117
|
+
*/
|
|
77
118
|
constructor() {
|
|
78
119
|
super(
|
|
79
120
|
(key) => key.toString(),
|