@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/filter.d.ts +55 -0
  3. package/dist/filter.d.ts.map +1 -1
  4. package/dist/filter.js +22 -0
  5. package/dist/filter.js.map +1 -1
  6. package/dist/formatter.d.ts +42 -1
  7. package/dist/formatter.d.ts.map +1 -1
  8. package/dist/formatter.js +25 -0
  9. package/dist/formatter.js.map +1 -1
  10. package/dist/index.d.ts +34 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +24 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/lex-builder.d.ts +69 -0
  15. package/dist/lex-builder.d.ts.map +1 -1
  16. package/dist/lex-builder.js +20 -0
  17. package/dist/lex-builder.js.map +1 -1
  18. package/dist/lex-def-builder.d.ts +37 -1
  19. package/dist/lex-def-builder.d.ts.map +1 -1
  20. package/dist/lex-def-builder.js +11 -1
  21. package/dist/lex-def-builder.js.map +1 -1
  22. package/dist/lexicon-directory-indexer.d.ts +13 -0
  23. package/dist/lexicon-directory-indexer.d.ts.map +1 -1
  24. package/dist/lexicon-directory-indexer.js +8 -0
  25. package/dist/lexicon-directory-indexer.js.map +1 -1
  26. package/dist/ref-resolver.d.ts +72 -2
  27. package/dist/ref-resolver.d.ts.map +1 -1
  28. package/dist/ref-resolver.js +47 -3
  29. package/dist/ref-resolver.js.map +1 -1
  30. package/dist/ts-lang.d.ts +44 -0
  31. package/dist/ts-lang.d.ts.map +1 -1
  32. package/dist/ts-lang.js +48 -1
  33. package/dist/ts-lang.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/filter.ts +55 -0
  36. package/src/formatter.ts +42 -1
  37. package/src/index.ts +34 -0
  38. package/src/lex-builder.ts +69 -0
  39. package/src/lex-def-builder.ts +37 -1
  40. package/src/lexicon-directory-indexer.ts +13 -0
  41. package/src/ref-resolver.test.ts +75 -0
  42. package/src/ref-resolver.ts +78 -6
  43. 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
+ })
@@ -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
- * Utility class to resolve lexicon references to TypeScript identifiers,
31
- * generating "import" statements as needed.
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 = `${asRelativePath(
153
- this.file.getDirectoryPath(),
154
- join('/', ...nsid.split('.')),
155
- )}.defs${this.options.importExt ?? '.js'}`
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
  }