@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.
- package/CHANGELOG.md +144 -0
- package/dist/filter.d.ts +7 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +30 -0
- package/dist/filter.js.map +1 -0
- package/dist/filtered-indexer.d.ts +2100 -0
- package/dist/filtered-indexer.d.ts.map +1 -0
- package/dist/filtered-indexer.js +56 -0
- package/dist/filtered-indexer.js.map +1 -0
- package/dist/formatter.d.ts +13 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +34 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/lex-builder.d.ts +36 -0
- package/dist/lex-builder.d.ts.map +1 -0
- package/dist/lex-builder.js +144 -0
- package/dist/lex-builder.js.map +1 -0
- package/dist/lex-def-builder.d.ts +69 -0
- package/dist/lex-def-builder.d.ts.map +1 -0
- package/dist/lex-def-builder.js +734 -0
- package/dist/lex-def-builder.js.map +1 -0
- package/dist/lexicon-directory-indexer.d.ts +11 -0
- package/dist/lexicon-directory-indexer.d.ts.map +1 -0
- package/dist/lexicon-directory-indexer.js +46 -0
- package/dist/lexicon-directory-indexer.js.map +1 -0
- package/dist/polyfill.d.ts +1 -0
- package/dist/polyfill.d.ts.map +1 -0
- package/dist/polyfill.js +7 -0
- package/dist/polyfill.js.map +1 -0
- package/dist/ref-resolver.d.ts +53 -0
- package/dist/ref-resolver.d.ts.map +1 -0
- package/dist/ref-resolver.js +277 -0
- package/dist/ref-resolver.js.map +1 -0
- package/dist/ts-lang.d.ts +6 -0
- package/dist/ts-lang.d.ts.map +1 -0
- package/dist/ts-lang.js +150 -0
- package/dist/ts-lang.js.map +1 -0
- package/dist/util.d.ts +12 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +72 -0
- package/dist/util.js.map +1 -0
- package/package.json +53 -0
- package/src/filter.ts +41 -0
- package/src/filtered-indexer.test.ts +84 -0
- package/src/filtered-indexer.ts +60 -0
- package/src/formatter.ts +42 -0
- package/src/index.ts +23 -0
- package/src/lex-builder.ts +186 -0
- package/src/lex-def-builder.ts +980 -0
- package/src/lexicon-directory-indexer.ts +52 -0
- package/src/polyfill.ts +7 -0
- package/src/ref-resolver.test.ts +75 -0
- package/src/ref-resolver.ts +368 -0
- package/src/ts-lang.ts +150 -0
- package/src/util.ts +72 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +7 -0
- package/tsconfig.tests.json +9 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { mkdir, rm, stat, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { IndentationText, Project } from 'ts-morph'
|
|
5
|
+
import { LexiconDocument, LexiconIndexer } from '@atproto/lex-document'
|
|
6
|
+
import { BuildFilterOptions, buildFilter } from './filter.js'
|
|
7
|
+
import { FilteredIndexer } from './filtered-indexer.js'
|
|
8
|
+
import { Formatter, FormatterOptions } from './formatter.js'
|
|
9
|
+
import { LexDefBuilder, LexDefBuilderOptions } from './lex-def-builder.js'
|
|
10
|
+
import {
|
|
11
|
+
LexiconDirectoryIndexer,
|
|
12
|
+
LexiconDirectoryIndexerOptions,
|
|
13
|
+
} from './lexicon-directory-indexer.js'
|
|
14
|
+
import { asNamespaceExport } from './ts-lang.js'
|
|
15
|
+
|
|
16
|
+
export type LexBuilderOptions = LexDefBuilderOptions & {
|
|
17
|
+
/**
|
|
18
|
+
* Whether to generate an index file at the root exporting all top-level
|
|
19
|
+
* namespaces.
|
|
20
|
+
*
|
|
21
|
+
* @note This could theoretically cause name conflicts if a
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
indexFile?: boolean
|
|
25
|
+
importExt?: string
|
|
26
|
+
fileExt?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type LexBuilderLoadOptions = LexiconDirectoryIndexerOptions &
|
|
30
|
+
BuildFilterOptions
|
|
31
|
+
|
|
32
|
+
export type LexBuilderSaveOptions = FormatterOptions & {
|
|
33
|
+
out: string
|
|
34
|
+
clear?: boolean
|
|
35
|
+
override?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class LexBuilder {
|
|
39
|
+
readonly #imported = new Set<string>()
|
|
40
|
+
readonly #project = new Project({
|
|
41
|
+
useInMemoryFileSystem: true,
|
|
42
|
+
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
constructor(private readonly options: LexBuilderOptions = {}) {}
|
|
46
|
+
|
|
47
|
+
get fileExt() {
|
|
48
|
+
return this.options.fileExt ?? '.ts'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get importExt() {
|
|
52
|
+
return this.options.importExt ?? '.js'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async load(options: LexBuilderLoadOptions) {
|
|
56
|
+
await using indexer = new FilteredIndexer(
|
|
57
|
+
new LexiconDirectoryIndexer(options),
|
|
58
|
+
buildFilter(options),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for await (const doc of indexer) {
|
|
62
|
+
if (!this.#imported.has(doc.id)) {
|
|
63
|
+
this.#imported.add(doc.id)
|
|
64
|
+
} else {
|
|
65
|
+
throw new Error(`Duplicate lexicon document id: ${doc.id}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await this.createDefsFile(doc, indexer)
|
|
69
|
+
await this.createExportTree(doc)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async save(options: LexBuilderSaveOptions) {
|
|
74
|
+
const files = this.#project.getSourceFiles()
|
|
75
|
+
|
|
76
|
+
const destination = resolve(options.out)
|
|
77
|
+
|
|
78
|
+
if (options.clear) {
|
|
79
|
+
await rm(destination, { recursive: true, force: true })
|
|
80
|
+
} else if (!options.override) {
|
|
81
|
+
await Promise.all(
|
|
82
|
+
files.map(async (f) =>
|
|
83
|
+
assertNotFileExists(join(destination, f.getFilePath())),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const formatter = new Formatter(options)
|
|
89
|
+
|
|
90
|
+
await Promise.all(
|
|
91
|
+
Array.from(files, async (file) => {
|
|
92
|
+
const filePath = join(destination, file.getFilePath())
|
|
93
|
+
const content = await formatter.format(file.getFullText())
|
|
94
|
+
await mkdir(join(filePath, '..'), { recursive: true })
|
|
95
|
+
await rm(filePath, { recursive: true, force: true })
|
|
96
|
+
await writeFile(filePath, content, 'utf8')
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private createFile(path: string) {
|
|
102
|
+
return this.#project.createSourceFile(path)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private getFile(path: string) {
|
|
106
|
+
return this.#project.getSourceFile(path) || this.createFile(path)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async createExportTree(doc: LexiconDocument) {
|
|
110
|
+
const namespaces = doc.id.split('.')
|
|
111
|
+
|
|
112
|
+
if (this.options.indexFile) {
|
|
113
|
+
const indexFile = this.getFile(`/index${this.fileExt}`)
|
|
114
|
+
|
|
115
|
+
const tldNs = namespaces[0]!
|
|
116
|
+
assert(
|
|
117
|
+
tldNs !== 'index',
|
|
118
|
+
'The "indexFile" options cannot be used with namespaces using a ".index" tld.',
|
|
119
|
+
)
|
|
120
|
+
const tldNsSpecifier = `./${tldNs}${this.importExt}`
|
|
121
|
+
if (!indexFile.getExportDeclaration(tldNsSpecifier)) {
|
|
122
|
+
indexFile.addExportDeclaration({
|
|
123
|
+
moduleSpecifier: tldNsSpecifier,
|
|
124
|
+
namespaceExport: asNamespaceExport(tldNs),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// First create the parent namespaces
|
|
130
|
+
for (let i = 0; i < namespaces.length - 1; i++) {
|
|
131
|
+
const currentNs = namespaces[i]
|
|
132
|
+
const childNs = namespaces[i + 1]
|
|
133
|
+
|
|
134
|
+
const path = join('/', ...namespaces.slice(0, i + 1))
|
|
135
|
+
const file = this.getFile(`${path}${this.fileExt}`)
|
|
136
|
+
|
|
137
|
+
const childModuleSpecifier = `./${currentNs}/${childNs}${this.importExt}`
|
|
138
|
+
const dec = file.getExportDeclaration(childModuleSpecifier)
|
|
139
|
+
if (!dec) {
|
|
140
|
+
file.addExportDeclaration({
|
|
141
|
+
moduleSpecifier: childModuleSpecifier,
|
|
142
|
+
namespaceExport: asNamespaceExport(childNs),
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The child file exports the schemas (as *)
|
|
148
|
+
const path = join('/', ...namespaces)
|
|
149
|
+
const file = this.getFile(`${path}${this.fileExt}`)
|
|
150
|
+
|
|
151
|
+
file.addExportDeclaration({
|
|
152
|
+
moduleSpecifier: `./${namespaces.at(-1)}.defs${this.importExt}`,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// @NOTE Individual exports exports from the defs file might conflict with
|
|
156
|
+
// child namespaces. For this reason, we also add a namespace export for the
|
|
157
|
+
// defs (export * as $defs from './xyz.defs'). This is an escape hatch
|
|
158
|
+
// allowing to still access the definitions if a hash get shadowed by a
|
|
159
|
+
// child namespace.
|
|
160
|
+
file.addExportDeclaration({
|
|
161
|
+
moduleSpecifier: `./${namespaces.at(-1)}.defs${this.importExt}`,
|
|
162
|
+
namespaceExport: '$defs',
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async createDefsFile(
|
|
167
|
+
doc: LexiconDocument,
|
|
168
|
+
indexer: LexiconIndexer,
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const path = join('/', ...doc.id.split('.'))
|
|
171
|
+
const file = this.createFile(`${path}.defs${this.fileExt}`)
|
|
172
|
+
|
|
173
|
+
const fileBuilder = new LexDefBuilder(this.options, file, doc, indexer)
|
|
174
|
+
await fileBuilder.build()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function assertNotFileExists(file: string): Promise<void> {
|
|
179
|
+
try {
|
|
180
|
+
await stat(file)
|
|
181
|
+
throw new Error(`File already exists: ${file}`)
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return
|
|
184
|
+
throw err
|
|
185
|
+
}
|
|
186
|
+
}
|