@gaearon/lex-builder 0.0.13

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 (62) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/filter.d.ts +7 -0
  3. package/dist/filter.d.ts.map +1 -0
  4. package/dist/filter.js +30 -0
  5. package/dist/filter.js.map +1 -0
  6. package/dist/filtered-indexer.d.ts +2100 -0
  7. package/dist/filtered-indexer.d.ts.map +1 -0
  8. package/dist/filtered-indexer.js +56 -0
  9. package/dist/filtered-indexer.js.map +1 -0
  10. package/dist/formatter.d.ts +13 -0
  11. package/dist/formatter.d.ts.map +1 -0
  12. package/dist/formatter.js +34 -0
  13. package/dist/formatter.js.map +1 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +16 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lex-builder.d.ts +36 -0
  19. package/dist/lex-builder.d.ts.map +1 -0
  20. package/dist/lex-builder.js +144 -0
  21. package/dist/lex-builder.js.map +1 -0
  22. package/dist/lex-def-builder.d.ts +69 -0
  23. package/dist/lex-def-builder.d.ts.map +1 -0
  24. package/dist/lex-def-builder.js +734 -0
  25. package/dist/lex-def-builder.js.map +1 -0
  26. package/dist/lexicon-directory-indexer.d.ts +11 -0
  27. package/dist/lexicon-directory-indexer.d.ts.map +1 -0
  28. package/dist/lexicon-directory-indexer.js +46 -0
  29. package/dist/lexicon-directory-indexer.js.map +1 -0
  30. package/dist/polyfill.d.ts +1 -0
  31. package/dist/polyfill.d.ts.map +1 -0
  32. package/dist/polyfill.js +7 -0
  33. package/dist/polyfill.js.map +1 -0
  34. package/dist/ref-resolver.d.ts +53 -0
  35. package/dist/ref-resolver.d.ts.map +1 -0
  36. package/dist/ref-resolver.js +277 -0
  37. package/dist/ref-resolver.js.map +1 -0
  38. package/dist/ts-lang.d.ts +6 -0
  39. package/dist/ts-lang.d.ts.map +1 -0
  40. package/dist/ts-lang.js +150 -0
  41. package/dist/ts-lang.js.map +1 -0
  42. package/dist/util.d.ts +12 -0
  43. package/dist/util.d.ts.map +1 -0
  44. package/dist/util.js +72 -0
  45. package/dist/util.js.map +1 -0
  46. package/package.json +53 -0
  47. package/src/filter.ts +41 -0
  48. package/src/filtered-indexer.test.ts +84 -0
  49. package/src/filtered-indexer.ts +60 -0
  50. package/src/formatter.ts +42 -0
  51. package/src/index.ts +23 -0
  52. package/src/lex-builder.ts +186 -0
  53. package/src/lex-def-builder.ts +980 -0
  54. package/src/lexicon-directory-indexer.ts +52 -0
  55. package/src/polyfill.ts +7 -0
  56. package/src/ref-resolver.test.ts +75 -0
  57. package/src/ref-resolver.ts +368 -0
  58. package/src/ts-lang.ts +150 -0
  59. package/src/util.ts +72 -0
  60. package/tsconfig.build.json +13 -0
  61. package/tsconfig.json +7 -0
  62. package/tsconfig.tests.json +9 -0
@@ -0,0 +1,52 @@
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
+ export type LexiconDirectoryIndexerOptions = ReadLexiconsOptions
10
+
11
+ export class LexiconDirectoryIndexer extends LexiconIterableIndexer {
12
+ constructor(options: LexiconDirectoryIndexerOptions) {
13
+ super(readLexicons(options))
14
+ }
15
+ }
16
+
17
+ type ReadLexiconsOptions = {
18
+ lexicons: string
19
+ ignoreInvalidLexicons?: boolean
20
+ }
21
+
22
+ async function* readLexicons(
23
+ options: ReadLexiconsOptions,
24
+ ): AsyncGenerator<LexiconDocument, void, unknown> {
25
+ for await (const filePath of listFiles(options.lexicons)) {
26
+ if (filePath.endsWith('.json')) {
27
+ try {
28
+ const data = await readFile(filePath, 'utf8')
29
+ yield lexiconDocumentSchema.parse(JSON.parse(data))
30
+ } catch (cause) {
31
+ const message = `Error parsing lexicon document ${filePath}`
32
+ if (options.ignoreInvalidLexicons) console.error(`${message}:`, cause)
33
+ else throw new Error(message, { cause })
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ async function* listFiles(dir: string): AsyncGenerator<string> {
40
+ const dirents = await readdir(dir, { withFileTypes: true }).catch((err) => {
41
+ if ((err as any)?.code === 'ENOENT') return []
42
+ throw err
43
+ })
44
+ for (const dirent of dirents) {
45
+ const res = join(dir, dirent.name)
46
+ if (dirent.isDirectory()) {
47
+ yield* listFiles(res)
48
+ } else if (dirent.isFile() || dirent.isSymbolicLink()) {
49
+ yield res
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,7 @@
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')
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { Project } from 'ts-morph'
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
+ })
@@ -0,0 +1,368 @@
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
+ export type RefResolverOptions = {
21
+ importExt?: string
22
+ moduleSpecifier?: (nsid: string) => string
23
+ }
24
+
25
+ export type ResolvedRef = {
26
+ varName: string
27
+ typeName: string
28
+ }
29
+
30
+ /**
31
+ * Utility class to resolve lexicon references to TypeScript identifiers,
32
+ * generating "import" statements as needed.
33
+ */
34
+ export class RefResolver {
35
+ constructor(
36
+ private doc: LexiconDocument,
37
+ private file: SourceFile,
38
+ private indexer: LexiconIndexer,
39
+ private options: RefResolverOptions,
40
+ ) {}
41
+
42
+ public readonly resolve = memoize(
43
+ async (ref: string): Promise<ResolvedRef> => {
44
+ const [nsid, hash = 'main'] = ref.split('#')
45
+
46
+ if (nsid === '' || nsid === this.doc.id) {
47
+ return this.resolveLocal(hash)
48
+ } else {
49
+ // @NOTE: Normalize (#main fragment) to ensure proper memoization
50
+ const fullRef = `${nsid}#${hash}`
51
+ return this.resolveExternal(fullRef)
52
+ }
53
+ },
54
+ )
55
+
56
+ #defCounters = new Map<string, number>()
57
+ private nextSafeDefinitionIdentifier(name: string) {
58
+ // use camelCase version of the hash as base name
59
+ const nameSafe =
60
+ startsWithLower(name) && isValidJsIdentifier(name)
61
+ ? name
62
+ : toCamelCase(name).replace(/^[0-9]+/g, '') || 'def'
63
+
64
+ const count = this.#defCounters.get(nameSafe) ?? 0
65
+ this.#defCounters.set(nameSafe, count + 1)
66
+
67
+ // @NOTE We don't need to check against local declarations in the file here
68
+ // since we are using a naming system that should guarantee no other
69
+ // identifier has a <nameSafe>$<number> format ("$" cannot appear in
70
+ // hashes so only *we* are generating such identifiers).
71
+
72
+ const identifier = `${nameSafe}$${count}`
73
+
74
+ assert(
75
+ isValidJsIdentifier(identifier),
76
+ `Unable to generate safe identifier for: "${name}"`,
77
+ )
78
+
79
+ return identifier
80
+ }
81
+
82
+ /**
83
+ * @note The returned `typeName` and `varName` are *both* guaranteed to be
84
+ * valid TypeScript identifiers.
85
+ */
86
+ public readonly resolveLocal = memoize(
87
+ async (hash: string): Promise<ResolvedRef> => {
88
+ const hashes = Object.keys(this.doc.defs)
89
+
90
+ if (!hashes.includes(hash)) {
91
+ throw new Error(`Definition ${hash} not found in ${this.doc.id}`)
92
+ }
93
+
94
+ // Because we are using predictable "public" identifiers for type names,
95
+ // we need to ensure there are no conflicts between different definitions
96
+ // in the same lexicon document.
97
+ //
98
+ // @NOTE It should be possible to implement a way to generate
99
+ // non-conflicting type names for all public (type) identifiers in a
100
+ // project. However, this would add a lot of complexity to the code
101
+ // generation process, and the likelihood of such conflicts happening in
102
+ // practice is very low, so we opt for a simpler approach of just throwing
103
+ // an error if a conflict is detected.
104
+ const pub = getPublicIdentifiers(hash)
105
+ for (const otherHash of hashes) {
106
+ if (otherHash === hash) continue
107
+ const otherPub = getPublicIdentifiers(otherHash)
108
+ if (otherPub.typeName === pub.typeName) {
109
+ throw new Error(
110
+ `Conflicting type names for definitions #${hash} and #${otherHash} in ${this.doc.id}`,
111
+ )
112
+ }
113
+ }
114
+
115
+ // Try to keep and identifier that resembles the original hash as identifier
116
+ const safeIdentifier = asSafeDefinitionIdentifier(hash)
117
+
118
+ // If the safe identifier is not conflicting with other definition names,
119
+ // or reserved words, we can use it as-is. Otherwise, we need to generate
120
+ // a unique safe identifier.
121
+ const varName = safeIdentifier
122
+ ? !hashes.some((otherHash) => {
123
+ if (otherHash === hash) return false
124
+ const otherIdentifier = asSafeDefinitionIdentifier(otherHash)
125
+ return otherIdentifier === safeIdentifier
126
+ })
127
+ ? // Safe identifier can be used as-is as it does not conflict with
128
+ // other definition names
129
+ safeIdentifier
130
+ : // In order to keep identifiers stable, we use the safe identifier
131
+ // as base, and append a counter to avoid conflicts
132
+ this.nextSafeDefinitionIdentifier(safeIdentifier)
133
+ : // hash only contained unsafe characters, generate a safe one
134
+ this.nextSafeDefinitionIdentifier(hash)
135
+
136
+ const typeName = ucFirst(varName)
137
+ assert(isSafeLocalIdentifier(typeName), 'Expected safe type identifier')
138
+ assert(varName !== typeName, 'Variable and type name should be different')
139
+
140
+ return { varName, typeName }
141
+ },
142
+ )
143
+
144
+ /**
145
+ * @note Since this is a memoized function, and is used to generate the name
146
+ * of local variables, we should avoid returning different results for
147
+ * similar, but non strictly equal, inputs (eg. normalized / non-normalized).
148
+ * @see {@link resolve}
149
+ */
150
+ private readonly resolveExternal = memoize(
151
+ async (fullRef: string): Promise<ResolvedRef> => {
152
+ const [nsid, hash] = fullRef.split('#')
153
+ const moduleSpecifier = this.options.moduleSpecifier
154
+ ? this.options.moduleSpecifier(nsid)
155
+ : `${asRelativePath(
156
+ this.file.getDirectoryPath(),
157
+ join('/', ...nsid.split('.')),
158
+ )}.defs${this.options.importExt ?? '.js'}`
159
+
160
+ // Lets first make sure the referenced lexicon exists
161
+ const srcDoc = await this.indexer.get(nsid)
162
+ const srcDef = Object.hasOwn(srcDoc.defs, hash) ? srcDoc.defs[hash] : null
163
+ if (!srcDef) {
164
+ throw new Error(
165
+ `Missing def "${hash}" in "${nsid}" (referenced from ${this.doc.id})`,
166
+ )
167
+ }
168
+
169
+ const publicIds = getPublicIdentifiers(hash)
170
+
171
+ if (!isValidJsIdentifier(publicIds.typeName)) {
172
+ // If <typeName> is not a valid identifier, we cannot access the type
173
+ // using dot notation (<nsIdentifier>.<typeName>). Note that, unlike js
174
+ // variables, types cannot be accessed using string indexing (like:
175
+ // <nsIdentifier>['<typeName>']) because it generates TypeScript errors:
176
+ //
177
+ // > "Cannot use namespace '<nsIdentifier>' as a type."
178
+
179
+ // Instead the generated code should look like:
180
+ // import { "<unsafeTypeName>" as <safeIdentifier> } from './<moduleSpecifier>'
181
+
182
+ // Because it requires more complex management of local variables names,
183
+ // and we don't expect this to actually happen with properly designed
184
+ // lexicons documents, we do not support this for now.
185
+
186
+ throw new Error(
187
+ 'Import of definitions with unsafe type names is not supported',
188
+ )
189
+ }
190
+
191
+ // import * as <nsIdentifier> from './<moduleSpecifier>'
192
+ const nsIdentifier = this.getNsIdentifier(nsid, moduleSpecifier)
193
+
194
+ return {
195
+ varName: isValidJsIdentifier(publicIds.varName)
196
+ ? `${nsIdentifier}.${publicIds.varName}`
197
+ : `${nsIdentifier}[${JSON.stringify(publicIds.varName)}]`,
198
+ typeName: `${nsIdentifier}.${publicIds.typeName}`,
199
+ }
200
+ },
201
+ )
202
+
203
+ private getNsIdentifier(nsid: string, moduleSpecifier: string) {
204
+ const namespaceImportDeclaration =
205
+ this.file.getImportDeclaration(
206
+ (imp) =>
207
+ !imp.isTypeOnly() &&
208
+ imp.getModuleSpecifierValue() === moduleSpecifier &&
209
+ imp.getNamespaceImport() != null,
210
+ ) ||
211
+ this.file.addImportDeclaration({
212
+ moduleSpecifier,
213
+ namespaceImport: this.computeSafeNamespaceIdentifierFor(nsid),
214
+ })
215
+
216
+ return namespaceImportDeclaration.getNamespaceImport()!.getText()
217
+ }
218
+
219
+ #nsIdentifiersCounters = new Map<string, number>()
220
+ private computeSafeNamespaceIdentifierFor(nsid: string) {
221
+ const baseName = nsidToIdentifier(nsid) || 'NS'
222
+
223
+ let name = baseName
224
+ while (this.isConflictingIdentifier(name)) {
225
+ const count = this.#nsIdentifiersCounters.get(baseName) ?? 0
226
+ this.#nsIdentifiersCounters.set(baseName, count + 1)
227
+ name = `${baseName}$$${count}`
228
+ }
229
+
230
+ return name
231
+ }
232
+
233
+ private isConflictingIdentifier(name: string) {
234
+ return (
235
+ this.conflictsWithKeywords(name) ||
236
+ this.conflictsWithUtils(name) ||
237
+ this.conflictsWithLocalDefs(name) ||
238
+ this.conflictsWithLocalDeclarations(name) ||
239
+ this.conflictsWithImports(name)
240
+ )
241
+ }
242
+
243
+ private conflictsWithKeywords(name: string) {
244
+ return isJsKeyword(name) || isGlobalIdentifier(name)
245
+ }
246
+
247
+ private conflictsWithUtils(name: string) {
248
+ // Do not allow "Main" as imported ns identifier since it has a special
249
+ // meaning in the context of lexicon definitions.
250
+ if (name === 'Main') return true
251
+
252
+ // When "useRecordExport" returns true, an export named "Record" will be
253
+ // used in addition to the hash named export. So we need to make sure both
254
+ // names are not conflicting with local variables.
255
+ if (name === 'Record') return true
256
+
257
+ // Utility functions generated for lexicon schemas are prefixed with "$"
258
+ return name.startsWith('$')
259
+ }
260
+
261
+ private conflictsWithLocalDefs(name: string) {
262
+ return Object.keys(this.doc.defs).some((hash) => {
263
+ const identifier = toCamelCase(hash)
264
+
265
+ // A safe identifier will be generated, no risk of conflict.
266
+ if (!identifier) return false
267
+
268
+ // The imported name conflicts with a local definition name
269
+ if (identifier === name || `_${identifier}` === name) return true
270
+
271
+ // The imported name conflicts with the type name of a local definition
272
+ const typeName = ucFirst(identifier)
273
+ if (typeName === name || `_${typeName}` === name) return true
274
+
275
+ return false
276
+ })
277
+ }
278
+
279
+ private conflictsWithLocalDeclarations(name: string) {
280
+ return (
281
+ this.file.getVariableDeclarations().some((v) => v.getName() === name) ||
282
+ this.file
283
+ .getVariableStatements()
284
+ .some((vs) => vs.getDeclarations().some((d) => d.getName() === name)) ||
285
+ this.file.getTypeAliases().some((t) => t.getName() === name) ||
286
+ this.file.getInterfaces().some((i) => i.getName() === name) ||
287
+ this.file.getClasses().some((c) => c.getName() === name) ||
288
+ this.file.getFunctions().some((f) => f.getName() === name) ||
289
+ this.file.getEnums().some((e) => e.getName() === name)
290
+ )
291
+ }
292
+
293
+ private conflictsWithImports(name: string) {
294
+ return this.file.getImportDeclarations().some(
295
+ (imp) =>
296
+ // import name from '...'
297
+ imp.getDefaultImport()?.getText() === name ||
298
+ // import * as name from '...'
299
+ imp.getNamespaceImport()?.getText() === name ||
300
+ imp.getNamedImports().some(
301
+ (named) =>
302
+ // import { name } from '...'
303
+ // import { foo as name } from '...'
304
+ (named.getAliasNode()?.getText() ?? named.getName()) === name,
305
+ ),
306
+ )
307
+ }
308
+ }
309
+
310
+ /**
311
+ * @see {@link https://atproto.com/specs/nsid NSID syntax spec}
312
+ */
313
+ function nsidToIdentifier(nsid: string) {
314
+ const parts = nsid.split('.')
315
+
316
+ // By default, try to keep only to the last two segments of the NSID as
317
+ // contextual information. If those do not form a safe identifier (typically
318
+ // because they start with a digit), try with more segments until we reach the
319
+ // full NSID.
320
+ for (let i = 2; i < parts.length; i++) {
321
+ const identifier = toPascalCase(parts.slice(-i).join('.'))
322
+ if (isSafeLocalIdentifier(identifier)) return identifier
323
+ }
324
+
325
+ return undefined
326
+ }
327
+
328
+ /**
329
+ * Generates predictable public identifiers for a given definition hash.
330
+ *
331
+ * @note The returned `typeName` is guaranteed to be a valid TypeScript
332
+ * identifier. `varName` may not be a valid identifier (eg. if the hash contains
333
+ * unsafe characters), and may need to be accessed using string indexing.
334
+ */
335
+ export function getPublicIdentifiers(hash: string): ResolvedRef {
336
+ const varName = hash
337
+
338
+ // @NOTE we try to circumvent the issue of unsafe type names described in
339
+ // `RefResolver.resolveExternal` by ensuring that type names are always safe
340
+ // identifiers, even if it means changing them from the original hash.
341
+ let typeName = toPascalCase(hash)
342
+
343
+ if (varName === typeName || !isValidJsIdentifier(typeName)) {
344
+ typeName = `TypeOf${typeName}`
345
+ }
346
+
347
+ assert(
348
+ isValidJsIdentifier(typeName),
349
+ `Unable to generate a predictable safe identifier for "${hash}"`,
350
+ )
351
+
352
+ return { varName, typeName }
353
+ }
354
+
355
+ function asSafeDefinitionIdentifier(name: string) {
356
+ if (
357
+ startsWithLower(name) &&
358
+ isSafeLocalIdentifier(name) &&
359
+ isSafeLocalIdentifier(ucFirst(name))
360
+ ) {
361
+ return name
362
+ }
363
+ const camel = toCamelCase(name)
364
+ if (isSafeLocalIdentifier(camel) && isSafeLocalIdentifier(ucFirst(camel))) {
365
+ return camel
366
+ }
367
+ return undefined
368
+ }
package/src/ts-lang.ts ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * JavaScript keywords
3
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar}
4
+ */
5
+ const JS_KEYWORDS = new Set([
6
+ 'abstract',
7
+ 'arguments',
8
+ 'as',
9
+ 'async',
10
+ 'await',
11
+ 'boolean',
12
+ 'break',
13
+ 'byte',
14
+ 'case',
15
+ 'catch',
16
+ 'char',
17
+ 'class',
18
+ 'const',
19
+ 'continue',
20
+ 'debugger',
21
+ 'default',
22
+ 'delete',
23
+ 'do',
24
+ 'double',
25
+ 'else',
26
+ 'enum',
27
+ 'eval',
28
+ 'export',
29
+ 'extends',
30
+ 'false',
31
+ 'final',
32
+ 'finally',
33
+ 'float',
34
+ 'for',
35
+ 'from',
36
+ 'function',
37
+ 'get',
38
+ 'goto',
39
+ 'if',
40
+ 'implements',
41
+ 'import',
42
+ 'in',
43
+ 'instanceof',
44
+ 'int',
45
+ 'interface',
46
+ 'let',
47
+ 'long',
48
+ 'native',
49
+ 'new',
50
+ 'null',
51
+ 'of',
52
+ 'package',
53
+ 'private',
54
+ 'protected',
55
+ 'public',
56
+ 'return',
57
+ 'set',
58
+ 'short',
59
+ 'static',
60
+ 'super',
61
+ 'switch',
62
+ 'synchronized',
63
+ 'this',
64
+ 'throw',
65
+ 'throws',
66
+ 'transient',
67
+ 'true',
68
+ 'try',
69
+ 'typeof',
70
+ 'undefined',
71
+ 'using',
72
+ 'var',
73
+ 'void',
74
+ 'volatile',
75
+ 'while',
76
+ 'with',
77
+ 'yield',
78
+ ])
79
+
80
+ export function isJsKeyword(word: string) {
81
+ return JS_KEYWORDS.has(word)
82
+ }
83
+
84
+ // Only important to list var/type names that are likely to be used in the
85
+ // generated code files.
86
+ const GLOBAL_IDENTIFIERS = new Set([
87
+ // import { l } from "@atproto/lex-schema"
88
+ 'l',
89
+ // JS Globals
90
+ 'self',
91
+ 'globalThis',
92
+ // ESM
93
+ 'import',
94
+ // CommonJS
95
+ '__dirname',
96
+ '__filename',
97
+ 'require',
98
+ 'module',
99
+ 'exports',
100
+ // TS Primitives
101
+ 'any',
102
+ 'bigint',
103
+ 'boolean',
104
+ 'declare',
105
+ 'never',
106
+ 'null',
107
+ 'number',
108
+ 'object',
109
+ 'string',
110
+ 'symbol',
111
+ 'undefined',
112
+ 'unknown',
113
+ 'void',
114
+ // TS Utility types
115
+ 'Record',
116
+ 'Partial',
117
+ 'Readonly',
118
+ 'Pick',
119
+ 'Omit',
120
+ 'Exclude',
121
+ 'Extract',
122
+ 'InstanceType',
123
+ 'ReturnType',
124
+ 'Required',
125
+ 'ThisType',
126
+ 'Uppercase',
127
+ 'Lowercase',
128
+ 'Capitalize',
129
+ 'Uncapitalize',
130
+ ])
131
+
132
+ export function isGlobalIdentifier(word: string) {
133
+ return GLOBAL_IDENTIFIERS.has(word)
134
+ }
135
+
136
+ export function isSafeLocalIdentifier(name: string) {
137
+ return !isGlobalIdentifier(name) && isValidJsIdentifier(name)
138
+ }
139
+
140
+ export function isValidJsIdentifier(name: string) {
141
+ return (
142
+ name.length > 0 &&
143
+ !isJsKeyword(name) &&
144
+ /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)
145
+ )
146
+ }
147
+
148
+ export function asNamespaceExport(name: string) {
149
+ return isValidJsIdentifier(name) ? name : JSON.stringify(name)
150
+ }