Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
epidemian
GitHub Repository: epidemian/eslint-plugin-import
Path: blob/main/tests/src/core/getExports.js
829 views
1
import { expect } from 'chai';
2
import semver from 'semver';
3
import sinon from 'sinon';
4
import eslintPkg from 'eslint/package.json';
5
import * as tsConfigLoader from 'tsconfig-paths/lib/tsconfig-loader';
6
import ExportMap from '../../../src/ExportMap';
7
8
import * as fs from 'fs';
9
10
import { getFilename } from '../utils';
11
import { test as testUnambiguous } from 'eslint-module-utils/unambiguous';
12
13
describe('ExportMap', function () {
14
const fakeContext = Object.assign(
15
semver.satisfies(eslintPkg.version, '>= 7.28') ? {
16
getFilename() { throw new Error('Should call getPhysicalFilename() instead of getFilename()'); },
17
getPhysicalFilename: getFilename,
18
} : {
19
getFilename,
20
},
21
{
22
settings: {},
23
parserPath: 'babel-eslint',
24
},
25
);
26
27
it('handles ExportAllDeclaration', function () {
28
let imports;
29
expect(function () {
30
imports = ExportMap.get('./export-all', fakeContext);
31
}).not.to.throw(Error);
32
33
expect(imports).to.exist;
34
expect(imports.has('foo')).to.be.true;
35
36
});
37
38
it('returns a cached copy on subsequent requests', function () {
39
expect(ExportMap.get('./named-exports', fakeContext))
40
.to.exist.and.equal(ExportMap.get('./named-exports', fakeContext));
41
});
42
43
it('does not return a cached copy after modification', (done) => {
44
const firstAccess = ExportMap.get('./mutator', fakeContext);
45
expect(firstAccess).to.exist;
46
47
// mutate (update modified time)
48
const newDate = new Date();
49
fs.utimes(getFilename('mutator.js'), newDate, newDate, (error) => {
50
expect(error).not.to.exist;
51
expect(ExportMap.get('./mutator', fakeContext)).not.to.equal(firstAccess);
52
done();
53
});
54
});
55
56
it('does not return a cached copy with different settings', () => {
57
const firstAccess = ExportMap.get('./named-exports', fakeContext);
58
expect(firstAccess).to.exist;
59
60
const differentSettings = Object.assign(
61
{},
62
fakeContext,
63
{ parserPath: 'espree' },
64
);
65
66
expect(ExportMap.get('./named-exports', differentSettings))
67
.to.exist.and
68
.not.to.equal(firstAccess);
69
});
70
71
it('does not throw for a missing file', function () {
72
let imports;
73
expect(function () {
74
imports = ExportMap.get('./does-not-exist', fakeContext);
75
}).not.to.throw(Error);
76
77
expect(imports).not.to.exist;
78
79
});
80
81
it('exports explicit names for a missing file in exports', function () {
82
let imports;
83
expect(function () {
84
imports = ExportMap.get('./exports-missing', fakeContext);
85
}).not.to.throw(Error);
86
87
expect(imports).to.exist;
88
expect(imports.has('bar')).to.be.true;
89
90
});
91
92
it('finds exports for an ES7 module with babel-eslint', function () {
93
const path = getFilename('jsx/FooES7.js');
94
const contents = fs.readFileSync(path, { encoding: 'utf8' });
95
const imports = ExportMap.parse(
96
path,
97
contents,
98
{ parserPath: 'babel-eslint', settings: {} },
99
);
100
101
expect(imports, 'imports').to.exist;
102
expect(imports.errors).to.be.empty;
103
expect(imports.get('default'), 'default export').to.exist;
104
expect(imports.has('Bar')).to.be.true;
105
});
106
107
context('deprecation metadata', function () {
108
109
function jsdocTests(parseContext, lineEnding) {
110
context('deprecated imports', function () {
111
let imports;
112
before('parse file', function () {
113
const path = getFilename('deprecated.js');
114
const contents = fs.readFileSync(path, { encoding: 'utf8' }).replace(/[\r]\n/g, lineEnding);
115
imports = ExportMap.parse(path, contents, parseContext);
116
117
// sanity checks
118
expect(imports.errors).to.be.empty;
119
});
120
121
it('works with named imports.', function () {
122
expect(imports.has('fn')).to.be.true;
123
124
expect(imports.get('fn'))
125
.to.have.nested.property('doc.tags[0].title', 'deprecated');
126
expect(imports.get('fn'))
127
.to.have.nested.property('doc.tags[0].description', 'please use \'x\' instead.');
128
});
129
130
it('works with default imports.', function () {
131
expect(imports.has('default')).to.be.true;
132
const importMeta = imports.get('default');
133
134
expect(importMeta).to.have.nested.property('doc.tags[0].title', 'deprecated');
135
expect(importMeta).to.have.nested.property('doc.tags[0].description', 'this is awful, use NotAsBadClass.');
136
});
137
138
it('works with variables.', function () {
139
expect(imports.has('MY_TERRIBLE_ACTION')).to.be.true;
140
const importMeta = imports.get('MY_TERRIBLE_ACTION');
141
142
expect(importMeta).to.have.nested.property(
143
'doc.tags[0].title', 'deprecated');
144
expect(importMeta).to.have.nested.property(
145
'doc.tags[0].description', 'please stop sending/handling this action type.');
146
});
147
148
context('multi-line variables', function () {
149
it('works for the first one', function () {
150
expect(imports.has('CHAIN_A')).to.be.true;
151
const importMeta = imports.get('CHAIN_A');
152
153
expect(importMeta).to.have.nested.property(
154
'doc.tags[0].title', 'deprecated');
155
expect(importMeta).to.have.nested.property(
156
'doc.tags[0].description', 'this chain is awful');
157
});
158
it('works for the second one', function () {
159
expect(imports.has('CHAIN_B')).to.be.true;
160
const importMeta = imports.get('CHAIN_B');
161
162
expect(importMeta).to.have.nested.property(
163
'doc.tags[0].title', 'deprecated');
164
expect(importMeta).to.have.nested.property(
165
'doc.tags[0].description', 'so awful');
166
});
167
it('works for the third one, etc.', function () {
168
expect(imports.has('CHAIN_C')).to.be.true;
169
const importMeta = imports.get('CHAIN_C');
170
171
expect(importMeta).to.have.nested.property(
172
'doc.tags[0].title', 'deprecated');
173
expect(importMeta).to.have.nested.property(
174
'doc.tags[0].description', 'still terrible');
175
});
176
});
177
});
178
179
context('full module', function () {
180
let imports;
181
before('parse file', function () {
182
const path = getFilename('deprecated-file.js');
183
const contents = fs.readFileSync(path, { encoding: 'utf8' });
184
imports = ExportMap.parse(path, contents, parseContext);
185
186
// sanity checks
187
expect(imports.errors).to.be.empty;
188
});
189
190
it('has JSDoc metadata', function () {
191
expect(imports.doc).to.exist;
192
});
193
});
194
}
195
196
context('default parser', function () {
197
jsdocTests({
198
parserPath: 'espree',
199
parserOptions: {
200
ecmaVersion: 2015,
201
sourceType: 'module',
202
attachComment: true,
203
},
204
settings: {},
205
}, '\n');
206
jsdocTests({
207
parserPath: 'espree',
208
parserOptions: {
209
ecmaVersion: 2015,
210
sourceType: 'module',
211
attachComment: true,
212
},
213
settings: {},
214
}, '\r\n');
215
});
216
217
context('babel-eslint', function () {
218
jsdocTests({
219
parserPath: 'babel-eslint',
220
parserOptions: {
221
ecmaVersion: 2015,
222
sourceType: 'module',
223
attachComment: true,
224
},
225
settings: {},
226
}, '\n');
227
jsdocTests({
228
parserPath: 'babel-eslint',
229
parserOptions: {
230
ecmaVersion: 2015,
231
sourceType: 'module',
232
attachComment: true,
233
},
234
settings: {},
235
}, '\r\n');
236
});
237
});
238
239
context('exported static namespaces', function () {
240
const espreeContext = { parserPath: 'espree', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {} };
241
const babelContext = { parserPath: 'babel-eslint', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {} };
242
243
it('works with espree & traditional namespace exports', function () {
244
const path = getFilename('deep/a.js');
245
const contents = fs.readFileSync(path, { encoding: 'utf8' });
246
const a = ExportMap.parse(path, contents, espreeContext);
247
expect(a.errors).to.be.empty;
248
expect(a.get('b').namespace).to.exist;
249
expect(a.get('b').namespace.has('c')).to.be.true;
250
});
251
252
it('captures namespace exported as default', function () {
253
const path = getFilename('deep/default.js');
254
const contents = fs.readFileSync(path, { encoding: 'utf8' });
255
const def = ExportMap.parse(path, contents, espreeContext);
256
expect(def.errors).to.be.empty;
257
expect(def.get('default').namespace).to.exist;
258
expect(def.get('default').namespace.has('c')).to.be.true;
259
});
260
261
it('works with babel-eslint & ES7 namespace exports', function () {
262
const path = getFilename('deep-es7/a.js');
263
const contents = fs.readFileSync(path, { encoding: 'utf8' });
264
const a = ExportMap.parse(path, contents, babelContext);
265
expect(a.errors).to.be.empty;
266
expect(a.get('b').namespace).to.exist;
267
expect(a.get('b').namespace.has('c')).to.be.true;
268
});
269
});
270
271
context('deep namespace caching', function () {
272
const espreeContext = { parserPath: 'espree', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {} };
273
let a;
274
before('sanity check and prime cache', function (done) {
275
// first version
276
fs.writeFileSync(getFilename('deep/cache-2.js'),
277
fs.readFileSync(getFilename('deep/cache-2a.js')));
278
279
const path = getFilename('deep/cache-1.js');
280
const contents = fs.readFileSync(path, { encoding: 'utf8' });
281
a = ExportMap.parse(path, contents, espreeContext);
282
expect(a.errors).to.be.empty;
283
284
expect(a.get('b').namespace).to.exist;
285
expect(a.get('b').namespace.has('c')).to.be.true;
286
287
// wait ~1s, cache check is 1s resolution
288
setTimeout(function reup() {
289
fs.unlinkSync(getFilename('deep/cache-2.js'));
290
// swap in a new file and touch it
291
fs.writeFileSync(getFilename('deep/cache-2.js'),
292
fs.readFileSync(getFilename('deep/cache-2b.js')));
293
done();
294
}, 1100);
295
});
296
297
it('works', function () {
298
expect(a.get('b').namespace.has('c')).to.be.false;
299
});
300
301
after('remove test file', (done) => fs.unlink(getFilename('deep/cache-2.js'), done));
302
});
303
304
context('Map API', function () {
305
context('#size', function () {
306
307
it('counts the names', () => expect(ExportMap.get('./named-exports', fakeContext))
308
.to.have.property('size', 12));
309
310
it('includes exported namespace size', () => expect(ExportMap.get('./export-all', fakeContext))
311
.to.have.property('size', 1));
312
313
});
314
});
315
316
context('issue #210: self-reference', function () {
317
it(`doesn't crash`, function () {
318
expect(() => ExportMap.get('./narcissist', fakeContext)).not.to.throw(Error);
319
});
320
it(`'has' circular reference`, function () {
321
expect(ExportMap.get('./narcissist', fakeContext))
322
.to.exist.and.satisfy(m => m.has('soGreat'));
323
});
324
it(`can 'get' circular reference`, function () {
325
expect(ExportMap.get('./narcissist', fakeContext))
326
.to.exist.and.satisfy(m => m.get('soGreat') != null);
327
});
328
});
329
330
context('issue #478: never parse non-whitelist extensions', function () {
331
const context = Object.assign({}, fakeContext,
332
{ settings: { 'import/extensions': ['.js'] } });
333
334
let imports;
335
before('load imports', function () {
336
imports = ExportMap.get('./typescript.ts', context);
337
});
338
339
it('returns nothing for a TypeScript file', function () {
340
expect(imports).not.to.exist;
341
});
342
343
});
344
345
context('alternate parsers', function () {
346
347
const configs = [
348
// ['string form', { 'typescript-eslint-parser': '.ts' }],
349
];
350
351
if (semver.satisfies(eslintPkg.version, '>5')) {
352
configs.push(['array form', { '@typescript-eslint/parser': ['.ts', '.tsx'] }]);
353
}
354
355
if (semver.satisfies(eslintPkg.version, '<6')) {
356
configs.push(['array form', { 'typescript-eslint-parser': ['.ts', '.tsx'] }]);
357
}
358
359
configs.forEach(([description, parserConfig]) => {
360
361
describe(description, function () {
362
const context = Object.assign({}, fakeContext,
363
{ settings: {
364
'import/extensions': ['.js'],
365
'import/parsers': parserConfig,
366
} });
367
368
let imports;
369
before('load imports', function () {
370
this.timeout(20000); // takes a long time :shrug:
371
sinon.spy(tsConfigLoader, 'tsConfigLoader');
372
imports = ExportMap.get('./typescript.ts', context);
373
});
374
after('clear spies', function () {
375
tsConfigLoader.tsConfigLoader.restore();
376
});
377
378
it('returns something for a TypeScript file', function () {
379
expect(imports).to.exist;
380
});
381
382
it('has no parse errors', function () {
383
expect(imports).property('errors').to.be.empty;
384
});
385
386
it('has exported function', function () {
387
expect(imports.has('getFoo')).to.be.true;
388
});
389
390
it('has exported typedef', function () {
391
expect(imports.has('MyType')).to.be.true;
392
});
393
394
it('has exported enum', function () {
395
expect(imports.has('MyEnum')).to.be.true;
396
});
397
398
it('has exported interface', function () {
399
expect(imports.has('Foo')).to.be.true;
400
});
401
402
it('has exported abstract class', function () {
403
expect(imports.has('Bar')).to.be.true;
404
});
405
406
it('should cache tsconfig until tsconfigRootDir parser option changes', function () {
407
const customContext = Object.assign(
408
{},
409
context,
410
{
411
parserOptions: {
412
tsconfigRootDir: null,
413
},
414
},
415
);
416
expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(0);
417
ExportMap.parse('./baz.ts', 'export const baz = 5', customContext);
418
expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1);
419
ExportMap.parse('./baz.ts', 'export const baz = 5', customContext);
420
expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1);
421
422
const differentContext = Object.assign(
423
{},
424
context,
425
{
426
parserOptions: {
427
tsconfigRootDir: process.cwd(),
428
},
429
},
430
);
431
432
ExportMap.parse('./baz.ts', 'export const baz = 5', differentContext);
433
expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(2);
434
});
435
});
436
});
437
});
438
439
// todo: move to utils
440
describe('unambiguous regex', function () {
441
const testFiles = [
442
['deep/b.js', true],
443
['bar.js', true],
444
['deep-es7/b.js', true],
445
['common.js', false],
446
];
447
448
for (const [testFile, expectedRegexResult] of testFiles) {
449
it(`works for ${testFile} (${expectedRegexResult})`, function () {
450
const content = fs.readFileSync('./tests/files/' + testFile, 'utf8');
451
expect(testUnambiguous(content)).to.equal(expectedRegexResult);
452
});
453
}
454
});
455
});
456
457