1import fs from 'fs'; 2import path from 'path'; 3import tmp from 'tmp'; 4import { clearCaches, parseAndGenerateServices } from '../../src'; 5 6const CONTENTS = { 7 foo: 'console.log("foo")', 8 bar: 'console.log("bar")', 9 'baz/bar': 'console.log("baz bar")', 10 'bat/baz/bar': 'console.log("bat/baz/bar")', 11 number: 'const foo = 1;', 12 object: '(() => { })();', 13 string: 'let a: "a" | "b";', 14}; 15 16const cwdCopy = process.cwd(); 17const tmpDirs = new Set<tmp.DirResult>(); 18afterEach(() => { 19 // stop watching the files and folders 20 clearCaches(); 21 22 // clean up the temporary files and folders 23 tmpDirs.forEach(t => t.removeCallback()); 24 tmpDirs.clear(); 25 26 // restore original cwd 27 process.chdir(cwdCopy); 28}); 29 30function writeTSConfig(dirName: string, config: Record<string, unknown>): void { 31 fs.writeFileSync(path.join(dirName, 'tsconfig.json'), JSON.stringify(config)); 32} 33function writeFile(dirName: string, file: keyof typeof CONTENTS): void { 34 fs.writeFileSync(path.join(dirName, 'src', `${file}.ts`), CONTENTS[file]); 35} 36function renameFile(dirName: string, src: 'bar', dest: 'baz/bar'): void { 37 fs.renameSync( 38 path.join(dirName, 'src', `${src}.ts`), 39 path.join(dirName, 'src', `${dest}.ts`), 40 ); 41} 42 43function createTmpDir(): tmp.DirResult { 44 const tmpDir = tmp.dirSync({ 45 keep: false, 46 unsafeCleanup: true, 47 }); 48 tmpDirs.add(tmpDir); 49 return tmpDir; 50} 51function setup(tsconfig: Record<string, unknown>, writeBar = true): string { 52 const tmpDir = createTmpDir(); 53 54 writeTSConfig(tmpDir.name, tsconfig); 55 56 fs.mkdirSync(path.join(tmpDir.name, 'src')); 57 fs.mkdirSync(path.join(tmpDir.name, 'src', 'baz')); 58 writeFile(tmpDir.name, 'foo'); 59 writeBar && writeFile(tmpDir.name, 'bar'); 60 61 return tmpDir.name; 62} 63 64function parseFile( 65 filename: keyof typeof CONTENTS, 66 tmpDir: string, 67 relative?: boolean, 68 ignoreTsconfigRootDir?: boolean, 69): void { 70 parseAndGenerateServices(CONTENTS[filename], { 71 project: './tsconfig.json', 72 tsconfigRootDir: ignoreTsconfigRootDir ? undefined : tmpDir, 73 filePath: relative 74 ? path.join('src', `${filename}.ts`) 75 : path.join(tmpDir, 'src', `${filename}.ts`), 76 }); 77} 78 79function existsSync(filename: keyof typeof CONTENTS, tmpDir = ''): boolean { 80 return fs.existsSync(path.join(tmpDir, 'src', `${filename}.ts`)); 81} 82 83function baseTests( 84 tsConfigExcludeBar: Record<string, unknown>, 85 tsConfigIncludeAll: Record<string, unknown>, 86): void { 87 it('parses both files successfully when included', () => { 88 const PROJECT_DIR = setup(tsConfigIncludeAll); 89 90 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 91 expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); 92 }); 93 94 it('parses included files, and throws on excluded files', () => { 95 const PROJECT_DIR = setup(tsConfigExcludeBar); 96 97 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 98 expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); 99 }); 100 101 it('allows parsing of new files', () => { 102 const PROJECT_DIR = setup(tsConfigIncludeAll, false); 103 104 // parse once to: assert the config as correct, and to make sure the program is setup 105 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 106 // bar should throw because it doesn't exist yet 107 expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); 108 109 // write a new file and attempt to parse it 110 writeFile(PROJECT_DIR, 'bar'); 111 112 // both files should parse fine now 113 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 114 expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); 115 }); 116 117 it('allows parsing of deeply nested new files', () => { 118 const PROJECT_DIR = setup(tsConfigIncludeAll, false); 119 const bazSlashBar = path.join('baz', 'bar') as 'baz/bar'; 120 121 // parse once to: assert the config as correct, and to make sure the program is setup 122 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 123 // bar should throw because it doesn't exist yet 124 expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); 125 126 // write a new file and attempt to parse it 127 writeFile(PROJECT_DIR, bazSlashBar); 128 129 // both files should parse fine now 130 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 131 expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); 132 }); 133 134 it('allows parsing of deeply nested new files in new folder', () => { 135 const PROJECT_DIR = setup(tsConfigIncludeAll); 136 137 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 138 139 // Create deep folder structure after first parse (this is important step) 140 // context: https://github.com/typescript-eslint/typescript-eslint/issues/1394 141 fs.mkdirSync(path.join(PROJECT_DIR, 'src', 'bat')); 142 fs.mkdirSync(path.join(PROJECT_DIR, 'src', 'bat', 'baz')); 143 144 const bazSlashBar = path.join('bat', 'baz', 'bar') as 'bat/baz/bar'; 145 146 // write a new file and attempt to parse it 147 writeFile(PROJECT_DIR, bazSlashBar); 148 149 expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); 150 }); 151 152 it('allows renaming of files', () => { 153 const PROJECT_DIR = setup(tsConfigIncludeAll, true); 154 const bazSlashBar = path.join('baz', 'bar') as 'baz/bar'; 155 156 // parse once to: assert the config as correct, and to make sure the program is setup 157 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 158 // bar should throw because it doesn't exist yet 159 expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); 160 161 // write a new file and attempt to parse it 162 renameFile(PROJECT_DIR, 'bar', bazSlashBar); 163 164 // both files should parse fine now 165 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 166 expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); 167 }); 168 169 it('reacts to changes in the tsconfig', () => { 170 const PROJECT_DIR = setup(tsConfigExcludeBar); 171 172 // parse once to: assert the config as correct, and to make sure the program is setup 173 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 174 expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); 175 176 // change the config file so it now includes all files 177 writeTSConfig(PROJECT_DIR, tsConfigIncludeAll); 178 179 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 180 expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); 181 }); 182 183 it('should work with relative paths', () => { 184 const PROJECT_DIR = setup(tsConfigIncludeAll, false); 185 186 // parse once to: assert the config as correct, and to make sure the program is setup 187 expect(() => parseFile('foo', PROJECT_DIR, true)).not.toThrow(); 188 // bar should throw because it doesn't exist yet 189 expect(() => parseFile('bar', PROJECT_DIR, true)).toThrow(); 190 191 // write a new file and attempt to parse it 192 writeFile(PROJECT_DIR, 'bar'); 193 194 // make sure that file is correctly created 195 expect(existsSync('bar', PROJECT_DIR)).toEqual(true); 196 197 // both files should parse fine now 198 expect(() => parseFile('foo', PROJECT_DIR, true)).not.toThrow(); 199 expect(() => parseFile('bar', PROJECT_DIR, true)).not.toThrow(); 200 }); 201 202 it('should work with relative paths without tsconfig root', () => { 203 const PROJECT_DIR = setup(tsConfigIncludeAll, false); 204 process.chdir(PROJECT_DIR); 205 206 // parse once to: assert the config as correct, and to make sure the program is setup 207 expect(() => parseFile('foo', PROJECT_DIR, true, true)).not.toThrow(); 208 // bar should throw because it doesn't exist yet 209 expect(() => parseFile('bar', PROJECT_DIR, true, true)).toThrow(); 210 211 // write a new file and attempt to parse it 212 writeFile(PROJECT_DIR, 'bar'); 213 214 // make sure that file is correctly created 215 expect(existsSync('bar')).toEqual(true); 216 expect(existsSync('bar', PROJECT_DIR)).toEqual(true); 217 218 // both files should parse fine now 219 expect(() => parseFile('foo', PROJECT_DIR, true, true)).not.toThrow(); 220 expect(() => parseFile('bar', PROJECT_DIR, true, true)).not.toThrow(); 221 }); 222} 223 224describe('persistent parse', () => { 225 describe('includes not ending in a slash', () => { 226 const tsConfigExcludeBar = { 227 include: ['src'], 228 exclude: ['./src/bar.ts'], 229 }; 230 const tsConfigIncludeAll = { 231 include: ['src'], 232 exclude: [], 233 }; 234 235 baseTests(tsConfigExcludeBar, tsConfigIncludeAll); 236 }); 237 238 /* 239 If the includes ends in a slash, typescript will ask for watchers ending in a slash. 240 These tests ensure the normalization of code works as expected in this case. 241 */ 242 describe('includes ending in a slash', () => { 243 const tsConfigExcludeBar = { 244 include: ['src/'], 245 exclude: ['./src/bar.ts'], 246 }; 247 const tsConfigIncludeAll = { 248 include: ['src/'], 249 exclude: [], 250 }; 251 252 baseTests(tsConfigExcludeBar, tsConfigIncludeAll); 253 }); 254 255 /* 256 If there is no includes, then typescript will ask for a slightly different set of watchers. 257 */ 258 describe('tsconfig with no includes / files', () => { 259 const tsConfigExcludeBar = { 260 exclude: ['./src/bar.ts'], 261 }; 262 const tsConfigIncludeAll = {}; 263 264 baseTests(tsConfigExcludeBar, tsConfigIncludeAll); 265 266 it('handles tsconfigs with no includes/excludes (single level)', () => { 267 const PROJECT_DIR = setup({}, false); 268 269 // parse once to: assert the config as correct, and to make sure the program is setup 270 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 271 expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); 272 273 // write a new file and attempt to parse it 274 writeFile(PROJECT_DIR, 'bar'); 275 276 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 277 expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); 278 }); 279 280 it('handles tsconfigs with no includes/excludes (nested)', () => { 281 const PROJECT_DIR = setup({}, false); 282 const bazSlashBar = path.join('baz', 'bar') as 'baz/bar'; 283 284 // parse once to: assert the config as correct, and to make sure the program is setup 285 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 286 expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); 287 288 // write a new file and attempt to parse it 289 writeFile(PROJECT_DIR, bazSlashBar); 290 291 expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); 292 expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); 293 }); 294 }); 295 296 /* 297 If there is no includes, then typescript will ask for a slightly different set of watchers. 298 */ 299 describe('tsconfig with overlapping globs', () => { 300 const tsConfigExcludeBar = { 301 include: ['./*', './**/*', './src/**/*'], 302 exclude: ['./src/bar.ts'], 303 }; 304 const tsConfigIncludeAll = { 305 include: ['./*', './**/*', './src/**/*'], 306 }; 307 308 baseTests(tsConfigExcludeBar, tsConfigIncludeAll); 309 }); 310 311 describe('tsconfig with module set', () => { 312 const moduleTypes = [ 313 'None', 314 'CommonJS', 315 'AMD', 316 'System', 317 'UMD', 318 'ES6', 319 'ES2015', 320 'ESNext', 321 ]; 322 323 for (const module of moduleTypes) { 324 describe(`module ${module}`, () => { 325 const tsConfigIncludeAll = { 326 compilerOptions: { module }, 327 include: ['./**/*'], 328 }; 329 330 const testNames = ['object', 'number', 'string', 'foo'] as const; 331 for (const name of testNames) { 332 it(`first parse of ${name} should not throw`, () => { 333 const PROJECT_DIR = setup(tsConfigIncludeAll); 334 writeFile(PROJECT_DIR, name); 335 expect(() => parseFile(name, PROJECT_DIR)).not.toThrow(); 336 }); 337 } 338 }); 339 } 340 }); 341}); 342