@tsslint/cli 1.3.6 → 1.4.1

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/index.js CHANGED
@@ -3,147 +3,313 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const ts = require("typescript");
4
4
  const path = require("path");
5
5
  const core = require("@tsslint/core");
6
- const cache = require("./lib/cache");
6
+ const cache = require("./lib/cache.js");
7
+ const worker = require("./lib/worker.js");
7
8
  const glob = require("glob");
8
9
  const fs = require("fs");
10
+ const os = require("os");
11
+ const languagePlugins = require("./lib/languagePlugins.js");
9
12
  const _reset = '\x1b[0m';
10
13
  const purple = (s) => '\x1b[35m' + s + _reset;
11
14
  const darkGray = (s) => '\x1b[90m' + s + _reset;
12
15
  const lightRed = (s) => '\x1b[91m' + s + _reset;
13
16
  const lightGreen = (s) => '\x1b[92m' + s + _reset;
14
17
  const lightYellow = (s) => '\x1b[93m' + s + _reset;
18
+ let threads = 1;
19
+ if (process.argv.includes('--threads')) {
20
+ const threadsIndex = process.argv.indexOf('--threads');
21
+ const threadsArg = process.argv[threadsIndex + 1];
22
+ if (!threadsArg || threadsArg.startsWith('-')) {
23
+ console.error(lightRed(`Missing argument for --threads.`));
24
+ process.exit(1);
25
+ }
26
+ threads = Math.min(os.availableParallelism(), Number(threadsArg));
27
+ }
28
+ class Project {
29
+ constructor(tsconfigOption, languages) {
30
+ this.languages = languages;
31
+ this.workers = [];
32
+ this.fileNames = [];
33
+ this.options = {};
34
+ this.currentFileIndex = 0;
35
+ this.cache = {};
36
+ try {
37
+ this.tsconfig = require.resolve(tsconfigOption, { paths: [process.cwd()] });
38
+ }
39
+ catch {
40
+ console.error(lightRed(`No such file: ${tsconfigOption}`));
41
+ process.exit(1);
42
+ }
43
+ }
44
+ async init(
45
+ // @ts-expect-error
46
+ clack) {
47
+ this.configFile = ts.findConfigFile(path.dirname(this.tsconfig), ts.sys.fileExists, 'tsslint.config.ts');
48
+ if (!this.configFile) {
49
+ clack.log.error(`${purple('[project]')} ${path.relative(process.cwd(), this.tsconfig)} ${darkGray('(No tsslint.config.ts found)')}`);
50
+ return this;
51
+ }
52
+ const commonLine = await parseCommonLine(this.tsconfig, this.languages);
53
+ this.fileNames = commonLine.fileNames;
54
+ this.options = commonLine.options;
55
+ if (!this.fileNames.length) {
56
+ clack.log.warn(`${purple('[project]')} ${path.relative(process.cwd(), this.tsconfig)} ${darkGray('(No included files)')}`);
57
+ return this;
58
+ }
59
+ clack.log.info(`${purple('[project]')} ${path.relative(process.cwd(), this.tsconfig)} ${darkGray(`(${this.fileNames.length})`)}`);
60
+ if (!process.argv.includes('--force')) {
61
+ this.cache = cache.loadCache(this.tsconfig, this.configFile, ts.sys.createHash);
62
+ }
63
+ return this;
64
+ }
65
+ }
15
66
  (async () => {
16
- let hasError = false;
17
- let projectVersion = 0;
18
- let typeRootsVersion = 0;
19
- let parsed;
67
+ const builtConfigs = new Map();
20
68
  const clack = await import('@clack/prompts');
21
- const snapshots = new Map();
22
- const versions = new Map();
23
- const configs = new Map();
24
- const languageServiceHost = {
25
- ...ts.sys,
26
- useCaseSensitiveFileNames() {
27
- return ts.sys.useCaseSensitiveFileNames;
28
- },
29
- getProjectVersion() {
30
- return projectVersion.toString();
31
- },
32
- getTypeRootsVersion() {
33
- return typeRootsVersion;
34
- },
35
- getCompilationSettings() {
36
- return parsed.options;
37
- },
38
- getScriptFileNames() {
39
- return parsed.fileNames;
40
- },
41
- getScriptVersion(fileName) {
42
- return versions.get(fileName)?.toString() ?? '0';
43
- },
44
- getScriptSnapshot(fileName) {
45
- if (!snapshots.has(fileName)) {
46
- snapshots.set(fileName, ts.ScriptSnapshot.fromString(ts.sys.readFile(fileName)));
69
+ const processFiles = new Set();
70
+ let projects = [];
71
+ let spinner = clack.spinner();
72
+ let lastSpinnerUpdate = Date.now();
73
+ let hasFix = false;
74
+ let allFilesNum = 0;
75
+ let processed = 0;
76
+ let excluded = 0;
77
+ let passed = 0;
78
+ let errors = 0;
79
+ let warnings = 0;
80
+ let cached = 0;
81
+ const tsconfigAndLanguages = new Map();
82
+ if (!process.argv.includes('--project')
83
+ && !process.argv.includes('--projects')
84
+ && !process.argv.includes('--vue-project')
85
+ && !process.argv.includes('--vue-projects')
86
+ && !process.argv.includes('--mdx-project')
87
+ && !process.argv.includes('--mdx-projects')
88
+ && !process.argv.includes('--astro-project')
89
+ && !process.argv.includes('--astro-projects')) {
90
+ const languages = await clack.multiselect({
91
+ required: false,
92
+ message: 'Select frameworks (optional)',
93
+ options: [{
94
+ label: 'Vue',
95
+ value: 'vue',
96
+ }, {
97
+ label: 'MDX',
98
+ value: 'mdx',
99
+ }, {
100
+ label: 'Astro',
101
+ value: 'astro',
102
+ }],
103
+ });
104
+ if (clack.isCancel(languages)) {
105
+ process.exit(1);
106
+ }
107
+ const tsconfigOptions = glob.sync('**/{tsconfig.json,jsconfig.json}');
108
+ let options = await Promise.all(tsconfigOptions.map(async (tsconfigOption) => {
109
+ const tsconfig = require.resolve(tsconfigOption.startsWith('.') ? tsconfigOption : `./${tsconfigOption}`, { paths: [process.cwd()] });
110
+ try {
111
+ const commonLine = await parseCommonLine(tsconfig, languages);
112
+ return {
113
+ label: path.relative(process.cwd(), tsconfig) + ` (${commonLine.fileNames.length})`,
114
+ value: tsconfigOption,
115
+ };
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ }));
121
+ options = options.filter(option => !!option);
122
+ if (!options.length) {
123
+ clack.log.error(lightRed('No projects found.'));
124
+ process.exit(1);
125
+ }
126
+ const selectedTsconfigs = await clack.multiselect({
127
+ message: 'Select one or multiple projects',
128
+ // @ts-expect-error
129
+ options,
130
+ });
131
+ if (clack.isCancel(selectedTsconfigs)) {
132
+ process.exit(1);
133
+ }
134
+ let command = 'tsslint';
135
+ if (!languages.length) {
136
+ if (selectedTsconfigs.length === 1) {
137
+ command += ' --project ' + selectedTsconfigs[0];
138
+ }
139
+ else {
140
+ command += ' --projects ' + selectedTsconfigs.join(' ');
47
141
  }
48
- return snapshots.get(fileName);
49
- },
50
- getDefaultLibFileName(options) {
51
- return ts.getDefaultLibFilePath(options);
52
- },
53
- };
54
- const languageService = ts.createLanguageService(languageServiceHost);
55
- if (process.argv.includes('--project')) {
56
- const projectIndex = process.argv.indexOf('--project');
57
- let tsconfig = process.argv[projectIndex + 1];
58
- if (tsconfig.startsWith('-') || !tsconfig) {
59
- clack.log.error(lightRed(`Missing argument for --project.`));
60
142
  }
61
143
  else {
144
+ for (const language of languages) {
145
+ if (selectedTsconfigs.length === 1) {
146
+ command += ` --${language}-project ` + selectedTsconfigs[0];
147
+ }
148
+ else {
149
+ command += ` --${language}-projects ` + selectedTsconfigs.join(' ');
150
+ }
151
+ }
152
+ }
153
+ clack.log.info(`Running: ${purple(command)}`);
154
+ for (let tsconfig of selectedTsconfigs) {
62
155
  if (!tsconfig.startsWith('.')) {
63
156
  tsconfig = `./${tsconfig}`;
64
157
  }
65
- await projectWorker(tsconfig);
158
+ tsconfigAndLanguages.set(tsconfig, languages);
66
159
  }
67
160
  }
68
- else if (process.argv.includes('--projects')) {
69
- const projectsIndex = process.argv.indexOf('--projects');
70
- for (let i = projectsIndex + 1; i < process.argv.length; i++) {
71
- if (process.argv[i].startsWith('-')) {
72
- break;
73
- }
74
- const searchGlob = process.argv[i];
75
- const tsconfigs = glob.sync(searchGlob);
76
- for (let tsconfig of tsconfigs) {
161
+ else {
162
+ const options = [
163
+ {
164
+ projectFlag: '--project',
165
+ projectsFlag: '--projects',
166
+ language: undefined,
167
+ },
168
+ {
169
+ projectFlag: '--vue-project',
170
+ projectsFlag: '--vue-projects',
171
+ language: 'vue',
172
+ },
173
+ {
174
+ projectFlag: '--mdx-project',
175
+ projectsFlag: '--mdx-projects',
176
+ language: 'mdx',
177
+ },
178
+ {
179
+ projectFlag: '--astro-project',
180
+ projectsFlag: '--astro-projects',
181
+ language: 'astro',
182
+ },
183
+ ];
184
+ for (const { projectFlag, projectsFlag, language } of options) {
185
+ if (process.argv.includes(projectFlag)) {
186
+ const projectIndex = process.argv.indexOf(projectFlag);
187
+ let tsconfig = process.argv[projectIndex + 1];
188
+ if (!tsconfig || tsconfig.startsWith('-')) {
189
+ clack.log.error(lightRed(`Missing argument for ${projectFlag}.`));
190
+ process.exit(1);
191
+ }
77
192
  if (!tsconfig.startsWith('.')) {
78
193
  tsconfig = `./${tsconfig}`;
79
194
  }
80
- await projectWorker(tsconfig);
195
+ if (!tsconfigAndLanguages.has(tsconfig)) {
196
+ tsconfigAndLanguages.set(tsconfig, []);
197
+ }
198
+ if (language) {
199
+ tsconfigAndLanguages.get(tsconfig).push(language);
200
+ }
201
+ }
202
+ if (process.argv.includes(projectsFlag)) {
203
+ const projectsIndex = process.argv.indexOf(projectsFlag);
204
+ let foundArg = false;
205
+ for (let i = projectsIndex + 1; i < process.argv.length; i++) {
206
+ if (process.argv[i].startsWith('-')) {
207
+ break;
208
+ }
209
+ foundArg = true;
210
+ const searchGlob = process.argv[i];
211
+ const tsconfigs = glob.sync(searchGlob);
212
+ if (!tsconfigs.length) {
213
+ clack.log.error(lightRed(`No projects found for ${projectsFlag} ${searchGlob}.`));
214
+ process.exit(1);
215
+ }
216
+ for (let tsconfig of tsconfigs) {
217
+ if (!tsconfig.startsWith('.')) {
218
+ tsconfig = `./${tsconfig}`;
219
+ }
220
+ if (!tsconfigAndLanguages.has(tsconfig)) {
221
+ tsconfigAndLanguages.set(tsconfig, []);
222
+ }
223
+ if (language) {
224
+ tsconfigAndLanguages.get(tsconfig).push(language);
225
+ }
226
+ }
227
+ }
228
+ if (!foundArg) {
229
+ clack.log.error(lightRed(`Missing argument for ${projectsFlag}.`));
230
+ process.exit(1);
231
+ }
81
232
  }
82
233
  }
83
234
  }
235
+ for (const [tsconfig, languages] of tsconfigAndLanguages) {
236
+ projects.push(await new Project(tsconfig, languages).init(clack));
237
+ }
238
+ spinner.start();
239
+ projects = projects.filter(project => !!project.configFile);
240
+ projects = projects.filter(project => !!project.fileNames.length);
241
+ for (const project of projects) {
242
+ project.builtConfig = await getBuiltConfig(project.configFile);
243
+ }
244
+ projects = projects.filter(project => !!project.builtConfig);
245
+ for (const project of projects) {
246
+ allFilesNum += project.fileNames.length;
247
+ }
248
+ if (allFilesNum === 0) {
249
+ spinner.stop(lightYellow('No input files.'));
250
+ process.exit(1);
251
+ }
252
+ if (threads === 1) {
253
+ await startWorker(worker.createLocal());
254
+ }
84
255
  else {
85
- const tsconfig = await askTSConfig();
86
- await projectWorker(tsconfig);
256
+ await Promise.all(new Array(threads).fill(0).map(() => {
257
+ return startWorker(worker.create());
258
+ }));
259
+ }
260
+ spinner.stop(darkGray(cached
261
+ ? `Processed ${processed} files with cache. (Use --force to ignore cache.)`
262
+ : `Processed ${processed} files.`));
263
+ const data = [
264
+ [passed, 'passed', lightGreen],
265
+ [errors, 'errors', lightRed],
266
+ [warnings, 'warnings', lightYellow],
267
+ [excluded, 'excluded', darkGray],
268
+ ];
269
+ let summary = data
270
+ .filter(([count]) => count)
271
+ .map(([count, label, color]) => color(`${count} ${label}`))
272
+ .join(darkGray(' | '));
273
+ if (hasFix) {
274
+ summary += darkGray(` (Use --fix to apply automatic fixes.)`);
87
275
  }
88
- process.exit(hasError ? 1 : 0);
89
- async function projectWorker(tsconfigOption) {
90
- const tsconfig = require.resolve(tsconfigOption, { paths: [process.cwd()] });
91
- clack.intro(`${purple('[project]')} ${path.relative(process.cwd(), tsconfig)}`);
92
- parsed = parseCommonLine(tsconfig);
93
- if (!parsed.fileNames.length) {
94
- clack.outro(lightYellow('No included files.'));
276
+ else if (errors || warnings) {
277
+ summary += darkGray(` (No fixes available.)`);
278
+ }
279
+ clack.outro(summary);
280
+ process.exit(errors ? 1 : 0);
281
+ async function startWorker(linterWorker) {
282
+ const unfinishedProjects = projects.filter(project => project.currentFileIndex < project.fileNames.length);
283
+ if (!unfinishedProjects.length) {
95
284
  return;
96
285
  }
97
- const configFile = ts.findConfigFile(path.dirname(tsconfig), ts.sys.fileExists, 'tsslint.config.ts');
98
- if (!configFile) {
99
- clack.outro(lightYellow('No tsslint.config.ts found.'));
100
- return;
286
+ // Select a project that has not has a worker yet
287
+ let project = unfinishedProjects.find(project => !project.workers.length);
288
+ if (!project) {
289
+ // Choose a project with the most files left per worker
290
+ project = unfinishedProjects.sort((a, b) => {
291
+ const aFilesPerWorker = (a.fileNames.length - a.currentFileIndex) / a.workers.length;
292
+ const bFilesPerWorker = (b.fileNames.length - b.currentFileIndex) / b.workers.length;
293
+ return bFilesPerWorker - aFilesPerWorker;
294
+ })[0];
101
295
  }
102
- if (!configs.has(configFile)) {
103
- try {
104
- configs.set(configFile, await core.buildConfigFile(configFile, ts.sys.createHash, clack));
105
- }
106
- catch (err) {
107
- configs.set(configFile, undefined);
108
- console.error(err);
109
- }
110
- }
111
- const tsslintConfig = configs.get(configFile);
112
- if (!tsslintConfig) {
296
+ project.workers.push(linterWorker);
297
+ const setupSuccess = await linterWorker.setup(project.tsconfig, project.languages, project.configFile, project.builtConfig, project.fileNames, project.options);
298
+ if (!setupSuccess) {
299
+ projects = projects.filter(p => p !== project);
300
+ startWorker(linterWorker);
113
301
  return;
114
302
  }
115
- projectVersion++;
116
- typeRootsVersion++;
117
- const lintCache = process.argv.includes('--force')
118
- ? {}
119
- : cache.loadCache(configFile, ts.sys.createHash);
120
- const projectContext = {
121
- configFile,
122
- languageService,
123
- languageServiceHost,
124
- typescript: ts,
125
- tsconfig: ts.server.toNormalizedPath(tsconfig),
126
- };
127
- const linter = core.createLinter(projectContext, tsslintConfig, 'cli', clack);
128
- let lintSpinner = clack.spinner();
129
- let hasFix = false;
130
- let excluded = 0;
131
- let passed = 0;
132
- let errors = 0;
133
- let warnings = 0;
134
- let cached = 0;
135
- let t = Date.now();
136
- lintSpinner.start(darkGray(`[1/${parsed.fileNames.length}] ${path.relative(process.cwd(), parsed.fileNames[0])}`));
137
- await new Promise(resolve => setTimeout(resolve, 100));
138
- for (let i = 0; i < parsed.fileNames.length; i++) {
139
- const fileName = parsed.fileNames[i];
140
- if (Date.now() - t > 100) {
141
- t = Date.now();
142
- lintSpinner.message(darkGray(`[${i + 1}/${parsed.fileNames.length}] ${path.relative(process.cwd(), fileName)}`));
303
+ while (project.currentFileIndex < project.fileNames.length) {
304
+ const i = project.currentFileIndex++;
305
+ const fileName = project.fileNames[i];
306
+ const fileMtime = fs.statSync(fileName).mtimeMs;
307
+ addProcessFile(fileName);
308
+ if (Date.now() - lastSpinnerUpdate > 100) {
309
+ lastSpinnerUpdate = Date.now();
143
310
  await new Promise(resolve => setTimeout(resolve, 0));
144
311
  }
145
- const fileMtime = fs.statSync(fileName).mtimeMs;
146
- let fileCache = lintCache[fileName];
312
+ let fileCache = project.cache[fileName];
147
313
  if (fileCache) {
148
314
  if (fileCache[0] !== fileMtime) {
149
315
  fileCache[0] = fileMtime;
@@ -157,48 +323,17 @@ const lightYellow = (s) => '\x1b[93m' + s + _reset;
157
323
  }
158
324
  }
159
325
  else {
160
- lintCache[fileName] = fileCache = [fileMtime, {}, [], [], {}];
326
+ project.cache[fileName] = fileCache = [fileMtime, {}, [], [], {}];
161
327
  }
162
328
  let diagnostics;
163
329
  if (process.argv.includes('--fix')) {
164
- let retry = 3;
165
- let shouldRetry = true;
166
- let newSnapshot;
167
- while (shouldRetry && retry) {
168
- shouldRetry = false;
169
- retry--;
170
- if (Object.values(fileCache[1]).some(fixes => fixes > 0)) {
171
- // Reset the cache if there are any fixes applied.
172
- fileCache[1] = {};
173
- fileCache[2].length = 0;
174
- fileCache[3].length = 0;
175
- }
176
- diagnostics = linter.lint(fileName, fileCache);
177
- const fixes = linter.getCodeFixes(fileName, 0, Number.MAX_VALUE, diagnostics, fileCache);
178
- const textChanges = core.combineCodeFixes(fileName, fixes);
179
- if (textChanges.length) {
180
- const oldSnapshot = snapshots.get(fileName);
181
- newSnapshot = core.applyTextChanges(oldSnapshot, textChanges);
182
- snapshots.set(fileName, newSnapshot);
183
- versions.set(fileName, (versions.get(fileName) ?? 0) + 1);
184
- projectVersion++;
185
- shouldRetry = true;
186
- }
187
- }
188
- if (newSnapshot) {
189
- ts.sys.writeFile(fileName, newSnapshot.getText(0, newSnapshot.getLength()));
190
- fileCache[0] = fs.statSync(fileName).mtimeMs;
191
- }
192
- if (shouldRetry) {
193
- diagnostics = linter.lint(fileName, fileCache);
194
- }
330
+ diagnostics = await linterWorker.lintAndFix(fileName, fileCache);
195
331
  }
196
332
  else {
197
- diagnostics = linter.lint(fileName, fileCache);
333
+ diagnostics = await linterWorker.lint(fileName, fileCache);
198
334
  }
199
335
  if (diagnostics.length) {
200
- hasFix ||= linter.hasCodeFixes(fileName);
201
- hasError ||= diagnostics.some(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error);
336
+ hasFix ||= Object.values(fileCache[1]).some(fixes => fixes > 0) || await linterWorker.hasCodeFixes(fileName);
202
337
  for (const diagnostic of diagnostics) {
203
338
  if (diagnostic.category === ts.DiagnosticCategory.Suggestion) {
204
339
  continue;
@@ -209,96 +344,64 @@ const lightYellow = (s) => '\x1b[93m' + s + _reset;
209
344
  getNewLine: () => ts.sys.newLine,
210
345
  });
211
346
  output = output.replace(`TS${diagnostic.code}`, String(diagnostic.code));
212
- if (lintSpinner) {
213
- if (diagnostic.category === ts.DiagnosticCategory.Error) {
214
- errors++;
215
- lintSpinner.stop(output, 1);
216
- }
217
- else if (diagnostic.category === ts.DiagnosticCategory.Warning) {
218
- warnings++;
219
- lintSpinner.stop(output, 2);
220
- }
221
- else {
222
- lintSpinner.stop(output);
223
- }
224
- lintSpinner = undefined;
347
+ if (diagnostic.category === ts.DiagnosticCategory.Error) {
348
+ errors++;
349
+ log(output, 1);
350
+ }
351
+ else if (diagnostic.category === ts.DiagnosticCategory.Warning) {
352
+ warnings++;
353
+ log(output, 2);
225
354
  }
226
355
  else {
227
- if (diagnostic.category === ts.DiagnosticCategory.Error) {
228
- errors++;
229
- clack.log.error(output);
230
- }
231
- else if (diagnostic.category === ts.DiagnosticCategory.Warning) {
232
- warnings++;
233
- clack.log.warning(output);
234
- }
235
- else {
236
- clack.log.info(output);
237
- }
356
+ log(output);
238
357
  }
239
358
  }
240
359
  }
241
- else if (!Object.keys(linter.getRules(fileName, fileCache)).length) {
360
+ else if (!(await linterWorker.hasRules(fileName, fileCache[4]))) {
242
361
  excluded++;
243
362
  }
244
363
  else {
245
364
  passed++;
246
365
  }
247
- if (!lintSpinner) {
248
- lintSpinner = clack.spinner();
249
- lintSpinner.start(darkGray(`[${i + 1}/${parsed.fileNames.length}] ${path.relative(process.cwd(), parsed.fileNames[i])}`));
250
- await new Promise(resolve => setTimeout(resolve, 100));
251
- }
252
- }
253
- if (cached) {
254
- lintSpinner.stop(darkGray(`Processed ${parsed.fileNames.length} files with cache. (Use --force to ignore cache.)`));
255
- }
256
- else {
257
- lintSpinner.stop(darkGray(`Processed ${parsed.fileNames.length} files.`));
258
- }
259
- const data = [
260
- [passed, 'passed', lightGreen],
261
- [errors, 'errors', lightRed],
262
- [warnings, 'warnings', lightYellow],
263
- [excluded, 'excluded', darkGray],
264
- ];
265
- let summary = data
266
- .filter(([count]) => count)
267
- .map(([count, label, color]) => color(`${count} ${label}`))
268
- .join(darkGray(' | '));
269
- if (hasFix) {
270
- summary += darkGray(` (Use --fix to apply automatic fixes.)`);
366
+ processed++;
367
+ removeProcessFile(fileName);
271
368
  }
272
- else if (errors || warnings) {
273
- summary += darkGray(` (No fixes available.)`);
369
+ cache.saveCache(project.tsconfig, project.configFile, project.cache, ts.sys.createHash);
370
+ await startWorker(linterWorker);
371
+ }
372
+ async function getBuiltConfig(configFile) {
373
+ if (!builtConfigs.has(configFile)) {
374
+ builtConfigs.set(configFile, core.buildConfig(configFile, ts.sys.createHash, spinner, (s, code) => log(darkGray(s), code)));
274
375
  }
275
- clack.outro(summary);
276
- cache.saveCache(configFile, lintCache, ts.sys.createHash);
376
+ return await builtConfigs.get(configFile);
377
+ }
378
+ function addProcessFile(fileName) {
379
+ processFiles.add(fileName);
380
+ updateSpinner();
277
381
  }
278
- async function askTSConfig() {
279
- const presetConfig = ts.findConfigFile(process.cwd(), ts.sys.fileExists);
280
- let shortTsconfig = presetConfig ? path.relative(process.cwd(), presetConfig) : undefined;
281
- if (!shortTsconfig?.startsWith('.')) {
282
- shortTsconfig = `./${shortTsconfig}`;
382
+ function removeProcessFile(fileName) {
383
+ processFiles.delete(fileName);
384
+ updateSpinner();
385
+ }
386
+ function updateSpinner() {
387
+ if (processFiles.size === 1) {
388
+ const fileName = processFiles.values().next().value;
389
+ spinner.message(`[${processed + processFiles.size}/${allFilesNum}] ${path.relative(process.cwd(), fileName)}`);
390
+ }
391
+ else {
392
+ spinner.message(`[${processed + processFiles.size}/${allFilesNum}] Processing ${processFiles.size} files`);
283
393
  }
284
- return await clack.text({
285
- message: 'Select the project. (Use --project or --projects to skip this prompt.)',
286
- placeholder: shortTsconfig ? `${shortTsconfig} (${parseCommonLine(presetConfig).fileNames.length} files)` : 'No tsconfig.json/jsconfig.json found, please enter the path to the tsconfig.json/jsconfig.json file.',
287
- defaultValue: shortTsconfig,
288
- validate(value) {
289
- value ||= shortTsconfig;
290
- try {
291
- require.resolve(value, { paths: [process.cwd()] });
292
- }
293
- catch {
294
- return 'No such file.';
295
- }
296
- },
297
- });
298
394
  }
299
- function parseCommonLine(tsconfig) {
300
- const jsonConfigFile = ts.readJsonConfigFile(tsconfig, ts.sys.readFile);
301
- return ts.parseJsonSourceFileConfigFileContent(jsonConfigFile, ts.sys, path.dirname(tsconfig), {}, tsconfig);
395
+ function log(msg, code) {
396
+ spinner.stop(msg, code);
397
+ spinner = clack.spinner();
398
+ spinner.start();
302
399
  }
303
400
  })();
401
+ async function parseCommonLine(tsconfig, languages) {
402
+ const jsonConfigFile = ts.readJsonConfigFile(tsconfig, ts.sys.readFile);
403
+ const plugins = await languagePlugins.load(tsconfig, languages);
404
+ const extraFileExtensions = plugins.flatMap(plugin => plugin.typescript?.extraFileExtensions ?? []).flat();
405
+ return ts.parseJsonSourceFileConfigFileContent(jsonConfigFile, ts.sys, path.dirname(tsconfig), {}, tsconfig, undefined, extraFileExtensions);
406
+ }
304
407
  //# sourceMappingURL=index.js.map
package/lib/cache.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import core = require('@tsslint/core');
2
2
  export type CacheData = Record<string, core.FileLintCache>;
3
- export declare function loadCache(configFilePath: string, createHash?: (path: string) => string): CacheData;
4
- export declare function saveCache(configFilePath: string, cache: CacheData, createHash?: (path: string) => string): void;
3
+ export declare function loadCache(tsconfig: string, configFilePath: string, createHash?: (path: string) => string): CacheData;
4
+ export declare function saveCache(tsconfig: string, configFilePath: string, cache: CacheData, createHash?: (path: string) => string): void;
package/lib/cache.js CHANGED
@@ -5,9 +5,9 @@ exports.saveCache = saveCache;
5
5
  const core = require("@tsslint/core");
6
6
  const path = require("path");
7
7
  const fs = require("fs");
8
- function loadCache(configFilePath, createHash = btoa) {
8
+ function loadCache(tsconfig, configFilePath, createHash = btoa) {
9
9
  const outDir = core.getDotTsslintPath(configFilePath);
10
- const cacheFileName = createHash(path.relative(outDir, configFilePath)) + '.cache.json';
10
+ const cacheFileName = createHash(path.relative(outDir, configFilePath)) + '_' + createHash(path.relative(outDir, tsconfig)) + '.cache.json';
11
11
  const cacheFilePath = path.join(outDir, cacheFileName);
12
12
  const cacheFileStat = fs.statSync(cacheFilePath, { throwIfNoEntry: false });
13
13
  const configFileStat = fs.statSync(configFilePath, { throwIfNoEntry: false });
@@ -21,9 +21,9 @@ function loadCache(configFilePath, createHash = btoa) {
21
21
  }
22
22
  return {};
23
23
  }
24
- function saveCache(configFilePath, cache, createHash = btoa) {
24
+ function saveCache(tsconfig, configFilePath, cache, createHash = btoa) {
25
25
  const outDir = core.getDotTsslintPath(configFilePath);
26
- const cacheFileName = createHash(path.relative(outDir, configFilePath)) + '.cache.json';
26
+ const cacheFileName = createHash(path.relative(outDir, configFilePath)) + '_' + createHash(path.relative(outDir, tsconfig)) + '.cache.json';
27
27
  const cacheFilePath = path.join(outDir, cacheFileName);
28
28
  fs.writeFileSync(cacheFilePath, JSON.stringify(cache));
29
29
  }
@@ -0,0 +1,2 @@
1
+ import { LanguagePlugin } from '@volar/language-core';
2
+ export declare function load(tsconfig: string, languages: string[]): Promise<LanguagePlugin<string, import("@volar/language-core").VirtualCode>[]>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.load = load;
4
+ const path = require("path");
5
+ const ts = require("typescript");
6
+ const cache = new Map();
7
+ async function load(tsconfig, languages) {
8
+ if (cache.has(tsconfig)) {
9
+ return cache.get(tsconfig);
10
+ }
11
+ const plugins = [];
12
+ if (languages.includes('vue')) {
13
+ let vue;
14
+ let vueTscPkgPath;
15
+ if (findPackageJson('@vue/language-core')) {
16
+ vue = require('@vue/language-core');
17
+ }
18
+ else if (vueTscPkgPath = findPackageJson('vue-tsc')) {
19
+ const vueTscPath = path.dirname(vueTscPkgPath);
20
+ vue = require(require.resolve('@vue/language-core', { paths: [vueTscPath] }));
21
+ }
22
+ else {
23
+ const pkg = ts.findConfigFile(path.dirname(tsconfig), ts.sys.fileExists, 'package.json');
24
+ if (pkg) {
25
+ throw new Error('Please install @vue/language-core or vue-tsc to ' + path.relative(process.cwd(), pkg));
26
+ }
27
+ else {
28
+ throw new Error('Please install @vue/language-core or vue-tsc for ' + path.relative(process.cwd(), tsconfig));
29
+ }
30
+ }
31
+ const commonLine = vue.createParsedCommandLine(ts, ts.sys, tsconfig);
32
+ const vueLanguagePlugin = vue.createVueLanguagePlugin(ts, commonLine.options, commonLine.vueOptions, fileName => fileName);
33
+ plugins.push(vueLanguagePlugin);
34
+ }
35
+ if (languages.includes('mdx')) {
36
+ let mdx;
37
+ try {
38
+ mdx = await import(require.resolve('@mdx-js/language-service', { paths: [path.dirname(tsconfig)] }));
39
+ }
40
+ catch {
41
+ const pkg = ts.findConfigFile(path.dirname(tsconfig), ts.sys.fileExists, 'package.json');
42
+ if (pkg) {
43
+ throw new Error('Please install @mdx-js/language-service to ' + path.relative(process.cwd(), pkg));
44
+ }
45
+ else {
46
+ throw new Error('Please install @mdx-js/language-service for ' + path.relative(process.cwd(), tsconfig));
47
+ }
48
+ }
49
+ const mdxLanguagePlugin = mdx.createMdxLanguagePlugin();
50
+ plugins.push(mdxLanguagePlugin);
51
+ }
52
+ if (languages.includes('astro')) {
53
+ let astro;
54
+ try {
55
+ astro = require(require.resolve('@astrojs/ts-plugin/dist/language.js', { paths: [path.dirname(tsconfig)] }));
56
+ }
57
+ catch (err) {
58
+ const pkg = ts.findConfigFile(path.dirname(tsconfig), ts.sys.fileExists, 'package.json');
59
+ if (pkg) {
60
+ throw new Error('Please install @astrojs/ts-plugin to ' + path.relative(process.cwd(), pkg));
61
+ }
62
+ else {
63
+ throw new Error('Please install @astrojs/ts-plugin for ' + path.relative(process.cwd(), tsconfig));
64
+ }
65
+ }
66
+ const astroLanguagePlugin = astro.getLanguagePlugin();
67
+ plugins.push(astroLanguagePlugin);
68
+ }
69
+ cache.set(tsconfig, plugins);
70
+ return plugins;
71
+ function findPackageJson(pkgName) {
72
+ try {
73
+ return require.resolve(`${pkgName}/package.json`, { paths: [path.dirname(tsconfig)] });
74
+ }
75
+ catch { }
76
+ }
77
+ }
78
+ //# sourceMappingURL=languagePlugins.js.map
@@ -0,0 +1,22 @@
1
+ import ts = require('typescript');
2
+ import core = require('@tsslint/core');
3
+ export declare function createLocal(): {
4
+ setup(tsconfig: string, languages: string[], configFile: string, builtConfig: string, _fileNames: string[], _options: ts.CompilerOptions): Promise<boolean>;
5
+ lint(fileName: string, fileCache: core.FileLintCache): ts.DiagnosticWithLocation[];
6
+ lintAndFix(fileName: string, fileCache: core.FileLintCache): ts.DiagnosticWithLocation[];
7
+ hasCodeFixes(fileName: string): boolean;
8
+ hasRules(fileName: string, minimatchCache: Record<string, boolean>): boolean;
9
+ };
10
+ export declare function create(): {
11
+ setup(tsconfig: string, languages: string[], configFile: string, builtConfig: string, _fileNames: string[], _options: ts.CompilerOptions): Promise<boolean>;
12
+ lint(fileName: string, fileCache: core.FileLintCache): Promise<ts.DiagnosticWithLocation[]>;
13
+ lintAndFix(fileName: string, fileCache: core.FileLintCache): Promise<ts.DiagnosticWithLocation[]>;
14
+ hasCodeFixes(fileName: string): Promise<boolean>;
15
+ hasRules(fileName: string, minimatchCache: Record<string, boolean>): Promise<boolean>;
16
+ };
17
+ declare function setup(tsconfig: string, languages: string[], configFile: string, builtConfig: string, _fileNames: string[], _options: ts.CompilerOptions): Promise<boolean>;
18
+ declare function lintAndFix(fileName: string, fileCache: core.FileLintCache): readonly [ts.DiagnosticWithLocation[], core.FileLintCache];
19
+ declare function lint(fileName: string, fileCache: core.FileLintCache): readonly [ts.DiagnosticWithLocation[], core.FileLintCache];
20
+ declare function hasCodeFixes(fileName: string): boolean;
21
+ declare function hasRules(fileName: string, minimatchCache: core.FileLintCache[4]): readonly [boolean, Record<string, boolean>];
22
+ export {};
package/lib/worker.js ADDED
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLocal = createLocal;
4
+ exports.create = create;
5
+ const ts = require("typescript");
6
+ const core = require("@tsslint/core");
7
+ const url = require("url");
8
+ const fs = require("fs");
9
+ const worker_threads = require("worker_threads");
10
+ const languagePlugins = require("./languagePlugins.js");
11
+ const language_core_1 = require("@volar/language-core");
12
+ const typescript_1 = require("@volar/typescript");
13
+ const transform_1 = require("@volar/typescript/lib/node/transform");
14
+ let projectVersion = 0;
15
+ let typeRootsVersion = 0;
16
+ let options = {};
17
+ let fileNames = [];
18
+ let language;
19
+ let linter;
20
+ let linterLanguageService;
21
+ const snapshots = new Map();
22
+ const versions = new Map();
23
+ const originalHost = {
24
+ ...ts.sys,
25
+ useCaseSensitiveFileNames() {
26
+ return ts.sys.useCaseSensitiveFileNames;
27
+ },
28
+ getProjectVersion() {
29
+ return projectVersion.toString();
30
+ },
31
+ getTypeRootsVersion() {
32
+ return typeRootsVersion;
33
+ },
34
+ getCompilationSettings() {
35
+ return options;
36
+ },
37
+ getScriptFileNames() {
38
+ return fileNames;
39
+ },
40
+ getScriptVersion(fileName) {
41
+ return versions.get(fileName)?.toString() ?? '0';
42
+ },
43
+ getScriptSnapshot(fileName) {
44
+ if (!snapshots.has(fileName)) {
45
+ snapshots.set(fileName, ts.ScriptSnapshot.fromString(ts.sys.readFile(fileName)));
46
+ }
47
+ return snapshots.get(fileName);
48
+ },
49
+ getDefaultLibFileName(options) {
50
+ return ts.getDefaultLibFilePath(options);
51
+ },
52
+ };
53
+ const linterHost = { ...originalHost };
54
+ const originalService = ts.createLanguageService(linterHost);
55
+ function createLocal() {
56
+ return {
57
+ setup(...args) {
58
+ return setup(...args);
59
+ },
60
+ lint(...args) {
61
+ return lint(...args)[0];
62
+ },
63
+ lintAndFix(...args) {
64
+ return lintAndFix(...args)[0];
65
+ },
66
+ hasCodeFixes(...args) {
67
+ return hasCodeFixes(...args);
68
+ },
69
+ hasRules(...args) {
70
+ return hasRules(...args)[0];
71
+ },
72
+ };
73
+ }
74
+ function create() {
75
+ const worker = new worker_threads.Worker(__filename);
76
+ return {
77
+ setup(...args) {
78
+ return sendRequest(setup, ...args);
79
+ },
80
+ async lint(...args) {
81
+ const [res, newCache] = await sendRequest(lint, ...args);
82
+ Object.assign(args[1], newCache); // Sync the cache
83
+ return res;
84
+ },
85
+ async lintAndFix(...args) {
86
+ const [res, newCache] = await sendRequest(lintAndFix, ...args);
87
+ Object.assign(args[1], newCache); // Sync the cache
88
+ return res;
89
+ },
90
+ hasCodeFixes(...args) {
91
+ return sendRequest(hasCodeFixes, ...args);
92
+ },
93
+ async hasRules(...args) {
94
+ const [res, newCache] = await sendRequest(hasRules, ...args);
95
+ Object.assign(args[1], newCache); // Sync the cache
96
+ return res;
97
+ },
98
+ };
99
+ function sendRequest(t, ...args) {
100
+ return new Promise(resolve => {
101
+ worker.once('message', json => {
102
+ resolve(JSON.parse(json));
103
+ });
104
+ worker.postMessage(JSON.stringify([t.name, ...args]));
105
+ });
106
+ }
107
+ }
108
+ worker_threads.parentPort?.on('message', async (json) => {
109
+ const data = JSON.parse(json);
110
+ const result = await handlers[data[0]](...data.slice(1));
111
+ worker_threads.parentPort.postMessage(JSON.stringify(result));
112
+ });
113
+ const handlers = {
114
+ setup,
115
+ lint,
116
+ lintAndFix,
117
+ hasCodeFixes,
118
+ hasRules,
119
+ };
120
+ async function setup(tsconfig, languages, configFile, builtConfig, _fileNames, _options) {
121
+ const clack = await import('@clack/prompts');
122
+ let config;
123
+ try {
124
+ config = (await import(url.pathToFileURL(builtConfig).toString())).default;
125
+ }
126
+ catch (err) {
127
+ if (err instanceof Error) {
128
+ clack.log.error(err.stack ?? err.message);
129
+ }
130
+ else {
131
+ clack.log.error(String(err));
132
+ }
133
+ return false;
134
+ }
135
+ for (let key in linterHost) {
136
+ if (!(key in originalHost)) {
137
+ // @ts-ignore
138
+ delete linterHost[key];
139
+ }
140
+ else {
141
+ // @ts-ignore
142
+ linterHost[key] = originalHost[key];
143
+ }
144
+ }
145
+ linterLanguageService = originalService;
146
+ language = undefined;
147
+ const plugins = await languagePlugins.load(tsconfig, languages);
148
+ if (plugins.length) {
149
+ const { getScriptSnapshot } = originalHost;
150
+ language = (0, language_core_1.createLanguage)([
151
+ ...plugins,
152
+ { getLanguageId: fileName => (0, typescript_1.resolveFileLanguageId)(fileName) },
153
+ ], new language_core_1.FileMap(ts.sys.useCaseSensitiveFileNames), fileName => {
154
+ const snapshot = getScriptSnapshot(fileName);
155
+ if (snapshot) {
156
+ language.scripts.set(fileName, snapshot);
157
+ }
158
+ });
159
+ (0, typescript_1.decorateLanguageServiceHost)(ts, language, linterHost);
160
+ const proxy = (0, typescript_1.createProxyLanguageService)(linterLanguageService);
161
+ proxy.initialize(language);
162
+ linterLanguageService = proxy.proxy;
163
+ }
164
+ projectVersion++;
165
+ typeRootsVersion++;
166
+ fileNames = _fileNames;
167
+ options = _options;
168
+ linter = core.createLinter({
169
+ configFile,
170
+ languageService: linterLanguageService,
171
+ languageServiceHost: linterHost,
172
+ typescript: ts,
173
+ tsconfig: ts.server.toNormalizedPath(tsconfig),
174
+ }, config, 'cli', clack);
175
+ return true;
176
+ }
177
+ function lintAndFix(fileName, fileCache) {
178
+ let retry = 1;
179
+ let shouldRetry = true;
180
+ let newSnapshot;
181
+ let diagnostics;
182
+ while (shouldRetry && retry--) {
183
+ if (Object.values(fileCache[1]).some(fixes => fixes > 0)) {
184
+ // Reset the cache if there are any fixes applied.
185
+ fileCache[1] = {};
186
+ fileCache[2].length = 0;
187
+ fileCache[3].length = 0;
188
+ }
189
+ diagnostics = linter.lint(fileName, fileCache);
190
+ let fixes = linter
191
+ .getCodeFixes(fileName, 0, Number.MAX_VALUE, diagnostics, fileCache[4])
192
+ .filter(fix => fix.fixId === 'tsslint');
193
+ if (language) {
194
+ fixes = fixes.map(fix => {
195
+ fix.changes = (0, transform_1.transformFileTextChanges)(language, fix.changes, false, language_core_1.isCodeActionsEnabled);
196
+ return fix;
197
+ });
198
+ }
199
+ const textChanges = core.combineCodeFixes(fileName, fixes);
200
+ if (textChanges.length) {
201
+ const oldSnapshot = snapshots.get(fileName);
202
+ newSnapshot = core.applyTextChanges(oldSnapshot, textChanges);
203
+ snapshots.set(fileName, newSnapshot);
204
+ versions.set(fileName, (versions.get(fileName) ?? 0) + 1);
205
+ projectVersion++;
206
+ shouldRetry = true;
207
+ }
208
+ }
209
+ if (newSnapshot) {
210
+ ts.sys.writeFile(fileName, newSnapshot.getText(0, newSnapshot.getLength()));
211
+ fileCache[0] = fs.statSync(fileName).mtimeMs;
212
+ fileCache[1] = {};
213
+ fileCache[2].length = 0;
214
+ fileCache[3].length = 0;
215
+ }
216
+ if (shouldRetry) {
217
+ diagnostics = linter.lint(fileName, fileCache);
218
+ }
219
+ if (language) {
220
+ diagnostics = diagnostics
221
+ .map(d => (0, transform_1.transformDiagnostic)(language, d, originalService.getCurrentProgram(), false))
222
+ .filter(d => !!d);
223
+ diagnostics = diagnostics.map(diagnostic => ({
224
+ ...diagnostic,
225
+ file: {
226
+ fileName: diagnostic.file.fileName,
227
+ text: getFileText(diagnostic.file.fileName),
228
+ },
229
+ relatedInformation: diagnostic.relatedInformation?.map(info => ({
230
+ ...info,
231
+ file: info.file ? {
232
+ fileName: info.file.fileName,
233
+ text: getFileText(info.file.fileName),
234
+ } : undefined,
235
+ })),
236
+ }));
237
+ }
238
+ else {
239
+ diagnostics = diagnostics.map(diagnostic => ({
240
+ ...diagnostic,
241
+ file: {
242
+ fileName: diagnostic.file.fileName,
243
+ text: diagnostic.file.text,
244
+ },
245
+ relatedInformation: diagnostic.relatedInformation?.map(info => ({
246
+ ...info,
247
+ file: info.file ? {
248
+ fileName: info.file.fileName,
249
+ text: info.file.text,
250
+ } : undefined,
251
+ })),
252
+ }));
253
+ }
254
+ return [diagnostics, fileCache];
255
+ }
256
+ function lint(fileName, fileCache) {
257
+ let diagnostics = linter.lint(fileName, fileCache);
258
+ if (language) {
259
+ diagnostics = diagnostics
260
+ .map(d => (0, transform_1.transformDiagnostic)(language, d, originalService.getCurrentProgram(), false))
261
+ .filter(d => !!d);
262
+ diagnostics = diagnostics.map(diagnostic => ({
263
+ ...diagnostic,
264
+ file: {
265
+ fileName: diagnostic.file.fileName,
266
+ text: getFileText(diagnostic.file.fileName),
267
+ },
268
+ relatedInformation: diagnostic.relatedInformation?.map(info => ({
269
+ ...info,
270
+ file: info.file ? {
271
+ fileName: info.file.fileName,
272
+ text: getFileText(info.file.fileName),
273
+ } : undefined,
274
+ })),
275
+ }));
276
+ }
277
+ else {
278
+ diagnostics = diagnostics.map(diagnostic => ({
279
+ ...diagnostic,
280
+ file: {
281
+ fileName: diagnostic.file.fileName,
282
+ text: diagnostic.file.text,
283
+ },
284
+ relatedInformation: diagnostic.relatedInformation?.map(info => ({
285
+ ...info,
286
+ file: info.file ? {
287
+ fileName: info.file.fileName,
288
+ text: info.file.text,
289
+ } : undefined,
290
+ })),
291
+ }));
292
+ }
293
+ return [diagnostics, fileCache];
294
+ }
295
+ function getFileText(fileName) {
296
+ return originalHost.getScriptSnapshot(fileName).getText(0, Number.MAX_VALUE);
297
+ }
298
+ function hasCodeFixes(fileName) {
299
+ return linter.hasCodeFixes(fileName);
300
+ }
301
+ function hasRules(fileName, minimatchCache) {
302
+ return [Object.keys(linter.getRules(fileName, minimatchCache)).length > 0, minimatchCache];
303
+ }
304
+ //# sourceMappingURL=worker.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsslint/cli",
3
- "version": "1.3.6",
3
+ "version": "1.4.1",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "tsslint": "./bin/tsslint.js"
@@ -16,12 +16,17 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@clack/prompts": "^0.8.2",
19
- "@tsslint/config": "1.3.6",
20
- "@tsslint/core": "1.3.6",
19
+ "@tsslint/config": "1.4.1",
20
+ "@tsslint/core": "1.4.1",
21
+ "@volar/language-core": "~2.4.0",
22
+ "@volar/typescript": "~2.4.0",
21
23
  "glob": "^10.4.1"
22
24
  },
23
25
  "peerDependencies": {
24
26
  "typescript": "*"
25
27
  },
26
- "gitHead": "d153aa87c92803b4c20fef1f5cf2799f1a599099"
28
+ "devDependencies": {
29
+ "@vue/language-core": "latest"
30
+ },
31
+ "gitHead": "54f42ec9414029a356fa19a762260f03392563fa"
27
32
  }