• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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