diff --git a/src/compiler.js b/src/compiler.js new file mode 100644 index 0000000..3b2e252 --- /dev/null +++ b/src/compiler.js @@ -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 +}; diff --git a/src/index.js b/src/index.js index 425b3c2..c078a9b 100644 --- a/src/index.js +++ b/src/index.js @@ -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 { @@ -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 ); @@ -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 ) { @@ -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 ? @@ -136,7 +143,7 @@ export default function typescript ( options ) { } }); - if ( fatalError ) { + if ( fatalError && isFirstRun ) { throw new Error( `There were TypeScript errors transpiling` ); } @@ -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; } }; }