@ithinkdt/lint 4.0.0-0

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.
@@ -0,0 +1,378 @@
1
+ import path from 'node:path'
2
+ import ts from 'typescript'
3
+ import { transformExtraFile } from './transform.js'
4
+
5
+ export class TSServiceManager {
6
+ tsServices = new Map()
7
+
8
+ getProgram(code, options) {
9
+ const tsconfigPath = options.project
10
+ const extraFileExtensions = [...new Set(options.extraFileExtensions)]
11
+
12
+ let serviceList = this.tsServices.get(tsconfigPath)
13
+ if (!serviceList) {
14
+ serviceList = []
15
+ this.tsServices.set(tsconfigPath, serviceList)
16
+ }
17
+
18
+ let service = serviceList.find((service) =>
19
+ extraFileExtensions.every((ext) => service.extraFileExtensions.includes(ext)),
20
+ )
21
+ if (!service) {
22
+ service = new TSService(tsconfigPath, extraFileExtensions)
23
+ serviceList.unshift(service)
24
+ }
25
+
26
+ return service.getProgram(code, options.filePath)
27
+ }
28
+ }
29
+
30
+ export class TSService {
31
+ watch
32
+
33
+ patchedHostSet = new WeakSet()
34
+
35
+ tsconfigPath
36
+
37
+ extraFileExtensions
38
+
39
+ currTarget = {
40
+ code: '',
41
+ filePath: '',
42
+ dirMap: new Map(),
43
+ }
44
+
45
+ fileWatchCallbacks = new Map()
46
+
47
+ constructor(tsconfigPath, extraFileExtensions) {
48
+ this.tsconfigPath = tsconfigPath
49
+ this.extraFileExtensions = extraFileExtensions
50
+ this.watch = this.createWatch(tsconfigPath, extraFileExtensions)
51
+ }
52
+
53
+ getProgram(code, filePath) {
54
+ const normalized = normalizeFileName(filePath)
55
+ const lastTarget = this.currTarget
56
+
57
+ const dirMap = new Map()
58
+ let childPath = normalized
59
+ for (const dirName of iterateDirs(normalized)) {
60
+ dirMap.set(dirName, { path: childPath, name: path.basename(childPath) })
61
+ childPath = dirName
62
+ }
63
+ this.currTarget = {
64
+ code,
65
+ filePath: normalized,
66
+ dirMap,
67
+ }
68
+ for (const { filePath: targetPath } of [this.currTarget, lastTarget]) {
69
+ if (!targetPath) continue
70
+ if (!ts.sys.fileExists(targetPath)) {
71
+ // Signal a directory change to request a re-scan of the directory
72
+ // because it targets a file that does not actually exist.
73
+ this.fileWatchCallbacks.get(normalizeFileName(this.tsconfigPath))?.update()
74
+ }
75
+ this.fileWatchCallbacks.get(normalizeFileName(targetPath))?.update()
76
+ }
77
+
78
+ const program = this.watch.getProgram().getProgram()
79
+ // sets parent pointers in source files
80
+ program.getTypeChecker()
81
+
82
+ return program
83
+ }
84
+
85
+ createWatch(tsconfigPath, extraFileExtensions) {
86
+ const createAbstractBuilder = (...args) => {
87
+ const [rootNames, options, argHost, oldProgram, configFileParsingDiagnostics, projectReferences] = args
88
+
89
+ const host = argHost
90
+ if (!this.patchedHostSet.has(host)) {
91
+ this.patchedHostSet.add(host)
92
+
93
+ const getTargetSourceFile = (fileName, languageVersionOrOptions) => {
94
+ if (
95
+ this.currTarget.filePath === normalizeFileName(fileName) &&
96
+ isExtra(fileName, extraFileExtensions)
97
+ ) {
98
+ // Parse the target file as TSX.
99
+ return (this.currTarget.sourceFile ??= ts.createSourceFile(
100
+ this.currTarget.filePath,
101
+ this.currTarget.code,
102
+ languageVersionOrOptions,
103
+ true,
104
+ ts.ScriptKind.TSX,
105
+ ))
106
+ }
107
+ return
108
+ }
109
+
110
+ const original = {
111
+ getSourceFile: host.getSourceFile,
112
+ getSourceFileByPath: host.getSourceFileByPath,
113
+ }
114
+
115
+ host.getSourceFile = (fileName, languageVersionOrOptions, ...args) => {
116
+ // Always call the original function, because it calls the file watcher.
117
+ const originalSourceFile = original.getSourceFile.call(
118
+ host,
119
+ fileName,
120
+ languageVersionOrOptions,
121
+ ...args,
122
+ )
123
+ return getTargetSourceFile(fileName, languageVersionOrOptions) ?? originalSourceFile
124
+ }
125
+ host.getSourceFileByPath = (fileName, path, languageVersionOrOptions, ...args) => {
126
+ // Always call the original function, because it calls the file watcher.
127
+ const originalSourceFile = original.getSourceFileByPath.call(
128
+ host,
129
+ fileName,
130
+ path,
131
+ languageVersionOrOptions,
132
+ ...args,
133
+ )
134
+ return getTargetSourceFile(fileName, languageVersionOrOptions) ?? originalSourceFile
135
+ }
136
+ }
137
+ return ts.createAbstractBuilder(
138
+ rootNames,
139
+ options,
140
+ host,
141
+ oldProgram,
142
+ configFileParsingDiagnostics,
143
+ projectReferences,
144
+ )
145
+ }
146
+
147
+ const watchCompilerHost = ts.createWatchCompilerHost(
148
+ tsconfigPath,
149
+ {
150
+ noEmit: true,
151
+ jsx: ts.JsxEmit.Preserve,
152
+
153
+ // This option is required if `includes` only includes `*.vue` files.
154
+ // However, the option is not in the documentation.
155
+ // https://github.com/microsoft/TypeScript/issues/28447
156
+ allowNonTsExtensions: true,
157
+ },
158
+ ts.sys,
159
+ createAbstractBuilder,
160
+ (diagnostic) => {
161
+ throw new Error(formatDiagnostics([diagnostic]))
162
+ },
163
+ () => {
164
+ // Not reported in reportWatchStatus.
165
+ },
166
+ undefined,
167
+ extraFileExtensions.map((extension) => ({
168
+ extension,
169
+ isMixedContent: true,
170
+ scriptKind: ts.ScriptKind.Deferred,
171
+ })),
172
+ )
173
+ const original = {
174
+ readFile: watchCompilerHost.readFile,
175
+ fileExists: watchCompilerHost.fileExists,
176
+ readDirectory: watchCompilerHost.readDirectory,
177
+ directoryExists: watchCompilerHost.directoryExists,
178
+ getDirectories: watchCompilerHost.getDirectories,
179
+ }
180
+ watchCompilerHost.getDirectories = (dirName, ...args) => {
181
+ const result = distinctArray(
182
+ ...original.getDirectories.call(watchCompilerHost, dirName, ...args),
183
+ // Include the path to the target file if the target file does not actually exist.
184
+ this.currTarget.dirMap.get(normalizeFileName(dirName))?.name,
185
+ )
186
+ return result
187
+ }
188
+ watchCompilerHost.directoryExists = (dirName, ...args) => {
189
+ return (
190
+ original.directoryExists.call(watchCompilerHost, dirName, ...args) ||
191
+ // Include the path to the target file if the target file does not actually exist.
192
+ this.currTarget.dirMap.has(normalizeFileName(dirName))
193
+ )
194
+ }
195
+ watchCompilerHost.readDirectory = (dirName, ...args) => {
196
+ let results = original.readDirectory.call(watchCompilerHost, dirName, ...args)
197
+
198
+ // Include the target file if the target file does not actually exist.
199
+ const file = this.currTarget.dirMap.get(normalizeFileName(dirName))
200
+ if (file) {
201
+ if (file.path === this.currTarget.filePath) {
202
+ results.push(file.path)
203
+ } else {
204
+ results = results.filter((f) => file.path !== f && file.name !== f)
205
+ }
206
+ }
207
+
208
+ return distinctArray(...results)
209
+ }
210
+ watchCompilerHost.readFile = (fileName, ...args) => {
211
+ const realFileName = getRealFileNameIfExist(fileName)
212
+ if (realFileName == undefined) {
213
+ return
214
+ }
215
+ if (this.currTarget.filePath === realFileName) {
216
+ // It is the file currently being parsed.
217
+ return transformExtraFile(this.currTarget.code, {
218
+ filePath: realFileName,
219
+ current: true,
220
+ })
221
+ }
222
+
223
+ const code = original.readFile.call(watchCompilerHost, realFileName, ...args)
224
+ if (!code) {
225
+ return code
226
+ }
227
+ return transformExtraFile(code, {
228
+ filePath: realFileName,
229
+ current: false,
230
+ })
231
+ }
232
+ // Modify it so that it can be determined that the virtual file actually exists.
233
+ watchCompilerHost.fileExists = (fileName) => {
234
+ return getRealFileNameIfExist(fileName) != undefined
235
+ }
236
+
237
+ const getRealFileNameIfExist = (fileName) => {
238
+ const normalizedFileName = normalizeFileName(fileName)
239
+ // Even if it is actually a file, if it is specified as a directory to the target file,
240
+ // it is assumed that it does not exist as a file.
241
+ if (this.currTarget.dirMap.has(normalizedFileName)) {
242
+ return
243
+ }
244
+ if (this.currTarget.filePath === normalizedFileName) {
245
+ // It is the file currently being parsed.
246
+ return normalizedFileName
247
+ }
248
+ const exists = original.fileExists.call(watchCompilerHost, normalizedFileName)
249
+ if (exists) {
250
+ return normalizedFileName
251
+ }
252
+ if (isVirtualTSX(normalizedFileName, extraFileExtensions)) {
253
+ const real = normalizedFileName.slice(0, -4)
254
+ for (const dts of toExtraDtsFileNames(real, extraFileExtensions)) {
255
+ if (original.fileExists.call(watchCompilerHost, dts)) {
256
+ // If the d.ts file exists, respect it and consider the virtual file not to exist.
257
+ return
258
+ }
259
+ }
260
+ if (original.fileExists.call(watchCompilerHost, real)) {
261
+ return real
262
+ }
263
+ }
264
+ return
265
+ }
266
+
267
+ // It keeps a callback to mark the parsed file as changed so that it can be re-parsed.
268
+ watchCompilerHost.watchFile = (fileName, callback) => {
269
+ const normalized = normalizeFileName(fileName)
270
+ this.fileWatchCallbacks.set(normalized, {
271
+ update: () => callback(fileName, ts.FileWatcherEventKind.Changed),
272
+ })
273
+
274
+ return {
275
+ close: () => {
276
+ this.fileWatchCallbacks.delete(normalized)
277
+ },
278
+ }
279
+ }
280
+ // Use watchCompilerHost but don't actually watch the files and directories.
281
+ watchCompilerHost.watchDirectory = () => {
282
+ return {
283
+ close: () => {
284
+ // noop
285
+ },
286
+ }
287
+ }
288
+
289
+ /**
290
+ * It heavily references typescript-eslint.
291
+ * @see https://github.com/typescript-eslint/typescript-eslint/blob/84e316be33dac5302bd0367c4d1960bef40c484d/packages/typescript-estree/src/create-program/createWatchProgram.ts#L297-L309
292
+ */
293
+ watchCompilerHost.afterProgramCreate = (program) => {
294
+ const originalDiagnostics = program.getConfigFileParsingDiagnostics()
295
+ const configFileDiagnostics = originalDiagnostics.filter(
296
+ (diag) => diag.category === ts.DiagnosticCategory.Error && diag.code !== 18_003,
297
+ )
298
+ if (configFileDiagnostics.length > 0) {
299
+ throw new Error(formatDiagnostics(configFileDiagnostics))
300
+ }
301
+ }
302
+
303
+ const watch = ts.createWatchProgram(watchCompilerHost)
304
+ return watch
305
+ }
306
+ }
307
+
308
+ /** If the given filename has extra extensions, returns the d.ts filename. */
309
+ function toExtraDtsFileNames(fileName, extraFileExtensions) {
310
+ const ext = getExtIfExtra(fileName, extraFileExtensions)
311
+ if (ext != undefined) {
312
+ return [`${fileName}.d.ts`, `${fileName.slice(0, -ext.length)}.d${ext}.ts`]
313
+ }
314
+ return []
315
+ }
316
+
317
+ /** Checks the given filename has extra extension or not. */
318
+ function isExtra(fileName, extraFileExtensions) {
319
+ return getExtIfExtra(fileName, extraFileExtensions) != undefined
320
+ }
321
+
322
+ /** Gets the file extension if the given file is an extra extension file. */
323
+ function getExtIfExtra(fileName, extraFileExtensions) {
324
+ for (const extraFileExtension of extraFileExtensions) {
325
+ if (fileName.endsWith(extraFileExtension)) {
326
+ return extraFileExtension
327
+ }
328
+ }
329
+ return
330
+ }
331
+
332
+ /** Checks the given filename is virtual file tsx or not. */
333
+ function isVirtualTSX(fileName, extraFileExtensions) {
334
+ for (const extraFileExtension of extraFileExtensions) {
335
+ if (fileName.endsWith(`${extraFileExtension}.tsx`)) {
336
+ return true
337
+ }
338
+ }
339
+ return false
340
+ }
341
+
342
+ function formatDiagnostics(diagnostics) {
343
+ return ts.formatDiagnostics(diagnostics, {
344
+ getCanonicalFileName: (f) => f,
345
+ getCurrentDirectory: () => process.cwd(),
346
+ getNewLine: () => '\n',
347
+ })
348
+ }
349
+
350
+ function normalizeFileName(fileName) {
351
+ let normalized = path.normalize(fileName)
352
+ if (normalized.endsWith(path.sep)) {
353
+ normalized = normalized.slice(0, -1)
354
+ }
355
+ if (ts.sys.useCaseSensitiveFileNames) {
356
+ return toAbsolutePath(normalized)
357
+ }
358
+ return toAbsolutePath(normalized.toLowerCase())
359
+ }
360
+
361
+ function toAbsolutePath(filePath, baseDir) {
362
+ return path.isAbsolute(filePath) ? filePath : path.join(baseDir || process.cwd(), filePath)
363
+ }
364
+
365
+ function* iterateDirs(filePath) {
366
+ let target = filePath
367
+ let parent
368
+ while ((parent = path.dirname(target)) !== target) {
369
+ yield parent
370
+ target = parent
371
+ }
372
+ }
373
+
374
+ function distinctArray(...list) {
375
+ return [...new Set(ts.sys.useCaseSensitiveFileNames ? list : list.map((s) => s?.toLowerCase()))].filter(
376
+ (s) => s != undefined,
377
+ )
378
+ }
@@ -0,0 +1,25 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export function getProjectConfigFiles(options) {
5
+ if (options.project !== true) {
6
+ return Array.isArray(options.project) ? options.project : [options.project]
7
+ }
8
+
9
+ let directory = path.dirname(options.filePath)
10
+ const checkedDirectories = [directory]
11
+
12
+ do {
13
+ const tsconfigPath = path.join(directory, 'tsconfig.json')
14
+ if (fs.existsSync(tsconfigPath)) {
15
+ return [tsconfigPath]
16
+ }
17
+
18
+ directory = path.dirname(directory)
19
+ checkedDirectories.push(directory)
20
+ } while (directory.length > 1 && directory.length >= options.tsconfigRootDir.length)
21
+
22
+ throw new Error(
23
+ `project was set to \`true\` but couldn't find any tsconfig.json relative to '${options.filePath}' within '${options.tsconfigRootDir}'.`,
24
+ )
25
+ }
@@ -0,0 +1,63 @@
1
+ import path from 'node:path'
2
+ import ts from 'typescript'
3
+ import { globbySync } from 'globby'
4
+ import isGlob from 'is-glob'
5
+ /**
6
+ * Normalizes, sanitizes, resolves and filters the provided project paths
7
+ */
8
+ export function resolveProjectList(options) {
9
+ const sanitizedProjects = []
10
+
11
+ // Normalize and sanitize the project paths
12
+ if (options.project) {
13
+ for (const project of options.project) {
14
+ if (typeof project === 'string') {
15
+ sanitizedProjects.push(project)
16
+ }
17
+ }
18
+ }
19
+
20
+ if (sanitizedProjects.length === 0) {
21
+ return []
22
+ }
23
+
24
+ const projectFolderIgnoreList = []
25
+ for (const folder of options.projectFolderIgnoreList ?? ['**/node_modules/**']) {
26
+ if (typeof folder === 'string') {
27
+ projectFolderIgnoreList.push(folder.startsWith('!') ? folder : `!${folder}`)
28
+ }
29
+ }
30
+
31
+ // Transform glob patterns into paths
32
+ const nonGlobProjects = sanitizedProjects.filter((project) => !isGlob(project))
33
+ const globProjects = sanitizedProjects.filter((project) => isGlob(project))
34
+
35
+ const uniqueCanonicalProjectPaths = new Set(
36
+ [
37
+ ...nonGlobProjects,
38
+ ...(globProjects.length === 0
39
+ ? []
40
+ : globbySync([...globProjects, ...projectFolderIgnoreList], {
41
+ cwd: options.tsconfigRootDir,
42
+ })),
43
+ ].map((project) => getCanonicalFileName(ensureAbsolutePath(project, options.tsconfigRootDir))),
44
+ )
45
+
46
+ return [...uniqueCanonicalProjectPaths]
47
+ }
48
+
49
+ // typescript doesn't provide a ts.sys implementation for browser environments
50
+ const useCaseSensitiveFileNames = ts.sys === undefined ? true : ts.sys.useCaseSensitiveFileNames
51
+ const correctPathCasing = useCaseSensitiveFileNames ? (filePath) => filePath : (filePath) => filePath.toLowerCase()
52
+
53
+ function getCanonicalFileName(filePath) {
54
+ let normalized = path.normalize(filePath)
55
+ if (normalized.endsWith(path.sep)) {
56
+ normalized = normalized.slice(0, -1)
57
+ }
58
+ return correctPathCasing(normalized)
59
+ }
60
+
61
+ function ensureAbsolutePath(p, tsconfigRootDir) {
62
+ return path.isAbsolute(p) ? p : path.join(tsconfigRootDir || process.cwd(), p)
63
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "exclude": ["node_modules", "dist"],
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "noImplicitOverride": true,
6
+ "target": "ESNext",
7
+ "module": "Preserve",
8
+ "resolveJsonModule": true,
9
+ "moduleResolution": "Bundler",
10
+ "lib": ["ESNext"],
11
+ "esModuleInterop": true,
12
+ "strictNullChecks": true
13
+ }
14
+ }