Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
65 changes: 65 additions & 0 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,71 @@ namespace Harness.LanguageService {
}),
error: undefined
};
case "mock-vue":
return {
module: () => ({
create(info: ts.server.PluginCreateInfo) {
const clssf = ts.createLanguageServiceSourceFile;
const ulssf = ts.updateLanguageServiceSourceFile;
ts.overrideCreateupdateLanguageServiceSourceFile(
(fileName, scriptSnapshot, scriptTarget, version, setNodeParents, scriptKind?) => {
if (interested(fileName)) {
const wrapped = scriptSnapshot;
scriptSnapshot = {
getChangeRange: old => wrapped.getChangeRange(old),
getLength: () => wrapped.getLength(),
getText: (start, end) => parse(wrapped.getText(0, wrapped.getLength())).slice(start, end),
};
}
var sourceFile = clssf(fileName, scriptSnapshot, scriptTarget, version, setNodeParents, scriptKind);
return sourceFile;
},
(sourceFile, scriptSnapshot, version, textChangeRange, aggressiveChecks?) => {
if (interested(sourceFile.fileName)) {
const wrapped = scriptSnapshot;
scriptSnapshot = {
getChangeRange: old => wrapped.getChangeRange(old),
getLength: () => wrapped.getLength(),
getText: (start, end) => parse(wrapped.getText(0, wrapped.getLength())).slice(start, end),
};
}
var sourceFile = ulssf(sourceFile, scriptSnapshot, version, textChangeRange, aggressiveChecks);
return sourceFile;
});
return makeDefaultProxy(info);

function interested(filename: string) {
return filename.length;
}
function parse(text: string) {
const start = text.indexOf("<script>") + "<script>".length;
const end = text.indexOf("</script>");
return text.slice(0, start).replace(/./g, ' ') + text.slice(start, end) + text.slice(end).replace(/./g, ' ');
}
},
resolveModules() {
return (rmn: any) =>
(moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, host: ts.ModuleResolutionHost, cache?: ts.ModuleResolutionCache) => {
if (importInterested(moduleName)) {
return {
resolvedModule: {
extension: ts.Extension.Ts,
isExternalLibraryImport: true,
resolvedFileName: containingFile.slice(0, containingFile.lastIndexOf("/")) + "/" + moduleName,
}
}
}
else {
return rmn(moduleName, containingFile, compilerOptions, host, cache);
}
};
function importInterested(filename: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fileName

return filename.charAt(0) === "." && filename.slice(-4) === ".vue";
}
}
}),
error: undefined
};

default:
return {
Expand Down
9 changes: 7 additions & 2 deletions src/server/lsHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace ts.server {

private filesWithChangedSetOfUnresolvedImports: Path[];

private readonly resolveModuleName: typeof resolveModuleName;
private resolveModuleName: typeof resolveModuleName;
readonly trace: (s: string) => void;
readonly realpath?: (path: string) => string;

Expand Down Expand Up @@ -46,6 +46,11 @@ namespace ts.server {
}
}

public overrideResolveModuleName(plugin: PluginResolveModules) {
const prevResolveModuleName = this.resolveModuleName;
this.resolveModuleName = plugin(prevResolveModuleName);
}

public startRecordingFilesWithChangedResolutions() {
this.filesWithChangedSetOfUnresolvedImports = [];
}
Expand Down Expand Up @@ -237,4 +242,4 @@ namespace ts.server {
this.compilationSettings = opt;
}
}
}
}
12 changes: 11 additions & 1 deletion src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ namespace ts.server {
export interface PluginModule {
create(createInfo: PluginCreateInfo): LanguageService;
getExternalFiles?(proj: Project): string[];
resolveModules?(createInfo: PluginCreateInfo): PluginResolveModules;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the plugin already has access to the lshost, it can just override resolveModule directly. not sure i see the need for this new API.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it does, but I'm not sure whether the API guarantees it. Regardless, it gets rid of a lot of code. @RyanCavanaugh can you take a look at the new commits that override resolveModuleNames on the language service host directly?

}

export type ModuleResolver = (moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache) => ResolvedModuleWithFailedLookupLocations;

export type PluginResolveModules = {
(plugin: ModuleResolver): ModuleResolver;
}

export interface PluginModuleFactory {
Expand Down Expand Up @@ -904,6 +911,9 @@ namespace ts.server {

const pluginModule = pluginModuleFactory({ typescript: ts });
this.languageService = pluginModule.create(info);
if (pluginModule.resolveModules) {
this.lsHost.overrideResolveModuleName(pluginModule.resolveModules(info));
}
this.plugins.push(pluginModule);
}
catch (e) {
Expand Down Expand Up @@ -1083,4 +1093,4 @@ namespace ts.server {
this.typeAcquisition = newTypeAcquisition;
}
}
}
}
7 changes: 7 additions & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,13 @@ namespace ts {
sourceFile.scriptSnapshot = scriptSnapshot;
}

export function overrideCreateupdateLanguageServiceSourceFile(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be a LanguageServiceHost optional functions. we do not want to override this for all instances of the language service.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also recommend splitting this into createSoruceFile and upadateSourceFile.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are global functions, so I'm not sure how to avoid overriding them for all instances of the language service. (That was probably the cause of the CI failures; bad overrides are also global.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have two options:

  1. in LanguageServiceHost:
export interface LanguageServiceHost {
    ...
    createSourceFile?(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile;
    updateSourceFile?(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string, textChangeRange: TextChangeRange, aggressiveChecks?: boolean): SourceFile;
    ....
}

Then in the language service, in places where we call ts.createLanguageServiceSourceFile or ts.updateLanguageServiceSourceFile, we can just check the host if it supports these APIs.

  1. users can wrap the DocumentRegistry for the language service, and do the needed work in acquireDocument and updateDocument.

create: (fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind, cheat?: string) => SourceFile,
update: (sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string, textChangeRange: TextChangeRange, aggressiveChecks?: boolean, cheat?: string) => SourceFile) {
ts.createLanguageServiceSourceFile = create;
ts.updateLanguageServiceSourceFile = update;
}

export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile {
const text = scriptSnapshot.getText(0, scriptSnapshot.getLength());
const sourceFile = createSourceFile(fileName, text, scriptTarget, setNodeParents, scriptKind);
Expand Down
44 changes: 44 additions & 0 deletions tests/cases/fourslash/server/vueProxy1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/// <reference path="../fourslash.ts"/>

// @Filename: tsconfig.json
//// {
//// "compilerOptions": {
//// "allowNonTsExtensions": true,
//// "plugins": [
//// { "name": "mock-vue" }
//// ]
//// },
//// "files": ["a.vue"]
//// }

// Note: This test does *not* implement the correct vue transformation.
// So it's other.data.property, not other.property or other.$data.property
// @Filename: other.vue
////<template>
////</template>
////<script>
////export default {
//// data: { property: "Example" }
////}
////</script>
////<style>
////</style>


// @Filename: a.vue
////<template>
////</template>
////<script>
////import other from './other.vue'
//// other.data.property/**/
////export default {
//// data: { greeting: "Hello" }
////}
////</script>
////<style>
////</style>

// LS shouldn't crash/fail if a plugin fails to init correctly
goTo.marker();
verify.quickInfoIs('(property) property: string');
verify.numberOfErrorsInCurrentFile(0);