@atproto/lex-builder 0.0.13 → 0.0.15
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 +16 -0
- package/dist/filter.d.ts +55 -0
- package/dist/filter.d.ts.map +1 -1
- package/dist/filter.js +22 -0
- package/dist/filter.js.map +1 -1
- package/dist/formatter.d.ts +42 -1
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +25 -0
- package/dist/formatter.js.map +1 -1
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/dist/lex-builder.d.ts +69 -0
- package/dist/lex-builder.d.ts.map +1 -1
- package/dist/lex-builder.js +20 -0
- package/dist/lex-builder.js.map +1 -1
- package/dist/lex-def-builder.d.ts +37 -1
- package/dist/lex-def-builder.d.ts.map +1 -1
- package/dist/lex-def-builder.js +11 -1
- package/dist/lex-def-builder.js.map +1 -1
- package/dist/lexicon-directory-indexer.d.ts +13 -0
- package/dist/lexicon-directory-indexer.d.ts.map +1 -1
- package/dist/lexicon-directory-indexer.js +8 -0
- package/dist/lexicon-directory-indexer.js.map +1 -1
- package/dist/ref-resolver.d.ts +72 -2
- package/dist/ref-resolver.d.ts.map +1 -1
- package/dist/ref-resolver.js +47 -3
- package/dist/ref-resolver.js.map +1 -1
- package/dist/ts-lang.d.ts +44 -0
- package/dist/ts-lang.d.ts.map +1 -1
- package/dist/ts-lang.js +48 -1
- package/dist/ts-lang.js.map +1 -1
- package/package.json +3 -3
- package/src/filter.ts +55 -0
- package/src/formatter.ts +42 -1
- package/src/index.ts +34 -0
- package/src/lex-builder.ts +69 -0
- package/src/lex-def-builder.ts +37 -1
- package/src/lexicon-directory-indexer.ts +13 -0
- package/src/ref-resolver.test.ts +75 -0
- package/src/ref-resolver.ts +78 -6
- package/src/ts-lang.ts +48 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Project } from 'ts-morph'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { LexiconDocument, LexiconIndexer } from '@atproto/lex-document'
|
|
4
|
+
import { RefResolver } from './ref-resolver.js'
|
|
5
|
+
|
|
6
|
+
class DummyIndexer implements LexiconIndexer, AsyncIterable<LexiconDocument> {
|
|
7
|
+
readonly docs: Map<string, LexiconDocument>
|
|
8
|
+
|
|
9
|
+
constructor(docs: LexiconDocument[]) {
|
|
10
|
+
this.docs = new Map(docs.map((doc) => [doc.id, doc]))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async get(id: string): Promise<LexiconDocument> {
|
|
14
|
+
const doc = this.docs.get(id)
|
|
15
|
+
if (!doc) {
|
|
16
|
+
throw new Error(`Document not found: ${id}`)
|
|
17
|
+
}
|
|
18
|
+
return doc
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async *[Symbol.asyncIterator]() {
|
|
22
|
+
for (const doc of this.docs.values()) {
|
|
23
|
+
yield doc
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('RefResolver', () => {
|
|
29
|
+
const docs: LexiconDocument[] = [
|
|
30
|
+
{
|
|
31
|
+
lexicon: 1,
|
|
32
|
+
id: 'com.example.foo',
|
|
33
|
+
defs: {
|
|
34
|
+
main: { type: 'token' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
lexicon: 1,
|
|
39
|
+
id: 'com.example.bar',
|
|
40
|
+
defs: {
|
|
41
|
+
main: { type: 'token' },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
it('uses default relative path for external refs', async () => {
|
|
47
|
+
const project = new Project({ useInMemoryFileSystem: true })
|
|
48
|
+
const file = project.createSourceFile('/com/example/foo.defs.ts')
|
|
49
|
+
const indexer = new DummyIndexer(docs)
|
|
50
|
+
const resolver = new RefResolver(docs[0], file, indexer, {})
|
|
51
|
+
|
|
52
|
+
await resolver.resolve('com.example.bar#main')
|
|
53
|
+
|
|
54
|
+
const imports = file.getImportDeclarations()
|
|
55
|
+
expect(imports).toHaveLength(1)
|
|
56
|
+
expect(imports[0].getModuleSpecifierValue()).toBe('./bar.defs.js')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('uses custom moduleSpecifier when provided', async () => {
|
|
60
|
+
const project = new Project({ useInMemoryFileSystem: true })
|
|
61
|
+
const file = project.createSourceFile('/com/example/foo.defs.ts')
|
|
62
|
+
const indexer = new DummyIndexer(docs)
|
|
63
|
+
const resolver = new RefResolver(docs[0], file, indexer, {
|
|
64
|
+
moduleSpecifier: (nsid) => `https://lex.example.com/${nsid}.ts`,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await resolver.resolve('com.example.bar#main')
|
|
68
|
+
|
|
69
|
+
const imports = file.getImportDeclarations()
|
|
70
|
+
expect(imports).toHaveLength(1)
|
|
71
|
+
expect(imports[0].getModuleSpecifierValue()).toBe(
|
|
72
|
+
'https://lex.example.com/com.example.bar.ts',
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
})
|
package/src/ref-resolver.ts
CHANGED
|
@@ -17,18 +17,68 @@ import {
|
|
|
17
17
|
ucFirst,
|
|
18
18
|
} from './util.js'
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Configuration options for the {@link RefResolver} class.
|
|
22
|
+
*/
|
|
20
23
|
export type RefResolverOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* The file extension to use for import specifiers when resolving
|
|
26
|
+
* external references.
|
|
27
|
+
*
|
|
28
|
+
* @default '.js'
|
|
29
|
+
*/
|
|
21
30
|
importExt?: string
|
|
31
|
+
moduleSpecifier?: (nsid: string) => string
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Represents a resolved lexicon reference as TypeScript identifiers.
|
|
36
|
+
*
|
|
37
|
+
* Contains the variable name (for runtime schema) and type name (for
|
|
38
|
+
* TypeScript type) that can be used to reference a lexicon definition.
|
|
39
|
+
*/
|
|
24
40
|
export type ResolvedRef = {
|
|
41
|
+
/**
|
|
42
|
+
* The variable name for the runtime schema.
|
|
43
|
+
*
|
|
44
|
+
* For local definitions, this is a simple identifier.
|
|
45
|
+
* For external definitions, this may be a qualified name like `ns.varName`
|
|
46
|
+
* or bracket notation like `ns["varName"]` for unsafe identifiers.
|
|
47
|
+
*/
|
|
25
48
|
varName: string
|
|
49
|
+
/**
|
|
50
|
+
* The type name for the TypeScript type alias.
|
|
51
|
+
*
|
|
52
|
+
* Always a valid TypeScript identifier, either simple or qualified.
|
|
53
|
+
*/
|
|
26
54
|
typeName: string
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
58
|
+
* Resolves lexicon references to TypeScript identifiers.
|
|
59
|
+
*
|
|
60
|
+
* This class handles the resolution of `ref` types in lexicon documents,
|
|
61
|
+
* converting lexicon reference strings (like `com.example.foo#bar`) into
|
|
62
|
+
* valid TypeScript identifiers. It automatically manages:
|
|
63
|
+
*
|
|
64
|
+
* - Local references within the same document
|
|
65
|
+
* - External references to other lexicon documents
|
|
66
|
+
* - Import statement generation for external references
|
|
67
|
+
* - Conflict avoidance with keywords, globals, and existing declarations
|
|
68
|
+
*
|
|
69
|
+
* Results are memoized to ensure consistent identifiers for the same
|
|
70
|
+
* reference throughout a file.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const resolver = new RefResolver(doc, sourceFile, indexer, options)
|
|
75
|
+
*
|
|
76
|
+
* // Resolve a local reference
|
|
77
|
+
* const local = await resolver.resolve('#myDef')
|
|
78
|
+
*
|
|
79
|
+
* // Resolve an external reference
|
|
80
|
+
* const external = await resolver.resolve('com.example.other#def')
|
|
81
|
+
* ```
|
|
32
82
|
*/
|
|
33
83
|
export class RefResolver {
|
|
34
84
|
constructor(
|
|
@@ -79,6 +129,19 @@ export class RefResolver {
|
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
/**
|
|
132
|
+
* Resolves a local definition hash to TypeScript identifiers.
|
|
133
|
+
*
|
|
134
|
+
* This method generates safe, non-conflicting identifiers for definitions
|
|
135
|
+
* within the current document. It handles edge cases like:
|
|
136
|
+
* - Hash names that are JavaScript keywords
|
|
137
|
+
* - Hash names that conflict with global identifiers
|
|
138
|
+
* - Multiple hashes that would produce the same identifier
|
|
139
|
+
*
|
|
140
|
+
* @param hash - The definition hash (e.g., 'main', 'record', 'myType')
|
|
141
|
+
* @returns A promise resolving to the TypeScript identifiers
|
|
142
|
+
* @throws Error if the hash does not exist in the document
|
|
143
|
+
* @throws Error if conflicting type names are detected
|
|
144
|
+
*
|
|
82
145
|
* @note The returned `typeName` and `varName` are *both* guaranteed to be
|
|
83
146
|
* valid TypeScript identifiers.
|
|
84
147
|
*/
|
|
@@ -149,10 +212,12 @@ export class RefResolver {
|
|
|
149
212
|
private readonly resolveExternal = memoize(
|
|
150
213
|
async (fullRef: string): Promise<ResolvedRef> => {
|
|
151
214
|
const [nsid, hash] = fullRef.split('#')
|
|
152
|
-
const moduleSpecifier =
|
|
153
|
-
this.
|
|
154
|
-
|
|
155
|
-
|
|
215
|
+
const moduleSpecifier = this.options.moduleSpecifier
|
|
216
|
+
? this.options.moduleSpecifier(nsid)
|
|
217
|
+
: `${asRelativePath(
|
|
218
|
+
this.file.getDirectoryPath(),
|
|
219
|
+
join('/', ...nsid.split('.')),
|
|
220
|
+
)}.defs${this.options.importExt ?? '.js'}`
|
|
156
221
|
|
|
157
222
|
// Lets first make sure the referenced lexicon exists
|
|
158
223
|
const srcDoc = await this.indexer.get(nsid)
|
|
@@ -325,6 +390,13 @@ function nsidToIdentifier(nsid: string) {
|
|
|
325
390
|
/**
|
|
326
391
|
* Generates predictable public identifiers for a given definition hash.
|
|
327
392
|
*
|
|
393
|
+
* This function creates the "public" names that will be exported from
|
|
394
|
+
* generated files. The variable name uses the original hash, while the
|
|
395
|
+
* type name is converted to PascalCase.
|
|
396
|
+
*
|
|
397
|
+
* @param hash - The definition hash (e.g., 'main', 'myType')
|
|
398
|
+
* @returns The public identifiers for the definition
|
|
399
|
+
*
|
|
328
400
|
* @note The returned `typeName` is guaranteed to be a valid TypeScript
|
|
329
401
|
* identifier. `varName` may not be a valid identifier (eg. if the hash contains
|
|
330
402
|
* unsafe characters), and may need to be accessed using string indexing.
|
package/src/ts-lang.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JavaScript keywords
|
|
2
|
+
* Set of JavaScript reserved keywords and future reserved words.
|
|
3
|
+
*
|
|
4
|
+
* These identifiers cannot be used as variable or type names in generated code.
|
|
5
|
+
*
|
|
3
6
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar}
|
|
4
7
|
*/
|
|
5
8
|
const JS_KEYWORDS = new Set([
|
|
@@ -77,6 +80,12 @@ const JS_KEYWORDS = new Set([
|
|
|
77
80
|
'yield',
|
|
78
81
|
])
|
|
79
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Checks if a word is a JavaScript reserved keyword.
|
|
85
|
+
*
|
|
86
|
+
* @param word - The identifier to check
|
|
87
|
+
* @returns `true` if the word is a reserved keyword
|
|
88
|
+
*/
|
|
80
89
|
export function isJsKeyword(word: string) {
|
|
81
90
|
return JS_KEYWORDS.has(word)
|
|
82
91
|
}
|
|
@@ -129,14 +138,42 @@ const GLOBAL_IDENTIFIERS = new Set([
|
|
|
129
138
|
'Uncapitalize',
|
|
130
139
|
])
|
|
131
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Checks if a word is a global identifier that should be avoided.
|
|
143
|
+
*
|
|
144
|
+
* This includes JavaScript globals, TypeScript built-in types, and
|
|
145
|
+
* identifiers commonly used in the generated code.
|
|
146
|
+
*
|
|
147
|
+
* @param word - The identifier to check
|
|
148
|
+
* @returns `true` if the word is a global identifier
|
|
149
|
+
*/
|
|
132
150
|
export function isGlobalIdentifier(word: string) {
|
|
133
151
|
return GLOBAL_IDENTIFIERS.has(word)
|
|
134
152
|
}
|
|
135
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Checks if a name is safe to use as a local identifier.
|
|
156
|
+
*
|
|
157
|
+
* A safe local identifier is a valid JavaScript identifier that does not
|
|
158
|
+
* conflict with global identifiers.
|
|
159
|
+
*
|
|
160
|
+
* @param name - The identifier to check
|
|
161
|
+
* @returns `true` if the name is safe to use locally
|
|
162
|
+
*/
|
|
136
163
|
export function isSafeLocalIdentifier(name: string) {
|
|
137
164
|
return !isGlobalIdentifier(name) && isValidJsIdentifier(name)
|
|
138
165
|
}
|
|
139
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Checks if a name is a valid JavaScript identifier.
|
|
169
|
+
*
|
|
170
|
+
* Valid identifiers start with a letter, underscore, or dollar sign,
|
|
171
|
+
* followed by any combination of letters, digits, underscores, or dollar
|
|
172
|
+
* signs. Reserved keywords are not valid identifiers.
|
|
173
|
+
*
|
|
174
|
+
* @param name - The string to check
|
|
175
|
+
* @returns `true` if the name is a valid identifier
|
|
176
|
+
*/
|
|
140
177
|
export function isValidJsIdentifier(name: string) {
|
|
141
178
|
return (
|
|
142
179
|
name.length > 0 &&
|
|
@@ -145,6 +182,16 @@ export function isValidJsIdentifier(name: string) {
|
|
|
145
182
|
)
|
|
146
183
|
}
|
|
147
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Converts a name to a valid namespace export identifier.
|
|
187
|
+
*
|
|
188
|
+
* If the name is a valid JavaScript identifier, it is returned as-is.
|
|
189
|
+
* Otherwise, it is returned as a quoted string for use in export statements
|
|
190
|
+
* like `export { foo as "unsafe-name" }`.
|
|
191
|
+
*
|
|
192
|
+
* @param name - The export name
|
|
193
|
+
* @returns The name as a valid export identifier
|
|
194
|
+
*/
|
|
148
195
|
export function asNamespaceExport(name: string) {
|
|
149
196
|
return isValidJsIdentifier(name) ? name : JSON.stringify(name)
|
|
150
197
|
}
|