├── .travis.yml ├── README.md ├── file-utils.js ├── logger.js ├── package.js ├── run_tests.sh ├── run_tests_ci.sh ├── tests └── server │ └── unit │ ├── compiler-tests_spec.js │ └── input-file.js ├── typescript-compiler.js ├── typescript.js └── utils.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | install: curl https://install.meteor.com | /bin/sh 4 | script: ./run_tests_ci.sh 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## TypeScript compiler for Meteor [![Build Status](https://travis-ci.org/barbatus/typescript-compiler.svg?branch=master)](https://travis-ci.org/barbatus/typescript-compiler) 2 | 3 | Exports two symbols: 4 | - `TypeScriptCompiler` - a compiler to be registered using `registerBuildPlugin` 5 | to compile TypeScript files. 6 | 7 | - `TypeScript` - an object with `compile` method. 8 | Use `TypeScript.compile(source, options)` to compile with preset options. 9 | -------------------------------------------------------------------------------- /file-utils.js: -------------------------------------------------------------------------------- 1 | const colors = Npm.require('colors'); 2 | 3 | export function isBare(inputFile) { 4 | const fileOptions = inputFile.getFileOptions(); 5 | return fileOptions && fileOptions.bare; 6 | } 7 | 8 | // Gets root app tsconfig. 9 | export function isMainConfig(inputFile) { 10 | if (! isWeb(inputFile)) return false; 11 | 12 | const filePath = inputFile.getPathInPackage(); 13 | return /^tsconfig\.json$/.test(filePath); 14 | } 15 | 16 | export function isConfig(inputFile) { 17 | const filePath = inputFile.getPathInPackage(); 18 | return /tsconfig\.json$/.test(filePath); 19 | } 20 | 21 | // Gets server tsconfig. 22 | export function isServerConfig(inputFile) { 23 | if (isWeb(inputFile)) return false; 24 | 25 | const filePath = inputFile.getPathInPackage(); 26 | return /^server\/tsconfig\.json$/.test(filePath); 27 | } 28 | 29 | // Checks if it's .d.ts-file. 30 | export function isDeclaration(inputFile) { 31 | return TypeScript.isDeclarationFile(inputFile.getBasename()); 32 | } 33 | 34 | export function isWeb(inputFile) { 35 | const arch = inputFile.getArch(); 36 | return /^web/.test(arch); 37 | } 38 | 39 | // Gets path with package prefix if any. 40 | export function getExtendedPath(inputFile) { 41 | let packageName = inputFile.getPackageName(); 42 | packageName = packageName ? 43 | (packageName.replace(':', '_') + '/') : ''; 44 | const inputFilePath = inputFile.getPathInPackage(); 45 | return packageName + inputFilePath; 46 | } 47 | 48 | export function getES6ModuleName(inputFile) { 49 | const extended = getExtendedPath(inputFile); 50 | return TypeScript.removeTsExt(extended); 51 | } 52 | 53 | export const WarnMixin = { 54 | warn(error) { 55 | console.log(`${error.sourcePath} (${error.line}, ${error.column}): ${error.message}`); 56 | }, 57 | logError(error) { 58 | console.log(colors.red( 59 | `${error.sourcePath} (${error.line}, ${error.column}): ${error.message}`)); 60 | } 61 | } 62 | 63 | export function extendFiles(inputFiles, fileMixin) { 64 | inputFiles.forEach(inputFile => _.defaults(inputFile, fileMixin)); 65 | } 66 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | const util = Npm.require('util'); 2 | 3 | class Logger_ { 4 | constructor() { 5 | this.llevel = process.env.TYPESCRIPT_LOG; 6 | } 7 | 8 | newProfiler(name) { 9 | let profiler = new Profiler(name); 10 | if (this.isProfile) profiler.start(); 11 | return profiler; 12 | } 13 | 14 | get isDebug() { 15 | return this.llevel >= 2; 16 | } 17 | 18 | get isProfile() { 19 | return this.llevel >= 3; 20 | } 21 | 22 | get isAssert() { 23 | return this.llevel >= 4; 24 | } 25 | 26 | log(msg, ...args) { 27 | if (this.llevel >= 1) { 28 | console.log.apply(null, [msg].concat(args)); 29 | } 30 | } 31 | 32 | debug(msg, ...args) { 33 | if (this.isDebug) { 34 | this.log.apply(this, msg, args); 35 | } 36 | } 37 | 38 | assert(msg, ...args) { 39 | if (this.isAssert) { 40 | this.log.apply(this, msg, args); 41 | } 42 | } 43 | }; 44 | 45 | Logger = new Logger_(); 46 | 47 | class Profiler { 48 | constructor(name) { 49 | this.name = name; 50 | } 51 | 52 | start() { 53 | console.log('%s started', this.name); 54 | console.time(util.format('%s time', this.name)); 55 | this._started = true; 56 | } 57 | 58 | end() { 59 | if (this._started) { 60 | console.timeEnd(util.format('%s time', this.name)); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'barbatus:typescript-compiler', 3 | version: '0.10.0', 4 | summary: 'TypeScript Compiler for Meteor', 5 | git: 'https://github.com/barbatus/typescript-compiler', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Npm.depends({ 10 | 'meteor-typescript': '0.9.0', 11 | 'async': '2.5.0', 12 | 'colors': '1.1.2', 13 | }); 14 | 15 | Package.onUse(function(api) { 16 | api.versionsFrom('1.4.1'); 17 | 18 | api.use([ 19 | 'ecmascript@0.10.8', 20 | 'check@1.0.5', 21 | 'underscore@1.0.4', 22 | ], 'server'); 23 | 24 | api.addFiles([ 25 | 'logger.js', 26 | 'file-utils.js', 27 | 'typescript-compiler.js', 28 | 'typescript.js', 29 | 'utils.js', 30 | ], 'server'); 31 | 32 | api.export([ 33 | 'TypeScript', 34 | 'TypeScriptCompiler', 35 | ], 'server'); 36 | }); 37 | 38 | Package.onTest(function(api) { 39 | api.use([ 40 | 'tinytest', 41 | 'ecmascript', 42 | 'underscore', 43 | 'practicalmeteor:sinon', 44 | 'practicalmeteor:chai', 45 | 'practicalmeteor:mocha', 46 | 'meteortesting:mocha', 47 | 'dispatch:mocha-phantomjs', 48 | ]); 49 | api.use('barbatus:typescript-compiler'); 50 | 51 | api.addFiles([ 52 | 'tests/server/unit/input-file.js', 53 | 'tests/server/unit/compiler-tests_spec.js', 54 | ], 'server'); 55 | }); 56 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -fr "~/.cache" 4 | 5 | TYPESCRIPT_LOG=1 TEST_CLIENT=0 METEOR_PROFILE=1000 TYPESCRIPT_CACHE_DIR="~/.cache" meteor test-packages --driver-package=meteortesting:mocha ./ 6 | -------------------------------------------------------------------------------- /run_tests_ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -fr "~/.cache" 4 | 5 | TYPESCRIPT_CACHE_DIR="~/.cache" TEST_CLIENT=0 meteor test-packages --once --driver-package=meteortesting:mocha ./ 6 | -------------------------------------------------------------------------------- /tests/server/unit/compiler-tests_spec.js: -------------------------------------------------------------------------------- 1 | import {chai} from 'meteor/practicalmeteor:chai'; 2 | import {sinon} from 'meteor/practicalmeteor:sinon'; 3 | 4 | const should = chai.should(); 5 | const expect = chai.expect; 6 | 7 | describe('typescript-compiler', () => { 8 | let testCodeLine = 'export const foo = "foo"'; 9 | 10 | describe('TypeScriptCompiler API', () => { 11 | let compiler = new TypeScriptCompiler(); 12 | expect(compiler.getFilesToProcess).to.be.a('function'); 13 | expect(compiler.getBuildOptions).to.be.a('function'); 14 | }); 15 | 16 | describe('testing options', () => { 17 | it('should have commonjs by default', () => { 18 | let compiler = new TypeScriptCompiler(); 19 | 20 | let inputFile = new InputFile(testCodeLine, 'foo1.ts'); 21 | compiler.processFilesForTarget([inputFile]); 22 | 23 | expect(inputFile.result).to.not.be.null; 24 | expect(inputFile.result.data).to.contain('exports.foo'); 25 | }); 26 | 27 | it('should have dom lib set by default for the web', () => { 28 | let compiler = new TypeScriptCompiler(); 29 | 30 | let inputFile = new InputFile('document.createElement("div")', 'foo.ts', 'web'); 31 | compiler.processFilesForTarget([inputFile]); 32 | inputFile.warn = sinon.spy(); 33 | 34 | expect(inputFile.calledOnce).to.not.be.true; 35 | }); 36 | 37 | it('should apply extra compiler options', () => { 38 | let compiler = new TypeScriptCompiler({ 39 | module: 'system' 40 | }); 41 | 42 | let inputFile = new InputFile(testCodeLine, 'foo2.ts'); 43 | compiler.processFilesForTarget([inputFile]); 44 | 45 | expect(inputFile.result.data).to.contain('System.register(\"foo2\"'); 46 | }); 47 | 48 | it('should exclude from node_modules by default', () => { 49 | let compiler = new TypeScriptCompiler(); 50 | 51 | let inputFile = new InputFile(testCodeLine, 'node_modules/foo3.ts'); 52 | compiler.processFilesForTarget([inputFile]); 53 | 54 | expect(inputFile.result).to.be.null; 55 | }); 56 | }); 57 | 58 | describe('testing tsconfig.json', () => { 59 | it('config should be recognized and watched', () => { 60 | let compiler = new TypeScriptCompiler(); 61 | 62 | let configFile = new ConfigFile({ 63 | compilerOptions: { 64 | module: 'system' 65 | } 66 | }); 67 | let inputFile = new InputFile(testCodeLine, 'foo3.ts'); 68 | compiler.processFilesForTarget([inputFile, configFile]); 69 | 70 | expect(inputFile.result.data).to.contain('System.register(\"foo3\"'); 71 | 72 | // Change config and test. 73 | configFile.compilerOptions.module = 'commonjs'; 74 | compiler.processFilesForTarget([inputFile, configFile]); 75 | expect(inputFile.result.data).to.contain('exports.foo'); 76 | }); 77 | 78 | 79 | it('should skip any other tsconfig.json', () => { 80 | let serverFile = new InputFile(testCodeLine, 'foo.ts', 'os'); 81 | 82 | let configFile = new ConfigFile({ 83 | compilerOptions: { 84 | module: 'system' 85 | } 86 | }, 'node_modules/foo/tsconfig.json'); 87 | let compiler = new TypeScriptCompiler(); 88 | compiler.processFilesForTarget([serverFile, configFile]); 89 | expect(serverFile.result.data).not.to.contain('System.register(\"foo\"'); 90 | }); 91 | 92 | // TODO: check out why for-of loop raises warning here. 93 | it('should apply target from the server tsconfig.json', () => { 94 | let code = ` 95 | async function test() {} 96 | `; 97 | let serverFile = new InputFile(code, 'foo.ts', 'os'); 98 | serverFile.warn = sinon.spy(); 99 | 100 | let configFile = new ConfigFile({ 101 | compilerOptions: { 102 | target: 'es6' 103 | } 104 | }, 'server/tsconfig.json', 'os'); 105 | let compiler = new TypeScriptCompiler(); 106 | compiler.processFilesForTarget([serverFile, configFile]); 107 | expect(serverFile.warn.calledOnce).to.not.be.true; 108 | }); 109 | 110 | describe('tsconfig.exclude', () => { 111 | it('should exclude files using glob and flat directory patterns', () => { 112 | let compiler = new TypeScriptCompiler(); 113 | 114 | let configFile = new ConfigFile({ 115 | exclude: ['foo1/**', 'foo2'] 116 | }); 117 | let inputFile1 = new InputFile(testCodeLine, 'foo1/foo.ts'); 118 | let inputFile2 = new InputFile(testCodeLine, 'foo2/foo.ts'); 119 | compiler.processFilesForTarget([inputFile1, inputFile2, configFile]); 120 | 121 | expect(inputFile1.result).to.be.null; 122 | expect(inputFile2.result).to.be.null; 123 | }); 124 | 125 | it('should exclude a file', () => { 126 | let compiler = new TypeScriptCompiler(); 127 | 128 | let configFile = new ConfigFile({ 129 | exclude: ['foo3/foo.ts'] 130 | }); 131 | let inputFile = new InputFile(testCodeLine, 'foo3/foo.ts'); 132 | compiler.processFilesForTarget([inputFile, configFile]); 133 | 134 | expect(inputFile.result).to.be.null; 135 | }); 136 | }); 137 | }); 138 | 139 | describe('testing diagnostics', () => { 140 | it('should log out diagnostics by default', () => { 141 | let compiler = new TypeScriptCompiler(); 142 | 143 | let configFile = new ConfigFile({ 144 | compilerOptions: { 145 | module: 'system' 146 | } 147 | }); 148 | let wrongImport = 'import {api} from "lib";'; 149 | let inputFile = new InputFile(wrongImport, 'foo4.ts'); 150 | inputFile.warn = sinon.spy(); 151 | compiler.processFilesForTarget([inputFile, configFile]); 152 | 153 | expect(inputFile.warn.calledOnce).to.be.true; 154 | expect(inputFile.warn.args[0][0]).to.be.an('object'); 155 | }); 156 | }); 157 | 158 | describe('testing modules', () => { 159 | it('should render bare source code if module set to none', () => { 160 | let compiler = new TypeScriptCompiler(); 161 | let configFile = new ConfigFile({ 162 | compilerOptions: { 163 | module: 'none' 164 | } 165 | }); 166 | let moduleFoo = 'module foo {}'; 167 | let inputFile = new InputFile(moduleFoo, 'foo5.ts'); 168 | compiler.processFilesForTarget([inputFile, configFile]); 169 | 170 | expect(inputFile.result.bare).to.be.true; 171 | }); 172 | 173 | it('should resolve module path that starts with /', () => { 174 | let compiler = new TypeScriptCompiler(); 175 | let file1 = 'import {api} from "/imports/foo7"'; 176 | let inputFile1 = new InputFile(file1, 'client/foo6.ts'); 177 | inputFile1.warn = sinon.spy(); 178 | 179 | let file2 = 'export const api = {}'; 180 | let inputFile2 = new InputFile(file2, 'imports/foo7.ts'); 181 | 182 | compiler.processFilesForTarget([inputFile1, inputFile2]); 183 | expect(inputFile1.warn.calledOnce).to.not.be.true; 184 | }); 185 | }); 186 | 187 | describe('testing architecture separation', () => { 188 | it('typings from typings/browser is used for the browser arch only', () => { 189 | let clientCode = 'var client: API.Client'; 190 | let serverCode = 'var server: API.Client'; 191 | let clientTypings = 'declare module API { interface Client {} };'; 192 | let serverTypings = 'declare module API { interface Server {} };'; 193 | 194 | let clientFile = new InputFile(clientCode, 'client.ts', 'web'); 195 | clientFile.warn = sinon.spy(); 196 | let typingsFile1 = new InputFile(clientTypings, 'typings/browser/client.d.ts', 'web'); 197 | let typingsFile2 = new InputFile(serverTypings, 'typings/main/server.d.ts', 'web'); 198 | 199 | let serverFile = new InputFile(serverCode, 'server.ts', 'os'); 200 | serverFile.warn = sinon.spy(); 201 | let typingsFile3 = new InputFile(clientTypings, 'typings/browser/client.d.ts', 'os'); 202 | let typingsFile4 = new InputFile(serverTypings, 'typings/main/server.d.ts', 'os'); 203 | let compiler = new TypeScriptCompiler(); 204 | compiler.processFilesForTarget([clientFile, typingsFile1, typingsFile2]); 205 | compiler.processFilesForTarget([serverFile, typingsFile3, typingsFile4]); 206 | 207 | expect(clientFile.warn.calledOnce).to.not.be.true; 208 | expect(serverFile.warn.calledOnce).to.be.true; 209 | expect(serverFile.warn.args[0][0].message).to.contain('Client'); 210 | }); 211 | 212 | it('same diagnostics messages are not more than once', () => { 213 | let wrongImport = 'import {api} from "lib";'; 214 | let clientFile = new InputFile(wrongImport, 'common.ts', 'web'); 215 | clientFile.warn = sinon.spy(); 216 | let serverFile = new InputFile(wrongImport, 'common.ts', 'os'); 217 | serverFile.warn = sinon.spy(); 218 | 219 | let compiler = new TypeScriptCompiler(); 220 | compiler.processFilesForTarget([clientFile]); 221 | compiler.processFilesForTarget([serverFile]); 222 | expect(clientFile.warn.calledOnce).to.be.true; 223 | expect(serverFile.warn.calledOnce).to.not.be.true; 224 | }); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /tests/server/unit/input-file.js: -------------------------------------------------------------------------------- 1 | const crypto = Npm.require('crypto'); 2 | 3 | function sha1(content) { 4 | let hash = crypto.createHash('sha1'); 5 | hash.update(content); 6 | return hash.digest('hex'); 7 | } 8 | 9 | InputFile = class InputFile { 10 | constructor(source, fileName, arch = 'os') { 11 | this.source = source; 12 | this.fileName = fileName; 13 | this.result = null; 14 | this.arch = arch; 15 | } 16 | 17 | getContentsAsString() { 18 | return this.source; 19 | } 20 | 21 | getPackageName() { 22 | return null; 23 | } 24 | 25 | getPathInPackage() { 26 | return this.fileName; 27 | } 28 | 29 | getBasename() { 30 | return this.fileName; 31 | } 32 | 33 | getFileOptions() { 34 | return this.options; 35 | } 36 | 37 | getSourceHash() { 38 | return sha1(this.getContentsAsString()); 39 | } 40 | 41 | addJavaScript(result) { 42 | this.result = result; 43 | } 44 | 45 | getArch() { 46 | return this.arch; 47 | } 48 | 49 | warn(error) { 50 | this.error = error; 51 | } 52 | } 53 | 54 | ConfigFile = class ConfigFile extends InputFile { 55 | constructor(config, path, arch = 'web') { 56 | super(JSON.stringify(config), path || 'tsconfig.json', arch); 57 | for (let key in config) { 58 | this[key] = config[key]; 59 | } 60 | } 61 | 62 | getContentsAsString() { 63 | return JSON.stringify(this); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /typescript-compiler.js: -------------------------------------------------------------------------------- 1 | const async = Npm.require('async'); 2 | const path = Npm.require('path'); 3 | const fs = Npm.require('fs'); 4 | const Future = Npm.require('fibers/future'); 5 | 6 | const { 7 | TSBuild, 8 | validateTsConfig, 9 | getExcludeRegExp, 10 | } = Npm.require('meteor-typescript'); 11 | 12 | const {createHash} = Npm.require('crypto'); 13 | 14 | import { 15 | getExtendedPath, 16 | isDeclaration, 17 | isConfig, 18 | isMainConfig, 19 | isServerConfig, 20 | isBare, 21 | getES6ModuleName, 22 | WarnMixin, 23 | extendFiles, 24 | isWeb, 25 | } from './file-utils'; 26 | 27 | import { 28 | getShallowHash, 29 | } from './utils'; 30 | 31 | // Default exclude paths. 32 | const defExclude = new RegExp( 33 | getExcludeRegExp(['node_modules/**'])); 34 | 35 | // What to exclude when compiling for the server. 36 | // typings/main and typings/browser seem to be not used 37 | // at all but let keep them for just in case. 38 | const exlWebRegExp = new RegExp( 39 | getExcludeRegExp(['typings/main/**', 'typings/main.d.ts'])); 40 | 41 | // What to exclude when compiling for the client. 42 | const exlMainRegExp = new RegExp( 43 | getExcludeRegExp(['typings/browser/**', 'typings/browser.d.ts'])); 44 | 45 | const COMPILER_REGEXP = /(\.d.ts|\.ts|\.tsx|\.tsconfig)$/; 46 | 47 | const TS_REGEXP = /(\.ts|\.tsx)$/; 48 | 49 | TypeScriptCompiler = class TypeScriptCompiler { 50 | constructor(extraOptions, maxParallelism) { 51 | TypeScript.validateExtraOptions(extraOptions); 52 | 53 | this.extraOptions = extraOptions; 54 | this.maxParallelism = maxParallelism || 10; 55 | this.serverOptions = null; 56 | this.tsconfig = TypeScript.getDefaultOptions(); 57 | this.cfgHash = null; 58 | this.diagHash = new Set; 59 | this.archSet = new Set; 60 | } 61 | 62 | getFilesToProcess(inputFiles) { 63 | const pexclude = Logger.newProfiler('exclude'); 64 | 65 | inputFiles = this._filterByDefault(inputFiles); 66 | 67 | this._processConfig(inputFiles); 68 | 69 | inputFiles = this._filterByConfig(inputFiles); 70 | 71 | if (inputFiles.length) { 72 | const arch = inputFiles[0].getArch(); 73 | inputFiles = this._filterByArch(inputFiles, arch); 74 | } 75 | 76 | pexclude.end(); 77 | 78 | return inputFiles; 79 | } 80 | 81 | getBuildOptions(inputFiles) { 82 | this._processConfig(inputFiles); 83 | 84 | const inputFile = inputFiles[0]; 85 | let { compilerOptions } = this.tsconfig; 86 | // Make a copy. 87 | compilerOptions = Object.assign({}, compilerOptions); 88 | if (! isWeb(inputFile) && this.serverOptions) { 89 | Object.assign(compilerOptions, this.serverOptions); 90 | } 91 | 92 | // Apply extra options. 93 | if (this.extraOptions) { 94 | Object.assign(compilerOptions, this.extraOptions); 95 | } 96 | 97 | const arch = inputFile.getArch(); 98 | const { typings, useCache } = this.tsconfig; 99 | return { arch, compilerOptions, typings, useCache }; 100 | } 101 | 102 | processFilesForTarget(inputFiles, getDepsContent) { 103 | extendFiles(inputFiles, WarnMixin); 104 | 105 | const options = this.getBuildOptions(inputFiles); 106 | Logger.log('compiler options: %j', options.compilerOptions); 107 | 108 | inputFiles = this.getFilesToProcess(inputFiles); 109 | 110 | if (! inputFiles.length) return; 111 | 112 | const pcompile = Logger.newProfiler('compilation'); 113 | const filePaths = inputFiles.map(file => getExtendedPath(file)); 114 | Logger.log('compile files: %s', filePaths); 115 | 116 | const pbuild = Logger.newProfiler('tsBuild'); 117 | const defaultGet = this._getContentGetter(inputFiles); 118 | const getContent = filePath => 119 | (getDepsContent && getDepsContent(filePath)) || defaultGet(filePath); 120 | const tsBuild = new TSBuild(filePaths, getContent, options); 121 | pbuild.end(); 122 | 123 | const pfiles = Logger.newProfiler('tsEmitFiles'); 124 | const future = new Future; 125 | // Don't emit typings. 126 | const compileFiles = inputFiles.filter(file => ! isDeclaration(file)); 127 | let throwSyntax = false; 128 | const results = new Map(); 129 | async.eachLimit(compileFiles, this.maxParallelism, (file, done) => { 130 | const co = options.compilerOptions; 131 | 132 | const filePath = getExtendedPath(file); 133 | const pemit = Logger.newProfiler('tsEmit'); 134 | const result = tsBuild.emit(filePath); 135 | results.set(file, result); 136 | pemit.end(); 137 | 138 | throwSyntax = throwSyntax | 139 | this._processDiagnostics(file, result.diagnostics, co); 140 | 141 | done(); 142 | }, future.resolver()); 143 | 144 | pfiles.end(); 145 | 146 | future.wait(); 147 | 148 | if (! throwSyntax) { 149 | results.forEach((result, file) => { 150 | const module = options.compilerOptions.module; 151 | this._addJavaScript(file, result, module === 'none'); 152 | }); 153 | } 154 | 155 | pcompile.end(); 156 | } 157 | 158 | _getContentGetter(inputFiles) { 159 | const filesMap = new Map; 160 | inputFiles.forEach((inputFile, index) => { 161 | filesMap.set(getExtendedPath(inputFile), index); 162 | }); 163 | 164 | return filePath => { 165 | let index = filesMap.get(filePath); 166 | if (index === undefined) { 167 | const filePathNoRootSlash = filePath.replace(/^\//, ''); 168 | index = filesMap.get(filePathNoRootSlash); 169 | } 170 | return index !== undefined ? 171 | inputFiles[index].getContentsAsString() : null; 172 | }; 173 | } 174 | 175 | _addJavaScript(inputFile, tsResult, forceBare) { 176 | const source = inputFile.getContentsAsString(); 177 | const inputPath = inputFile.getPathInPackage(); 178 | const outputPath = TypeScript.removeTsExt(inputPath) + '.js'; 179 | const toBeAdded = { 180 | sourcePath: inputPath, 181 | path: outputPath, 182 | data: tsResult.code, 183 | hash: tsResult.hash, 184 | sourceMap: tsResult.sourceMap, 185 | bare: forceBare || isBare(inputFile) 186 | }; 187 | inputFile.addJavaScript(toBeAdded); 188 | } 189 | 190 | _processDiagnostics(inputFile, diagnostics, tsOptions) { 191 | // Remove duplicated warnings for shared files 192 | // by saving hashes of already shown warnings. 193 | const reduce = (diagnostic, cb) => { 194 | let dob = { 195 | message: diagnostic.message, 196 | sourcePath: getExtendedPath(inputFile), 197 | line: diagnostic.line, 198 | column: diagnostic.column 199 | }; 200 | const arch = inputFile.getArch(); 201 | // TODO: find out how to get list of architectures. 202 | this.archSet.add(arch); 203 | 204 | let shown = false; 205 | for (const key of this.archSet.keys()) { 206 | if (key !== arch) { 207 | dob.arch = key; 208 | const hash = getShallowHash(dob); 209 | if (this.diagHash.has(hash)) { 210 | shown = true; break; 211 | } 212 | } 213 | } 214 | 215 | if (! shown) { 216 | dob.arch = arch; 217 | const hash = getShallowHash(dob); 218 | this.diagHash.add(hash); 219 | cb(dob); 220 | } 221 | } 222 | 223 | // Always throw syntax errors. 224 | const throwSyntax = !! diagnostics.syntacticErrors.length; 225 | diagnostics.syntacticErrors.forEach(diagnostic => { 226 | reduce(diagnostic, dob => { 227 | inputFile.error(dob); 228 | }); 229 | }); 230 | 231 | const packageName = inputFile.getPackageName(); 232 | if (packageName) return throwSyntax; 233 | 234 | // And log out other errors except package files. 235 | if (tsOptions && tsOptions.diagnostics) { 236 | diagnostics.semanticErrors.forEach(diagnostic => { 237 | reduce(diagnostic, dob => inputFile.warn(dob)); 238 | }); 239 | } 240 | 241 | return throwSyntax; 242 | } 243 | 244 | _getFileModuleName(inputFile, options) { 245 | if (options.module === 'none') return null; 246 | 247 | return getES6ModuleName(inputFile); 248 | } 249 | 250 | _processConfig(inputFiles) { 251 | const tsFiles = inputFiles 252 | .map(inputFile => inputFile.getPathInPackage()) 253 | .filter(filePath => TS_REGEXP.test(filePath)); 254 | 255 | for (const inputFile of inputFiles) { 256 | // Parse root config. 257 | if (isMainConfig(inputFile)) { 258 | const source = inputFile.getContentsAsString(); 259 | const hash = inputFile.getSourceHash(); 260 | // If hashes differ, create new tsconfig. 261 | if (hash !== this.cfgHash) { 262 | this.tsconfig = this._parseConfig(source, tsFiles); 263 | this.cfgHash = hash; 264 | } 265 | return; 266 | } 267 | 268 | // Parse server config. 269 | // Take only target and lib values. 270 | if (isServerConfig(inputFile)) { 271 | const source = inputFile.getContentsAsString(); 272 | const { compilerOptions } = this._parseConfig(source, tsFiles); 273 | if (compilerOptions) { 274 | const { target, lib } = compilerOptions; 275 | this.serverOptions = { target, lib }; 276 | } 277 | return; 278 | } 279 | } 280 | } 281 | 282 | _parseConfig(cfgContent, tsFiles) { 283 | let tsconfig = null; 284 | 285 | try { 286 | tsconfig = JSON.parse(cfgContent); 287 | // Define files since if it's not defined 288 | // validation throws an exception. 289 | const files = tsconfig.files || tsFiles; 290 | tsconfig.files = files; 291 | 292 | validateTsConfig(tsconfig); 293 | } catch(err) { 294 | throw new Error(`Format of the tsconfig is invalid: ${err}`); 295 | } 296 | 297 | const exclude = tsconfig.exclude || []; 298 | try { 299 | const regExp = getExcludeRegExp(exclude); 300 | tsconfig.exclude = regExp && new RegExp(regExp); 301 | } catch(err) { 302 | throw new Error(`Format of an exclude path is invalid: ${err}`); 303 | } 304 | 305 | return tsconfig; 306 | } 307 | 308 | _filterByDefault(inputFiles) { 309 | inputFiles = inputFiles.filter(inputFile => { 310 | const path = inputFile.getPathInPackage(); 311 | return COMPILER_REGEXP.test(path) && ! defExclude.test('/' + path); 312 | }); 313 | return inputFiles; 314 | } 315 | 316 | _filterByConfig(inputFiles) { 317 | let resultFiles = inputFiles; 318 | if (this.tsconfig.exclude) { 319 | resultFiles = resultFiles.filter(inputFile => { 320 | const path = inputFile.getPathInPackage(); 321 | // There seems to an issue with getRegularExpressionForWildcard: 322 | // result regexp always starts with /. 323 | return ! this.tsconfig.exclude.test('/' + path); 324 | }); 325 | } 326 | return resultFiles; 327 | } 328 | 329 | _filterByArch(inputFiles, arch) { 330 | check(arch, String); 331 | 332 | /** 333 | * Include only typings that current arch needs, 334 | * typings/main is for the server only and 335 | * typings/browser - for the client. 336 | */ 337 | const filterRegExp = /^web/.test(arch) ? exlWebRegExp : exlMainRegExp; 338 | inputFiles = inputFiles.filter(inputFile => { 339 | const path = inputFile.getPathInPackage(); 340 | return ! filterRegExp.test('/' + path); 341 | }); 342 | 343 | return inputFiles; 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /typescript.js: -------------------------------------------------------------------------------- 1 | const meteorTS = Npm.require('meteor-typescript'); 2 | 3 | TypeScript = { 4 | validateOptions(options) { 5 | if (! options) return; 6 | 7 | meteorTS.validateAndConvertOptions(options); 8 | }, 9 | 10 | // Extra options are the same compiler options 11 | // but passed in the compiler constructor. 12 | validateExtraOptions(options) { 13 | if (! options) return; 14 | 15 | meteorTS.validateAndConvertOptions({ 16 | compilerOptions: options 17 | }); 18 | }, 19 | 20 | getDefaultOptions: meteorTS.getDefaultOptions, 21 | 22 | compile(source, options) { 23 | options = options || meteorTS.getDefaultOptions(); 24 | return meteorTS.compile(source, options); 25 | }, 26 | 27 | setCacheDir(cacheDir) { 28 | meteorTS.setCacheDir(cacheDir); 29 | }, 30 | 31 | isDeclarationFile(filePath) { 32 | return /^.*\.d\.ts$/.test(filePath); 33 | }, 34 | 35 | removeTsExt(path) { 36 | return path && path.replace(/(\.tsx|\.ts)$/g, ''); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const {createHash} = Npm.require('crypto'); 2 | 3 | export function getShallowHash(ob) { 4 | const hash = createHash('sha1'); 5 | const keys = Object.keys(ob); 6 | keys.sort(); 7 | 8 | keys.forEach(key => { 9 | hash.update(key).update('' + ob[key]); 10 | }); 11 | 12 | return hash.digest('hex'); 13 | } 14 | --------------------------------------------------------------------------------