Skip to content
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

fix(typescript-estree): pass extraFileExtensions to projectService #9051

11 changes: 11 additions & 0 deletions packages/typescript-estree/src/useProgramFromProjectService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import debug from 'debug';
import { minimatch } from 'minimatch';
import path from 'path';
import { ScriptKind } from 'typescript';

import { createProjectProgram } from './create-program/createProjectProgram';
import type { ProjectServiceSettings } from './create-program/createProjectService';
Expand Down Expand Up @@ -31,6 +32,16 @@ export function useProgramFromProjectService(
filePathAbsolute,
);

if (parseSettings.extraFileExtensions.length) {
service.setHostConfiguration({
extraFileExtensions: parseSettings.extraFileExtensions.map(extension => ({
extension,
isMixedContent: false,
scriptKind: ScriptKind.Deferred,
})),
});
}

const opened = service.openClientFile(
filePathAbsolute,
parseSettings.codeFullText,
Expand Down
62 changes: 60 additions & 2 deletions packages/typescript-estree/tests/lib/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,6 @@ describe('parseAndGenerateServices', () => {
range: true,
loc: true,
tsconfigRootDir: PROJECT_DIR,
project: './tsconfig.json',
};
const testParse =
(filePath: string, extraFileExtensions: string[] = ['.vue']) =>
Expand All @@ -362,11 +361,26 @@ describe('parseAndGenerateServices', () => {
...config,
extraFileExtensions,
filePath: join(PROJECT_DIR, filePath),
project: './tsconfig.json',
});
} catch (error) {
alignErrorPath(error as Error);
}
};
const testExtraFileExtensions =
(filePath: string, extraFileExtensions: string[]) => (): void => {
const result = parser.parseAndGenerateServices(code, {
...config,
extraFileExtensions,
filePath: join(PROJECT_DIR, filePath),
projectService: true,
});
const compilerOptions = result.services.program?.getCompilerOptions();

if (!compilerOptions?.configFilePath) {
throw new Error('No config file found, using inferred project');
}
};

describe('project includes', () => {
it("doesn't error for matched files", () => {
Expand Down Expand Up @@ -479,6 +493,50 @@ describe('parseAndGenerateServices', () => {
`);
});
});

describe('"parserOptions.extraFileExtensions" is non-empty and projectService is true', () => {
describe('the extension matches', () => {
it('the file is included', () => {
expect(
testExtraFileExtensions('other/included.vue', ['.vue']),
).not.toThrow();
});

it("the file isn't included", () => {
expect(
testExtraFileExtensions('other/notIncluded.vue', ['.vue']),
).toThrowErrorMatchingInlineSnapshot(
`"No config file found, using inferred project"`,
);
});

it('duplicate extension', () => {
expect(
testExtraFileExtensions('ts/notIncluded.ts', ['.ts']),
).toThrowErrorMatchingInlineSnapshot(
`"No config file found, using inferred project"`,
);
});
});

it('invalid extension', () => {
expect(
testExtraFileExtensions('other/unknownFileType.unknown', [
'unknown',
]),
).toThrowErrorMatchingInlineSnapshot(
`"No config file found, using inferred project"`,
);
});

it('the extension does not match', () => {
expect(
testExtraFileExtensions('other/unknownFileType.unknown', ['.vue']),
).toThrowErrorMatchingInlineSnapshot(
`"No config file found, using inferred project"`,
);
});
});
});

describe('invalid project error messages', () => {
Expand Down Expand Up @@ -508,7 +566,7 @@ describe('parseAndGenerateServices', () => {

expect(testParse('ts/notIncluded0j1.ts'))
.toThrowErrorMatchingInlineSnapshot(`
"ESLint was configured to run on \`<tsconfigRootDir>/ts/notIncluded0j1.ts\` using \`parserOptions.project\`:
"ESLint was configured to run on \`<tsconfigRootDir>/ts/notIncluded0j1.ts\` using \`parserOptions.project\`:
- <tsconfigRootDir>/tsconfig.json
- <tsconfigRootDir>/tsconfig.extra.json
However, none of those TSConfigs include this file. Either:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type -- Fancy mocks */
import path from 'path';
import { ScriptKind } from 'typescript';

import type {
ProjectServiceSettings,
Expand All @@ -22,6 +23,7 @@ const currentDirectory = '/repos/repo';

function createMockProjectService() {
const openClientFile = jest.fn();
const setHostConfiguration = jest.fn();
const service = {
getDefaultProjectForFile: () => ({
getLanguageService: () => ({
Expand All @@ -33,6 +35,7 @@ function createMockProjectService() {
getCurrentDirectory: () => currentDirectory,
},
openClientFile,
setHostConfiguration,
};

return {
Expand All @@ -43,6 +46,7 @@ function createMockProjectService() {

const mockParseSettings = {
filePath: 'path/PascalCaseDirectory/camelCaseFile.ts',
extraFileExtensions: [] as readonly string[],
} as ParseSettings;

const createProjectServiceSettings = <
Expand Down Expand Up @@ -297,4 +301,47 @@ If you absolutely need more files included, set parserOptions.projectService.max

expect(actual).toBe(program);
});

it('does not call setHostConfiguration if extraFileExtensions are not provided', () => {
const { service } = createMockProjectService();

useProgramFromProjectService(
createProjectServiceSettings({
allowDefaultProject: [mockParseSettings.filePath],
service,
}),
mockParseSettings,
false,
new Set(),
);

expect(service.setHostConfiguration).not.toHaveBeenCalled();
});

it('calls setHostConfiguration on the service to use extraFileExtensions when it is provided', () => {
const { service } = createMockProjectService();

useProgramFromProjectService(
createProjectServiceSettings({
allowDefaultProject: [mockParseSettings.filePath],
service,
}),
{
...mockParseSettings,
extraFileExtensions: ['.vue'],
},
false,
new Set(),
);

expect(service.setHostConfiguration).toHaveBeenCalledWith({
extraFileExtensions: [
{
extension: '.vue',
isMixedContent: false,
scriptKind: ScriptKind.Deferred,
},
],
});
});
});