├── .clang-format ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json ├── src └── main.ts ├── test ├── e2e │ └── small_e2e.ts ├── input │ ├── a.ts │ ├── b.ts │ ├── e2e_input.ts │ ├── external_call_site.ts │ └── external_return.ts └── unit │ └── main_test.ts ├── tsconfig.json └── tsd.json /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Typescript Specific 30 | typings/ 31 | 32 | build/ 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | before_script: 5 | - npm install 6 | script: 7 | - npm test 8 | cache: 9 | directories: 10 | - node_modules 11 | - typings 12 | env: 13 | global: 14 | # https://github.com/DefinitelyTyped/tsd#tsdrc 15 | # Token has no scope (read-only access to public information) 16 | - TSD_GITHUB_TOKEN=15cc257af05b77cc99ce080a1ddfcceb6e012ce2 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TS-Minify (Experimental) [![Build Status](https://travis-ci.org/angular/ts-minify.svg)](https://travis-ci.org/angular/ts-minify) 2 | 3 | ### About 4 | 5 | --- 6 | TS-Minify is tool to aid in the reduction of code size for programs written in the [TypeScript](http://www.typescriptlang.org/) language. It is currently **highly experimental**. 7 | 8 | This tool is developed on TypeScript and NodeJS, and transpiled to ES5; it uses the CommonJS module system. 9 | 10 | **There is currently no CLI or build-tool integration for TS-Minify.** 11 | 12 | ### Table of Contents 13 | 14 | ---------- 15 | 16 | - [Motivation](#motivation) 17 | - [What it Does](#what-it-does) 18 | - [How Does it Work? The TL;DR](#how-does-it-work-the-tldr) 19 | - [External vs. Internal Types](#external-vs-internal-types) 20 | - [Structural Typing](#structural-typing) 21 | - [Usage](#usage) 22 | - [TSConfig Users](#tsconfig-users) 23 | - [Scope of Minification](#scope-of-minification) 24 | - [Caveats/Warnings](#caveatswarnings) 25 | - [Sample Measurements](#sample-measurements) 26 | - [Contributing](#contributing) 27 | - [Contributors](#contributors) 28 | - [License: Apache 2.0](#license-apache-20) 29 | - [Future](#future) 30 | 31 | ### Motivation 32 | 33 | --- 34 | Angular 2 (which is written in TypeScript) currently sits at around 135kb after being [Uglified](https://github.com/mishoo/UglifyJS) and compressed through GZIP. In comparison, Angular 1.4 is about 50kb, minified and compressed. 35 | 36 | A smaller bundle size means that less data needs to be transferred and loaded by the browser. This contributes to a better user experience. 37 | 38 | The impetus for this tool was to reduce the code size of the Angular 2 bundle, but TS-Minify is meant to be a generic tool that can be used on programs written in TypeScript. 39 | 40 | 41 | ### What it Does 42 | 43 | --- 44 | To achieve code size reduction, TS-Minify uses the idea of property renaming: take a TypeScript source file and rename properties to shorter property names, then re-emit the file as valid TypeScript. 45 | 46 | ![TS-Minify role in minification pipeline](http://i.imgur.com/7iH4RyF.png) This diagram demonstrates TS-Minify's role in the intended minification pipeline. 47 | 48 | TS-Minify *only* targets property renaming. Minification tactics like whitespace removal, dead code removal, variable name mangling, etc. are taken care of by tools such as [UglifyJS](https://github.com/mishoo/UglifyJS). TS-Minify specifically targets property renaming since it is something that is difficult to achieve *safely* without type information. As such, TS-Minify requires a program to be correctly and throughly typed, otherwise, unwanted renaming may occur. 49 | 50 | ### How Does it Work? The TL;DR 51 | 52 | --- 53 | The TypeScript Compiler API is utilized in order to access the Abstract Syntax Tree of a TypeScript source file. Identifiers that might be considered properties (such as identifiers in property declarations, property access expressions, method names in method declarations, etc.) are renamed to short names such as `a`, `b`, etc. after determining their renaming eligibility. A “renaming” global map is created with mappings from the original property name to the new generated property name so that re-namings are kept consistent between properties with the same names. 54 | 55 | ```javascript 56 | { maybeHandleCall: g } 57 | { reportMissingType: h } 58 | { getHandler: i } 59 | { handlePropertyAccess: j } 60 | { emitExtraImports: k } 61 | { emit: l } 62 | { visitTypeName: m } 63 | ``` 64 | An example of some mappings from the original property name to the shorter, generated property name. 65 | 66 | The renaming eligibility of a property takes several factors into consideration: 67 | 68 | - Does the property belong to an object declared in an external file ([`.d.ts file`](http://definitelytyped.org/))? 69 | - Does the property belong to a [standard built-in object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects)? 70 | - Does the property name belong to the DOM? 71 | 72 | ### External vs. Internal Types 73 | 74 | --- 75 | TS-Minify is able to safely rename properties by understanding which types are *external*, and which are not, to one's program. 76 | 77 | An *external* type is something that you are bringing into your program. It is a type which you have not defined. This includes objects and functions from the JavaScript standard library, and JavaScript libraries like underscore.js or jQuery. If you are using external libraries, it is imperative that you include their corresponding `.d.ts` files. 78 | 79 | Typings for the standard library are included in the default library typings, `lib.d.ts`, which the TypeScript compiler uses during transpilation (it will typecheck your program at compile time) and does not need to be explicitly included. 80 | 81 | *Internal* types are ones that you, the programmer, have defined yourselves. Declaring a class `Foo` in your program tells the minifier that properties on `Foo` objects are within the renaming scope. Internal types also include things like object literal types. 82 | 83 | **Note**: If you want all internal sources to contain the same renamings, you should pass the relevant files together through a single pass of the minifier. 84 | 85 | An example of what types are considered *external* and *internal*: 86 | 87 | ```javascript 88 | var x: { name: string, message: string }; // The object literal is an internal type 89 | 90 | var x: Error; // Error is an external type 91 | 92 | // Structurally, the two are compatible, which brings us to the next section... 93 | ``` 94 | 95 | ### Structural Typing 96 | 97 | --- 98 | TypeScript uses a [structural type system](https://en.wikipedia.org/wiki/Structural_type_system). This means that objects and variables can be casted from one type to another as long as they are structurally compatible. Sometimes this means that external objects might be casted to internal objects, or vise versa. 99 | 100 | Here is an example of an external type being casted to an internal type: 101 | 102 | ```javascript 103 | function x(foo: { name: string, message: string }) { 104 | foo.name; 105 | return foo; 106 | } 107 | 108 | x(new Error()); 109 | ``` 110 | 111 | Function `x` *expects* an internal object literal type. The call site `x(new Error())` passes an `Error` object as the parameter. 112 | 113 | [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) is an object in the JavaScript standard library. Hence it is external to our small program above. Structurally, an `Error` object and the object literal `{ name: string, message: string }` are compatible. However, this means that we do not want to rename the properties on the object literal because we will likely want to access the properties of the `Error` object as we know them: `name` and `message`. If we renamed the properties of the object literal, it means that if an `Error` object is passed into a function call for `x()`, `foo.$some_renaming` will throw an error, because `Error` does not have property `$some_renaming`. 114 | 115 | Here is an example of an internal type being coerced into an external type: 116 | 117 | ```javascript 118 | function ff(e: Error) { 119 | return e.name; 120 | } 121 | 122 | ff({ name: null, message: null }); 123 | ``` 124 | 125 | The parameter `e` on function `ff` is an external type to our program. We do not want to rename its properties. At the function call site, we are passing in an object literal type with properties `name` and `message`, which is structurally compatible with `Error`. We do not want to rename properties in the object literal because we need to coerce it into an external typing, which is expected to have properties `name` and `message`. 126 | 127 | All of this is to say, the tool relies on type information to figure out if two types can be coerced into each other and whether their properties can be renamed based on the internal/external distinction. Type your programs! :) 128 | 129 | ### Usage 130 | 131 | --- 132 | 133 | First clone the repo and run `npm install`. 134 | 135 | ```shell 136 | $ git clone git@github.com:angular/ts-minify.git 137 | $ cd ts-minify 138 | $ npm install 139 | ``` 140 | 141 | Create a new TypeScript file: 142 | 143 | ```javascript 144 | import {Minifier, options} from './src/main'; 145 | var minifier = new Minifier(); 146 | minifier.renameProgram(['path/to/file.ts', 'path/to/another/file.ts', 'some/other/file.d.ts'], 'path/to/destination'); 147 | ``` 148 | 149 | Transpile the above script into JavaScript using the TypeScript compiler and run it with Node: 150 | 151 | ```shell 152 | tsc --module commonjs script.ts 153 | node script.js 154 | ``` 155 | 156 | A new instance of the minifier takes an optional constructor argument: 157 | 158 | ```javascript 159 | MinifierOptions { 160 | failFast?: boolean; 161 | basePath?: string; 162 | } 163 | ``` 164 | 165 | `failFast`: Setting `failFast` to true will throw an error when the minifier hits one. 166 | 167 | `basePath`: Specifies the base path that the minifier uses to figure out to where to write the minfied TypeScript file. The `basePath` maybe be relative or absolute. All paths are resolved from the directory in which the minifier is executed. If there is no base path, the minified TypeScript file is outputted to the specified destination using a flattened file structure. 168 | 169 | `minifier.renameProgram()` takes a list of file names and an optional destination path: 170 | 171 | ```javascript 172 | renameProgram(fileNames: string[], destination?: string) 173 | ``` 174 | 175 | `renameProgram` accepts TypeScript files and `.d.ts` files. If you are not explicitly including typings using the reference path syntax in your TypeScript file, please pass in the `.d.ts` file so that the minifier can use the type information. 176 | 177 | The minifier will uniformly rename the properties (which are available for renaming) across all the files that were passed in. 178 | 179 | ### TSConfig Users 180 | 181 | --- 182 | If you are using a `tsconfig.json` file to reference your type definitions, pass in the `.d.ts` files for libraries used in your program to the minifier so that it can reference the type information for property renaming. The minfier does not have access to the `tsconfig.json` file. Otherwise, make sure to have `/// ` at the top of your programs if you are using any external libraries. 183 | 184 | ### Scope of Minification 185 | 186 | --- 187 | Currently, TS-Minify will do property renaming throughout all the files that are passed into it. The minifier excludes `.d.ts` files from renaming because those are the typing definitions of external libraries. If your TypeScript program exports objects across those files, the minifier will rename them uniformly in their declarations and their usage sites. 188 | 189 | ### Caveats/Warnings 190 | 191 | --- 192 | In order to get the most out of the minifier, it is recommended that the user types their program as specifically and thoroughly as possible. In general, you should avoid using the `any` type, and implicit `any`s in your code as this might result in unwanted naming. 193 | 194 | We recommend that you compile your TypeScript program with the `noImplicitAny` compiler flag enabled before trying the minifier. 195 | 196 | Please avoid explicit any casting: 197 | 198 | ```javascript 199 | // example of explicit any casting 200 | var x = 7; 201 | x; 202 | ``` 203 | 204 | ### Sample Measurements 205 | 206 | --- 207 | TS-Minify was run on a well-typed TypeScript program with the following stats and results: 208 | 209 | - Approximately 2100 lines of code (split across 10 files) 210 | - Unminified and Uglified: 72kb 211 | - Minified and Uglified: 56kb (codesize reduction of about 20%) 212 | - About 6% - 8% codesize reduction after minification, Uglification, and GZIP compression. 213 | 214 | ### Contributing 215 | 216 | --- 217 | Clone the repository from GitHub: 218 | 219 | ```shell 220 | $ git clone git@github.com:angular/ts-minify.git 221 | $ cd ts-minify 222 | $ npm install 223 | ``` 224 | 225 | Run the unit tests with `gulp unit.test` 226 | 227 | Run the end-to-end tests with `gulp e2e.test`. 228 | 229 | This project uses `clang-format`. Run `gulp test.check-format` to make sure code you write conforms to this standard. 230 | 231 | - If you need some guidance on understanding TypeScript, look at the [TypeScript GitHub Wiki](https://github.com/Microsoft/TypeScript/wiki). 232 | - If you need a quick introduction to the TS Compiler API, take a look at the page on using the [TS Compiler](https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API) API. 233 | - Take a look at the `typescript.d.ts` for type signatures, and to understand what is available to you from the TS toolchain. 234 | 235 | If you need some help debugging, there is a `DEBUG` flag that can be enabled in `src/main.ts`: 236 | 237 | ```javascript 238 | const DEBUG = true; // switch from false to true to enable the console.logs 239 | ``` 240 | 241 | There are some helpful print statements which print out: 242 | 243 | - the `SyntaxKind` of the nodes being traversed 244 | - the minified string output 245 | - a dictionary of external/internal type casts 246 | 247 | Remember to switch the `DEBUG` flag off afterwards. 248 | 249 | ### Contributors 250 | 251 | --- 252 | Daria Jung (Google intern) 253 | 254 | 255 | ### License: Apache 2.0 256 | 257 | --- 258 | ``` 259 | Copyright 2015 Google, Inc. http://angular.io 260 | 261 | Licensed under the Apache License, Version 2.0 (the "License"); 262 | you may not use this file except in compliance with the License. 263 | You may obtain a copy of the License at 264 | 265 | http://www.apache.org/licenses/LICENSE-2.0 266 | 267 | Unless required by applicable law or agreed to in writing, software 268 | distributed under the License is distributed on an "AS IS" BASIS, 269 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 270 | See the License for the specific language governing permissions and 271 | limitations under the License. 272 | ``` 273 | ### Future 274 | 275 | --- 276 | 277 | Property renaming is hard and there are a lot of [issues](https://github.com/angular/ts-minify/issues)! If you're interesting in contributing to this project, please check some of them out. 278 | 279 | Issues are labelled `easy`, `medium`, and `hard`. Some have a `Needs Exploration` label which means you might have to poke around a bit before reaching conclusions about how to tackle the problem! 280 | 281 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var mocha = require('gulp-mocha'); 3 | var typescript = require('typescript'); 4 | var tsc = require('gulp-typescript'); 5 | var del = require('del'); 6 | var exec = require('child_process').exec; 7 | 8 | var clangFormat = require('clang-format'); 9 | var formatter = require('gulp-clang-format'); 10 | 11 | var TSC_OPTIONS = { 12 | module: "commonjs", 13 | noExternalResolve: false, 14 | declarationFiles: true, 15 | typescript: require('typescript'), 16 | }; 17 | 18 | var tsProject = tsc.createProject(TSC_OPTIONS); 19 | 20 | gulp.task('test.check-format', function() { 21 | return gulp.src(['*.js', 'src/**/*.ts', 'test/**/*.ts']) 22 | .pipe(formatter.checkFormat('file', clangFormat)); 23 | }); 24 | 25 | gulp.task('clean', function(callback) { del(['./build/'], callback); }); 26 | 27 | gulp.task('compile', function() { 28 | var sourceTsFiles = ['./src/*.ts']; 29 | 30 | var tsResult = gulp.src(sourceTsFiles).pipe(tsc(tsProject)); 31 | 32 | tsResult.dts.pipe(gulp.dest('./build/src')); 33 | return tsResult.js.pipe(gulp.dest('./build/src')); 34 | }); 35 | 36 | gulp.task('test.compile', ['compile'], function(done) { 37 | return gulp.src(['test/**/*.ts'], {base: '.'}).pipe(tsc(tsProject)).pipe(gulp.dest('build/')); 38 | }); 39 | 40 | gulp.task('unit.test', ['test.compile'], function() { 41 | var mochaOptions = {}; 42 | return gulp.src('build/test/unit/main_test.js', {read: false}).pipe(mocha(mochaOptions)); 43 | }); 44 | 45 | gulp.task('e2e.test', ['test.compile'], function() { 46 | var e2e = require('./build/test/e2e/small_e2e'); 47 | e2e.runE2ETests(); 48 | }); 49 | 50 | gulp.task('watch', function() { 51 | gulp.watch(['./src/*.ts', './test/**/*.ts'], ['compile', 'test.compile', 'unit.test', 'e2e.test']); 52 | }); 53 | 54 | gulp.task('default', ['compile', 'unit.test', 'e2e.test', 'watch']); 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-minify", 3 | "version": "0.0.1", 4 | "description": "A tool to aid in the minification of Typescript code.", 5 | "repository": "https://github.com/angular/ts-minify", 6 | "license": "Apache-2.0", 7 | "scripts": { 8 | "prepublish": "npm install tsd@^0.6.3 && tsd reinstall --overwrite", 9 | "test": "gulp unit.test" 10 | }, 11 | "dependencies": { 12 | "typescript": "alexeagle/TypeScript#error_is_class", 13 | "gulp": ">=3.9.0", 14 | "fs-extra": "^0.23.0" 15 | }, 16 | "devDependencies": { 17 | "clang-format": "^1.0.25", 18 | "gulp-clang-format": "^1.0.21", 19 | "gulp-mocha": "^2.1.1", 20 | "chai": "*", 21 | "mocha": "*", 22 | "gulp-util": ">=3.0.6", 23 | "gulp-typescript": "^2.8.0", 24 | "del": ">=1.2.0", 25 | "tsd": "^0.6.3" 26 | } 27 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import * as ts from 'typescript'; 6 | import * as path from 'path'; 7 | import * as fsx from 'fs-extra'; 8 | import * as fs from 'fs'; 9 | 10 | const DEBUG = false; 11 | 12 | export const options: ts.CompilerOptions = { 13 | allowNonTsExtensions: true, 14 | module: ts.ModuleKind.CommonJS, 15 | target: ts.ScriptTarget.ES5, 16 | }; 17 | 18 | export interface MinifierOptions { 19 | failFast?: boolean; 20 | basePath?: string; 21 | } 22 | 23 | export class Minifier { 24 | static reservedJSKeywords = Minifier.buildReservedKeywordsMap(); 25 | // Key: (Eventually fully qualified) original property name 26 | // Value: new generated property name 27 | private _renameMap: {[name: string]: string} = {}; 28 | 29 | // Key: Type symbol at actual use sites (from) 30 | // Value: A list of the expected type symbol (to) 31 | private _typeCasting: Map = >(new Map()); 32 | private _reverseTypeCasting: Map = 33 | >(new Map()); 34 | 35 | private _lastGeneratedPropName: string = ''; 36 | private _typeChecker: ts.TypeChecker; 37 | private _errors: string[] = []; 38 | 39 | constructor(private _minifierOptions: MinifierOptions = {}) {} 40 | 41 | checkForErrors(program: ts.Program) { 42 | var errors = []; 43 | var emitResult = program.emit(); 44 | var allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); 45 | 46 | allDiagnostics.forEach(diagnostic => { 47 | if (diagnostic.file && !diagnostic.file.fileName.match(/\.d\.ts$/)) { 48 | var {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 49 | var message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 50 | errors.push(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); 51 | } 52 | }); 53 | 54 | if (errors.length > 0) { 55 | throw new Error( 56 | 'Malformed TypeScript: Please check your source-files before attempting to use ts-minify.\n' + 57 | errors.join('\n')); 58 | } 59 | } 60 | 61 | setTypeChecker(typeChecker: ts.TypeChecker) { this._typeChecker = typeChecker; } 62 | 63 | reportError(n: ts.Node, message: string) { 64 | var file = n.getSourceFile(); 65 | var fileName = file.fileName; 66 | var start = n.getStart(file); 67 | var pos = file.getLineAndCharacterOfPosition(start); 68 | var fullMessage = `${fileName}:${pos.line + 1}:${pos.character + 1}: ${message}`; 69 | this._errors.push(fullMessage); 70 | if (this._minifierOptions.failFast) { 71 | throw new Error(fullMessage); 72 | } 73 | } 74 | 75 | // This method assumes that the user has taken care of getting and setting the TypeChecker for 76 | // an instance of the Minifier class. 77 | // 78 | // Example: 79 | // var minifier = new Minifier(); 80 | // var typeChecker = program.getTypeChecker(); 81 | // var sourceFile = program.getSourceFile(); 82 | // minifier.setTypeChecker(typeChecker); 83 | renameProgramFromNode(node: ts.Node): string { 84 | this._preprocessVisit(node); 85 | return this._visit(node); 86 | } 87 | 88 | // Renaming goes through a pre-processing step and an emitting step. 89 | renameProgram(fileNames: string[], destination?: string) { 90 | var host = ts.createCompilerHost(options); 91 | var program = ts.createProgram(fileNames, options, host); 92 | this._typeChecker = program.getTypeChecker(); 93 | 94 | let sourceFiles = program.getSourceFiles().filter((sf) => !sf.fileName.match(/\.d\.ts$/)); 95 | 96 | sourceFiles.forEach((f) => { this._preprocessVisit(f); }); 97 | 98 | if (DEBUG) { 99 | this._typeCasting.forEach((value, key) => { 100 | value.forEach((val) => console.log('cast type from: ' + key.name, 'to: ' + val.name)); 101 | }); 102 | } 103 | 104 | sourceFiles.forEach((f) => { 105 | var renamedTSCode = this._visit(f); 106 | var fileName = this.getOutputPath(f.fileName, destination); 107 | if (DEBUG) { 108 | console.log(renamedTSCode); 109 | } 110 | fsx.mkdirsSync(path.dirname(fileName)); 111 | fs.writeFileSync(fileName, renamedTSCode); 112 | }); 113 | } 114 | 115 | getOutputPath(filePath: string, destination: string = '.'): string { 116 | destination = path.resolve(process.cwd(), destination); 117 | var absFilePath = path.resolve(process.cwd(), filePath); 118 | 119 | // no base path, flatten file structure and output to destination 120 | if (!this._minifierOptions.basePath) { 121 | return path.join(destination, path.basename(filePath)); 122 | } 123 | 124 | this._minifierOptions.basePath = path.resolve(process.cwd(), this._minifierOptions.basePath); 125 | 126 | // given a base path, preserve file directory structure 127 | var subFilePath = absFilePath.replace(this._minifierOptions.basePath, ''); 128 | 129 | if (subFilePath === absFilePath) { 130 | return path.join(destination, filePath); 131 | } 132 | 133 | return path.join(destination, subFilePath); 134 | } 135 | 136 | isExternal(symbol: ts.Symbol): boolean { 137 | // TODO: figure out how to deal with undefined symbols 138 | // (ie: in case of string literal, or something like true.toString(), 139 | // the TypeScript typechecker will give an undefined symbol) 140 | if (!symbol) return true; 141 | 142 | return symbol.declarations.some((decl) => !!(decl.getSourceFile().fileName.match(/\.d\.ts/))); 143 | } 144 | 145 | isRenameable(symbol: ts.Symbol): boolean { 146 | if (this.isExternal(symbol)) return false; 147 | if (!this._typeCasting.has(symbol) && !this._reverseTypeCasting.has(symbol)) return true; 148 | 149 | let boolArrTypeCasting: boolean[] = []; 150 | let boolRevArrTypeCasting: boolean[] = []; 151 | 152 | // Three cases to consider: 153 | // CANNOT RENAME: Use site passes an internally typed object, expected site wants an externally 154 | // typed object OR use site passes externally typed object, but expected site wants an 155 | // internally typed object 156 | // CAN RENAME: Use site type symbols are internal, expected type symbols are internal 157 | // ERROR: Expected symbol is external, use sites are both internal and external 158 | 159 | // Create boolean array of whether or not actual sites (type to which a symbol is being cast) 160 | // are internal 161 | if (this._typeCasting.has(symbol)) { 162 | for (let castType of this._typeCasting.get(symbol)) { 163 | boolArrTypeCasting.push(!this.isExternal(castType)); 164 | } 165 | 166 | // Check if there are both true and false values in boolArrTypeCasting, throw Error 167 | if (boolArrTypeCasting.indexOf(true) >= 0 && boolArrTypeCasting.indexOf(false) >= 0) { 168 | throw new Error( 169 | 'ts-minify does not support accepting both internal and external types at a use site\n' + 170 | 'Symbol name: ' + symbol.getName()); 171 | } 172 | } 173 | 174 | // REVERSE 175 | if (this._reverseTypeCasting.has(symbol)) { 176 | for (let castType of this._reverseTypeCasting.get(symbol)) { 177 | boolRevArrTypeCasting.push(!this.isExternal(castType)); 178 | } 179 | 180 | // Check if there are both true and false values in boolArrTypeCasting, throw Error 181 | if (boolRevArrTypeCasting.indexOf(true) >= 0 && boolRevArrTypeCasting.indexOf(false) >= 0) { 182 | throw new Error( 183 | 'ts-minify does not support accepting both internal and external types at a use site\n' + 184 | 'Symbol name: ' + symbol.getName()); 185 | } 186 | } 187 | 188 | if (boolArrTypeCasting.length === 0) { 189 | return boolRevArrTypeCasting[0]; 190 | } 191 | 192 | if (boolRevArrTypeCasting.length === 0) { 193 | return boolArrTypeCasting[0]; 194 | } 195 | 196 | // Since all values in boolArrayTypeCasting are all the same value, just return the first value 197 | return boolArrTypeCasting[0] && boolRevArrTypeCasting[0]; 198 | } 199 | 200 | private _getAncestor(n: ts.Node, kind: ts.SyntaxKind): ts.Node { 201 | for (var parent = n; parent; parent = parent.parent) { 202 | if (parent.kind === kind) return parent; 203 | } 204 | return null; 205 | } 206 | 207 | private _preprocessVisitChildren(node: ts.Node) { 208 | node.getChildren().forEach((child) => { this._preprocessVisit(child); }); 209 | } 210 | 211 | // Key - To: the expected type symbol 212 | // Value - From: the actual type symbol 213 | // IE: Coercing from type A to type B 214 | private _recordCast(from: ts.Symbol, to: ts.Symbol) { 215 | if (!from || !to) return; 216 | if (this._typeCasting.has(from)) { 217 | this._typeCasting.get(from).push(to); 218 | } else { 219 | this._typeCasting.set(from, [to]); 220 | } 221 | 222 | if (this._reverseTypeCasting.has(to)) { 223 | this._reverseTypeCasting.get(to).push(from); 224 | } else { 225 | this._reverseTypeCasting.set(to, [from]); 226 | } 227 | } 228 | 229 | // The preprocessing step is necessary in order to to find all typecasts (explicit and implicit) 230 | // in the given source file(s). During the visit step (where renaming and emitting occurs), 231 | // the information gathered from this step are used to figure out which types are internal to the 232 | // scope that the minifier is working with and which are external. This allows the minifier to 233 | // rename properties more correctly. 234 | private _preprocessVisit(node: ts.Node) { 235 | switch (node.kind) { 236 | case ts.SyntaxKind.CallExpression: { 237 | var callExpr = node; 238 | var lhsSymbol = this._typeChecker.getSymbolAtLocation(callExpr.expression); 239 | 240 | let paramSymbols: ts.Symbol[] = []; 241 | 242 | // TODO: understand cases of multiple declarations, pick first declaration for now 243 | if (!lhsSymbol || !((lhsSymbol.declarations[0]).parameters)) { 244 | this._preprocessVisitChildren(node); 245 | break; 246 | } else { 247 | (lhsSymbol.declarations[0]) 248 | .parameters.forEach((param) => { 249 | if (param.type && param.type.typeName) { 250 | let paramSymbol = 251 | this._typeChecker.getTypeAtLocation( 252 | (param.type).typeName) 253 | .symbol; 254 | paramSymbols.push(paramSymbol); 255 | } else if (param.type) { 256 | let paramSymbol = this._typeChecker.getTypeAtLocation(param).symbol; 257 | paramSymbols.push(paramSymbol); 258 | } 259 | 260 | }); 261 | 262 | let argsSymbols: ts.Symbol[] = []; 263 | 264 | // right hand side argument has actual type of parameter 265 | callExpr.arguments.forEach( 266 | (arg) => { argsSymbols.push(this._typeChecker.getTypeAtLocation(arg).symbol); }); 267 | 268 | // Casting from: Use site symbol, to: expected parameter type 269 | paramSymbols.forEach((sym, i) => { this._recordCast(argsSymbols[i], sym); }); 270 | 271 | this._preprocessVisitChildren(node); 272 | break; 273 | } 274 | } 275 | case ts.SyntaxKind.VariableDeclaration: { 276 | let varDecl = node; 277 | if (varDecl.initializer && varDecl.type) { 278 | let varDeclTypeSymbol = this._typeChecker.getTypeAtLocation(varDecl.type).symbol; 279 | let initTypeSymbol = this._typeChecker.getTypeAtLocation(varDecl.initializer).symbol; 280 | 281 | // Casting from: initializer's type symbol, to: actual variable declaration's annotated 282 | // type 283 | this._recordCast(initTypeSymbol, varDeclTypeSymbol); 284 | } 285 | this._preprocessVisitChildren(node); 286 | break; 287 | } 288 | case ts.SyntaxKind.ReturnStatement: { 289 | // check if there is an expression on the return statement since it's optional 290 | if (node.parent.kind !== ts.SyntaxKind.SourceFile && 291 | (node).expression) { 292 | let symbolReturn = 293 | this._typeChecker.getTypeAtLocation((node).expression).symbol; 294 | 295 | let methodDeclAncestor = this._getAncestor(node, ts.SyntaxKind.MethodDeclaration); 296 | let funcDeclAncestor = this._getAncestor(node, ts.SyntaxKind.FunctionDeclaration); 297 | 298 | // early exit if no ancestor that is method or function declaration 299 | if (!methodDeclAncestor && !funcDeclAncestor) { 300 | this._preprocessVisitChildren(node); 301 | break; 302 | } 303 | 304 | let funcLikeDecl = (methodDeclAncestor || funcDeclAncestor); 305 | 306 | if (!funcLikeDecl.type) { 307 | this._preprocessVisitChildren(node); 308 | break; 309 | } 310 | 311 | if (funcLikeDecl.type && (funcLikeDecl.type).typeName) { 312 | let funcLikeDeclSymbol = this._typeChecker.getSymbolAtLocation( 313 | (funcLikeDecl.type).typeName); 314 | // Casting from: return expression's type symbol, to: actual function/method 315 | // declaration's return type 316 | this._recordCast(symbolReturn, funcLikeDeclSymbol); 317 | } 318 | } 319 | 320 | this._preprocessVisitChildren(node); 321 | break; 322 | } 323 | default: { 324 | this._preprocessVisitChildren(node); 325 | break; 326 | } 327 | } 328 | } 329 | 330 | // Recursively visits every child node, emitting text of the sourcefile that is not a part of 331 | // a child node. 332 | private _visit(node: ts.Node): string { 333 | switch (node.kind) { 334 | case ts.SyntaxKind.PropertyAccessExpression: { 335 | let pae = node; 336 | let exprSymbol = this._getExpressionSymbol(pae); 337 | let output = ''; 338 | let children = pae.getChildren(); 339 | 340 | output += this._visit(pae.expression); 341 | output += pae.dotToken.getFullText(); 342 | 343 | // if LHS is a module, do not rename property name 344 | var lhsTypeSymbol = this._typeChecker.getTypeAtLocation(pae.expression).symbol; 345 | var lhsIsModule = lhsTypeSymbol && ts.SymbolFlags.ValueModule === lhsTypeSymbol.flags; 346 | 347 | // Early exit when exprSymbol is undefined. 348 | if (!exprSymbol) { 349 | this.reportError(pae.name, 'Symbol information could not be extracted.\n'); 350 | return; 351 | } 352 | 353 | var isExternal = this.isExternal(exprSymbol); 354 | if (!this.isRenameable(lhsTypeSymbol) || isExternal || lhsIsModule) { 355 | return output + this._ident(pae.name); 356 | } 357 | return output + this._renameIdent(pae.name); 358 | } 359 | // TODO: A parameter property will need to also be renamed in the 360 | // constructor body if the parameter is used there. 361 | // Look at Issue #39 for an example. 362 | case ts.SyntaxKind.Parameter: { 363 | var paramDecl = node; 364 | 365 | // if there are modifiers, then we know this is a declaration and an initialization at once 366 | // we need to rename the property 367 | if (this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Public) || 368 | this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Private) || 369 | this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Protected)) { 370 | return this.contextEmit(node, true); 371 | } 372 | return this.contextEmit(node); 373 | } 374 | case ts.SyntaxKind.PropertySignature: { 375 | if (node.parent.kind === ts.SyntaxKind.TypeLiteral || 376 | node.parent.kind === ts.SyntaxKind.InterfaceDeclaration) { 377 | let parentSymbol = this._typeChecker.getTypeAtLocation(node.parent).symbol; 378 | let rename = this.isRenameable(parentSymbol); 379 | return this.contextEmit(node, rename); 380 | } 381 | return this.contextEmit(node); 382 | } 383 | // All have same wanted behavior. 384 | case ts.SyntaxKind.MethodDeclaration: 385 | case ts.SyntaxKind.PropertyAssignment: 386 | case ts.SyntaxKind.PropertyDeclaration: { 387 | let parentTypeSymbol = this._typeChecker.getTypeAtLocation(node.parent).symbol; 388 | let renameable = this.isRenameable(parentTypeSymbol); 389 | return this.contextEmit(node, renameable); 390 | } 391 | default: { return this.contextEmit(node); } 392 | } 393 | } 394 | 395 | // if renameIdent is true, rename children identifiers in this node 396 | private contextEmit(node: ts.Node, renameIdent: boolean = false) { 397 | // The indicies of nodeText range from 0 ... nodeText.length - 1. However, the start and end 398 | // positions of nodeText that .getStart() and .getEnd() return are relative to 399 | // the entire sourcefile. 400 | let nodeText = node.getFullText(); 401 | let children = node.getChildren(); 402 | let output = ''; 403 | // prevEnd is used to keep track of how much of nodeText has been copied over. It is updated 404 | // within the for loop below, and text from nodeText(0, prevEnd), including children text 405 | // that fall within the range, has already been copied over to output. 406 | let prevEnd = 0; 407 | let nameChildNode = (node).name; 408 | // Loop-invariant: prevEnd should always be less than or equal to the start position of 409 | // an unvisited child node because the text before a child's text must be copied over to 410 | // the new output before anything else. 411 | children.forEach((child) => { 412 | // The start and end positions of the child's text must be updated so that they 413 | // are relative to the indicies of the parent's text range (0 ... nodeText.length - 1), by 414 | // off-setting by the value of the parent's start position. Now childStart and childEnd 415 | // are relative to the range of (0 ... nodeText.length). 416 | let childStart = child.getFullStart() - node.getFullStart(); 417 | let childEnd = child.getEnd() - node.getFullStart(); 418 | output += nodeText.substring(prevEnd, childStart); 419 | let childText = ''; 420 | if (renameIdent && child === nameChildNode && child.kind === ts.SyntaxKind.Identifier) { 421 | childText = this._renameIdent(child); 422 | } else { 423 | childText = this._visit(child); 424 | } 425 | output += childText; 426 | prevEnd = childEnd; 427 | }); 428 | output += nodeText.substring(prevEnd, nodeText.length); 429 | return output; 430 | } 431 | 432 | // n: modifiers array, flag: the flag we are looking for 433 | private hasFlag(n: {flags: number}, flag: ts.NodeFlags): boolean { 434 | return n && (n.flags & flag) !== 0; 435 | } 436 | 437 | private _getExpressionSymbol(node: ts.PropertyAccessExpression) { 438 | let exprSymbol = this._typeChecker.getSymbolAtLocation(node.name); 439 | // Sometimes the RHS expression does not have a symbol, so use the symbol at the property access 440 | // expression 441 | if (!exprSymbol) { 442 | exprSymbol = this._typeChecker.getSymbolAtLocation(node); 443 | } 444 | return exprSymbol; 445 | } 446 | 447 | // rename the identifier, but retain comments/spacing since we are using getFullText(); 448 | private _renameIdent(node: ts.Node) { 449 | let fullText = node.getFullText(); 450 | let fullStart = node.getFullStart(); 451 | let regStart = node.getStart() - fullStart; 452 | let preIdent = fullText.substring(0, regStart); 453 | return preIdent + this.renameProperty(node.getText()); 454 | } 455 | 456 | private _ident(node: ts.Node) { return node.getFullText(); } 457 | 458 | // Alphabet: ['$', '_','0' - '9', 'a' - 'z', 'A' - 'Z']. 459 | // Generates the next char in the alphabet, starting from '$', 460 | // and ending in 'Z'. If nextChar is passed in 'Z', it will 461 | // start over from the beginning of the alphabet and return '$'. 462 | private _nextChar(str: string): string { 463 | switch (str) { 464 | case '$': 465 | return '_'; 466 | case '_': 467 | return '0'; 468 | case '9': 469 | return 'a'; 470 | case 'z': 471 | return 'A'; 472 | case 'Z': 473 | return '$'; 474 | default: 475 | return String.fromCharCode(str.charCodeAt(0) + 1); 476 | } 477 | } 478 | 479 | static buildReservedKeywordsMap(): {[name: string]: boolean} { 480 | var map: {[name: string]: boolean} = {}; 481 | // From MDN's Lexical Grammar page 482 | // (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar) 483 | var keywordList = 484 | ('break case class catch const continue debugger delete do else export extends finally for' + 485 | ' function if import in instanceof let new return super switch this throw try typeof var' + 486 | ' void while with yield enum await int byte char goto long final float short double' + 487 | ' native throws boolean abstract volatile transient synchronized') 488 | .split(' '); 489 | for (var i in keywordList) { 490 | map[keywordList[i]] = true; 491 | } 492 | return map; 493 | } 494 | 495 | private _checkReserved(str: string): boolean { 496 | return Minifier.reservedJSKeywords.hasOwnProperty(str); 497 | } 498 | 499 | renameProperty(name: string): string { 500 | if (!this._renameMap.hasOwnProperty(name)) { 501 | this._renameMap[name] = this.generateNextPropertyName(this._lastGeneratedPropName); 502 | } 503 | return this._renameMap[name]; 504 | } 505 | 506 | // Given the last code, returns a string for the new property name. 507 | // ie: given 'a', will return 'b', given 'az', will return 'aA', etc. ... 508 | // public so it is visible for testing 509 | generateNextPropertyName(code: string): string { 510 | var newName = this._generateNextPropertyNameHelper(code); 511 | this._lastGeneratedPropName = newName; 512 | return newName; 513 | } 514 | 515 | private _generateNextPropertyNameHelper(code: string) { 516 | var chars = code.split(''); 517 | var len: number = code.length; 518 | var firstChar = '$'; 519 | var lastChar = 'Z'; 520 | var firstAlpha = 'a'; 521 | 522 | if (len === 0) { 523 | return firstChar; 524 | } 525 | 526 | for (var i = len - 1; i >= 0; i--) { 527 | if (chars[i] !== lastChar) { 528 | chars[i] = this._nextChar(chars[i]); 529 | break; 530 | } else { 531 | chars[i] = firstChar; 532 | if (i === 0) { 533 | let newName = firstChar + (chars.join('')); 534 | return newName; 535 | } 536 | } 537 | } 538 | var newName = chars.join(''); 539 | if (this._checkReserved(newName)) { 540 | return this.generateNextPropertyName(newName); 541 | // Property names cannot start with a number. Generate next possible property name that starts 542 | // with the first alpha character. 543 | } else if (chars[0].match(/[0-9]/)) { 544 | return (firstAlpha + Array(len).join(firstChar)); 545 | } else { 546 | return newName; 547 | } 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /test/e2e/small_e2e.ts: -------------------------------------------------------------------------------- 1 | import {Minifier, options, MinifierOptions} from '../../src/main'; 2 | import * as ts from 'typescript'; 3 | var exec = require('child_process').exec; 4 | 5 | // running as if in the ts-minify/ directory 6 | export function runE2ETests() { 7 | // base path is set to ts-minify/ 8 | var minifier = new Minifier({ failFast: true, basePath: '.' }); 9 | // rename program from current executing directory (which is /ts-minify when run by gulp) 10 | minifier.renameProgram(['./test/input/e2e_input.ts', './typings/node/node.d.ts'], 11 | './build/output'); 12 | 13 | minifier.renameProgram(['./test/input/a.ts', './test/input/b.ts', './typings/node/node.d.ts'], 14 | './build/output'); 15 | 16 | minifier.renameProgram(['./test/input/external_return.ts', './node_modules/typescript/bin/typescript.d.ts'], './build/output'); 17 | 18 | minifier.renameProgram(['./test/input/external_call_site.ts', './node_modules/typescript/bin/typescript.d.ts'], './build/output'); 19 | 20 | // compile renamed program 21 | var child = exec('tsc', function(error, stdout, stderr) { 22 | if (stdout) console.log(stdout); 23 | if (stderr) console.log(stderr); 24 | if (error !== null) { 25 | console.log('exec error: ' + error); 26 | } 27 | }); 28 | // execute the renamed and compiled program 29 | require('../../output/test/input/e2e_input.js'); 30 | require('../../output/test/input/b.js'); 31 | require('../../output/test/input/external_return.js'); 32 | require('../../output/test/input/external_call_site.js'); 33 | 34 | } -------------------------------------------------------------------------------- /test/input/a.ts: -------------------------------------------------------------------------------- 1 | export class Foo { 2 | bar: string; 3 | } -------------------------------------------------------------------------------- /test/input/b.ts: -------------------------------------------------------------------------------- 1 | import * as a from './a'; 2 | import * as assert from 'assert'; 3 | 4 | var foo = new a.Foo(); 5 | 6 | foo.bar = 'bar'; 7 | 8 | assert.equal(foo.bar, 'bar'); 9 | console.log('b.ts: Assertion Passed'); -------------------------------------------------------------------------------- /test/input/e2e_input.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | class Animal { 4 | name: string; 5 | constructor(n: string) { this.name = n; } 6 | } 7 | 8 | function main() { 9 | var animal = new Animal('cat'); 10 | return animal.name; 11 | } 12 | 13 | assert.equal(main(), 'cat'); 14 | console.log('e2e_input.ts: Assertion Passed'); -------------------------------------------------------------------------------- /test/input/external_call_site.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as assert from 'assert'; 3 | 4 | function f(a: { newText: string }) { 5 | a.newText = 'hello!'; 6 | return a; 7 | } 8 | 9 | var x = f(new ts.TextChange()); 10 | assert(x.hasOwnProperty('newText'), 'true'); 11 | console.log('external_call_site.ts: Assertion Passed'); 12 | -------------------------------------------------------------------------------- /test/input/external_return.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as assert from 'assert'; 3 | 4 | function bar(): ts.TextChange { 5 | return { span: null, newText: 'hi' }; 6 | } 7 | 8 | var x = bar(); 9 | assert(x.hasOwnProperty('span'), 'true'); 10 | 11 | var y: ts.TextChange = { span: null, newText: 'omg!' }; 12 | assert(y.hasOwnProperty('newText'), 'true'); 13 | 14 | console.log('external_return.ts: Assertions Passed'); 15 | -------------------------------------------------------------------------------- /test/unit/main_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import * as assert from 'assert'; 6 | import * as ts from 'typescript'; 7 | import * as chai from 'chai'; 8 | import * as fs from 'fs'; 9 | import {Minifier, options} from '../../src/main'; 10 | 11 | function expectTranslate(code: string) { 12 | var result = translateSource(code); 13 | return chai.expect(result); 14 | } 15 | 16 | var defaultLibName = ts.getDefaultLibFilePath(options); 17 | var libSource = fs.readFileSync(ts.getDefaultLibFilePath(options), 'utf-8'); 18 | var libSourceFile: ts.SourceFile; 19 | 20 | function parseFile(fileName: string, fileContent: string): ts.Program { 21 | var compilerHost: ts.CompilerHost = { 22 | 23 | getSourceFile: function(sourceName, languageVersion) { 24 | if (sourceName === defaultLibName) { 25 | if (!libSourceFile) { 26 | libSourceFile = ts.createSourceFile(sourceName, libSource, options.target, true); 27 | } 28 | return libSourceFile; 29 | } 30 | return ts.createSourceFile(sourceName, fileContent, options.target, true); 31 | }, 32 | writeFile: function(name, text, writeByteOrderMark) { var result = text; }, 33 | getDefaultLibFileName: () => defaultLibName, 34 | useCaseSensitiveFileNames: () => false, 35 | getCanonicalFileName: (filename) => filename, 36 | getCurrentDirectory: () => '', 37 | getNewLine: () => '\n' 38 | }; 39 | 40 | var sourceFile = ts.createSourceFile(fileName, fileContent, options.target, true); 41 | var program = ts.createProgram([fileName], options, compilerHost); 42 | 43 | return program; 44 | } 45 | 46 | function translateSource(content: string): string { 47 | var minifier = new Minifier(); 48 | var program = parseFile('test.ts', content); 49 | var typeChecker = program.getTypeChecker(); 50 | var sourceFiles = program.getSourceFiles(); 51 | var namesToContents = {}; 52 | 53 | minifier.setTypeChecker(typeChecker); 54 | 55 | sourceFiles.forEach((sf) => { 56 | // if (not a .d.ts file) and (is a .js or .ts file) 57 | if (!sf.fileName.match(/\.d\.ts$/) && !!sf.fileName.match(/\.[jt]s$/)) { 58 | namesToContents[sf.fileName] = minifier.renameProgramFromNode(sf); 59 | } 60 | }); 61 | return namesToContents['test.ts']; 62 | } 63 | 64 | describe('Equality statement', () => {it('shows that 1 equals 1', () => { assert.equal(1, 1); })}); 65 | 66 | describe('Recognizes invalid TypeScript inputs', () => { 67 | it('expects a "Malformed TypeScript" error when fed invalid TypeScript', () => { 68 | var minifier = new Minifier(); 69 | var program = parseFile('test.ts', 'function x console.log("hello"); }'); 70 | chai.expect(() => minifier.checkForErrors(program)).to.throw(/Malformed TypeScript/); 71 | }); 72 | it('does not throw an error when fed valid TypeScript', () => { 73 | var minifer = new Minifier(); 74 | var program = parseFile('test.ts', '(function blah() {})'); 75 | chai.expect(() => minifer.checkForErrors(program)).to.not.throw(); 76 | }) 77 | }); 78 | 79 | describe('Visitor pattern', () => { 80 | it('renames identifiers of property declarations/assignments', () => { 81 | expectTranslate('var foo = { bar: { baz: 12; } }; foo.bar.baz;') 82 | .to.equal('var foo = { $: { _: 12; } }; foo.$._;'); 83 | expectTranslate('var x = "something"; var foo = { bar: { baz: x; } }; foo.bar.baz;') 84 | .to.equal('var x = "something"; var foo = { $: { _: x; } }; foo.$._;'); 85 | expectTranslate('class Foo {bar: string;} class Baz {bar: string;}') 86 | .to.equal('class Foo {$: string;} class Baz {$: string;}'); 87 | }); 88 | it('renames identifiers of property access expressions', () => { 89 | expectTranslate('class Foo { bar: string; constructor() {} baz() { this.bar = "hello"; } }') 90 | .to.equal('class Foo { $: string; constructor() {} _() { this.$ = "hello"; } }'); 91 | }); 92 | it('renames properties on Interfaces', () => { 93 | expectTranslate('interface LabelledValue { label: string; }') 94 | .to.equal('interface LabelledValue { $: string; }'); 95 | }); 96 | it('renames properties on type literals', () => { 97 | expectTranslate( 98 | 'function x(): { foo: string, bar: string } { return { foo: "foo", bar: "bar" }; }') 99 | .to.equal('function x(): { $: string, _: string } { return { $: "foo", _: "bar" }; }'); 100 | }); 101 | it('preserves spacing of original code', () => { 102 | expectTranslate('class Foo { constructor(public bar: string) {} }') 103 | .to.equal('class Foo { constructor(public $: string) {} }'); 104 | expectTranslate('class Foo { constructor(private bar: string) {} }') 105 | .to.equal('class Foo { constructor(private $: string) {} }'); 106 | expectTranslate('class Foo { constructor(protected bar: string) {} }') 107 | .to.equal('class Foo { constructor(protected $: string) {} }'); 108 | expectTranslate('class Foo { constructor() {} private bar() {} }') 109 | .to.equal('class Foo { constructor() {} private $() {} }'); 110 | }); 111 | it('throws an error when symbol information cannot be extracted from a property access expression', 112 | () => { 113 | chai.expect(() => { 114 | expectTranslate('var x = {}; x.y = {}; x.y.z = 12;') 115 | .to.throw(/Symbol information could not be extracted/); 116 | }); 117 | }); 118 | }); 119 | 120 | describe('structural type coersion', () => { 121 | it('correctly does not rename internal objects cast to external objects', () => { 122 | expectTranslate('function f(): Error { return { name: null, message: null }; }') 123 | .to.equal('function f(): Error { return { name: null, message: null }; }'); 124 | expectTranslate('function f(e: Error) { return e.name; } f({ name: null, message: null });') 125 | .to.equal('function f(e: Error) { return e.name; } f({ name: null, message: null });'); 126 | }); 127 | it('correctly does not rename internal objects when external objects are cast to them', () => { 128 | expectTranslate('function x(foo: { name: string, message: string }) { return foo; } x(new Error());') 129 | .to.equal('function x(foo: { name: string, message: string }) { return foo; } x(new Error());'); 130 | }); 131 | }); 132 | 133 | describe('Selective renaming', () => { 134 | it('does not rename property names from the standard library', () => { 135 | expectTranslate('Math.random();').to.equal('Math.random();'); 136 | expectTranslate('document.getElementById("foo");').to.equal('document.getElementById("foo");'); 137 | expectTranslate('[1, 4, 9].map(Math.sqrt);').to.equal('[1, 4, 9].map(Math.sqrt);'); 138 | expectTranslate('"hello".substring(0, 2);').to.equal('"hello".substring(0, 2);'); 139 | }); 140 | }); 141 | 142 | describe('Full-text emit', () => { 143 | it('retains typings at the top of file', () => { 144 | expectTranslate('/// var x = "hello";') 145 | .to.equal('/// var x = "hello";'); 146 | }); 147 | it('retains comments for a node', () => { 148 | expectTranslate('// This comment should show up\n' + 149 | 'class Foo {\n' + 150 | '// these should also show up\n' + 151 | 'bar: string;\n' + 152 | '}') 153 | .to.equal('// This comment should show up\n' + 154 | 'class Foo {\n' + 155 | '// these should also show up\n' + 156 | '$: string;\n' + 157 | '}'); 158 | }); 159 | }); 160 | 161 | describe('Next property name generation', () => { 162 | it('correctly generates a new shortname/alias', () => { 163 | var minifier = new Minifier(); 164 | assert.equal(minifier.generateNextPropertyName('a'), 'b'); 165 | assert.equal(minifier.generateNextPropertyName('ab'), 'ac'); 166 | assert.equal(minifier.generateNextPropertyName(''), '$'); 167 | assert.equal(minifier.generateNextPropertyName('$'), '_'); 168 | assert.equal(minifier.generateNextPropertyName('_'), 'a'); 169 | assert.equal(minifier.generateNextPropertyName('1'), 'a'); 170 | assert.equal(minifier.generateNextPropertyName('$a'), '$b'); 171 | assert.equal(minifier.generateNextPropertyName('$_'), '$0'); 172 | assert.equal(minifier.generateNextPropertyName('z'), 'A'); 173 | assert.equal(minifier.generateNextPropertyName('A'), 'B'); 174 | assert.equal(minifier.generateNextPropertyName('9'), 'a'); 175 | assert.equal(minifier.generateNextPropertyName('Z'), '$$'); 176 | assert.equal(minifier.generateNextPropertyName('az'), 'aA'); 177 | assert.equal(minifier.generateNextPropertyName('0a'), 'a$'); 178 | assert.equal(minifier.generateNextPropertyName('0a00'), 'a$$$'); 179 | assert.equal(minifier.generateNextPropertyName('a$'), 'a_'); 180 | }); 181 | it('correctly renames a property based on the last generated property name', () => { 182 | var minifier = new Minifier(); 183 | 184 | assert.equal(minifier.renameProperty('first'), '$'); 185 | assert.equal(minifier.renameProperty('second'), '_'); 186 | assert.equal(minifier.renameProperty('third'), 'a'); 187 | assert.equal(minifier.renameProperty('fourth'), 'b'); 188 | }); 189 | it('correctly skips over reserved keywords', () => { 190 | var minifier = new Minifier(); 191 | // skips generating 'in', which is a reserved word 192 | assert.equal(minifier.generateNextPropertyName('im'), 'io'); 193 | }); 194 | }); 195 | 196 | describe('output paths', () => { 197 | it('correctly flattens file structure when no base path specified', () => { 198 | var minifier = new Minifier(); 199 | chai.expect(minifier.getOutputPath('/a/b/c.ts', '/x')).to.equal('/x/c.ts'); 200 | }); 201 | it('correctly outputs file with file directory structure when given a base path', () => { 202 | var minifier = new Minifier({basePath: '/a'}); 203 | chai.expect(minifier.getOutputPath('/a/b/c/d.ts', '/x')).to.equal('/x/b/c/d.ts'); 204 | chai.expect(minifier.getOutputPath('/a/b/c/d.ts')).to.equal(process.cwd() + '/b/c/d.ts'); 205 | }); 206 | // . 207 | // ├── output 208 | // ├── something 209 | // │   └── test 210 | // │   └── input 211 | // │   └── math.ts 212 | // └── test 213 | // └── input 214 | it('correctly outputs file with file directory structure when given basePath that appears inside filePath', 215 | () => { 216 | var minifier = new Minifier({basePath: 'test/input'}); 217 | chai.expect(minifier.getOutputPath('something/test/input/math.ts', 'output')) 218 | .to.equal(process.cwd() + '/output/something/test/input/math.ts'); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs" 4 | }, 5 | "files": [ 6 | "./typings/chai/chai.d.ts", 7 | "./typings/node/node.d.ts", 8 | "./typings/fs-extra/fs-extra.d.ts", 9 | "./typings/mocha/mocha.d.ts", 10 | "./node_modules/typescript/bin/typescript.d.ts", 11 | "./build/output/test/input/e2e_input.ts", 12 | "./build/output/test/input/b.ts", 13 | "./build/output/test/input/external_return.ts", 14 | "./build/output/test/input/external_call_site.ts" 15 | ] 16 | } -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "fs-extra/fs-extra.d.ts": { 9 | "commit": "a95ee80de0d4a56b7c5fc82ca19145fc239e76f4" 10 | }, 11 | "node/node.d.ts": { 12 | "commit": "a95ee80de0d4a56b7c5fc82ca19145fc239e76f4" 13 | }, 14 | "mocha/mocha.d.ts": { 15 | "commit": "a95ee80de0d4a56b7c5fc82ca19145fc239e76f4" 16 | }, 17 | "chai/chai.d.ts": { 18 | "commit": "a95ee80de0d4a56b7c5fc82ca19145fc239e76f4" 19 | } 20 | } 21 | } --------------------------------------------------------------------------------