@atproto/lex-builder 0.1.3 → 0.1.5

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 +22 -0
  2. package/dist/filtered-indexer.d.ts +2 -2
  3. package/dist/filtered-indexer.d.ts.map +1 -1
  4. package/dist/filtered-indexer.js.map +1 -1
  5. package/dist/formatter.d.ts +1 -1
  6. package/dist/formatter.d.ts.map +1 -1
  7. package/dist/formatter.js +1 -1
  8. package/dist/formatter.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/lex-builder.d.ts +4 -4
  14. package/dist/lex-builder.d.ts.map +1 -1
  15. package/dist/lex-builder.js +53 -2
  16. package/dist/lex-builder.js.map +1 -1
  17. package/dist/lex-def-builder.d.ts +2 -2
  18. package/dist/lex-def-builder.d.ts.map +1 -1
  19. package/dist/lex-def-builder.js +1 -1
  20. package/dist/lex-def-builder.js.map +1 -1
  21. package/dist/lexicon-directory-indexer.d.ts.map +1 -1
  22. package/dist/lexicon-directory-indexer.js.map +1 -1
  23. package/dist/ref-resolver.d.ts +1 -1
  24. package/dist/ref-resolver.d.ts.map +1 -1
  25. package/dist/ref-resolver.js +1 -0
  26. package/dist/ref-resolver.js.map +1 -1
  27. package/package.json +6 -10
  28. package/src/filter.ts +0 -96
  29. package/src/filtered-indexer.test.ts +0 -84
  30. package/src/filtered-indexer.ts +0 -60
  31. package/src/formatter.ts +0 -83
  32. package/src/index.ts +0 -56
  33. package/src/lex-builder.ts +0 -299
  34. package/src/lex-def-builder.ts +0 -1035
  35. package/src/lexicon-directory-indexer.ts +0 -65
  36. package/src/polyfill.ts +0 -7
  37. package/src/ref-resolver.test.ts +0 -75
  38. package/src/ref-resolver.ts +0 -437
  39. package/src/ts-lang.ts +0 -197
  40. package/src/util.ts +0 -72
  41. package/tsconfig.build.json +0 -13
  42. package/tsconfig.json +0 -7
  43. package/tsconfig.tests.json +0 -8
@@ -1,65 +0,0 @@
1
- import { readFile, readdir } from 'node:fs/promises'
2
- import { join } from 'node:path'
3
- import {
4
- LexiconDocument,
5
- LexiconIterableIndexer,
6
- lexiconDocumentSchema,
7
- } from '@atproto/lex-document'
8
-
9
- /**
10
- * Options for the {@link LexiconDirectoryIndexer}.
11
- *
12
- * @see {@link ReadLexiconsOptions} for available options
13
- */
14
- export type LexiconDirectoryIndexerOptions = ReadLexiconsOptions
15
-
16
- /**
17
- * Indexes lexicon documents from a filesystem directory.
18
- *
19
- * This class recursively scans a directory for JSON files, parses them as
20
- * lexicon documents, and provides an iterable interface for processing them.
21
- * It extends {@link LexiconIterableIndexer} to support both iteration and
22
- * lookup by NSID.
23
- */
24
- export class LexiconDirectoryIndexer extends LexiconIterableIndexer {
25
- constructor(options: LexiconDirectoryIndexerOptions) {
26
- super(readLexicons(options))
27
- }
28
- }
29
-
30
- type ReadLexiconsOptions = {
31
- lexicons: string
32
- ignoreInvalidLexicons?: boolean
33
- }
34
-
35
- async function* readLexicons(
36
- options: ReadLexiconsOptions,
37
- ): AsyncGenerator<LexiconDocument, void, unknown> {
38
- for await (const filePath of listFiles(options.lexicons)) {
39
- if (filePath.endsWith('.json')) {
40
- try {
41
- const data = await readFile(filePath, 'utf8')
42
- yield lexiconDocumentSchema.parse(JSON.parse(data))
43
- } catch (cause) {
44
- const message = `Error parsing lexicon document ${filePath}`
45
- if (options.ignoreInvalidLexicons) console.error(`${message}:`, cause)
46
- else throw new Error(message, { cause })
47
- }
48
- }
49
- }
50
- }
51
-
52
- async function* listFiles(dir: string): AsyncGenerator<string> {
53
- const dirents = await readdir(dir, { withFileTypes: true }).catch((err) => {
54
- if ((err as any)?.code === 'ENOENT') return []
55
- throw err
56
- })
57
- for (const dirent of dirents) {
58
- const res = join(dir, dirent.name)
59
- if (dirent.isDirectory()) {
60
- yield* listFiles(res)
61
- } else if (dirent.isFile() || dirent.isSymbolicLink()) {
62
- yield res
63
- }
64
- }
65
- }
package/src/polyfill.ts DELETED
@@ -1,7 +0,0 @@
1
- // Node <18.18, 19.x, <20.4 and 21.x do not have these symbols defined
2
-
3
- // @ts-expect-error
4
- Symbol.asyncDispose ??= Symbol.for('nodejs.asyncDispose')
5
-
6
- // @ts-expect-error
7
- Symbol.dispose ??= Symbol.for('nodejs.dispose')
@@ -1,75 +0,0 @@
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
- })
@@ -1,437 +0,0 @@
1
- import assert from 'node:assert'
2
- import { join } from 'node:path'
3
- import { SourceFile } from 'ts-morph'
4
- import { LexiconDocument, LexiconIndexer } from '@atproto/lex-document'
5
- import {
6
- isGlobalIdentifier,
7
- isJsKeyword,
8
- isSafeLocalIdentifier,
9
- isValidJsIdentifier,
10
- } from './ts-lang.js'
11
- import {
12
- asRelativePath,
13
- memoize,
14
- startsWithLower,
15
- toCamelCase,
16
- toPascalCase,
17
- ucFirst,
18
- } from './util.js'
19
-
20
- /**
21
- * Configuration options for the {@link RefResolver} class.
22
- */
23
- export type RefResolverOptions = {
24
- /**
25
- * The file extension to use for import specifiers when resolving
26
- * external references.
27
- *
28
- * @default '.js'
29
- */
30
- importExt?: string
31
- moduleSpecifier?: (nsid: string) => string
32
- }
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
- */
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
- */
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
- */
54
- typeName: string
55
- }
56
-
57
- /**
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
- * ```
82
- */
83
- export class RefResolver {
84
- constructor(
85
- private doc: LexiconDocument,
86
- private file: SourceFile,
87
- private indexer: LexiconIndexer,
88
- private options: RefResolverOptions,
89
- ) {}
90
-
91
- public readonly resolve = memoize(
92
- async (ref: string): Promise<ResolvedRef> => {
93
- const [nsid, hash = 'main'] = ref.split('#')
94
-
95
- if (nsid === '' || nsid === this.doc.id) {
96
- return this.resolveLocal(hash)
97
- } else {
98
- // @NOTE: Normalize (#main fragment) to ensure proper memoization
99
- const fullRef = `${nsid}#${hash}`
100
- return this.resolveExternal(fullRef)
101
- }
102
- },
103
- )
104
-
105
- #defCounters = new Map<string, number>()
106
- private nextSafeDefinitionIdentifier(name: string) {
107
- // use camelCase version of the hash as base name
108
- const nameSafe =
109
- startsWithLower(name) && isValidJsIdentifier(name)
110
- ? name
111
- : toCamelCase(name).replace(/^[0-9]+/g, '') || 'def'
112
-
113
- const count = this.#defCounters.get(nameSafe) ?? 0
114
- this.#defCounters.set(nameSafe, count + 1)
115
-
116
- // @NOTE We don't need to check against local declarations in the file here
117
- // since we are using a naming system that should guarantee no other
118
- // identifier has a <nameSafe>$<number> format ("$" cannot appear in
119
- // hashes so only *we* are generating such identifiers).
120
-
121
- const identifier = `${nameSafe}$${count}`
122
-
123
- assert(
124
- isValidJsIdentifier(identifier),
125
- `Unable to generate safe identifier for: "${name}"`,
126
- )
127
-
128
- return identifier
129
- }
130
-
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
- *
145
- * @note The returned `typeName` and `varName` are *both* guaranteed to be
146
- * valid TypeScript identifiers.
147
- */
148
- public readonly resolveLocal = memoize(
149
- async (hash: string): Promise<ResolvedRef> => {
150
- const hashes = Object.keys(this.doc.defs)
151
-
152
- if (!hashes.includes(hash)) {
153
- throw new Error(`Definition ${hash} not found in ${this.doc.id}`)
154
- }
155
-
156
- // Because we are using predictable "public" identifiers for type names,
157
- // we need to ensure there are no conflicts between different definitions
158
- // in the same lexicon document.
159
- //
160
- // @NOTE It should be possible to implement a way to generate
161
- // non-conflicting type names for all public (type) identifiers in a
162
- // project. However, this would add a lot of complexity to the code
163
- // generation process, and the likelihood of such conflicts happening in
164
- // practice is very low, so we opt for a simpler approach of just throwing
165
- // an error if a conflict is detected.
166
- const pub = getPublicIdentifiers(hash)
167
- for (const otherHash of hashes) {
168
- if (otherHash === hash) continue
169
- const otherPub = getPublicIdentifiers(otherHash)
170
- if (otherPub.typeName === pub.typeName) {
171
- throw new Error(
172
- `Conflicting type names for definitions #${hash} and #${otherHash} in ${this.doc.id}`,
173
- )
174
- }
175
- }
176
-
177
- // Try to keep and identifier that resembles the original hash as identifier
178
- const safeIdentifier = asSafeDefinitionIdentifier(hash)
179
-
180
- // If the safe identifier is not conflicting with other definition names,
181
- // or reserved words, we can use it as-is. Otherwise, we need to generate
182
- // a unique safe identifier.
183
- const varName = safeIdentifier
184
- ? !hashes.some((otherHash) => {
185
- if (otherHash === hash) return false
186
- const otherIdentifier = asSafeDefinitionIdentifier(otherHash)
187
- return otherIdentifier === safeIdentifier
188
- })
189
- ? // Safe identifier can be used as-is as it does not conflict with
190
- // other definition names
191
- safeIdentifier
192
- : // In order to keep identifiers stable, we use the safe identifier
193
- // as base, and append a counter to avoid conflicts
194
- this.nextSafeDefinitionIdentifier(safeIdentifier)
195
- : // hash only contained unsafe characters, generate a safe one
196
- this.nextSafeDefinitionIdentifier(hash)
197
-
198
- const typeName = ucFirst(varName)
199
- assert(isSafeLocalIdentifier(typeName), 'Expected safe type identifier')
200
- assert(varName !== typeName, 'Variable and type name should be different')
201
-
202
- return { varName, typeName }
203
- },
204
- )
205
-
206
- /**
207
- * @note Since this is a memoized function, and is used to generate the name
208
- * of local variables, we should avoid returning different results for
209
- * similar, but non strictly equal, inputs (eg. normalized / non-normalized).
210
- * @see {@link resolve}
211
- */
212
- private readonly resolveExternal = memoize(
213
- async (fullRef: string): Promise<ResolvedRef> => {
214
- const [nsid, hash] = fullRef.split('#')
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'}`
221
-
222
- // Lets first make sure the referenced lexicon exists
223
- const srcDoc = await this.indexer.get(nsid)
224
- const srcDef = Object.hasOwn(srcDoc.defs, hash) ? srcDoc.defs[hash] : null
225
- if (!srcDef) {
226
- throw new Error(
227
- `Missing def "${hash}" in "${nsid}" (referenced from ${this.doc.id})`,
228
- )
229
- }
230
-
231
- const publicIds = getPublicIdentifiers(hash)
232
-
233
- if (!isValidJsIdentifier(publicIds.typeName)) {
234
- // If <typeName> is not a valid identifier, we cannot access the type
235
- // using dot notation (<nsIdentifier>.<typeName>). Note that, unlike js
236
- // variables, types cannot be accessed using string indexing (like:
237
- // <nsIdentifier>['<typeName>']) because it generates TypeScript errors:
238
- //
239
- // > "Cannot use namespace '<nsIdentifier>' as a type."
240
-
241
- // Instead the generated code should look like:
242
- // import { "<unsafeTypeName>" as <safeIdentifier> } from './<moduleSpecifier>.js'
243
-
244
- // Because it requires more complex management of local variables names,
245
- // and we don't expect this to actually happen with properly designed
246
- // lexicons documents, we do not support this for now.
247
-
248
- throw new Error(
249
- 'Import of definitions with unsafe type names is not supported',
250
- )
251
- }
252
-
253
- // import * as <nsIdentifier> from './<moduleSpecifier>.js'
254
- const nsIdentifier = this.getNsIdentifier(nsid, moduleSpecifier)
255
-
256
- return {
257
- varName: isValidJsIdentifier(publicIds.varName)
258
- ? `${nsIdentifier}.${publicIds.varName}`
259
- : `${nsIdentifier}[${JSON.stringify(publicIds.varName)}]`,
260
- typeName: `${nsIdentifier}.${publicIds.typeName}`,
261
- }
262
- },
263
- )
264
-
265
- private getNsIdentifier(nsid: string, moduleSpecifier: string) {
266
- const namespaceImportDeclaration =
267
- this.file.getImportDeclaration(
268
- (imp) =>
269
- !imp.isTypeOnly() &&
270
- imp.getModuleSpecifierValue() === moduleSpecifier &&
271
- imp.getNamespaceImport() != null,
272
- ) ||
273
- this.file.addImportDeclaration({
274
- moduleSpecifier,
275
- namespaceImport: this.computeSafeNamespaceIdentifierFor(nsid),
276
- })
277
-
278
- return namespaceImportDeclaration.getNamespaceImport()!.getText()
279
- }
280
-
281
- #nsIdentifiersCounters = new Map<string, number>()
282
- private computeSafeNamespaceIdentifierFor(nsid: string) {
283
- const baseName = nsidToIdentifier(nsid) || 'NS'
284
-
285
- let name = baseName
286
- while (this.isConflictingIdentifier(name)) {
287
- const count = this.#nsIdentifiersCounters.get(baseName) ?? 0
288
- this.#nsIdentifiersCounters.set(baseName, count + 1)
289
- name = `${baseName}$$${count}`
290
- }
291
-
292
- return name
293
- }
294
-
295
- private isConflictingIdentifier(name: string) {
296
- return (
297
- this.conflictsWithKeywords(name) ||
298
- this.conflictsWithUtils(name) ||
299
- this.conflictsWithLocalDefs(name) ||
300
- this.conflictsWithLocalDeclarations(name) ||
301
- this.conflictsWithImports(name)
302
- )
303
- }
304
-
305
- private conflictsWithKeywords(name: string) {
306
- return isJsKeyword(name) || isGlobalIdentifier(name)
307
- }
308
-
309
- private conflictsWithUtils(name: string) {
310
- // Do not allow "Main" as imported ns identifier since it has a special
311
- // meaning in the context of lexicon definitions.
312
- if (name === 'Main') return true
313
-
314
- // When "useRecordExport" returns true, an export named "Record" will be
315
- // used in addition to the hash named export. So we need to make sure both
316
- // names are not conflicting with local variables.
317
- if (name === 'Record') return true
318
-
319
- // Utility functions generated for lexicon schemas are prefixed with "$"
320
- return name.startsWith('$')
321
- }
322
-
323
- private conflictsWithLocalDefs(name: string) {
324
- return Object.keys(this.doc.defs).some((hash) => {
325
- const identifier = toCamelCase(hash)
326
-
327
- // A safe identifier will be generated, no risk of conflict.
328
- if (!identifier) return false
329
-
330
- // The imported name conflicts with a local definition name
331
- if (identifier === name || `_${identifier}` === name) return true
332
-
333
- // The imported name conflicts with the type name of a local definition
334
- const typeName = ucFirst(identifier)
335
- if (typeName === name || `_${typeName}` === name) return true
336
-
337
- return false
338
- })
339
- }
340
-
341
- private conflictsWithLocalDeclarations(name: string) {
342
- return (
343
- this.file.getVariableDeclarations().some((v) => v.getName() === name) ||
344
- this.file
345
- .getVariableStatements()
346
- .some((vs) => vs.getDeclarations().some((d) => d.getName() === name)) ||
347
- this.file.getTypeAliases().some((t) => t.getName() === name) ||
348
- this.file.getInterfaces().some((i) => i.getName() === name) ||
349
- this.file.getClasses().some((c) => c.getName() === name) ||
350
- this.file.getFunctions().some((f) => f.getName() === name) ||
351
- this.file.getEnums().some((e) => e.getName() === name)
352
- )
353
- }
354
-
355
- private conflictsWithImports(name: string) {
356
- return this.file.getImportDeclarations().some(
357
- (imp) =>
358
- // import name from '...'
359
- imp.getDefaultImport()?.getText() === name ||
360
- // import * as name from '...'
361
- imp.getNamespaceImport()?.getText() === name ||
362
- imp.getNamedImports().some(
363
- (named) =>
364
- // import { name } from '...'
365
- // import { foo as name } from '...'
366
- (named.getAliasNode()?.getText() ?? named.getName()) === name,
367
- ),
368
- )
369
- }
370
- }
371
-
372
- /**
373
- * @see {@link https://atproto.com/specs/nsid NSID syntax spec}
374
- */
375
- function nsidToIdentifier(nsid: string) {
376
- const parts = nsid.split('.')
377
-
378
- // By default, try to keep only to the last two segments of the NSID as
379
- // contextual information. If those do not form a safe identifier (typically
380
- // because they start with a digit), try with more segments until we reach the
381
- // full NSID.
382
- for (let i = 2; i < parts.length; i++) {
383
- const identifier = toPascalCase(parts.slice(-i).join('.'))
384
- if (isSafeLocalIdentifier(identifier)) return identifier
385
- }
386
-
387
- return undefined
388
- }
389
-
390
- /**
391
- * Generates predictable public identifiers for a given definition hash.
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
- *
400
- * @note The returned `typeName` is guaranteed to be a valid TypeScript
401
- * identifier. `varName` may not be a valid identifier (eg. if the hash contains
402
- * unsafe characters), and may need to be accessed using string indexing.
403
- */
404
- export function getPublicIdentifiers(hash: string): ResolvedRef {
405
- const varName = hash
406
-
407
- // @NOTE we try to circumvent the issue of unsafe type names described in
408
- // `RefResolver.resolveExternal` by ensuring that type names are always safe
409
- // identifiers, even if it means changing them from the original hash.
410
- let typeName = toPascalCase(hash)
411
-
412
- if (varName === typeName || !isValidJsIdentifier(typeName)) {
413
- typeName = `TypeOf${typeName}`
414
- }
415
-
416
- assert(
417
- isValidJsIdentifier(typeName),
418
- `Unable to generate a predictable safe identifier for "${hash}"`,
419
- )
420
-
421
- return { varName, typeName }
422
- }
423
-
424
- function asSafeDefinitionIdentifier(name: string) {
425
- if (
426
- startsWithLower(name) &&
427
- isSafeLocalIdentifier(name) &&
428
- isSafeLocalIdentifier(ucFirst(name))
429
- ) {
430
- return name
431
- }
432
- const camel = toCamelCase(name)
433
- if (isSafeLocalIdentifier(camel) && isSafeLocalIdentifier(ucFirst(camel))) {
434
- return camel
435
- }
436
- return undefined
437
- }