Skip to content
This repository was archived by the owner on Aug 4, 2021. It is now read-only.

Use LanguageService as solution for emit-less types and semantic diagnostics #84

Closed
wants to merge 1 commit 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
142 changes: 142 additions & 0 deletions src/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import * as fs from 'fs';
import compareVersions from 'compare-versions';

const fileCache = new Map();
const tsFiles = new Set();

let typescript = undefined;
let compilerOptions = undefined;
let entryFile = undefined;
let languageService = undefined;


function getFile(fileName) {
let file = fileCache.get(fileName);

if(file === undefined) {
const version = Date.now().toString();

if (!fs.existsSync(fileName)) {
file = {
snapshot: undefined,
version: version
};
}
else {
file = {
snapshot: typescript.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString()),
version: version
};
}

fileCache.set(fileName, file);
}

return file;
}

function setFileContent(fileName, content, override) {
if(!override && fileCache.get(fileName)) {
return;
}

tsFiles.add(fileName);

fileCache.set(fileName, {
snapshot: typescript.ScriptSnapshot.fromString(content),
version: Date.now().toString()
});
}

function initLanguageService() {
if(languageService) return;

fixTypeLookup();

const languageServiceHost = {
getScriptFileNames: () => Array.from(tsFiles),
getScriptVersion: (fileName) => getFile(fileName).version,
getScriptSnapshot: (fileName) => getFile(fileName).snapshot,
getCurrentDirectory: () => process.cwd(),
getCompilationSettings: () => compilerOptions,
getDefaultLibFileName: (options) => typescript.getDefaultLibFilePath(options)
};

languageService = typescript.createLanguageService(languageServiceHost, typescript.createDocumentRegistry());
}

/**
* Workaround for the LanguageService not finding typings in node_modules/@types:
* Manually set the "types" option to the folder names in node_modules/@types
*/
function fixTypeLookup() {
if(compilerOptions.types || compilerOptions.typeRoots) return;

if ( compareVersions( typescript.version, '2.0.0' ) < 0 ) {
return;
}

if(fs.existsSync('./node_modules/@types')) {
compilerOptions.types = fs.readdirSync('./node_modules/@types');
}
}


function compileUsingLanguageService(fileName, content, refreshFile) {
setFileContent(fileName, content, refreshFile);

const result = {
outputText: undefined,
diagnostics: undefined,
sourceMapText: undefined
};

const compiled = languageService.getEmitOutput(fileName);

result.diagnostics = languageService.getCompilerOptionsDiagnostics()
.concat(languageService.getSyntacticDiagnostics(fileName))
.concat(languageService.getSemanticDiagnostics(fileName));

compiled.outputFiles.forEach(outputFile => {
if(outputFile.name.slice(-3) === '.js') {
result.outputText = outputFile.text;
}
else if(outputFile.name.slice(-4) === '.map') {
result.sourceMapText = outputFile.text;
}
});

return result;
}

function compileUsingSimpleApi(fileName, content) {
return typescript.transpileModule(content, {
fileName,
reportDiagnostics: true,
compilerOptions
});
}

function init(ts, compilerOpts, entry, useLanguageService) {
typescript = ts;
compilerOptions = compilerOpts;
entryFile = entry;

tsFiles.add(entryFile);

if(useLanguageService) {
initLanguageService();
}
}

function compileFile(fileName, content, refreshFile) {
return languageService
? compileUsingLanguageService(fileName, content, refreshFile)
: compileUsingSimpleApi(fileName, content);
}


export default {
init,
compileFile
};
23 changes: 17 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { endsWith } from './string';
import { getDefaultOptions, compilerOptionsFromTsConfig, adjustCompilerOptions } from './options.js';
import fixExportClass from './fixExportClass';
import resolveHost from './resolveHost';
import compiler from './compiler';

/*
interface Options {
Expand Down Expand Up @@ -45,6 +46,9 @@ export default function typescript ( options ) {

delete options.tsconfig;

const useLanguageService = options.useLanguageService;
delete options.useLanguageService;

// Since the CompilerOptions aren't designed for the Rollup
// use case, we'll adjust them for use with Rollup.
adjustCompilerOptions( typescript, tsconfig );
Expand All @@ -68,7 +72,14 @@ export default function typescript ( options ) {

const compilerOptions = parsed.options;

let isFirstRun = true;

return {
options (opts) {
const entryFile = path.resolve(process.cwd(), opts.entry);
compiler.init(typescript, compilerOptions, entryFile, useLanguageService);
},

resolveId ( importee, importer ) {
// Handle the special `typescript-helpers` import itself.
if ( importee === helpersId ) {
Expand Down Expand Up @@ -108,11 +119,7 @@ export default function typescript ( options ) {
transform ( code, id ) {
if ( !filter( id ) ) return null;

const transformed = typescript.transpileModule( fixExportClass( code, id ), {
fileName: id,
reportDiagnostics: true,
compilerOptions
});
const transformed = compiler.compileFile(id, fixExportClass( code, id ), !isFirstRun);

// All errors except `Cannot compile modules into 'es6' when targeting 'ES5' or lower.`
const diagnostics = transformed.diagnostics ?
Expand All @@ -136,7 +143,7 @@ export default function typescript ( options ) {
}
});

if ( fatalError ) {
if ( fatalError && isFirstRun ) {
throw new Error( `There were TypeScript errors transpiling` );
}

Expand All @@ -148,6 +155,10 @@ export default function typescript ( options ) {
// Rollup expects `map` to be an object so we must parse the string
map: transformed.sourceMapText ? JSON.parse(transformed.sourceMapText) : null
};
},

onwrite() {
isFirstRun = false;
}
};
}