Skip to content

fix(typescript-estree): replace fast-glob with tinyglobby #10544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
"@types/babel__code-frame": "^7.0.6",
"@types/babel__core": "^7.20.5",
"@types/debug": "^4.1.12",
"@types/is-glob": "^4.0.4",
"@types/jest": "29.5.13",
"@types/jest-specific-snapshot": "^0.5.9",
"@types/natural-compare": "^1.4.3",
Expand Down
3 changes: 1 addition & 2 deletions packages/typescript-estree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,9 @@
"@typescript-eslint/types": "8.18.1",
"@typescript-eslint/visitor-keys": "8.18.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"tinyglobby": "^0.2.10",
"ts-api-utils": "^1.3.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import debug from 'debug';
import { sync as globSync } from 'fast-glob';
import isGlob from 'is-glob';
import { globSync, isDynamicPattern } from 'tinyglobby';

import type { CanonicalPath } from '../create-program/shared';
import type { TSESTreeOptions } from '../parser-options';
Expand Down Expand Up @@ -59,8 +58,7 @@ export function resolveProjectList(
options.projectFolderIgnoreList ?? ['**/node_modules/**']
)
.filter(folder => typeof folder === 'string')
// prefix with a ! for not match glob
.map(folder => (folder.startsWith('!') ? folder : `!${folder}`));
.map(folder => (folder.startsWith('!') ? folder.slice(1) : folder));

const cacheKey = getHash({
project: sanitizedProjects,
Expand All @@ -86,15 +84,16 @@ export function resolveProjectList(
}

// Transform glob patterns into paths
const nonGlobProjects = sanitizedProjects.filter(project => !isGlob(project));
const globProjects = sanitizedProjects.filter(project => isGlob(project));
const nonGlobProjects = sanitizedProjects.filter(
project => !isDynamicPattern(project),
);
const globProjects = sanitizedProjects.filter(project =>
isDynamicPattern(project),
);

let globProjectPaths: string[] = [];

if (globProjects.length > 0) {
// Although fast-glob supports multiple patterns, fast-glob returns arbitrary order of results
// to improve performance. To ensure the order is correct, we need to call fast-glob for each pattern
// separately and then concatenate the results in patterns' order.
globProjectPaths = globProjects.flatMap(pattern =>
globSync(pattern, {
cwd: options.tsconfigRootDir,
Expand Down
46 changes: 21 additions & 25 deletions packages/typescript-estree/tests/lib/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { CacheDurationSeconds } from '@typescript-eslint/types';
import type * as typescriptModule from 'typescript';

import debug from 'debug';
import * as fastGlobModule from 'fast-glob';
import { join, resolve } from 'node:path';
import * as tinyglobby from 'tinyglobby';

import type { TSESTreeOptions } from '../../src/parser-options';

Expand Down Expand Up @@ -40,11 +40,11 @@ jest.mock('typescript', () => {
};
});

jest.mock('fast-glob', () => {
const fastGlob = jest.requireActual<typeof fastGlobModule>('fast-glob');
jest.mock('tinyglobby', () => {
const tg = jest.requireActual<typeof tinyglobby>('tinyglobby');
return {
...fastGlob,
sync: jest.fn(fastGlob.sync),
...tg,
globSync: jest.fn(tg.globSync),
};
});

Expand All @@ -53,7 +53,7 @@ const hrtimeSpy = jest.spyOn(process, 'hrtime');
const createDefaultCompilerOptionsFromExtra = jest.mocked(
sharedParserUtilsModule.createDefaultCompilerOptionsFromExtra,
);
const fastGlobSyncMock = jest.mocked(fastGlobModule.sync);
const globSyncMock = jest.mocked(tinyglobby.globSync);

/**
* Aligns paths between environments, node for windows uses `\`, for linux and mac uses `/`
Expand Down Expand Up @@ -815,11 +815,11 @@ describe('parseAndGenerateServices', () => {
describe('cacheLifetime', () => {
describe('glob', () => {
const project = ['./**/tsconfig.json', './**/tsconfig.extra.json'];
// fast-glob returns arbitrary order of results to improve performance.
// `resolveProjectList()` calls fast-glob for each pattern to ensure the
// tinyglobby returns arbitrary order of results to improve performance.
// `resolveProjectList()` calls tinyglobby for each pattern to ensure the
// order is correct.
// Thus the expected call time of spy is the number of patterns.
const expectFastGlobCalls = project.length;
const expectTinyglobbyCalls = project.length;
function doParse(lifetime: CacheDurationSeconds): void {
parser.parseAndGenerateServices('const x = 1', {
cacheLifetime: {
Expand All @@ -834,50 +834,46 @@ describe('parseAndGenerateServices', () => {

it('should cache globs if the lifetime is non-zero', () => {
doParse(30);
expect(fastGlobSyncMock).toHaveBeenCalledTimes(expectFastGlobCalls);
expect(globSyncMock).toHaveBeenCalledTimes(expectTinyglobbyCalls);
doParse(30);
// shouldn't call fast-glob again due to the caching
expect(fastGlobSyncMock).toHaveBeenCalledTimes(expectFastGlobCalls);
// shouldn't call tinyglobby again due to the caching
expect(globSyncMock).toHaveBeenCalledTimes(expectTinyglobbyCalls);
});

it('should not cache globs if the lifetime is zero', () => {
doParse(0);
expect(fastGlobSyncMock).toHaveBeenCalledTimes(expectFastGlobCalls);
expect(globSyncMock).toHaveBeenCalledTimes(expectTinyglobbyCalls);
doParse(0);
// should call fast-glob again because we specified immediate cache expiry
expect(fastGlobSyncMock).toHaveBeenCalledTimes(
expectFastGlobCalls * 2,
);
// should call tinyglobby again because we specified immediate cache expiry
expect(globSyncMock).toHaveBeenCalledTimes(expectTinyglobbyCalls * 2);
});

it('should evict the cache if the entry expires', () => {
hrtimeSpy.mockReturnValueOnce([1, 0]);

doParse(30);
expect(fastGlobSyncMock).toHaveBeenCalledTimes(expectFastGlobCalls);
expect(globSyncMock).toHaveBeenCalledTimes(expectTinyglobbyCalls);

// wow so much time has passed
hrtimeSpy.mockReturnValueOnce([Number.MAX_VALUE, 0]);

doParse(30);
// shouldn't call fast-glob again due to the caching
expect(fastGlobSyncMock).toHaveBeenCalledTimes(
expectFastGlobCalls * 2,
);
// shouldn't call tinyglobby again due to the caching
expect(globSyncMock).toHaveBeenCalledTimes(expectTinyglobbyCalls * 2);
});

it('should infinitely cache if passed Infinity', () => {
hrtimeSpy.mockReturnValueOnce([1, 0]);

doParse('Infinity');
expect(fastGlobSyncMock).toHaveBeenCalledTimes(expectFastGlobCalls);
expect(globSyncMock).toHaveBeenCalledTimes(expectTinyglobbyCalls);

// wow so much time has passed
hrtimeSpy.mockReturnValueOnce([Number.MAX_VALUE, 0]);

doParse('Infinity');
// shouldn't call fast-glob again due to the caching
expect(fastGlobSyncMock).toHaveBeenCalledTimes(expectFastGlobCalls);
// shouldn't call tinyglobby again due to the caching
expect(globSyncMock).toHaveBeenCalledTimes(expectTinyglobbyCalls);
});
});
});
Expand Down
11 changes: 1 addition & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5213,13 +5213,6 @@ __metadata:
languageName: node
linkType: hard

"@types/is-glob@npm:^4.0.4":
version: 4.0.4
resolution: "@types/is-glob@npm:4.0.4"
checksum: c790125e2d133d15c9783f6468995841cb06b5634b5c7b30aa32d23129f19d7dc271ec1a904bea4ca1e6a5ba19218a6602753d558f343b4fb8402fed25d17219
languageName: node
linkType: hard

"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1":
version: 2.0.4
resolution: "@types/istanbul-lib-coverage@npm:2.0.4"
Expand Down Expand Up @@ -5816,7 +5809,6 @@ __metadata:
"@types/babel__code-frame": ^7.0.6
"@types/babel__core": ^7.20.5
"@types/debug": ^4.1.12
"@types/is-glob": ^4.0.4
"@types/jest": 29.5.13
"@types/jest-specific-snapshot": ^0.5.9
"@types/natural-compare": ^1.4.3
Expand Down Expand Up @@ -5879,14 +5871,13 @@ __metadata:
"@typescript-eslint/types": 8.18.1
"@typescript-eslint/visitor-keys": 8.18.1
debug: ^4.3.4
fast-glob: ^3.3.2
glob: "*"
is-glob: ^4.0.3
jest: 29.7.0
minimatch: ^9.0.4
prettier: ^3.2.5
rimraf: "*"
semver: ^7.6.0
tinyglobby: ^0.2.10
tmp: "*"
ts-api-utils: ^1.3.0
typescript: "*"
Expand Down
Loading