diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index 9223107986af..38bd209ff3f2 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -1,6 +1,5 @@ import debug from 'debug'; import fs from 'fs'; -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'; import { WatchCompilerHostOfConfigFile } from './WatchCompilerHostOfConfigFile'; @@ -67,7 +66,7 @@ function saveWatchCallback( fileName: string, callback: ts.FileWatcherCallback, ): ts.FileWatcher => { - const normalizedFileName = getCanonicalFileName(path.normalize(fileName)); + const normalizedFileName = getCanonicalFileName(fileName); const watchers = ((): Set => { let watchers = trackingMap.get(normalizedFileName); if (!watchers) { @@ -246,8 +245,7 @@ function createWatchProgram( watchCompilerHost.readFile = (filePathIn, encoding): string | undefined => { const filePath = getCanonicalFileName(filePathIn); const fileContent = - path.normalize(filePath) === - path.normalize(currentLintOperationState.filePath) + filePath === currentLintOperationState.filePath ? currentLintOperationState.code : oldReadFile(filePath, encoding); if (fileContent) { diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index 6509997094f9..a19bef274139 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -20,14 +20,21 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { // This narrows the type so we can be sure we're passing canonical names in the correct places type CanonicalPath = string & { __brand: unknown }; + // typescript doesn't provide a ts.sys implementation for browser environments const useCaseSensitiveFileNames = ts.sys !== undefined ? ts.sys.useCaseSensitiveFileNames : true; -const getCanonicalFileName = useCaseSensitiveFileNames - ? (filePath: string): CanonicalPath => - path.normalize(filePath) as CanonicalPath - : (filePath: string): CanonicalPath => - path.normalize(filePath).toLowerCase() as CanonicalPath; +const correctPathCasing = useCaseSensitiveFileNames + ? (filePath: string): string => filePath + : (filePath: string): string => filePath.toLowerCase(); + +function getCanonicalFileName(filePath: string): CanonicalPath { + let normalized = path.normalize(filePath); + if (normalized.endsWith('/')) { + normalized = normalized.substr(0, normalized.length - 1); + } + return correctPathCasing(normalized) as CanonicalPath; +} function getTsconfigPath(tsconfigPath: string, extra: Extra): CanonicalPath { return getCanonicalFileName( diff --git a/packages/typescript-estree/tests/lib/persistentParse.ts b/packages/typescript-estree/tests/lib/persistentParse.ts index e1cb8b9f9b76..502245977bdc 100644 --- a/packages/typescript-estree/tests/lib/persistentParse.ts +++ b/packages/typescript-estree/tests/lib/persistentParse.ts @@ -3,14 +3,6 @@ import path from 'path'; import tmp from 'tmp'; import { clearCaches, parseAndGenerateServices } from '../../src/parser'; -const tsConfigExcludeBar = { - include: ['src'], - exclude: ['./src/bar.ts'], -}; -const tsConfigIncludeAll = { - include: ['src'], - exclude: [], -}; const CONTENTS = { foo: 'console.log("foo")', bar: 'console.log("bar")', @@ -69,7 +61,10 @@ function parseFile(filename: 'foo' | 'bar' | 'baz/bar', tmpDir: string): void { }); } -describe('persistent lint session', () => { +function baseTests( + tsConfigExcludeBar: Record, + tsConfigIncludeAll: Record, +): void { it('parses both files successfully when included', () => { const PROJECT_DIR = setup(tsConfigIncludeAll); @@ -175,4 +170,48 @@ describe('persistent lint session', () => { }); // TODO - support the complex monorepo case with a tsconfig with no include/exclude +} + +describe('persistent parse', () => { + describe('includes not ending in a slash', () => { + const tsConfigExcludeBar = { + include: ['src'], + exclude: ['./src/bar.ts'], + }; + const tsConfigIncludeAll = { + include: ['src'], + exclude: [], + }; + + baseTests(tsConfigExcludeBar, tsConfigIncludeAll); + }); + + /* + If the includes ends in a slash, typescript will ask for watchers ending in a slash. + These tests ensure the normalisation code works as expected in this case. + */ + describe('includes ending in a slash', () => { + const tsConfigExcludeBar = { + include: ['src/'], + exclude: ['./src/bar.ts'], + }; + const tsConfigIncludeAll = { + include: ['src/'], + exclude: [], + }; + + baseTests(tsConfigExcludeBar, tsConfigIncludeAll); + }); + + /* + If there is no includes, then typescript will ask for a slightly different set of watchers. + */ + describe('tsconfig with no includes / files', () => { + const tsConfigExcludeBar = { + exclude: ['./src/bar.ts'], + }; + const tsConfigIncludeAll = {}; + + baseTests(tsConfigExcludeBar, tsConfigIncludeAll); + }); });