Skip to content

Commit

Permalink
feat: allow explicit ScriptKind extraFileExtensions
Browse files Browse the repository at this point in the history
When configuring the language service host with extra file extensions,
the file extension is only regarded as supported if its `Deferred` or like
JS. If the host can delare a specific ScriptKind such as `TSX` for a file
then it will be stripped out with getSupportedExtensions.

This then later causes the programs rootFiles to not include the extra file
extension, and thus be excluded from the program.

If using `Deferred` then the file when parsed will be treated as TS and fail to
compile when using (J)TSX syntax. In majority of cases the script kind will be
known by the consumer and they can set the explicit script kind for the extra
file extension.

Adjust the logic for `getSupportedExtensions` to include any extra file extension
that are not built in. This way all explicit extra file extensions added
will be included in the project/programs root files and an explicit `scriptKind`
can be set.

resolves: microsoft#60125
  • Loading branch information
blake-newman committed Oct 9, 2024
1 parent a53c37d commit f8cc8c7
Show file tree
Hide file tree
Showing 7 changed files with 1,127 additions and 13 deletions.
6 changes: 1 addition & 5 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9864,7 +9864,7 @@ export function getSupportedExtensions(options?: CompilerOptions, extraFileExten
const flatBuiltins = flatten(builtins);
const extensions = [
...builtins,
...mapDefined(extraFileExtensions, x => x.scriptKind === ScriptKind.Deferred || needJsExtensions && isJSLike(x.scriptKind) && !flatBuiltins.includes(x.extension as Extension) ? [x.extension] : undefined),
...mapDefined(extraFileExtensions, x => !flatBuiltins.includes(x.extension as Extension) ? [x.extension] : undefined),
];

return extensions;
Expand All @@ -9882,10 +9882,6 @@ export function getSupportedExtensionsWithJsonIfResolveJsonModule(options: Compi
return [...supportedExtensions, [Extension.Json]];
}

function isJSLike(scriptKind: ScriptKind | undefined): boolean {
return scriptKind === ScriptKind.JS || scriptKind === ScriptKind.JSX;
}

/** @internal */
export function hasJSFileExtension(fileName: string): boolean {
return some(supportedJSExtensionsFlat, extension => fileExtensionIs(fileName, extension));
Expand Down
106 changes: 106 additions & 0 deletions src/testRunner/unittests/tsserver/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,112 @@ describe("unittests:: tsserver:: projects::", () => {
});
});

describe("includes extra extensions file with explicit TS script kind in the project context", () => {
function verifyDeferredContext(lazyConfiguredProjectsFromExternalProject: boolean) {
const file1 = {
path: "/home/src/projects/project/a.extra",
content: "const a = 1;",
};
const tsconfig = {
path: "/home/src/projects/project/tsconfig.json",
content: "",
};

const host = TestServerHost.createServerHost([file1, tsconfig]);
const session = new TestSession(host);
session.executeCommandSeq<ts.server.protocol.ConfigureRequest>({
command: ts.server.protocol.CommandTypes.Configure,
arguments: { preferences: { lazyConfiguredProjectsFromExternalProject } },
});

// Configure the deferred extension.
const extraFileExtensions = [{ extension: ".extra", scriptKind: ts.ScriptKind.TS, isMixedContent: false }];
session.executeCommandSeq<ts.server.protocol.ConfigureRequest>({
command: ts.server.protocol.CommandTypes.Configure,
arguments: { extraFileExtensions },
});

// Open external project
const projectFileName = "/home/src/projects/project/proj1";
openExternalProjectForSession({
projectFileName,
rootFiles: toExternalFiles([file1.path, tsconfig.path]),
options: {},
}, session);

if (lazyConfiguredProjectsFromExternalProject) {
// configured project is just created and not yet loaded
session.logger.info("Calling ensureInferredProjectsUpToDate_TestOnly");
session.getProjectService().ensureInferredProjectsUpToDate_TestOnly();
}

// Allow allowNonTsExtensions will be set to true for deferred extensions.
session.logger.info(`Has allowNonTsExtension: ${session.getProjectService().configuredProjects.get(tsconfig.path)!.getCompilerOptions().allowNonTsExtensions}`);

baselineTsserverLogs("projects", `extra file extensions with explicit TS script kind in the project context${lazyConfiguredProjectsFromExternalProject ? " with lazyConfiguredProjectsFromExternalProject" : ""}`, session);
}

it("when lazyConfiguredProjectsFromExternalProject not set", () => {
verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ false);
});
it("when lazyConfiguredProjectsFromExternalProject is set", () => {
verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ true);
});
});

describe("includes extra extensions file with explicit TS script kind in the project context", () => {
function verifyDeferredContext(lazyConfiguredProjectsFromExternalProject: boolean) {
const file1 = {
path: "/home/src/projects/project/a.extra",
content: "const a = <div />;",
};
const tsconfig = {
path: "/home/src/projects/project/tsconfig.json",
content: "",
};

const host = TestServerHost.createServerHost([file1, tsconfig]);
const session = new TestSession(host);
session.executeCommandSeq<ts.server.protocol.ConfigureRequest>({
command: ts.server.protocol.CommandTypes.Configure,
arguments: { preferences: { lazyConfiguredProjectsFromExternalProject } },
});

// Configure the deferred extension.
const extraFileExtensions = [{ extension: ".extra", scriptKind: ts.ScriptKind.TSX, isMixedContent: false }];
session.executeCommandSeq<ts.server.protocol.ConfigureRequest>({
command: ts.server.protocol.CommandTypes.Configure,
arguments: { extraFileExtensions },
});

// Open external project
const projectFileName = "/home/src/projects/project/proj1";
openExternalProjectForSession({
projectFileName,
rootFiles: toExternalFiles([file1.path, tsconfig.path]),
options: {},
}, session);

if (lazyConfiguredProjectsFromExternalProject) {
// configured project is just created and not yet loaded
session.logger.info("Calling ensureInferredProjectsUpToDate_TestOnly");
session.getProjectService().ensureInferredProjectsUpToDate_TestOnly();
}

// Allow allowNonTsExtensions will be set to true for deferred extensions.
session.logger.info(`Has allowNonTsExtension: ${session.getProjectService().configuredProjects.get(tsconfig.path)!.getCompilerOptions().allowNonTsExtensions}`);

baselineTsserverLogs("projects", `extra file extensions with explicit TSX script kind in the project context${lazyConfiguredProjectsFromExternalProject ? " with lazyConfiguredProjectsFromExternalProject" : ""}`, session);
}

it("when lazyConfiguredProjectsFromExternalProject not set", () => {
verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ false);
});
it("when lazyConfiguredProjectsFromExternalProject is set", () => {
verifyDeferredContext(/*lazyConfiguredProjectsFromExternalProject*/ true);
});
});

it("Orphan source files are handled correctly on watch trigger", () => {
const file1: File = {
path: `/user/username/projects/myproject/src/file1.ts`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
Info seq [hh:mm:ss:mss] currentDirectory:: /home/src/Vscode/Projects/bin useCaseSensitiveFileNames:: false
Info seq [hh:mm:ss:mss] libs Location:: /home/src/tslibs/TS/Lib
Info seq [hh:mm:ss:mss] globalTypingsCacheLocation:: /home/src/Library/Caches/typescript
Info seq [hh:mm:ss:mss] Provided types map file "/home/src/tslibs/TS/Lib/typesMap.json" doesn't exist
Before request
//// [/home/src/projects/project/a.extra]
const a = 1;
//// [/home/src/projects/project/tsconfig.json]
//// [/home/src/tslibs/TS/Lib/lib.d.ts]
/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }
interface ReadonlyArray<T> {}
declare const console: { log(msg: any): void; };
Info seq [hh:mm:ss:mss] request:
{
"command": "configure",
"arguments": {
"preferences": {
"lazyConfiguredProjectsFromExternalProject": true
}
},
"seq": 1,
"type": "request"
}
Info seq [hh:mm:ss:mss] response:
{
"seq": 0,
"type": "response",
"command": "configure",
"request_seq": 1,
"success": true
}
After request
Before request
Info seq [hh:mm:ss:mss] request:
{
"command": "configure",
"arguments": {
"extraFileExtensions": [
{
"extension": ".extra",
"scriptKind": 3,
"isMixedContent": false
}
]
},
"seq": 2,
"type": "request"
}
Info seq [hh:mm:ss:mss] reload projects.
Info seq [hh:mm:ss:mss] Before ensureProjectForOpenFiles:
Info seq [hh:mm:ss:mss] Open files:
Info seq [hh:mm:ss:mss] After ensureProjectForOpenFiles:
Info seq [hh:mm:ss:mss] Open files:
Info seq [hh:mm:ss:mss] After reloading projects..
Info seq [hh:mm:ss:mss] Open files:
Info seq [hh:mm:ss:mss] Host file extension mappings updated
Info seq [hh:mm:ss:mss] response:
{
"seq": 0,
"type": "response",
"command": "configure",
"request_seq": 2,
"success": true
}
After request
Before request
Info seq [hh:mm:ss:mss] request:
{
"command": "openExternalProject",
"arguments": {
"projectFileName": "/home/src/projects/project/proj1",
"rootFiles": [
{
"fileName": "/home/src/projects/project/a.extra"
},
{
"fileName": "/home/src/projects/project/tsconfig.json"
}
],
"options": {}
},
"seq": 3,
"type": "request"
}
Info seq [hh:mm:ss:mss] Creating ConfiguredProject: /home/src/projects/project/tsconfig.json, currentDirectory: /home/src/projects/project
Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /home/src/projects/project/tsconfig.json 2000 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Config file
Info seq [hh:mm:ss:mss] Project '/home/src/projects/project/tsconfig.json' (Configured)
Info seq [hh:mm:ss:mss] Files (0) InitialLoadPending
Info seq [hh:mm:ss:mss] -----------------------------------------------
Info seq [hh:mm:ss:mss] Open files:
Info seq [hh:mm:ss:mss] response:
{
"response": true,
"responseRequired": true
}
After request
FsWatches::
/home/src/projects/project/tsconfig.json: *new*
{}
Projects::
/home/src/projects/project/tsconfig.json (Configured) *new*
projectStateVersion: 1
projectProgramVersion: 0
dirty: true
initialLoadPending: true
Info seq [hh:mm:ss:mss] Calling ensureInferredProjectsUpToDate_TestOnly
Info seq [hh:mm:ss:mss] event:
{
"seq": 0,
"type": "event",
"event": "projectLoadingStart",
"body": {
"projectName": "/home/src/projects/project/tsconfig.json",
"reason": "Creating configured project in external project: /home/src/projects/project/proj1"
}
}
Info seq [hh:mm:ss:mss] Config: /home/src/projects/project/tsconfig.json : {
"rootNames": [
"/home/src/projects/project/a.extra"
],
"options": {
"configFilePath": "/home/src/projects/project/tsconfig.json"
}
}
Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project 1 undefined Config: /home/src/projects/project/tsconfig.json WatchType: Wild card directory
Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project 1 undefined Config: /home/src/projects/project/tsconfig.json WatchType: Wild card directory
Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /home/src/projects/project/a.extra 500 undefined WatchType: Closed Script info
Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /home/src/projects/project/tsconfig.json
Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /home/src/tslibs/TS/Lib/lib.d.ts 500 undefined WatchType: Closed Script info
Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project/node_modules/@types 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Type roots
Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project/node_modules/@types 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Type roots
Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/node_modules/@types 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Type roots
Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/node_modules/@types 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Type roots
Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /home/src/projects/project/tsconfig.json projectStateVersion: 1 projectProgramVersion: 0 structureChanged: true structureIsReused:: Not Elapsed:: *ms
Info seq [hh:mm:ss:mss] Project '/home/src/projects/project/tsconfig.json' (Configured)
Info seq [hh:mm:ss:mss] Files (2)
/home/src/tslibs/TS/Lib/lib.d.ts Text-1 "/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { length: number; [n: number]: T; }\ninterface ReadonlyArray<T> {}\ndeclare const console: { log(msg: any): void; };"
/home/src/projects/project/a.extra Text-1 "const a = 1;"
../../tslibs/TS/Lib/lib.d.ts
Default library for target 'es5'
a.extra
Matched by default include pattern '**/*'

Info seq [hh:mm:ss:mss] -----------------------------------------------
Info seq [hh:mm:ss:mss] event:
{
"seq": 0,
"type": "event",
"event": "projectLoadingFinish",
"body": {
"projectName": "/home/src/projects/project/tsconfig.json"
}
}
Info seq [hh:mm:ss:mss] event:
{
"seq": 0,
"type": "event",
"event": "telemetry",
"body": {
"telemetryEventName": "projectInfo",
"payload": {
"projectId": "1097a5f82e8323ba7aba7567ec06402f7ad4ea74abce44ec5efd223ac77ff169",
"fileStats": {
"js": 0,
"jsSize": 0,
"jsx": 0,
"jsxSize": 0,
"ts": 1,
"tsSize": 12,
"tsx": 0,
"tsxSize": 0,
"dts": 1,
"dtsSize": 413,
"deferred": 0,
"deferredSize": 0
},
"compilerOptions": {},
"typeAcquisition": {
"enable": false,
"include": false,
"exclude": false
},
"extends": false,
"files": false,
"include": false,
"exclude": false,
"compileOnSave": false,
"configFileName": "tsconfig.json",
"projectType": "configured",
"languageServiceEnabled": true,
"version": "FakeVersion"
}
}
}
Info seq [hh:mm:ss:mss] event:
{
"seq": 0,
"type": "event",
"event": "configFileDiag",
"body": {
"triggerFile": "/home/src/projects/project/tsconfig.json",
"configFile": "/home/src/projects/project/tsconfig.json",
"diagnostics": []
}
}
Info seq [hh:mm:ss:mss] Before ensureProjectForOpenFiles:
Info seq [hh:mm:ss:mss] Project '/home/src/projects/project/tsconfig.json' (Configured)
Info seq [hh:mm:ss:mss] Files (2)

Info seq [hh:mm:ss:mss] -----------------------------------------------
Info seq [hh:mm:ss:mss] Open files:
Info seq [hh:mm:ss:mss] After ensureProjectForOpenFiles:
Info seq [hh:mm:ss:mss] Project '/home/src/projects/project/tsconfig.json' (Configured)
Info seq [hh:mm:ss:mss] Files (2)

Info seq [hh:mm:ss:mss] -----------------------------------------------
Info seq [hh:mm:ss:mss] Open files:
Info seq [hh:mm:ss:mss] Has allowNonTsExtension: true
Loading

0 comments on commit f8cc8c7

Please sign in to comment.