From 6698063b3828bf6f8a6506d7d32ff273641b5b14 Mon Sep 17 00:00:00 2001 From: barbatus Date: Tue, 16 Feb 2016 00:28:12 +0700 Subject: [PATCH] Add typescript package plus some initial runtime tests This is part of the effort to create an official TypeScript compiler: https://github.com/Urigo/angular2-meteor/issues/89 https://github.com/Urigo/angular2-meteor/issues/90 https://github.com/Urigo/angular2-meteor/issues/102 --- README.md | 112 +------------ package.js | 35 ++-- plugin.js | 9 ++ runtime-tests.ts | 14 ++ typescript-helpers.js | 36 +++++ typescript.js | 359 ------------------------------------------ 6 files changed, 79 insertions(+), 486 deletions(-) create mode 100644 plugin.js create mode 100644 runtime-tests.ts create mode 100644 typescript-helpers.js delete mode 100644 typescript.js diff --git a/README.md b/README.md index 75f66e6..331ab2e 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,5 @@ -## TypeScript packaged for Meteor. -TypeScript API that is adapted to be used in Meteor packages. - -### Getting Started -Install package `meteor add barbatus:typescript` and start using TypeScript right away, e.g.: -````js - let result = TypeScript.transpile(fileContent, { - compilerOptions: {module: 'system'}, - typings: ['typings/angular2.d.ts'], // typings that will be compiled together with the given content. - filePath: some_path, // file path relative to the app root. - moduleName: some_module // set module name if you want to use ES6 modules. - }) -```` -Package's API strives to be close to the Babel package's [one](https://atmospherejs.com/meteor/babel-compiler). - -### API -#### TypeScript.transpileFiles(files, options, fileReadyCallback) -**`files`** param is expected to be Meteor's file objects in a compiler plugin. -Method eventually uses only `getContentsAsString()` inside, -access to other file properties can be defined in the **`options`**. - -**`options`** should have the following structure: -````js -{ - compilerOptions: Object, - typings?: Array, - filePath: file => file.getPathInPackage() - moduleName: file => getModuleName(file) -} -```` -`compilerOptions` TypeScript compiler options. See the next paragraph for detailed description. - -`typings` (optional) is expected be an array of declaration file paths. If provided, these files will be compiled together with .ts-files, thus, -eliminating the need to use `/// ` syntax. - -`filePath` field is expected to be a function that gets in a file object and return its file path. -Field is **required**. - -`moduleName` (optional) field. If you want to use modules, you set the `module` field of the `compilerOptions` (e.g., `compilerOptions.module = ts.ModuleKind.System`) and define a `moduleName` function that gets in a file and retuns its module name. - -**`fileReadyCallback`** — callback that is being executed each time file transpilation is completed. - -Callback's params are defined as follows: -````js - TypeScript.transpileFiles(files, options, - (file, referencedPaths, diagnostics, resultData) => { - - }) -```` - -`file` — file that has been compiled. - -`referencedPaths` — paths of the TypeScript modules and declaration files that current file uses. -Param is useful for Meteor compilers since allows to watch for changes in the file's dependencies and re-compile file when used methods or classes have been changed. - -`diagnostics` — an diagnostics object that provides diagnostics of the file transpilation. -Structure of this object is described below. - -`resultData` — result of the transpilation: -````js - { - path: filePath, // normalized relative path of the transpiled file (no ./, ../ and \ inside) - data: content, // transpiled content - sourceMap: sourceMapContent // source map content - } -``` -`sourceMap` is not null only if you set `sourceMap: true` in the `compilerOptions`. - -#### TypeScript.transpile(fileContent, options); -Same as `transpileFiles` but only for one file's string content. String content can be taken by file API's method -`file.getContentsAsString()`. - -Method returns a result object of the following structure: -````js - { - data: content, - sourceMap: sourceMapContent, - referencedPaths: filePaths, // file paths of other modules and declaration files - diagnostics: diagnosticsObject - } -```` - -### Compiler Options -Compiler options are pretty much the same as described [here](https://github.com/Microsoft/TypeScript/wiki/Compiler-Options) with few exceptions. - -Package restricts usage of options that potentially conflics or not supported by the API. - -Namely options that are set to false for time being are `declaration`, `project`, `watch` (file changes watch is expected to done via Meteor plugins), `inlineSourceMap`, `inlineSources`, `outDir`, `outFile`, `rootDir`, `sourceRoot`. - -Package also extends usage of some of the options for the API. For example, if you set ``noResolve`` to true `referencedFiles` array will be always empty. - -### Diagnostics -As now diagnostics object consists of two properties: syntactic and semantic. - -Syntactic is an array of all syntax errors. Semantic is an array of basically all type-cheking erros including unresolved modules errors, missing variables etc. -In the future versions semactic errors are expected to be structured into more parts for convenience. - -Diagnostics object has two convenient methods for logging out error details to the terminal, e.g.: -````js - diagnostics.logSyntactic(); // prints all syntactic errors - diagnostics.logSemantic(); // prints all semantics errors - // Or you can - diagnostics.semantic.forEach(message => ...); // iterate semantic messages - diagnostics.syntactic.forEach(message => ...); // iterate syntactic messages -```` - -### Example of Usage -You can check out TS caching [compiler](https://github.com/barbatus/angular2/blob/master/packages/ts-compilers/compilers/ts_caching_compiler.js) where this package is used. +## TypeScript +This package adds TypeScript to Meteor projects. +TypeScript syntax are transpiled into ECMAScript 3 (for older browsers compatibility) and +CommonJS modules by default. diff --git a/package.js b/package.js index 2965394..9856fa9 100644 --- a/package.js +++ b/package.js @@ -1,34 +1,31 @@ Package.describe({ - name: 'barbatus:typescript', - version: '0.1.5', - summary: 'TypeScript Package for Meteor', - git: 'https://github.com/barbatus/angular2/packages/typescript', + name: 'typescript', + version: '0.1.0', + summary: 'TypeScript for Meteor', + git: 'https://github.com/barbatus/typescript', documentation: 'README.md' }); -Npm.depends({ - 'typescript': '1.7.5' +Package.registerBuildPlugin({ + name: 'typescript', + use: ['typescript-compiler'], + sources: ['plugin.js'] }); -var server = 'server'; - Package.onUse(function(api) { - api.versionsFrom('1.2.0.1'); + api.use('isobuild:compiler-plugin@1.0.0'); + api.use('typescript-compiler'); - api.use([ - 'ecmascript@0.1.4', - 'check@1.0.5', - 'underscore@1.0.4' - ], server); + api.addFiles(['typescript-helpers.js']); - api.addFiles([ - 'typescript.js' - ], server); + api.export(['__extends', '__decorate', '__metadata', '__param', '__awaiter']); - api.export(['TypeScript'], server); + api.imply('promise'); }); Package.onTest(function(api) { api.use('tinytest'); - api.use('barbatus:typescript'); + api.use('typescript'); + + api.addFiles('runtime-tests.ts'); }); diff --git a/plugin.js b/plugin.js new file mode 100644 index 0000000..eebbe0f --- /dev/null +++ b/plugin.js @@ -0,0 +1,9 @@ +Plugin.registerCompiler({ + extensions: ['ts'], + filename: ['tsconfig.json'] +}, function () { + return new TypeScriptCompiler({ + // We define own helpers. + noEmitHelpers: true + }); +}); diff --git a/runtime-tests.ts b/runtime-tests.ts new file mode 100644 index 0000000..e740159 --- /dev/null +++ b/runtime-tests.ts @@ -0,0 +1,14 @@ +Tinytest.add('typescript - runtime - decorators', (test) => { + { + function classDecorator() { + return function(cls) { + cls.prototype.foo = 'foo'; + }; + } + + @classDecorator() + class Foo {} + + test.equal((new Foo()).foo, 'foo'); + } +}); diff --git a/typescript-helpers.js b/typescript-helpers.js new file mode 100644 index 0000000..b94e2d8 --- /dev/null +++ b/typescript-helpers.js @@ -0,0 +1,36 @@ +// There is no no-global helpers available for TypeScript as now, +// check out this issue https://github.com/Microsoft/TypeScript/issues/3364. +// In order to avoid generating them for each ts-file, they are added here +// while TypeScript always runs with noEmitHelpers = true. +// It might be useful as well if we need to override some of them +// to support old browsers. + +__extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; + +__decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; + +__metadata = (this && this.__metadata) || function (k, v) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); +}; + +__param = (this && this.__param) || function (paramIndex, decorator) { + return function (target, key) { decorator(target, key, paramIndex); } +}; + +__awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments)).next()); + }); +}; diff --git a/typescript.js b/typescript.js deleted file mode 100644 index da2c4c6..0000000 --- a/typescript.js +++ /dev/null @@ -1,359 +0,0 @@ -'use strict'; - -var ts = Npm.require('typescript'); - -class DiagnosticMessage { - constructor(filePath, message, line, column) { - this.filePath = filePath; - this.message = message; - this.line = line; - this.column = column; - } - - get formattedMsg() { - return `${this.filePath} (${this.line}, ${this.column}): ${this.message}`; - } -} - -class CompilerDiagnostics { - constructor( - syntactic: DiagnosticMessage[], - semantic: DiagnosticMessage[] - ) { - this.syntactic = syntactic; - this.semantic = semantic; - } - - forEachSyntactic(callback: Function) { - check(callback, Function); - - this.syntactic.forEach(diagnostic => { - callback(diagnostic); - }); - } - - forEachSemantic(callback: Function) { - check(callback, Function); - - this.semantic.forEach(diagnostic => { - callback(diagnostic); - }); - } -} - -function assert(expression: Boolean, message?: String) { - if (!expression) { - message = message || '[TypeScript]: assert failure'; - throw new Error(message); - } -} - -// Normalizes file reference path to be relative to the root app path: -// 1) resolves every ../ in the front of the path -// 2) resolves every other ../ -function normalizeRef(refPath, filePath) { - let refDir = ts.getDirectoryPath(ts.normalizeSlashes(refPath)); - let fileDir = ts.getDirectoryPath(ts.normalizeSlashes(filePath)); - - // Split and remove empty strings. - let refParts = _.compact(refDir.split('/')); - let fileParts = _.compact(fileDir.split('/')).reverse(); - - let count = 0; - // Resolve every front ../ - for (let part of refParts) { - if (part !== '..' && part !== '.') { - break; - } - if (part === '..') { - fileParts.pop(); - } - count++; - } - - let resultPath = fileParts.reverse().concat( - refParts.slice(count)); - resultPath.push(ts.getBaseFileName(refPath)); - - // Resolve every other ../ - return ts.normalizePath(resultPath.join('/')); -} - -TypeScript = class TypeScript { - - // Transpiles Meteor plugin's file objects. - // To avoid holding compilation results in the memory, - // it executes a callback with the results on each file compiled. - // - // TODO: add exact type for the onFileReadyCallback. - // @param {Function} onFileReadyCallback - // Callback to be called with the result of file compilation. - static transpileFiles(files, options, onFileReadyCallback: Function) { - - assert(Match.test(options.filePath, Function), - '[TypeScript.transpileFiles]: options.filePath should be a function'); - - assert(Match.test(options.moduleName, Match.Optional(Function)), - '[TypeScript.transpileFiles]: options.moduleName should be a function'); - - TypeScript._transpileFiles(files, options, onFileReadyCallback); - } - - static transpile(fileContent, options) { - - assert(Match.test(options.filePath, String), - '[TypeScript.transpile]: options.filePath should be a string'); - - return TypeScript._transpile(fileContent, options); - } - - static getCompilerOptions(customOptions) { - let compilerOptions = ts.getDefaultCompilerOptions(); - - _.extend(compilerOptions, customOptions); - - // Support decorators by default. - compilerOptions.experimentalDecorators = true; - - // Declaration files are expected to - // be generated separately. - compilerOptions.declaration = false; - - // Overrides watching, - // it is handled by Meteor itself. - compilerOptions.watch = false; - - // We use source maps via Meteor file API, - // This class's API provides source maps - // separately but alongside compilation results. - // Hence, skip generating inline source maps. - compilerOptions.inlineSourceMap = false; - compilerOptions.inlineSources = false; - - // Always emit. - compilerOptions.noEmit = false; - compilerOptions.noEmitOnError = false; - - // Don't generate any files, hence, - // skip setting outDir and outFile. - compilerOptions.outDir = null; - compilerOptions.outFile = null; - - // This is not need as well. - // API doesn't have paramless methods. - compilerOptions.rootDir = null; - compilerOptions.sourceRoot = null; - - return compilerOptions; - } - - // 1) Normalizes slashes in the file path - // 2) Removes file extension - static normalizePath(filePath) { - var resultName = filePath; - if (ts.fileExtensionIs(resultName, '.map')) { - resultName = resultName.replace('.map', ''); - } - return ts.removeFileExtension( - ts.normalizeSlashes(resultName)); - } - - static isDeclarationFile(filePath) { - return filePath.match(/^.*\.d\.ts$/); - } - - static _transpileFiles(files, options, onFileReadyCallback) { - let compilerOptions = TypeScript.getCompilerOptions( - options.compilerOptions); - - let defaultHost = ts.createCompilerHost(compilerOptions); - - let fileMap = ts.createFileMap(TypeScript.normalizePath); - files.forEach(file => - fileMap.set(options.filePath(file), file)); - - let fileResultMap = ts.createFileMap(TypeScript.normalizePath); - - let customHost = { - getSourceFile: (fileName, target) => { - let file = fileMap.get(fileName); - if (!file) return defaultHost.getSourceFile(fileName, target); - let sourceFile = ts.createSourceFile(options.filePath(file), - file.getContentsAsString(), target); - - sourceFile.moduleName = options.moduleName && - options.moduleName(file); - return sourceFile; - } - }; - - let compilerHost = _.extend({}, defaultHost, customHost); - let fileNames = _.union( - files.map(file => options.filePath(file)), - options.typings || []); - let program = ts.createProgram(fileNames, compilerOptions, compilerHost); - - // Emit - program.emit(undefined, (fileName, outputText, writeByteOrderMark) => { - let file = fileMap.get(fileName); - if (!file) return; - - let fileResult = fileResultMap.get(fileName) || {}; - - if (ts.fileExtensionIs(fileName, '.map')) { - // Gets source map path as a module name - // in order to keep package prefix for - // files from a package. - let sourceMapPath = options.moduleName ? - options.moduleName(file) : options.filePath(file); - let sourceMap = TypeScript._prepareSourceMap( - outputText, file.getContentsAsString(), sourceMapPath); - fileResult.sourceMapPath = sourceMapPath; - fileResult.sourceMap = sourceMap; - } else { - fileResult.path = fileName; - fileResult.data = outputText; - } - - let isFileReady = fileResult.data && - (!compilerOptions.sourceMap || fileResult.sourceMap); - - if (isFileReady) { - fileResultMap.remove(fileName); - let diagnostics = TypeScript._readDiagnostics(program, - options.filePath(file)); - - let referencedPaths = []; - if (!compilerOptions.noResolve) { - // Source files are already processed, - // each one is retreived from the internal map here. - let sourceFile = customHost.getSourceFile(fileName); - let referencedPaths = !compilerOptions.noResolve ? - TypeScript._getReferencedPaths(sourceFile): []; - } - - onFileReadyCallback(file, referencedPaths, diagnostics, fileResult); - return; - } - - fileResultMap.set(fileName, fileResult); - }); - } - - static _transpile(fileContent, options) { - let compilerOptions = TypeScript.getCompilerOptions( - options.compilerOptions); - - let sourceFile = ts.createSourceFile(options.filePath, - fileContent, compilerOptions.target); - if (options.moduleName) { - sourceFile.moduleName = options.moduleName; - } - - let defaultHost = ts.createCompilerHost(compilerOptions); - - let customHost = { - getSourceFile: (fileName, target) => { - // We already have content of the target file, - // skip reading it again. - if (fileName === ts.normalizeSlashes(options.filePath)) { - return sourceFile; - } - return defaultHost.getSourceFile(fileName, target); - } - }; - - let compilerHost = _.extend({}, defaultHost, customHost); - let fileNames = _.union([options.filePath], options.typings || []); - let program = ts.createProgram(fileNames, compilerOptions, compilerHost); - - let data, sourceMap; - program.emit(sourceFile, (fileName, outputText, writeByteOrderMark) => { - if (TypeScript.normalizePath(fileName) !== - TypeScript.normalizePath(options.filePath)) return; - - if (ts.fileExtensionIs(fileName, '.map')) { - let sourceMapPath = options.moduleName ? - options.moduleName : options.filePath; - sourceMap = TypeScript._prepareSourceMap( - outputText, fileContent, sourceMapPath); - } else { - data = outputText; - } - }); - - let referencedPaths = !compilerOptions.noResolve ? - TypeScript._getReferencedPaths(sourceFile): []; - - let diagnostics = this._readDiagnostics(program, options.filePath); - - return { data, sourceMap, referencedPaths, diagnostics }; - } - - static _prepareSourceMap(sourceMapContent, fileContent, sourceMapPath) { - let sourceMapJson = JSON.parse(sourceMapContent); - sourceMapJson.sourcesContent = [fileContent]; - sourceMapJson.sources = [sourceMapPath]; - return sourceMapJson; - } - - static _getReferencedPaths(sourceFile) { - let referencedPaths = []; - - // Get resolved modules. - if (sourceFile.resolvedModules) { - for (let moduleName in sourceFile.resolvedModules) { - let module = sourceFile.resolvedModules[moduleName]; - if (module && module.resolvedFileName) { - referencedPaths.push(module.resolvedFileName); - } - } - } - - // Get declaration file references. - if (sourceFile.referencedFiles) { - let refFiles = sourceFile.referencedFiles.map((ref) => { - return ref.fileName; - }); - for (let path of refFiles) { - referencedPaths.push(normalizeRef(path, sourceFile.fileName)); - } - } - return referencedPaths; - } - - // TODO: try slitting semantic diagnostics into realted to unresolved modules - // and all other remaining. - static _readDiagnostics(program, filePath: String): CompilerDiagnostics { - let sourceFile; - if (filePath) { - sourceFile = program.getSourceFile(filePath); - } - - let syntactic = TypeScript._flattenDiagnostics( - program.getSyntacticDiagnostics(sourceFile)); - let semantic = TypeScript._flattenDiagnostics( - program.getSemanticDiagnostics(sourceFile)); - let diagnostics = new CompilerDiagnostics(syntactic, semantic); - - return diagnostics; - } - - static _flattenDiagnostics(tsDiagnostics: Array) { - let diagnostics: DiagnosticMessage[] = []; - - tsDiagnostics.forEach((diagnostic) => { - if (!diagnostic.file) return; - - let pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - let line = pos.line + 1; - let column = pos.character + 1; - - diagnostics.push( - new DiagnosticMessage(diagnostic.file.fileName, message, line, column)); - }); - - return diagnostics; - } -}