diff --git a/.eslintrc.js b/.eslintrc.js index 6ae76ac3b6cb..337e239983b8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { // 'comma-dangle': ['error', 'always-multiline'], + 'constructor-super': 'off', curly: ['error', 'all'], 'no-mixed-operators': 'error', 'no-console': 'error', @@ -113,7 +114,8 @@ module.exports = { ecmaFeatures: { jsx: false, }, - project: './tsconfig.base.json', + project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'], + tsconfigRootDir: __dirname, }, overrides: [ { diff --git a/.gitignore b/.gitignore index 3a06baccf6f5..582f78d2d60e 100644 --- a/.gitignore +++ b/.gitignore @@ -57,9 +57,12 @@ jspm_packages/ # next.js build output .next -.DS_Store -.idea -dist # Editor-specific metadata folders .vs + +.DS_Store +.idea +dist +*.tsbuildinfo +.watchmanconfig diff --git a/.vscode/settings.json b/.vscode/settings.json index adcf2422e8f3..9f2782e2bb55 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,5 +22,6 @@ "javascript.preferences.importModuleSpecifier": "auto", "typescript.preferences.importModuleSpecifier": "auto", "javascript.preferences.quoteStyle": "single", - "typescript.preferences.quoteStyle": "single" +"typescript.preferences.quoteStyle": "single", +"editor.defaultFormatter": "esbenp.prettier-vscode" } diff --git a/package.json b/package.json index 2dce8eb93355..e4d690b0bb42 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "pre-push": "yarn format-check", "postinstall": "lerna bootstrap && yarn build && lerna link && npm run check-clean-workspace-after-install && opencollective-postinstall", "check-clean-workspace-after-install": "git diff --quiet --exit-code", - "test": "lerna run test --parallel", + "test": "lerna run test --concurrency 1", "typecheck": "lerna run typecheck" }, "config": { @@ -70,7 +70,6 @@ "lint-staged": "^9.2.5", "opencollective-postinstall": "^2.0.2", "prettier": "^1.18.2", - "rimraf": "^3.0.0", "ts-jest": "^24.0.0", "ts-node": "^8.3.0", "tslint": "^5.19.0", diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index 1944e16e02e9..78d58d38427b 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -23,12 +23,12 @@ }, "license": "MIT", "scripts": { - "build": "tsc -p tsconfig.build.json", - "clean": "rimraf dist/", + "build": "tsc -b tsconfig.build.json", + "clean": "tsc -b tsconfig.build.json --clean", "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", - "prebuild": "npm run clean", + "lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'", "test": "jest --coverage", - "typecheck": "tsc --noEmit" + "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { "@typescript-eslint/experimental-utils": "2.3.3", diff --git a/packages/eslint-plugin-tslint/tsconfig.build.json b/packages/eslint-plugin-tslint/tsconfig.build.json index b0fced27d72d..d6987c27afe7 100644 --- a/packages/eslint-plugin-tslint/tsconfig.build.json +++ b/packages/eslint-plugin-tslint/tsconfig.build.json @@ -1,7 +1,14 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true }, - "include": ["src"] + "include": ["src"], + "references": [ + { "path": "../experimental-utils/tsconfig.build.json" }, + { "path": "../parser/tsconfig.build.json" }, + { "path": "../typescript-estree/tsconfig.build.json" } + ] } diff --git a/packages/eslint-plugin-tslint/tsconfig.json b/packages/eslint-plugin-tslint/tsconfig.json index 7db2d0520ffa..5362fa1c79c4 100644 --- a/packages/eslint-plugin-tslint/tsconfig.json +++ b/packages/eslint-plugin-tslint/tsconfig.json @@ -1,5 +1,9 @@ { "extends": "./tsconfig.build.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, "include": ["src", "tests"], "exclude": ["tests/test-project", "tests/test-tslint-rules-directory"] } diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 2458e18eb1be..9eeb7f31616e 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -29,15 +29,15 @@ "license": "MIT", "main": "dist/index.js", "scripts": { - "build": "tsc -p tsconfig.build.json", + "build": "tsc -b tsconfig.build.json", "check:docs": "../../node_modules/.bin/ts-node --files ./tools/validate-docs/index.ts", "check:configs": "../../node_modules/.bin/ts-node --files ./tools/validate-configs/index.ts", - "clean": "rimraf dist/", + "clean": "tsc -b tsconfig.build.json --clean", "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", "generate:configs": "../../node_modules/.bin/ts-node --files tools/generate-configs.ts", - "prebuild": "npm run clean", + "lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'", "test": "jest --coverage", - "typecheck": "tsc --noEmit" + "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { "@typescript-eslint/experimental-utils": "2.3.3", diff --git a/packages/eslint-plugin/tests/RuleTester.ts b/packages/eslint-plugin/tests/RuleTester.ts index 978adc109669..f96d1739f182 100644 --- a/packages/eslint-plugin/tests/RuleTester.ts +++ b/packages/eslint-plugin/tests/RuleTester.ts @@ -1,4 +1,5 @@ import { TSESLint, ESLintUtils } from '@typescript-eslint/experimental-utils'; +import { clearCaches } from '@typescript-eslint/parser'; import * as path from 'path'; const parser = '@typescript-eslint/parser'; @@ -74,4 +75,10 @@ function getFixturesRootDir(): string { const { batchedSingleLineTests } = ESLintUtils; +// make sure that the parser doesn't hold onto file handles between tests +// on linux (i.e. our CI env), there can be very a limited number of watch handles available +afterAll(() => { + clearCaches(); +}); + export { RuleTester, getFixturesRootDir, batchedSingleLineTests }; diff --git a/packages/eslint-plugin/tsconfig.build.json b/packages/eslint-plugin/tsconfig.build.json index 1ab98da19abf..86f3842dae31 100644 --- a/packages/eslint-plugin/tsconfig.build.json +++ b/packages/eslint-plugin/tsconfig.build.json @@ -5,12 +5,13 @@ "declaration": false, "declarationMap": false, "outDir": "./dist", + "rootDir": "./src", "resolveJsonModule": true }, - "include": [ - "src", - "typings", - // include the parser's ambient typings because the parser exports them in its type defs - "../parser/typings" + "include": ["src", "typings"], + "references": [ + { "path": "../experimental-utils/tsconfig.build.json" }, + { "path": "../parser/tsconfig.build.json" }, + { "path": "../typescript-estree/tsconfig.build.json" } ] } diff --git a/packages/eslint-plugin/tsconfig.json b/packages/eslint-plugin/tsconfig.json index fb7e21da237d..4ef2253e8b4e 100644 --- a/packages/eslint-plugin/tsconfig.json +++ b/packages/eslint-plugin/tsconfig.json @@ -1,4 +1,8 @@ { "extends": "./tsconfig.build.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, "include": ["src", "typings", "tests", "tools"] } diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index b17f79a2ca30..e312f6b11140 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -28,12 +28,12 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "test": "jest --coverage", - "prebuild": "npm run clean", - "build": "tsc -p tsconfig.build.json", - "clean": "rimraf dist/", + "build": "tsc -b tsconfig.build.json", + "clean": "tsc -b tsconfig.build.json --clean", "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", - "typecheck": "tsc --noEmit" + "lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'", + "test": "jest --coverage", + "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { "@types/json-schema": "^7.0.3", diff --git a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts index eea1a9577684..87f919129053 100644 --- a/packages/experimental-utils/src/ts-eslint/ParserOptions.ts +++ b/packages/experimental-utils/src/ts-eslint/ParserOptions.ts @@ -1,21 +1,22 @@ export interface ParserOptions { - loc?: boolean; comment?: boolean; - range?: boolean; - tokens?: boolean; - sourceType?: 'script' | 'module'; - ecmaVersion?: 3 | 5 | 6 | 7 | 8 | 9 | 10 | 2015 | 2016 | 2017 | 2018 | 2019; ecmaFeatures?: { globalReturn?: boolean; jsx?: boolean; }; + ecmaVersion?: 3 | 5 | 6 | 7 | 8 | 9 | 10 | 2015 | 2016 | 2017 | 2018 | 2019; + errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + errorOnUnknownASTType?: boolean; + extraFileExtensions?: string[]; // ts-estree specific filePath?: string; + loc?: boolean; + noWatch?: boolean; project?: string | string[]; - useJSXTextNode?: boolean; - errorOnUnknownASTType?: boolean; - errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + range?: boolean; + sourceType?: 'script' | 'module'; + tokens?: boolean; tsconfigRootDir?: string; - extraFileExtensions?: string[]; + useJSXTextNode?: boolean; warnOnUnsupportedTypeScriptVersion?: boolean; } diff --git a/packages/experimental-utils/src/ts-eslint/RuleTester.ts b/packages/experimental-utils/src/ts-eslint/RuleTester.ts index 863d0c076f23..84da228fb81e 100644 --- a/packages/experimental-utils/src/ts-eslint/RuleTester.ts +++ b/packages/experimental-utils/src/ts-eslint/RuleTester.ts @@ -50,16 +50,32 @@ interface RuleTesterConfig { parser: string; parserOptions?: ParserOptions; } -declare interface RuleTester { + +// the cast on the extends is so that we don't want to have the built type defs to attempt to import eslint +class RuleTester extends (ESLintRuleTester as { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new (...args: unknown[]): any; +}) { + constructor(config?: RuleTesterConfig) { + super(config); + + // nobody will ever need watching in tests + // so we can give everyone a perf win by disabling watching + if (config && config.parserOptions && config.parserOptions.project) { + config.parserOptions.noWatch = + typeof config.parserOptions.noWatch === 'boolean' || true; + } + } + run>( name: string, rule: RuleModule, tests: RunTests, - ): void; + ): void { + // this method is only defined here because we lazily type the eslint import with `any` + super.run(name, rule, tests); + } } -const RuleTester = ESLintRuleTester as { - new (config?: RuleTesterConfig): RuleTester; -}; export { InvalidTestCase, diff --git a/packages/experimental-utils/tsconfig.build.json b/packages/experimental-utils/tsconfig.build.json index c052e5211304..e1b499764611 100644 --- a/packages/experimental-utils/tsconfig.build.json +++ b/packages/experimental-utils/tsconfig.build.json @@ -1,9 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "composite": true, "outDir": "./dist", "rootDir": "./src", "resolveJsonModule": true }, - "include": ["src", "typings"] + "include": ["src", "typings"], + "references": [{ "path": "../typescript-estree/tsconfig.build.json" }] } diff --git a/packages/experimental-utils/tsconfig.json b/packages/experimental-utils/tsconfig.json index f469d044ef4b..4ef2253e8b4e 100644 --- a/packages/experimental-utils/tsconfig.json +++ b/packages/experimental-utils/tsconfig.json @@ -1,8 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "./tsconfig.build.json", "compilerOptions": { - "outDir": "./dist", - "resolveJsonModule": true + "rootDir": ".", + "noEmit": true }, "include": ["src", "typings", "tests", "tools"] } diff --git a/packages/parser/package.json b/packages/parser/package.json index a5a641ef5170..9979d210ed8a 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -31,12 +31,12 @@ "eslint" ], "scripts": { - "build": "tsc -p tsconfig.build.json", - "clean": "rimraf dist/", + "build": "tsc -b tsconfig.build.json", + "clean": "tsc -b tsconfig.build.json --clean", "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", - "prebuild": "npm run clean", + "lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'", "test": "jest --coverage", - "typecheck": "tsc --noEmit" + "typecheck": "tsc -p tsconfig.json --noEmit" }, "peerDependencies": { "eslint": "^5.0.0 || ^6.0.0" diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 830a282da1e4..bcb6dde759a4 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -115,3 +115,4 @@ export function parseForESLint( } export { ParserServices, ParserOptions }; +export { clearCaches } from '@typescript-eslint/typescript-estree'; diff --git a/packages/parser/tests/lib/jsx.ts b/packages/parser/tests/lib/jsx.ts index 530b26c444b5..39548ea1b3b8 100644 --- a/packages/parser/tests/lib/jsx.ts +++ b/packages/parser/tests/lib/jsx.ts @@ -1,6 +1,6 @@ +import filesWithKnownIssues from '@typescript-eslint/shared-fixtures/jsx-known-issues'; import fs from 'fs'; import glob from 'glob'; -import filesWithKnownIssues from '../../../shared-fixtures/jsx-known-issues'; import { createScopeSnapshotTestBlock, formatSnapshotName, diff --git a/packages/parser/tsconfig.build.json b/packages/parser/tsconfig.build.json index a94802fb081a..b1d34cc5c3d9 100644 --- a/packages/parser/tsconfig.build.json +++ b/packages/parser/tsconfig.build.json @@ -1,8 +1,14 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, - "outDir": "./dist" + "composite": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true }, - "include": ["src"] + "include": ["src"], + "references": [ + { "path": "../experimental-utils/tsconfig.build.json" }, + { "path": "../typescript-estree/tsconfig.build.json" } + ] } diff --git a/packages/parser/tsconfig.json b/packages/parser/tsconfig.json index 6f763b588b53..adda86ea16c6 100644 --- a/packages/parser/tsconfig.json +++ b/packages/parser/tsconfig.json @@ -1,5 +1,9 @@ { "extends": "./tsconfig.build.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, "include": ["src", "tests", "tools"], "exclude": ["tests/fixtures"] } diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index b5918d8d939e..0e2dcc979145 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -31,16 +31,15 @@ "syntax" ], "scripts": { - "ast-alignment-tests": "jest spec.ts", - "build": "tsc -p tsconfig.build.json", - "clean": "rimraf dist/", + "build": "tsc -b tsconfig.build.json", + "clean": "tsc -b tsconfig.build.json --clean", "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", - "prebuild": "npm run clean", + "lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'", "test": "jest --coverage", - "typecheck": "tsc --noEmit", - "unit-tests": "jest \"./tests/lib/.*\"" + "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "chokidar": "^3.0.2", "glob": "^7.1.4", "is-glob": "^4.0.1", "lodash.unescape": "4.0.1", @@ -56,10 +55,12 @@ "@types/lodash.isplainobject": "^4.0.4", "@types/lodash.unescape": "^4.0.4", "@types/semver": "^6.0.1", + "@types/tmp": "^0.1.0", "@typescript-eslint/shared-fixtures": "2.3.3", "babel-code-frame": "^6.26.0", "glob": "^7.1.4", "lodash.isplainobject": "4.0.6", + "tmp": "^0.1.0", "typescript": "*" } } diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 77a649f6f309..1a47f30f16d6 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -2,41 +2,43 @@ import { Program } from 'typescript'; import { TSESTree, TSNode } from './ts-estree'; export interface Extra { - errorOnUnknownASTType: boolean; - errorOnTypeScriptSyntacticAndSemanticIssues: boolean; - useJSXTextNode: boolean; - tokens: null | TSESTree.Token[]; - comment: boolean; code: string; - range: boolean; - loc: boolean; + comment: boolean; comments: TSESTree.Comment[]; - strict: boolean; + createDefaultProgram: boolean; + errorOnTypeScriptSyntacticAndSemanticIssues: boolean; + errorOnUnknownASTType: boolean; + extraFileExtensions: string[]; jsx: boolean; + loc: boolean; log: Function; + noWatch?: boolean; + preserveNodeMaps?: boolean; projects: string[]; + range: boolean; + strict: boolean; + tokens: null | TSESTree.Token[]; tsconfigRootDir: string; - extraFileExtensions: string[]; - preserveNodeMaps?: boolean; - createDefaultProgram: boolean; + useJSXTextNode: boolean; } export interface TSESTreeOptions { - range?: boolean; - loc?: boolean; - tokens?: boolean; comment?: boolean; - jsx?: boolean; - errorOnUnknownASTType?: boolean; + createDefaultProgram?: boolean; errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; - useJSXTextNode?: boolean; + errorOnUnknownASTType?: boolean; + extraFileExtensions?: string[]; + filePath?: string; + jsx?: boolean; + loc?: boolean; loggerFn?: Function | false; + noWatch?: boolean; + preserveNodeMaps?: boolean; project?: string | string[]; - filePath?: string; + range?: boolean; + tokens?: boolean; tsconfigRootDir?: string; - extraFileExtensions?: string[]; - preserveNodeMaps?: boolean; - createDefaultProgram?: boolean; + useJSXTextNode?: boolean; } // This lets us use generics to type the return value, and removes the need to diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 882a8bbff7e5..75d3983cf55c 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -46,23 +46,24 @@ function getFileName({ jsx }: { jsx?: boolean }): string { */ function resetExtra(): void { extra = { - tokens: null, - range: false, - loc: false, + code: '', comment: false, comments: [], - strict: false, + createDefaultProgram: false, + errorOnTypeScriptSyntacticAndSemanticIssues: false, + errorOnUnknownASTType: false, + extraFileExtensions: [], jsx: false, - useJSXTextNode: false, + loc: false, log: console.log, // eslint-disable-line no-console + noWatch: false, + preserveNodeMaps: undefined, projects: [], - errorOnUnknownASTType: false, - errorOnTypeScriptSyntacticAndSemanticIssues: false, - code: '', + range: false, + strict: false, + tokens: null, tsconfigRootDir: process.cwd(), - extraFileExtensions: [], - preserveNodeMaps: undefined, - createDefaultProgram: false, + useJSXTextNode: false, }; } @@ -225,6 +226,8 @@ function getProgramAndAST( } function applyParserOptionsToExtra(options: TSESTreeOptions): void { + extra.noWatch = typeof options.noWatch === 'boolean' && options.noWatch; + /** * Track range information in the AST */ @@ -287,19 +290,25 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { extra.projects = options.project; } + if (typeof options.tsconfigRootDir === 'string') { + extra.tsconfigRootDir = options.tsconfigRootDir; + } + // Transform glob patterns into paths if (extra.projects) { extra.projects = extra.projects.reduce( (projects, project) => - projects.concat(isGlob(project) ? globSync(project) : project), + projects.concat( + isGlob(project) + ? globSync(project, { + cwd: extra.tsconfigRootDir || process.cwd(), + }) + : project, + ), [], ); } - if (typeof options.tsconfigRootDir === 'string') { - extra.tsconfigRootDir = options.tsconfigRootDir; - } - if ( Array.isArray(options.extraFileExtensions) && options.extraFileExtensions.every(ext => typeof ext === 'string') @@ -500,3 +509,4 @@ export function parseAndGenerateServices< export { TSESTreeOptions, ParserServices }; export * from './ts-estree'; +export { clearCaches } from './tsconfig-parser'; diff --git a/packages/typescript-estree/src/tsconfig-parser.ts b/packages/typescript-estree/src/tsconfig-parser.ts index e6ef356b6f30..4dab9030a55b 100644 --- a/packages/typescript-estree/src/tsconfig-parser.ts +++ b/packages/typescript-estree/src/tsconfig-parser.ts @@ -1,3 +1,4 @@ +import chokidar from 'chokidar'; import path from 'path'; import * as ts from 'typescript'; // leave this as * as ts so people using util package don't need syntheticDefaultImports import { Extra } from './parser-options'; @@ -30,18 +31,37 @@ const knownWatchProgramMap = new Map< * Maps file paths to their set of corresponding watch callbacks * There may be more than one per file if a file is shared between projects */ -const watchCallbackTrackingMap = new Map(); +const watchCallbackTrackingMap = new Map>(); + +/** + * Tracks the ts.sys.watchFile watchers that we've opened for config files. + * We store these so we can clean up our handles if required. + */ +const configSystemFileWatcherTrackingSet = new Set(); +/** + * Tracks the ts.sys.watchDirectory watchers that we've opened for project folders. + * We store these so we can clean up our handles if required. + */ +const directorySystemFileWatcherTrackingSet = new Set(); const parsedFilesSeen = new Set(); /** - * Clear tsconfig caches. - * Primarily used for testing. + * Clear all of the parser caches. + * This should only be used in testing to ensure the parser is clean between tests. */ export function clearCaches(): void { knownWatchProgramMap.clear(); watchCallbackTrackingMap.clear(); parsedFilesSeen.clear(); + + // stop tracking config files + configSystemFileWatcherTrackingSet.forEach(cb => cb.close()); + configSystemFileWatcherTrackingSet.clear(); + + // stop tracking folders + directorySystemFileWatcherTrackingSet.forEach(cb => cb.close()); + directorySystemFileWatcherTrackingSet.clear(); } /** @@ -62,14 +82,42 @@ function diagnosticReporter(diagnostic: ts.Diagnostic): void { ); } -const noopFileWatcher = { close: (): void => {} }; - function getTsconfigPath(tsconfigPath: string, extra: Extra): string { return path.isAbsolute(tsconfigPath) ? tsconfigPath : path.join(extra.tsconfigRootDir || process.cwd(), tsconfigPath); } +interface Watcher { + close(): void; + on(evt: 'add', listener: (file: string) => void): void; + on(evt: 'change', listener: (file: string) => void): void; +} +/** + * Watches a file or directory for changes + */ +function watch( + path: string, + options: chokidar.WatchOptions, + extra: Extra, +): Watcher { + // an escape hatch to disable the file watchers as they can take a bit to initialise in some cases + // this also supports an env variable so it's easy to switch on/off from the CLI + if (process.env.PARSER_NO_WATCH === 'true' || extra.noWatch === true) { + return { + close: (): void => {}, + on: (): void => {}, + }; + } + + return chokidar.watch(path, { + ignoreInitial: true, + persistent: false, + useFsEvents: false, + ...options, + }); +} + /** * Calculate project environments using options provided by consumer and paths from config * @param code The code being linted @@ -91,9 +139,13 @@ export function calculateProjectParserOptions( // Update file version if necessary // TODO: only update when necessary, currently marks as changed on every lint - const watchCallback = watchCallbackTrackingMap.get(filePath); - if (parsedFilesSeen.has(filePath) && typeof watchCallback !== 'undefined') { - watchCallback(filePath, ts.FileWatcherEventKind.Changed); + const watchCallbacks = watchCallbackTrackingMap.get(filePath); + if ( + parsedFilesSeen.has(filePath) && + watchCallbacks && + watchCallbacks.size > 0 + ) { + watchCallbacks.forEach(cb => cb(filePath, ts.FileWatcherEventKind.Changed)); } for (const rawTsconfigPath of extra.projects) { @@ -146,19 +198,80 @@ export function calculateProjectParserOptions( } }; - // register callbacks to trigger program updates without using fileWatchers - watchCompilerHost.watchFile = (fileName, callback): ts.FileWatcher => { + // in watch mode, eslint will give us the latest file contents + // store the watch callback so we can trigger an update with eslint's content + watchCompilerHost.watchFile = ( + fileName, + callback, + interval, + ): ts.FileWatcher => { + // specifically (and separately) watch the tsconfig file + // this allows us to react to changes in the tsconfig's include/exclude options + let watcher: Watcher | null = null; + if (fileName.includes(tsconfigPath)) { + watcher = watch( + fileName, + { + interval, + }, + extra, + ); + watcher.on('change', path => { + callback(path, ts.FileWatcherEventKind.Changed); + }); + configSystemFileWatcherTrackingSet.add(watcher); + } + const normalizedFileName = path.normalize(fileName); - watchCallbackTrackingMap.set(normalizedFileName, callback); + const watchers = ((): Set => { + let watchers = watchCallbackTrackingMap.get(normalizedFileName); + if (!watchers) { + watchers = new Set(); + watchCallbackTrackingMap.set(normalizedFileName, watchers); + } + return watchers; + })(); + watchers.add(callback); + return { close: (): void => { - watchCallbackTrackingMap.delete(normalizedFileName); + watchers.delete(callback); + + if (watcher) { + watcher.close(); + configSystemFileWatcherTrackingSet.delete(watcher); + } }, }; }; - // ensure fileWatchers aren't created for directories - watchCompilerHost.watchDirectory = (): ts.FileWatcher => noopFileWatcher; + // when new files are added in watch mode, we need to tell typescript about those files + // if we don't then typescript will act like they don't exist. + watchCompilerHost.watchDirectory = ( + dirPath, + callback, + recursive, + ): ts.FileWatcher => { + const watcher = watch( + dirPath, + { + depth: recursive ? 0 : undefined, + interval: 250, + }, + extra, + ); + watcher.on('add', path => { + callback(path); + }); + directorySystemFileWatcherTrackingSet.add(watcher); + + return { + close(): void { + watcher.close(); + directorySystemFileWatcherTrackingSet.delete(watcher); + }, + }; + }; // allow files with custom extensions to be included in program (uses internal ts api) const oldOnDirectoryStructureHostCreate = diff --git a/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts b/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts index dfa1b047f36b..59c7b8b2b554 100644 --- a/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts +++ b/packages/typescript-estree/tests/ast-alignment/fixtures-to-test.ts @@ -1,8 +1,8 @@ +import jsxKnownIssues from '@typescript-eslint/shared-fixtures/jsx-known-issues'; import fs from 'fs'; import glob from 'glob'; import path from 'path'; -import jsxKnownIssues from '../../../shared-fixtures/jsx-known-issues'; import { isJSXFileType } from '../../tools/test-utils'; interface Fixture { diff --git a/packages/typescript-estree/tests/lib/jsx.ts b/packages/typescript-estree/tests/lib/jsx.ts index a3e0148907cc..f5d3f31b27cb 100644 --- a/packages/typescript-estree/tests/lib/jsx.ts +++ b/packages/typescript-estree/tests/lib/jsx.ts @@ -1,3 +1,4 @@ +import filesWithKnownIssues from '@typescript-eslint/shared-fixtures/jsx-known-issues'; import { readFileSync } from 'fs'; import glob from 'glob'; import { TSESTreeOptions } from '../../src/parser-options'; @@ -5,7 +6,6 @@ import { createSnapshotTestBlock, formatSnapshotName, } from '../../tools/test-utils'; -import filesWithKnownIssues from '../../../shared-fixtures/jsx-known-issues'; const JSX_FIXTURES_DIR = '../../node_modules/@typescript-eslint/shared-fixtures/fixtures/jsx'; diff --git a/packages/typescript-estree/tests/lib/parse.ts b/packages/typescript-estree/tests/lib/parse.ts index 6ed8db76cbc9..38b80aafbaaf 100644 --- a/packages/typescript-estree/tests/lib/parse.ts +++ b/packages/typescript-estree/tests/lib/parse.ts @@ -81,20 +81,21 @@ describe('parse()', () => { code: 'let foo = bar;', comment: true, comments: [], + createDefaultProgram: false, errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: false, extraFileExtensions: [], jsx: false, loc: true, log: loggerFn, + noWatch: false, + preserveNodeMaps: false, projects: [], range: true, strict: false, tokens: expect.any(Array), tsconfigRootDir: expect.any(String), useJSXTextNode: false, - preserveNodeMaps: false, - createDefaultProgram: false, }, false, ); diff --git a/packages/typescript-estree/tests/lib/persistentParse.ts b/packages/typescript-estree/tests/lib/persistentParse.ts new file mode 100644 index 000000000000..c588570bf732 --- /dev/null +++ b/packages/typescript-estree/tests/lib/persistentParse.ts @@ -0,0 +1,136 @@ +import fs from 'fs'; +import path from 'path'; +import tmp from 'tmp'; +import { parseAndGenerateServices } from '../../src/parser'; +import { clearCaches } from '../../src/tsconfig-parser'; + +const tsConfigExcludeBar = { + include: ['./*.ts'], + exclude: ['./bar.ts'], +}; +const tsConfigIncludeAll = { + include: ['./*.ts'], + exclude: [], +}; +const CONTENTS = { + foo: 'console.log("foo")', + bar: 'console.log("bar")', +}; + +const tmpDirs = new Set(); +afterEach(() => { + // stop watching the files and folders + clearCaches(); + + // clean up the temporary files and folders + tmpDirs.forEach(t => t.removeCallback()); + tmpDirs.clear(); +}); + +function writeTSConfig( + dirName: string, + config: Record, +): void { + fs.writeFileSync(path.join(dirName, 'tsconfig.json'), JSON.stringify(config)); +} +function writeFile(dirName: string, file: 'foo' | 'bar'): void { + fs.writeFileSync(path.join(dirName, `${file}.ts`), CONTENTS[file]); +} + +function setup(tsconfig: Record, writeBar = true): string { + const tmpDir = tmp.dirSync({ + keep: false, + unsafeCleanup: true, + }); + tmpDirs.add(tmpDir); + + writeTSConfig(tmpDir.name, tsconfig); + + writeFile(tmpDir.name, 'foo'); + writeBar && writeFile(tmpDir.name, 'bar'); + + return tmpDir.name; +} + +function parseFile(filename: 'foo' | 'bar', tmpDir: string): void { + parseAndGenerateServices(CONTENTS.foo, { + project: './tsconfig.json', + tsconfigRootDir: tmpDir, + filePath: path.join(tmpDir, `${filename}.ts`), + }); +} + +// https://github.com/microsoft/TypeScript/blob/a4bacf3bfaf77213c1ef4ddecaf3689837e20ac5/src/compiler/sys.ts#L46-L50 +enum PollingInterval { + High = 2000, + Medium = 500, + Low = 250, +} +async function runTimer(interval: PollingInterval): Promise { + // would love to use jest fake timers, but ts stores references to the standard timeout functions + // so we can't switch to fake timers on the fly :( + await new Promise((resolve): void => { + setTimeout(resolve, interval); + }); +} +async function waitForChokidar(): Promise { + // wait for chokidar to be ready + // this isn't won't be a problem when running the eslint CLI in watch mode because the init takes a few hundred ms + await runTimer(PollingInterval.Medium); +} + +describe('persistent lint session', () => { + it('parses both files successfully when included', () => { + const PROJECT_DIR = setup(tsConfigIncludeAll); + + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); + }); + + it('parses included files, and throws on excluded files', () => { + const PROJECT_DIR = setup(tsConfigExcludeBar); + + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + }); + + it('reacts to changes in the tsconfig', async () => { + const PROJECT_DIR = setup(tsConfigExcludeBar); + + // parse once to: assert the config as correct, and to make sure the program is setup + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + + await waitForChokidar(); + + // change the config file so it now includes all files + writeTSConfig(PROJECT_DIR, tsConfigIncludeAll); + + // wait for TS to pick up the change to the config file + await runTimer(PollingInterval.High); + + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); + }); + + it('allows parsing of new files', async () => { + const PROJECT_DIR = setup(tsConfigIncludeAll, false); + + // parse once to: assert the config as correct, and to make sure the program is setup + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + // bar should throw because it doesn't exist yet + expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + + await waitForChokidar(); + + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, 'bar'); + + // wait for TS to pick up the new file + await runTimer(PollingInterval.Medium); + + // both files should parse fine now + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); + }); +}); diff --git a/packages/typescript-estree/tsconfig.build.json b/packages/typescript-estree/tsconfig.build.json index 792172fb82f6..145fd09f633b 100644 --- a/packages/typescript-estree/tsconfig.build.json +++ b/packages/typescript-estree/tsconfig.build.json @@ -1,8 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "composite": true, "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "resolveJsonModule": true }, "include": ["src"] } diff --git a/packages/typescript-estree/tsconfig.json b/packages/typescript-estree/tsconfig.json index 2ea9199d2638..7dc0e63275b4 100644 --- a/packages/typescript-estree/tsconfig.json +++ b/packages/typescript-estree/tsconfig.json @@ -1,7 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "./tsconfig.build.json", "compilerOptions": { - "outDir": "./dist" + "rootDir": ".", + "noEmit": true }, "include": ["src", "tests", "tools"], "exclude": ["tests/fixtures/**/*"] diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 000000000000..2d9938163eac --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.base.json", + "include": ["tests/**/*.ts", "tools/**/*.ts"] +} diff --git a/yarn.lock b/yarn.lock index 9f41d090fe99..8054910b5668 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1473,6 +1473,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/tmp@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd" + integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA== + "@types/yargs-parser@*": version "13.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0" @@ -1843,6 +1848,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.0.tgz#e609350e50a9313b472789b2f14ef35808ee14d6" + integrity sha512-Ozz7l4ixzI7Oxj2+cw+p0tVUt27BpaJ+1+q1TCeANWxHpvyn2+Un+YamBdfKu0uh8xLodGhoa1v7595NhKDAuA== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -2172,6 +2185,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: version "3.5.5" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" @@ -2206,7 +2224,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -2559,6 +2577,21 @@ chokidar@^2.0.2: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.0.2.tgz#0d1cd6d04eb2df0327446188cd13736a3367d681" + integrity sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA== + dependencies: + anymatch "^3.0.1" + braces "^3.0.2" + glob-parent "^5.0.0" + is-binary-path "^2.1.0" + is-glob "^4.0.1" + normalize-path "^3.0.0" + readdirp "^3.1.1" + optionalDependencies: + fsevents "^2.0.6" + chownr@^1.1.1, chownr@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" @@ -4226,6 +4259,11 @@ fsevents@^1.2.7: nan "^2.12.1" node-pre-gyp "^0.12.0" +fsevents@^2.0.6: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a" + integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4907,6 +4945,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -7253,7 +7298,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.5: +picomatch@^2.0.4, picomatch@^2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== @@ -7702,6 +7747,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.2.tgz#fa85d2d14d4289920e4671dead96431add2ee78a" + integrity sha512-8rhl0xs2cxfVsqzreYCvs8EwBfn/DhVdqtoLmw19uI3SC5avYX9teCurlErfpPXGmYtMHReGaP2RsLnFvz/lnw== + dependencies: + picomatch "^2.0.4" + realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -8719,6 +8771,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" + integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== + dependencies: + rimraf "^2.6.3" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"