├── .angular-cli.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── LIBRARY_MODE.md ├── LICENSE ├── README.md ├── README_OLD.md ├── index.ts ├── jest.config.json ├── package.json ├── publish.sh ├── setup-jest.ts ├── src ├── README.md ├── _resource-loader.ts ├── angular-compiler-execution-host.ts ├── cli │ ├── cli-compiler-host.ts │ ├── cli-context.ts │ ├── cli-execution-host.ts │ ├── cli.ts │ ├── config.ts │ ├── index.ts │ ├── inline-metadata.ts │ ├── ng-cli.ts │ ├── perform_compile_async.ts │ ├── transformers │ │ ├── fw │ │ │ ├── ast_helpers.ts │ │ │ ├── interfaces.ts │ │ │ └── make_transform.ts │ │ └── inline-resources.ts │ └── util.ts ├── execution-models.ts ├── patch-angular-compiler-cli.ts ├── plugin-options.ts ├── plugin.ts ├── resource-loader.ts └── utils.ts ├── test ├── cli.spec.ts ├── ng-app │ ├── app │ │ ├── about │ │ │ ├── about.component.ts │ │ │ └── index.ts │ │ ├── app.component.css │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.routes.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.ts │ │ │ ├── index.ts │ │ │ ├── title │ │ │ │ ├── index.ts │ │ │ │ └── title.service.ts │ │ │ └── x-large │ │ │ │ ├── index.ts │ │ │ │ └── x-large.directive.ts │ │ ├── index.ts │ │ ├── lazy │ │ │ ├── detail.component.html │ │ │ ├── detail.component.scss │ │ │ ├── detail.component.ts │ │ │ ├── index.ts │ │ │ └── lazy.module.ts │ │ └── no-content │ │ │ ├── index.ts │ │ │ └── no-content.component.ts │ ├── assets │ │ ├── avatar.png │ │ ├── check-off.png │ │ ├── check-on.png │ │ └── on-off.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles │ │ ├── additional.css │ │ └── main.scss │ └── tsconfig.app.json ├── ng-lib │ └── src │ │ ├── index.ts │ │ ├── lib-component │ │ ├── lib-component.component.html │ │ ├── lib-component.component.scss │ │ └── lib-component.component.ts │ │ ├── lib-module.module.ts │ │ └── lib-service.service.ts ├── ngc-webpack.spec.ts ├── ngtools-webpack-flat-module-support.ts ├── no-hooks.spec.ts ├── patch-angular-compiler-cli.spec.ts └── testing │ ├── buildConfig │ ├── base-webpack-config.js │ ├── webpack.ngtools-full.js │ ├── webpack.plugin-full.js │ └── webpack.plugin-lib.js │ ├── replaced-resource.scss │ └── utils.ts ├── tsconfig.json ├── tsconfig.ngtools-full.json ├── tsconfig.plugin-full.json ├── tsconfig.plugin-lib-ngcli.json ├── tsconfig.plugin-lib.json ├── tsconfig.test.json ├── webpack-debug.js └── yarn.lock /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "testproj" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "test/ng-app", 9 | "outDir": "dist/test/ng-app-cli", 10 | "assets": [ 11 | "assets" 12 | ], 13 | "index": "index.html", 14 | "main": "main.ts", 15 | "tsconfig": "tsconfig.app.json", 16 | "prefix": "app", 17 | "styles": [ 18 | "styles/main.scss", 19 | "styles/additional.css" 20 | ], 21 | "scripts": [], 22 | "environmentSource": "environments/environment.ts", 23 | "environments": { 24 | "dev": "environments/environment.ts", 25 | "prod": "environments/environment.prod.ts" 26 | } 27 | } 28 | ], 29 | "defaults": { 30 | "styleExt": "scss", 31 | "component": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | 51 | 52 | .idea 53 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | addons: 4 | apt: 5 | sources: 6 | - google-chrome 7 | packages: 8 | - google-chrome-stable 9 | language: node_js 10 | node_js: 11 | - "5" 12 | - "6" 13 | - "node" 14 | matrix: 15 | fast_finish: true 16 | allow_failures: 17 | - node_js: "5" 18 | install: 19 | - npm install 20 | before_script: 21 | - export DISPLAY=:99.0 22 | - sh -e /etc/init.d/xvfb start 23 | - sleep 3 24 | script: 25 | - npm run ci -------------------------------------------------------------------------------- /LIBRARY_MODE.md: -------------------------------------------------------------------------------- 1 | Use the AOT compiler to compile angular libraries suitable for AOT and 2 | JIT consumption. 3 | 4 | - Compile resources (HTML, CSS, SCSS etc..) through webpack loaders 5 | - Use same configuration for development and library production build. 6 | - Inline resources 7 | 8 | ## Background 9 | The `@ngtools/webpack` package is built for webpack bundling and does not 10 | support the process of generating libraries for angular. 11 | 12 | Library compilation has a specific process where each TS file is compiled 13 | to JS a file without bundling the output. 14 | 15 | `@angular/compiler-cli` supports this mode. By using the proper 16 | configuration you can compile TS to JS for libraries with full AOT support. 17 | 18 | The problem with using the `@angular/compiler-cli` is that it does not 19 | know about webpack, resources will not go through the loader chain and so 20 | using formats not supported the the angular cli will not work (SCSS, LESS etc). 21 | 22 | Additionally, `templareUrl` and `stylesUrls` are left as is which is not 23 | suitable for libraries, they most be inlined into the sources code (JS) 24 | and the AOT generated `metadata.json` files. 25 | 26 | `ngc-webpack` library mode allows AOT compilation from libraries through 27 | a CLI interface or directly using it via node API. 28 | 29 | ## How 30 | 31 | Using library mode, `ngc-webpack` will use `webpack` to pass all resources 32 | through the loader chain and passing the output to the angular AOT compiler. 33 | Using `webpack` ensures consistency, whatever loaders you used to build 34 | your demo app are also used to build the library. 35 | 36 | `ngc-webpack` can also inline your resources to both JS output files and 37 | to the `.metadata.json` files generated by angular. Inlining is automatically 38 | applied based on your AOT configuration set on `tsconfig` 39 | 40 | Library mode does not require specific configuration, it is configured 41 | using existing configuration in `tsconfig` and `webpack.config`. 42 | 43 | The output is controlled by the `tsconfig` supplied. 44 | 45 | `ngc-webpack` will inline all resources to both `metadata.json` files and `js` source code. 46 | If `skipTemplateCodegen` is set to **false** the compiler will emit source code for all resource in dedicated modules 47 | and so `ngc-webpack` will not perform the inline operation. 48 | 49 | 50 | ## Usage 51 | 52 | The examples refers to a typical library project structure where 53 | a demo app is used in development and consumes the library. 54 | 55 | ```bash 56 | ngc-w --webpack webpack.config.packge.js 57 | ``` 58 | 59 | You'r webpack config should contain the loaders you use in your dev and 60 | prod builds and `NgcWebpackPlugin` instance must exists in the plugins collection. 61 | 62 | The `NgcWebpackPlugin` instance option's `tsConfigPath` property should 63 | point to a `tsconfig` file that is properly set for library compilation. 64 | 65 | Another options is to use the webpack config you are using for prod/dev 66 | with a command line param pointing at the tsconfig, `ngc-webpack` will 67 | replace the `tsConfigPath` in run time. 68 | 69 | ```bash 70 | ngc-w --webpack webpack.config.app.js -p tsconfig.package.json 71 | ``` 72 | 73 | 74 | ### Angular CLI library build: 75 | Angular CLI does not support library builds out of the box but with some 76 | magic it is possible. 77 | As we now know, `webpack` is not used for bundling when compiling libraries 78 | so actually what we need is only the webpack configuration that the cli creates. 79 | 80 | `ngc-webpack` invokes the cli and captures the configuration files and then 81 | uses it with a different `tsconfig` to build the library. 82 | 83 | ```bash 84 | ngc-w-cli build -p tsconfig.package.json 85 | ``` 86 | 87 | We do not need to specify a webpack config, we get it from the cli but 88 | we do need to specify the `build` command so the cli will not complain. 89 | The tsconfig (-p) is not mandatory, if not set it is taken from the plugin 90 | but ofcourse this is not recommended so your will need to set it. 91 | 92 | 93 | ## Node API: 94 | 95 | ----- 96 | 97 | TODO... 98 | 99 | ------- 100 | 101 | For now, see the test (`cli.spec.ts) 102 | 103 | ## `tsconfig.json` for library compilation 104 | A `tsconfig` for library compilation is quite similar to your application 105 | configuration with some modification: 106 | 107 | - You can not use `include`, a specific `files` entry must be set 108 | with 1 `ts` module set at index 0 (and optional d.ts afterwards) 109 | 110 | - An `outDir` must be set, this is where the output of the compilation is saved 111 | 112 | - `declaration` should be set to **true** so `d.ts` files are shipped with your library. 113 | 114 | - `angularCompilerOptions` should be set for flat module output and other 115 | instructions: 116 | ``` 117 | angularCompilerOptions: { 118 | annotateForClosureCompiler: true, 119 | skipMetadataEmit: false, 120 | skipTemplateCodegen: true, 121 | strictMetadataEmit: true, 122 | flatModuleOutFile: 'my-lib.ng-flat.js', 123 | flatModuleId: 'my-lib' 124 | } 125 | ``` 126 | 127 | ## Monorepos: 128 | Mono-repos are a great way to publish libraries under the same npm scope 129 | or as part of a big project. 130 | 131 | `ngc-webpack` library mode can work well with monorepo setup, the trick 132 | with monorepo setup is configuration, especially `paths` 133 | 134 | For a fully managed monorepo solution, integrated with the angular-cli 135 | you can use [nrwl's `Nx`](https://github.com/nrwl/nx) 136 | 137 | For a custom setup, please take a look at [angular-library-starter](https://github.com/shlomiassaf/angular-library-starter) 138 | which has a setup running with the library mode of `ngc-webpack` 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016-2017 Shlomi Assaf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/shlomiassaf/ngc-webpack.svg?branch=master)](https://travis-ci.org/shlomiassaf/ngc-webpack) 2 | 3 | # ngc-webpack 4 | [@ngtools/webpack](https://github.com/angular/angular-cli/tree/master/packages/%40ngtools/webpack) wrapper with hooks into 5 | the compilation process and library mode compilation support. 6 | 7 | ## Application mode: 8 | AOT compilation for an application. 9 | 10 | - [Background](#background) 11 | - [Porting to/from `@ngtools/webpack](#porting) 12 | - [Usage](#usage) 13 | - [Advanced AOT production builds](#advanced-aot-production-builds) 14 | - [Options](#ngcwebpackpluginoptions) 15 | - [Optional Patching](#optional-patching) 16 | 17 | ## Library mode: 18 | AOT compilation for a library. 19 | 20 | Library mode is the simple **compile** process we know from `tsc` / `ngc` 21 | where each module (`TS` file) is compiled into a matching `JS` file. 22 | 23 | The output files can then bundle up with RollUp to create various bundle 24 | formats for published libraries (FESM, FESM2015, UMD, etc.) 25 | 26 | This process is fairly simple as is but with the angular AOT compiler 27 | in the middle things are a bit more complex. 28 | 29 | `@ngtools/webpack` does not support library compilation and it is (1.8.x) 30 | designed for application bundling only. 31 | 32 | The `@angular/compiler-cli` does support library compilation through its 33 | `ngc` command line utility but it does not know about webpack, 34 | resources will not go through the loader chain and so using formats not 35 | supported by the angular cli will not work (SCSS, LESS etc). 36 | 37 | Additionally, `templareUrl` and `stylesUrls` are left as is which is not 38 | suitable for libraries, resources must get inlined into the sources code (JS) 39 | and the AOT generated `metadata.json` files. 40 | 41 | ### Webpack based projects: 42 | `ngc-webpack` library mode allows AOT compilation for libraries through 43 | a CLI interface (`ngc-w`) or directly using it via node API with 44 | full support for inline and complete webpack loader chain compilation (for resources). 45 | 46 | ### Angular CLI based projects: 47 | `ngc-webpack` also support library compilation for `@angular/cli` projects 48 | by importing the configuration from the cli and using it to build libraries. 49 | This works great with monorepos and setup's based on [nrwl's `Nx`](https://github.com/nrwl/nx). 50 | Also available by CLI interface (`ngc-w-cli`) or node API. 51 | 52 | 53 | For more information see: 54 | - [Library compilation mode](LIBRARY_MODE.md) 55 | 56 | > Library mode is experimental as it uses experimental API from angular 57 | packages. 58 | 59 | ## Background: 60 | `ngc-webpack` started as a wrapper for `@angular/compiler-cli` when angular 61 | build tools were limited. 62 | 63 | It offered non `@angular/cli` users the ability to perform an AOT builds 64 | with all the required operations while still using a dedicated typescript 65 | loader (e.g. `ts-loader`, `awesome-typescript-loader`). 66 | 67 | With version 5 of angular, the `compiler-cli` introduces a dramatic 68 | refactor in the compilation process, enabling watch mode for AOT and 69 | moving to a (almost) native TS compilation process using transformers. 70 | 71 | The support angular 5, a complete rewrite for `ngc-webpack` was required. 72 | Since `@ngtools/webpack` is now a mature plugin with a rich feature set 73 | and core team support it is not smart (IMHO) to try and re-implement it. 74 | 75 | This is why, from version 4 of `ngc-webpack`, the library will wrap 76 | `@ngtools/webpack` and only provide hooks into the compilation process. 77 | 78 | The implications are: 79 | - Using `ngc-webpack` is safe, at any point you can move to `@ngtools/webpack`. 80 | - All features of `@ngtools/webpack` will work since `ngc-webpack` acts as a proxy. 81 | This includes i18n support which was not included in `ngc-webpack` 3.x.x 82 | - You can hack your way into the AOT compilation process, which opens 83 | a lot of options, especially for library compilation. 84 | - Using a custom typescript loader is no longer supported, you need to 85 | use the loader provided with `@ngtools/webpack` (for JIT see Using custom TypeScript loaders) 86 | 87 | ## Porting to/from `@ngtools/webpack 88 | Using `ngc-webpack` as a proxy to `@ngtools/webpack` is safe and allows 89 | quick and transparent porting between the libraries. 90 | 91 | In fact, if you use `ngc-webpack` without using it's extensibility 92 | features you probably better of using `@ngtools/webpack` directly instead. 93 | 94 | When using `ngc-webpack` features, including library compilation mode, 95 | you should be aware that `ngc-webpack` is using experimental angular APIs 96 | as well as internal implementation of angular code to allow extensibility. 97 | 98 | ## Usage: 99 | ```bash 100 | npm install ngc-webpack -D 101 | ``` 102 | 103 | **webpack.config.js** 104 | ```js 105 | { 106 | module: { 107 | rules: [ 108 | { 109 | test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, 110 | use: [ '@ngtools/webpack' ] 111 | } 112 | ] 113 | }, 114 | plugins: [ 115 | new ngcWebpack.NgcWebpackPlugin({ 116 | AOT: true, // alias for skipCodeGeneration: false 117 | tsConfigPath: './tsconfig.json', 118 | mainPath: 'src/main.ts' // will auto-detect the root NgModule. 119 | }) 120 | ] 121 | } 122 | ``` 123 | 124 | ### Advanced AOT production builds: 125 | Production builds must be AOT compiled, this is clear, but we can optimize 126 | the build even further, and the angular team has us covered using 127 | `'@angular-devkit/build-optimizer`: 128 | 129 | **webpack.config.js** 130 | ```js 131 | const PurifyPlugin = require('@angular-devkit/build-optimizer').PurifyPlugin; 132 | 133 | const AOT = true; 134 | 135 | const tsLoader = { 136 | test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, 137 | use: [ '@ngtools/webpack' ] 138 | }; 139 | 140 | if (AOT) { 141 | tsLoader.use.unshift({ 142 | loader: '@angular-devkit/build-optimizer/webpack-loader', 143 | // options: { sourceMap: true } 144 | }); 145 | } 146 | 147 | return { 148 | module: { 149 | rules: [ 150 | tsLoader 151 | ] 152 | }, 153 | plugins: [ 154 | new ngcWebpack.NgcWebpackPlugin({ 155 | AOT, // alias for skipCodeGeneration: false 156 | tsConfigPath: './tsconfig.json', 157 | mainPath: 'src/main.ts' // will auto-detect the root NgModule. 158 | }).concat(AOT ? [ new PurifyPlugin() ] : []), 159 | ] 160 | } 161 | ``` 162 | 163 | > The examples above are super simplified and describe the basic units 164 | for compilation, the `@angular/cli` uses them but with a lot more loaders/plugins/logic. 165 | 166 | For more information about setting up the plugin see [@ngtools/webpack](https://github.com/angular/angular-cli/tree/master/packages/%40ngtools/webpack) 167 | 168 | ### NgcWebpackPluginOptions: 169 | The plugin accepts an options object of type `NgcWebpackPluginOptions`. 170 | 171 | `NgcWebpackPluginOptions` extends [AngularCompilerPluginOptions](https://github.com/angular/angular-cli/blob/master/packages/%40ngtools/webpack/src/plugin.ts) so 172 | all `@ngtools/webpack` options apply. 173 | 174 | `NgcWebpackPluginOptions` adds the following options: 175 | ```ts 176 | export interface NgcWebpackPluginOptions extends AngularCompilerPluginOptions { 177 | 178 | /** 179 | * An alias for `AngularCompilerPluginOptions.skipCodeGeneration` simply to make it more readable. 180 | * If `skipCodeGeneration` is set, this value is ignored. 181 | * If this value is not set, the default value is taken from `skipCodeGeneration` 182 | * (which means AOT = true) 183 | */ 184 | AOT?: boolean; 185 | 186 | /** 187 | * A hook that invokes before the plugin start the compilation process (compiler 'run' event). 188 | * ( resourceCompiler: { get(filename: string): Promise }) => Promise; 189 | * 190 | * The hook accepts a resource compiler which able (using webpack) to perform compilation on 191 | * files using webpack's loader chain and return the final content. 192 | * @param resourceCompiler 193 | */ 194 | beforeRun?: BeforeRunHandler 195 | 196 | /** 197 | * Transform a source file (ts, js, metadata.json, summery.json). 198 | * If `predicate` is true invokes `transform` 199 | * 200 | * > Run's in both AOT and JIT mode on all files, internal and external as well as resources. 201 | * 202 | * 203 | * - Do not apply changes to resource files using this hook when in AOT mode, it will not commit. 204 | * - Do not apply changes to resource files in watch mode. 205 | * 206 | * Note that source code transformation is sync, you can't return a promise (contrary to `resourcePathTransformer`). 207 | * This means that you can not use webpack compilation (or any other async process) to alter source code context. 208 | * If you know the files you need to transform, use the `beforeRun` hook. 209 | */ 210 | readFileTransformer?: ReadFileTransformer; 211 | 212 | 213 | /** 214 | * Transform the path of a resource (html, css, etc) 215 | * (path: string) => string; 216 | * 217 | * > Run's in AOT mode only and on metadata resource files (templateUrl, styleUrls) 218 | */ 219 | resourcePathTransformer?: ResourcePathTransformer; 220 | 221 | /** 222 | * Transform a resource (html, css etc) 223 | * (path: string, source: string) => string | Promise; 224 | * 225 | * > Run's in AOT mode only and on metadata resource files (templateUrl, styleUrls) 226 | */ 227 | resourceTransformer?: ResourceTransformer; 228 | 229 | /** 230 | * Add custom TypeScript transformers to the compilation process. 231 | * 232 | * Transformers are applied after the transforms added by `@angular/compiler-cli` and 233 | * `@ngtools/webpack`. 234 | * 235 | * > `after` transformers are currently not supported. 236 | */ 237 | tsTransformers?: ts.CustomTransformers; 238 | } 239 | ``` 240 | 241 | ## Optional Patching: 242 | `ngc-webpack` comes with optional patches to angular, these are workarounds 243 | to existing issue that will probably get fixed in the future making the patch 244 | obsolete. Patch's address specific use case so make sure you apply them only 245 | if required. 246 | 247 | ### `disableExpressionLowering` fix (`@angular/compiler-cli`): 248 | The `compiler-cli` (version 5.0.1) comes with a new feature called 249 | **lowering expressions** which basically means we can now use arrow 250 | functions in decorator metadata (usually provider metadata) 251 | 252 | This feature has bug the will throw when setting an arrow function: 253 | ```ts 254 | export function MyPropDecorator(value: () => any) { 255 | return (target: Object, key: string) => { } 256 | } 257 | 258 | export class MyClass { 259 | @MyPropDecorator(() => 15) // <- will throw because of this 260 | prop: string; 261 | } 262 | ``` 263 | 264 | The compiler will lower the expression to: 265 | ```ts 266 | export const ɵ0 = function () { return 15; }; 267 | ``` 268 | 269 | but in the TS compilation process will fail because of a TS bug. 270 | 271 | This is an edge case which you probably don't care about, but if so 272 | there are 2 options to workaround: 273 | 274 | 1. Set `disableExpressionLowering` to false in `tsconfig.json` `angularCompilerOptions` 275 | 2. Import a patch, at the top of your webpack config module: 276 | ```js 277 | require('ngc-webpack/src/patch-angular-compiler-cli'); 278 | ``` 279 | 280 | The issue should be fixed in next versions. 281 | See https://github.com/angular/angular/issues/20216 282 | 283 | #### Using custom TypeScript loaders 284 | From `ngc-webpack` 4 using a custom ts loader is not supported for AOT 285 | compilation and partially supported for JIT. 286 | 287 | If you must use your own TS Loader for JIT, you can do so. 288 | This is not recommended mainly because of the mis alignment between the 289 | compilations. 290 | 291 | To use a custom loader (JIT only), remove the `@ngtools/webpack` loader 292 | and set your own loader. To support lazy loaded modules, use a module 293 | loader that can detect them (e.g. [ng-router-loader](https://github.com/shlomiassaf/ng-router-loader)) 294 | 295 | ## Use case 296 | The feature set within `ngc-webpack` is getting more and more specific. 297 | The target audience is small as most developers will not require hooking 298 | into the compilation. 299 | 300 | It is mostly suitable for library builds, where you can control the 301 | metadata output, inline code and more... 302 | 303 | I personally use it to restyle material from the ground. 304 | The plugin enables re-writing of the `index.metadata.json` files on 305 | the fly which allows sending custom styles to the compiler instead of 306 | the ones that comes with material. 307 | 308 | 309 | ## Future 310 | Because `ngc-webpack` becomes a niche, I believe integrating the hooks 311 | into `@ngtools/webpack` makes sense and then deprecating the library while 312 | easy porting to `@ngtools/webpack`. If someone would like to help working 313 | on it, please come forward :) 314 | 315 | I believe it angular team is open to such idea since `@ngtools/webpack` 316 | is separated from the cli. 317 | 318 | -------------------------------------------------------------------------------- /README_OLD.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/shlomiassaf/ngc-webpack.svg?branch=master)](https://travis-ci.org/shlomiassaf/ngc-webpack) 2 | 3 | ### Version 3.1 - AOT Cleanup loader support 4 | Added **AOT cleanup loader** (read below) 5 | 6 | Added **AOT cleanup transformer** (Do not use) 7 | 8 | ### Version 3.0 - BREAKING CHANGE 9 | Version 3.0.0 does not contain API breaking changes but does contain a logical 10 | breaking change that might affect some setups. 11 | 12 | The only change concerns automatically registering **ts-node** 13 | 14 | Up to 2.0.0 **ngc-webpack** automatically loaded **ts-node**: 15 | ```js 16 | require('ts-node/register'); 17 | ``` 18 | 19 | This is ok when running **ngc-webpack** from the CLI. 20 | However, when using the **ngc-webpack programmatically it might cause 21 | unexpected errors, for example if one wants to invoke a custom ts-node registration. 22 | 23 | From **ngc-webpack@3.0.0** using **ngc-webpack** from your code you need 24 | to register ts-node manually. 25 | 26 | > Most setups will run **ngc-webpack** using the webpack plugin, which is 27 | running it from code (and not from CLI) but Webpack (and ts loaders) 28 | should automatically register ts-node so the impact should be minimal. 29 | 30 | 31 | # ngc-webpack 32 | `@angular/compiler-cli` Wrapper for Webpack 33 | 34 | Key features: 35 | - Angular AOT compilation webpack plugin outside of the `angular-cli` eco-system 36 | - Pass resources through webpack's loader chain (template, styles, etc...) 37 | - Hooks into the AOT compilation process (replaces source files, metadata files, resource files) 38 | - Not restricted to a TypeScript loader, use any TS loader you want 39 | - Does not contain an `@angular/router` lazy module loader (you can use [ng-router-loader](https://github.com/shlomiassaf/ng-router-loader)) 40 | 41 | 42 | **ngc-webpack** is quite similar to [@ngtools/webpack](https://github.com/angular/angular-cli/tree/master/packages/%40ngtools/webpack). 43 | It does not do any actual compilation, this is done by angular tools. It just allows some 44 | customization to the process. 45 | 46 | > `ngc-webpack` is built of some constructs from `@ngtools/webpack`. 47 | 48 | 49 | ## Usage 50 | To install `npm install -g ngc-webpack` 51 | 52 | There are 2 approaches to running the ngc-w: 53 | 54 | ### Build steps 55 | Run `ngc-webpack` first, when done run webpack. 56 | Use a AOT dedicated entry point to point to that file, from there on all references are fine. 57 | 58 | > `ngc-webpack` does not care about SCSS, LESS or any intermediate resource that requires transformation. Each resource will follow the chain defined in the webpack configuration supplied. You get identical result in but development and prod (with AOT) builds. 59 | 60 | **This approach does not require using the plugin but its limits your control over the bundle.** 61 | 62 | ```shell 63 | ngc-w -p tsconfig.json --webpack webpack.aot.json 64 | ``` 65 | 66 | `ngc-webpack` wraps `compiler-cli` so all cli parameters sent to `ngc` are valid here (e.g: -p for ts configuration file). 67 | The only additional parameter is the `--webpack` parameter used to point to the webpack configuration factory. 68 | 69 | ### AOT Cleanup loader 70 | 71 | The AOT cleanup loader is a an optional loader to be added to webpack that will remove angular decorators from the TypeScript source code. 72 | 73 | As the name suggests, the loader should run **only** when compiling AOT, if you run it when the target is JIT the application will fail to run. 74 | 75 | The **AOT cleanup loader** removes all angular decorators (e.g. `NgModel`, `Component`, `Inject` etc...) from TypeScript code before the main TS loader kicks in (`ts-loader`, `awesome-typescript-loader`, etc...). 76 | The decorators are not needed in AOT mode since the AOT compiler converts the metadata in them into code and saves it in `ngfactory.ts` files. 77 | 78 | It is always recommended to run the **AOT cleanup loader** for AOT production build as it will: 79 | 80 | 1. Reduces the bundle size 81 | 2. Speeds up initial bootstrap time and any future `NgModule` lazy loading 82 | 83 | The impact volume depends on the application size. 84 | Bigger application = more decorators = more effect. 85 | 86 | > Speed up in initial bootstrap is not significant and unnoticeable in most cases. 87 | 88 | #### Loader options: 89 | ```ts 90 | export interface AotCleanupLoaderOptions { 91 | /** 92 | * If false the plugin is a ghost, it will not perform any action. 93 | * This property can be used to trigger AOT on/off depending on your build target (prod, staging etc...) 94 | * 95 | * The state can not change after initializing the plugin. 96 | * @default true 97 | */ 98 | disable?: false; 99 | 100 | /** 101 | * A path to a TSConfig file, optional if a plugin is supplied. 102 | * When both are available `tsConfigPath` wins. 103 | */ 104 | tsConfigPath?: any; 105 | 106 | /** 107 | * Optional TS compiler options. 108 | * 109 | * > Some options set by the loader can not change. 110 | */ 111 | compilerOptions?: any; 112 | } 113 | ``` 114 | 115 | #### Loader VS TypeScript transformers 116 | The **AOT cleanup loader** is a temporary solution to solve the cleanup problem. It is not the optimal one. 117 | 118 | The optimal solution is to use the `Transformers API` in **TypeScript**. 119 | The API is not complete nor stable which is why the loader approach is used. 120 | **ngc-webpack** library has a transformer implementation ready and exposed but not documented yet since it will fail on certain use cases due to bugs in the transformers API. 121 | 122 | #### Webpack config example: 123 | ``` 124 | { 125 | test: /\.ts$/, 126 | use: [ 127 | { 128 | loader: 'awesome-typescript-loader', 129 | options: { 130 | configFileName: 'tsconfig.webpack.json', 131 | } 132 | }, 133 | { 134 | loader: 'ngc-webpack', 135 | options: { 136 | disable: false, // SET TO TRUE ON NON AOT PROD BUILDS 137 | } 138 | }, 139 | { 140 | loader: 'angular2-template-loader' 141 | } 142 | ] 143 | } 144 | 145 | // This setup assumes NgcWebpackPlugin is set in the plugins array. 146 | ``` 147 | 148 | #### Real time loader analysis 149 | 150 | The following table displays an analysis of the bundling process with 151 | and without the loader. The source is an Angular application (v 4.3.1) 152 | with a total **177** angular decorators spread across 140,527 TypeScript lines 153 | of code (42,796 net total of actual source LOC). 154 | 155 | This a small to medium size application. 156 | 157 | > Note that 177 decorators means a combination of all angular decorators, some emit more boilerplate then others (e.g. `@Component` vs `@Injectable`) 158 | 159 |                                                    **Non Minified**                                                  **Minified (UglifyJS)** 160 | 161 | | | Webpack compile time (sec) | Final Bundle Size (kb) | | Webpack compile time (sec) | Final Bundle Size (kb) | 162 | |----------------|:--------------------------:|:----------------------:|:-:|:--------------------------:|:----------------------:| 163 | | With Loader | 115 | 1721 | | 138 | 467 | 164 | | Without Loader | 118 | 1848 | | 143 | 491 | 165 | | Diff | **3** | **127** | | **5** | **24** | 166 | 167 | 168 | > Running **without the loader** was done using the `resourceOverride` feature of **ngc-webpack** plugin. It means that the resources are not present in both cases and does not effect the result. 169 | 170 | ##### Bundle Size 171 | The bundle size is also reduces, around 7% for non minified and 5% for minified. 172 | This is substantial and will increase over time. 173 | 174 | 175 | > Initial bootstrap improvement was not measured, I don't think it is 176 | noticeable. 177 | 178 | 179 | ##### Time 180 | Time is not that interesting as bundle size since it's not effecting the 181 | user but the results surprised me so I dag in. 182 | 183 | We can see a small decrease of webpack runtime. 184 | While we add an extra loader that does TS processing we reduce the 185 | payload for following loaders and plugins. Decorators emit boilerplate 186 | that they won't need to process. 187 | The additional processing we add is less then we remove. It get stronger 188 | When using UglifyJS, again, it has less data to minify. 189 | 190 | 191 | ##### Memory footprint (webpack) 192 | The loader use's it's own TS compilation process, this is an additional 193 | process that consumes memory. The compilation example ran with 194 | `--max_old_space_size=4096`. 195 | 196 | > Using `resourceOverride` plugin option has no effect when using the loader. 197 | 198 | ### Plugin 199 | `ngc-webpack` comes with an optional plugin called `NgcWebpackPlugin` 200 | The plugin allows hooking into the resource compilation process. 201 | 202 | ```ts 203 | export interface NgcWebpackPluginOptions { 204 | /** 205 | * If false the plugin is a ghost, it will not perform any action. 206 | * This property can be used to trigger AOT on/off depending on your build target (prod, staging etc...) 207 | * 208 | * The state can not change after initializing the plugin. 209 | * @default true 210 | */ 211 | disabled?: boolean; 212 | 213 | /** 214 | * A hook that invokes before the `compiler-cli` start the compilation process. 215 | * (loader: { get(filename: string): Promise }) => Promise; 216 | * 217 | * The hook accepts an object with a `get` method that acts as a webpack compilation, being able to compile a file and return it's content. 218 | * @param loader 219 | */ 220 | beforeRun?: BeforeRunHandler 221 | 222 | /** 223 | * Transform a source file (ts, js, metadata.json, summery.json) 224 | * (path: string, source: string) => string; 225 | * 226 | * Note that source code transformation is sync, you can't return a promise (contrary to `resourcePathTransformer`). 227 | * This means that you can not use webpack compilation (or any other async process) to alter source code context. 228 | * If you know the files you need to transform, use the `beforeRun` hook. 229 | */ 230 | readFileTransformer?: ReadFileTransformer; 231 | 232 | 233 | /** 234 | * Transform the path of a resource (html, css, etc) 235 | * (path: string) => string; 236 | */ 237 | resourcePathTransformer?: ResourcePathTransformer; 238 | 239 | /** 240 | * Transform a resource (html, css etc) 241 | * (path: string, source: string) => string | Promise; 242 | */ 243 | resourceTransformer?: ResourceTransformer; 244 | 245 | /** 246 | * Fires then the compilation ended with no errors. 247 | * () => void; 248 | * 249 | * > If you throw from the callback the process will exit with failure and print the error message. 250 | * This allows some validation for `resourcePathTransformer`, to check the state one finished and conclude about the result. 251 | */ 252 | 253 | onCompilationSuccess?: OnCompilationSuccess; 254 | /** 255 | * Fires then the compilation ended with an error. 256 | * (err: Error) => void; 257 | * 258 | * > If you throw from the callback the process will exit with failure and print the error message. 259 | * This allows some validation for `resourcePathTransformer`, to check the state one finished and conclude about the result. 260 | * 261 | * > Throwing from `onCompilationError` is like re-throw with a new error. 262 | * Currently it's not possible to suppress an error. 263 | */ 264 | onCompilationError?: OnCompilationError; 265 | 266 | /** 267 | * A path to a tsconfig file, if set the AOT compilation is triggered from the plugin. 268 | * When setting a tsconfig you do not need to run the compiler from the command line. 269 | * 270 | * If you are not setting a config file the compilation will not run and you need to run it before webpack starts. 271 | * When AOT compiling outside of the plugin (i.e. no tsconfig property), you can still use the 272 | * plugin to access the hooks, but remember that the hooks will run from the command line process (e.g: `ngc-w`) 273 | * @default undefined 274 | */ 275 | tsConfig?: string; 276 | 277 | /** 278 | * A path to a file (resource) that will replace all resource referenced in @Components. 279 | * For each `@Component` the AOT compiler compiles it creates new representation for the templates (html, styles) 280 | * of that `@Components`. It means that there is no need for the source templates, they take a lot of 281 | * space and they will be replaced by the content of this resource. 282 | * 283 | * To leave the template as is set to a falsy value (the default). 284 | * 285 | * TIP: Use an empty file as an overriding resource. It is recommended to use a ".js" file which 286 | * usually has small amount of loaders hence less performance impact. 287 | * 288 | * > This feature is doing NormalModuleReplacementPlugin for AOT compiled resources. 289 | * 290 | * ### resourceOverride and assets 291 | * If you reference assets in your styles/html that are not inlined and you expect a loader (e.g. url-loader) 292 | * to copy them, don't use the `resourceOverride` feature as it does not support this feature at the moment. 293 | * With `resourceOverride` the end result is that webpack will replace the asset with an href to the public 294 | * assets folder but it will not copy the files. This happens because the replacement is done in the AOT compilation 295 | * phase but in the bundling it won't happen (it's being replaced with and empty file...) 296 | * 297 | * @default undefined 298 | */ 299 | resourceOverride?: string; 300 | 301 | /** 302 | * Angular compiler CLI options 303 | */ 304 | cliOptions?: any; 305 | } 306 | ``` 307 | 308 | ## Background 309 | The angular compiler generate additional JS runtime files that are part of the final bundle, 310 | these files reflect the `@Component` resources (html, css) as JS executable code. 311 | 312 | When compiling AOT we need to add them to the final bundle. 313 | > When compiling JIT these files are added to the VM on runtime, but that's not relevant for our context. 314 | 315 | The angular compiler performs static analysis on our app, thus it needs to run before **webpack** (it needs the TS files). 316 | This process create 2 problems: 317 | 318 | - The generated files are not referenced in our app (webpack won't bundle them) 319 | 320 | - The `Compiler` compiles resources such as HTML, CSS, SCSS... 321 | In a webpack environment we expect these resources to pass through the loader chain **BEFORE** they are process by the angular `Compiler`. 322 | This is the case when we develop using JIT. 323 | 324 | `@ngtools/webpack` is the tools used by the `angular-cli`. 325 | 326 | ## What does ngc-webpack do? 327 | `ngc-webpack` integrates with webpack to run `@Component` resources such as HTML, CSS, SCSS etc through 328 | the webpack loader chain. e.g. usually you will need to do some pre/post processing to your styles... 329 | 330 | If you use `ngc-webpack` through the plugin you can also fine tune the bundling process, this can help with 331 | reducing the bundle size, keep reading to get more information (resourceOverride). 332 | 333 | ### Build steps 334 | Run the `compiler-cli` to generate files. 335 | Use a AOT dedicated entry point to point to that file, from there on all references are fine. 336 | 337 | This approach requires you to have 1 extra file, no big deal. 338 | 339 | The problem with this approach is the resources, `compiler-cli` runs before webpack so it gets raw files, e.g A SCSS file is passes as is. 340 | 341 | `ngc-webpack` solves this by running each of the resources through webpack using the webpack configuration file supplied. 342 | 343 | > `ngc-webpack` does not care about SCSS, LESS or any intermediate resource that requires transformation. Each resource will follow the chain defined in the webpack configuration supplied. You get identical result in but development and prod (with AOT) builds. 344 | 345 | ## Why? 346 | Initially, `ngc-webpack` was built to cover the gap between "vanilla" webpack driven angular applications 347 | and `angular-cli` application. There was no tool to handle that and production builds for angular application 348 | was impossible unless using the cli. `ngc-webpack` covered that gap. 349 | 350 | Nowdays, the `angular-cli` is pretty mature, especially with the webpack export capability. 351 | If you have a simple build process I suggest you use the CLI, in fact I suggest you use the 352 | CLI by default and only if you face a scenario that **ngc-webpack** can solve, use it. 353 | 354 | ## My use-case 355 | In the company I work for, the build process requires some modification to 3rd-party libraries. 356 | This modification involves recompiling SCSS files and other funky stuff. Using **ngc-webpack** 357 | we are able to change `ComponentMetadata#styles` of already AOT compiled angular components. 358 | 359 | ## Blog post: 360 | If time allows, I will write a blog post on how we completely restyled the `@angular/material` 361 | library by compiling our versions of material components SCSS files and replacing them with the, already compiled, styles. 362 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/plugin'; 2 | export * from './src/plugin-options'; 3 | export { runCli, runNgCli } from './src/cli'; 4 | 5 | import loader from '@ngtools/webpack'; 6 | export default loader; 7 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "jest-preset-angular", 3 | "setupTestFrameworkScriptFile": "/setup-jest.ts", 4 | "globals": { 5 | "ts-jest": { 6 | "tsConfigFile": "tsconfig.spec.json", 7 | "skipBabel": true 8 | }, 9 | "__TRANSFORM_HTML__": true 10 | }, 11 | "transform": { 12 | "^.+\\.(ts|js|html)$": "/node_modules/jest-preset-angular/preprocessor.js" 13 | }, 14 | "testRegex": "test\\/.+\\.spec\\.ts$", 15 | "moduleFileExtensions": [ 16 | "ts", 17 | "js", 18 | "html", 19 | "json" 20 | ], 21 | "mapCoverage": true, 22 | "moduleDirectories": [ 23 | "node_modules", 24 | "test" 25 | ], 26 | "testPathIgnorePatterns": [ 27 | "/node_modules/" 28 | ], 29 | "transformIgnorePatterns": [ 30 | "node_modules/(?!@ngrx)" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngc-webpack", 3 | "version": "4.1.2", 4 | "description": "A wrapper for the @ngtools/webpack with hooks into the compilation process", 5 | "author": "Shlomi Assaf ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/shlomiassaf/ngc-webpack.git" 10 | }, 11 | "main": "index.js", 12 | "bin": { 13 | "ngc-w": "src/cli/cli.js", 14 | "ngc-w-cli": "src/cli/ng-cli.js" 15 | }, 16 | "typings": "index.d.ts", 17 | "keywords": [ 18 | "angular", 19 | "compiler", 20 | "webpack", 21 | "laoder", 22 | "plugin" 23 | ], 24 | "scripts": { 25 | "clean:build": "npm run rimraf -- dist", 26 | "ci": "node -e \"console.log('TypeScript VERSION: ' + require('typescript').version)\" && npm run test", 27 | "test": "npm run build-test && ./node_modules/.bin/mocha dist/test/*.spec.js --recursive", 28 | "watch": "npm run build -- -w", 29 | "build": "npm run clean:build && tsc --project tsconfig.json", 30 | "watch-test": "npm run build-test -- -w", 31 | "build-test": "npm run clean:build && tsc --project tsconfig.test.json", 32 | "rimraf": "rimraf" 33 | }, 34 | "dependencies": { 35 | "@types/minimist": "^1.2.0", 36 | "loader-utils": "^1.1.0", 37 | "magic-string": "^0.22.3", 38 | "minimist": "^1.2.0", 39 | "reflect-metadata": "^0.1.10", 40 | "resolve": "^1.5.0", 41 | "semver": "^5.4.1", 42 | "source-map": "^0.5.6", 43 | "ts-node": "^3.2.0" 44 | }, 45 | "peerDependencies": { 46 | "@angular/compiler-cli": "^5.0.0", 47 | "@ngtools/webpack": "^1.8.0" 48 | }, 49 | "devDependencies": { 50 | "@angular-devkit/build-optimizer": "^0.0.32", 51 | "@angular/cli": "^1.5.0", 52 | "@angular/common": "^5.0.0", 53 | "@angular/compiler": "^5.0.0", 54 | "@angular/compiler-cli": "^5.0.0", 55 | "@angular/core": "^5.0.0", 56 | "@angular/forms": "^5.0.0", 57 | "@angular/http": "^5.0.0", 58 | "@angular/platform-browser": "^5.0.0", 59 | "@angular/platform-browser-dynamic": "^5.0.0", 60 | "@angular/router": "^5.0.0", 61 | "@ngtools/webpack": "^1.8.0", 62 | "@types/chai": "^3.4.34", 63 | "@types/fs-extra": "^4.0.3", 64 | "@types/jest": "^20.0.4", 65 | "@types/mocha": "^2.2.37", 66 | "@types/node": "^7.0.0", 67 | "@types/resolve": "^0.0.4", 68 | "@types/rimraf": "^2.0.2", 69 | "@types/semver": "^5.3.32", 70 | "@types/source-map": "^0.5.2", 71 | "@types/webpack": "^3.0.14", 72 | "angular2-template-loader": "^0.6.2", 73 | "awesome-typescript-loader": "^3.2.1", 74 | "chai": "^3.5.0", 75 | "cli-table": "^0.3.1", 76 | "css-loader": "^0.28.4", 77 | "extract-text-webpack-plugin": "^3.0.2", 78 | "file-loader": "^0.11.2", 79 | "fs-extra": "^4.0.2", 80 | "html-loader": "^0.5.0", 81 | "html-webpack-plugin": "^2.30.1", 82 | "jest": "^20.0.4", 83 | "mocha": "^3.4.2", 84 | "ng-router-loader": "^2.1.0", 85 | "node-map-directory": "0.1.0", 86 | "node-sass": "^4.6.0", 87 | "raw-loader": "0.5.1", 88 | "rimraf": "~2.5.4", 89 | "rxjs": "^5.5.2", 90 | "sass-loader": "^6.0.6", 91 | "string-replace-loader": "^1.3.0", 92 | "style-loader": "^0.18.2", 93 | "to-string-loader": "^1.1.4", 94 | "ts-jest": "^20.0.7", 95 | "tsconfig-paths": "^2.3.0", 96 | "typescript": "~2.4.2", 97 | "webpack": "^3.8.1", 98 | "zone.js": "^0.8.14" 99 | }, 100 | "bugs": { 101 | "url": "https://github.com/shlomiassaf/ngc-webpack/issues" 102 | }, 103 | "homepage": "https://github.com/shlomiassaf/ngc-webpack#readme" 104 | } 105 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf ./dist 4 | 5 | npm run build 2> /dev/null 6 | 7 | if [ $? -eq 0 ] 8 | then 9 | echo "Compilation OK, publishing" 10 | cp README.md ./dist/README.md 11 | cp package.json ./dist/package.json 12 | cp .npmignore ./dist/.npmignore 13 | 14 | NPM_USER=$(npm whoami 2> /dev/null) 15 | if [ "${NPM_USER}" != "shlomiassaf" ]; then 16 | echo "You must be logged in as 'shlomiassaf' to publish. Use 'npm login'." 17 | exit 18 | fi 19 | 20 | set -ex 21 | 22 | npm publish --access public ./dist 23 | 24 | else 25 | echo "Compilation failed" >&2 26 | fi 27 | -------------------------------------------------------------------------------- /setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | 3 | const mock = () => { 4 | let storage = {}; 5 | return { 6 | getItem: key => key in storage ? storage[key] : null, 7 | setItem: (key, value) => storage[key] = value || '', 8 | removeItem: key => delete storage[key], 9 | clear: () => storage = {}, 10 | }; 11 | }; 12 | 13 | Object.defineProperty(window, 'localStorage', {value: mock()}); 14 | Object.defineProperty(window, 'sessionStorage', {value: mock()}); 15 | Object.defineProperty(window, 'getComputedStyle', { 16 | value: () => ['-webkit-appearance'] 17 | }); 18 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | Notes: 2 | 3 | - In most part, `ngc-webpack` is a proxy to `@ngtools/webpack` 4 | 5 | - Some functionality, mostly around library mode support (CLI), 6 | is based on angular experimental APIs. 7 | 8 | - In situations where angular APIs / constructs are not exposed, 9 | a local implementation is provided. 10 | 11 | - Local implementation are based on source code from the angular 12 | project, mostly with local modification for `ngc-webpack` logic but sometimes 13 | a complete copy. 14 | For example, the code in [cli/transformers/fw](cli/transformers/fw) is copy 15 | of the transformation helper used by `@ngtools/webpack` 16 | The code in [inline-resources.ts](cli/transformers/inline-resources.ts) is 17 | a local implementation inspired by the [replace resource transformer](https://github.com/angular/angular-cli/blob/master/packages/%40ngtools/webpack/src/transformers/replace_resources.ts) in `@ngtools/webpack` 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/_resource-loader.ts: -------------------------------------------------------------------------------- 1 | // Taken from 2 | // https://github.com/angular/angular-cli/blob/master/packages/%40ngtools/webpack/src/resource_loader.ts 3 | 4 | import * as vm from 'vm'; 5 | import * as path from 'path'; 6 | 7 | const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin'); 8 | const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin'); 9 | const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin'); 10 | const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); 11 | 12 | 13 | export interface CompilationOutput { 14 | outputName: string; 15 | source: string; 16 | } 17 | 18 | export class WebpackResourceLoader { 19 | private _parentCompilation: any; 20 | private _context: string; 21 | private _uniqueId = 0; 22 | private _resourceDependencies = new Map(); 23 | 24 | constructor() {} 25 | 26 | update(parentCompilation: any) { 27 | this._parentCompilation = parentCompilation; 28 | this._context = parentCompilation.context; 29 | this._uniqueId = 0; 30 | } 31 | 32 | getResourceDependencies(filePath: string) { 33 | return this._resourceDependencies.get(filePath) || []; 34 | } 35 | 36 | private _compile(filePath: string): Promise { 37 | 38 | if (!this._parentCompilation) { 39 | throw new Error('WebpackResourceLoader cannot be used without parentCompilation'); 40 | } 41 | 42 | const compilerName = `compiler(${this._uniqueId++})`; 43 | const outputOptions = { filename: filePath }; 44 | const relativePath = path.relative(this._context || '', filePath); 45 | const childCompiler = this._parentCompilation.createChildCompiler(relativePath, outputOptions); 46 | childCompiler.context = this._context; 47 | childCompiler.apply( 48 | new NodeTemplatePlugin(outputOptions), 49 | new NodeTargetPlugin(), 50 | new SingleEntryPlugin(this._context, filePath), 51 | new LoaderTargetPlugin('node') 52 | ); 53 | 54 | // Store the result of the parent compilation before we start the child compilation 55 | let assetsBeforeCompilation = Object.assign( 56 | {}, 57 | this._parentCompilation.assets[outputOptions.filename] 58 | ); 59 | 60 | // Fix for "Uncaught TypeError: __webpack_require__(...) is not a function" 61 | // Hot module replacement requires that every child compiler has its own 62 | // cache. @see https://github.com/ampedandwired/html-webpack-plugin/pull/179 63 | childCompiler.plugin('compilation', function (compilation: any) { 64 | if (compilation.cache) { 65 | if (!compilation.cache[compilerName]) { 66 | compilation.cache[compilerName] = {}; 67 | } 68 | compilation.cache = compilation.cache[compilerName]; 69 | } 70 | }); 71 | 72 | // Compile and return a promise 73 | return new Promise((resolve, reject) => { 74 | childCompiler.runAsChild((err: Error, entries: any[], childCompilation: any) => { 75 | // Resolve / reject the promise 76 | if (childCompilation && childCompilation.errors && childCompilation.errors.length) { 77 | const errorDetails = childCompilation.errors.map(function (error: any) { 78 | return error.message + (error.error ? ':\n' + error.error : ''); 79 | }).join('\n'); 80 | reject(new Error('Child compilation failed:\n' + errorDetails)); 81 | } else if (err) { 82 | reject(err); 83 | } else { 84 | // Replace [hash] placeholders in filename 85 | const outputName = this._parentCompilation.mainTemplate.applyPluginsWaterfall( 86 | 'asset-path', outputOptions.filename, { 87 | hash: childCompilation.hash, 88 | chunk: entries[0] 89 | }); 90 | 91 | // Restore the parent compilation to the state like it was before the child compilation. 92 | Object.keys(childCompilation.assets).forEach((fileName) => { 93 | // If it wasn't there and it's a source file (absolute path) - delete it. 94 | if (assetsBeforeCompilation[fileName] === undefined && path.isAbsolute(fileName)) { 95 | delete this._parentCompilation.assets[fileName]; 96 | } else { 97 | // Otherwise, add it to the parent compilation. 98 | this._parentCompilation.assets[fileName] = childCompilation.assets[fileName]; 99 | } 100 | }); 101 | 102 | // Save the dependencies for this resource. 103 | this._resourceDependencies.set(outputName, childCompilation.fileDependencies); 104 | 105 | resolve({ 106 | // Output name. 107 | outputName, 108 | // Compiled code. 109 | source: childCompilation.assets[outputName].source() 110 | }); 111 | } 112 | }); 113 | }); 114 | } 115 | 116 | private _evaluate(output: CompilationOutput): Promise { 117 | try { 118 | const outputName = output.outputName; 119 | const vmContext = vm.createContext(Object.assign({ require: require }, global)); 120 | const vmScript = new vm.Script(output.source, { filename: outputName }); 121 | 122 | // Evaluate code and cast to string 123 | let evaluatedSource: string; 124 | evaluatedSource = vmScript.runInContext(vmContext); 125 | 126 | if (typeof evaluatedSource == 'string') { 127 | return Promise.resolve(evaluatedSource); 128 | } 129 | 130 | return Promise.reject('The loader "' + outputName + '" didn\'t return a string.'); 131 | } catch (e) { 132 | return Promise.reject(e); 133 | } 134 | } 135 | 136 | get(filePath: string): Promise { 137 | return this._compile(filePath) 138 | .then((result: CompilationOutput) => this._evaluate(result)); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/angular-compiler-execution-host.ts: -------------------------------------------------------------------------------- 1 | import { AngularCompilerPlugin } from '@ngtools/webpack'; 2 | 3 | import { NgcWebpackPluginOptions } from './plugin-options' 4 | import { isValidAngularCompilerPlugin } from './utils'; 5 | import { NgcCompilerExecutionHost, MonkeyAngularCompilerPlugin, MonkeyWebpackCompilerHost } from './execution-models'; 6 | 7 | 8 | export function createAngularCompilerPluginExecutionHost(options: NgcWebpackPluginOptions): NgcCompilerExecutionHost { 9 | const ngPlugin: MonkeyAngularCompilerPlugin = new AngularCompilerPlugin(options); 10 | 11 | if (!isValidAngularCompilerPlugin(ngPlugin)) { 12 | throw new Error('The "@ngtools/webpack" package installed is not compatible with this ' + 13 | 'version of "ngc-webpack"'); 14 | } 15 | 16 | // we must use the base instance because AngularCompilerPlugin use it. 17 | const compilerHost = ngPlugin._compilerHost; 18 | 19 | Object.defineProperty(compilerHost, 'resourceLoader', { 20 | get: function(this: MonkeyWebpackCompilerHost) { 21 | return this._resourceLoader; 22 | } 23 | }); 24 | 25 | return { 26 | execute(compiler: any): void { 27 | ngPlugin.apply(compiler); 28 | }, 29 | compilerHost, 30 | transformers: ngPlugin._transformers, 31 | hookOverride: { 32 | readFileTransformer: readFileTransformer => { 33 | const orgReadFile = compilerHost.readFile; 34 | const { predicate, transform } = readFileTransformer; 35 | const predicateFn = typeof predicate === 'function' 36 | ? predicate 37 | : (fileName: string) => predicate.test(fileName) 38 | ; 39 | 40 | Object.defineProperty(compilerHost, 'readFile', { 41 | value: function(this: MonkeyWebpackCompilerHost, fileName: string): string { 42 | if (predicateFn(fileName)) { 43 | let stats = compilerHost._files[fileName]; 44 | if (!stats) { 45 | const content = transform(fileName, orgReadFile.call(compilerHost, fileName)); 46 | stats = compilerHost._files[fileName]; 47 | if (stats) { 48 | stats.content = content; 49 | } 50 | return content; 51 | } 52 | } 53 | return orgReadFile.call(compilerHost, fileName); 54 | } 55 | }); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/cli/cli-compiler-host.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {basename, dirname, join, sep} from 'path'; 3 | import * as fs from 'fs'; 4 | import { WebpackResourceLoader } from '../resource-loader'; 5 | 6 | function denormalizePath(path: string): string { 7 | return path.replace(/\//g, sep); 8 | } 9 | 10 | export interface OnErrorFn { 11 | (message: string): void; 12 | } 13 | 14 | export class CliCompilerHost implements ts.CompilerHost { 15 | private resourceCache = new Map(); 16 | private host: ts.CompilerHost; 17 | 18 | constructor(private options: ts.CompilerOptions, public resourceLoader?: WebpackResourceLoader) { 19 | this.host = ts.createCompilerHost(this.options, true); 20 | } 21 | 22 | fileExists(fileName: string): boolean { 23 | return this.host.fileExists(fileName); 24 | } 25 | 26 | readFile(fileName: string): string { 27 | return this.host.readFile(fileName); 28 | } 29 | 30 | getDirectories(path: string): string[] { 31 | return this.host.getDirectories(path); 32 | } 33 | 34 | getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, _onError?: OnErrorFn) { 35 | return this.host.getSourceFile(fileName, languageVersion, _onError); 36 | } 37 | 38 | getCancellationToken() { 39 | return this.host.getCancellationToken!(); 40 | } 41 | 42 | getDefaultLibFileName(options: ts.CompilerOptions) { 43 | return this.host.getDefaultLibFileName(options); 44 | } 45 | 46 | writeFile(fileName: string, data: string, _writeByteOrderMark: boolean, 47 | _onError?: (message: string) => void, _sourceFiles?: ts.SourceFile[]) { 48 | return this.host.writeFile(fileName, data, _writeByteOrderMark, _onError, _sourceFiles); 49 | } 50 | 51 | getCurrentDirectory(): string { 52 | return this.host.getCurrentDirectory(); 53 | } 54 | 55 | getCanonicalFileName(fileName: string): string { 56 | return this.host.getCanonicalFileName(fileName); 57 | } 58 | 59 | useCaseSensitiveFileNames(): boolean { 60 | return this.host.useCaseSensitiveFileNames(); 61 | } 62 | 63 | getNewLine(): string { 64 | return this.host.getNewLine(); 65 | } 66 | 67 | readResource(fileName: string): Promise | string { 68 | if (this.resourceLoader) { 69 | // These paths are meant to be used by the loader so we must denormalize them. 70 | const denormalizedFileName = denormalizePath(fileName); 71 | return this.resourceLoader.get(denormalizedFileName) 72 | .then( content => { 73 | this.resourceCache.set(denormalizedFileName, content); 74 | return content; 75 | }); 76 | } else { 77 | return this.readFile(fileName); 78 | } 79 | } 80 | 81 | /** 82 | * Returns a cached resource, if the resource is not cached returns undefined. 83 | * Will not try to get the resource if it does not exists. 84 | * @param {string} fileName 85 | * @returns {string} 86 | */ 87 | getResource(fileName: string): string | undefined { 88 | return this.resourceCache.get(fileName); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/cli/cli-context.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | import * as Path from 'path'; 3 | import * as ts from 'typescript'; 4 | 5 | import { TsEmitArguments } from '@angular/compiler-cli'; 6 | import { WebpackResourceLoader } from '../resource-loader'; 7 | 8 | import { NgcParsedConfiguration } from './config'; 9 | import { createTsickleEmitCallback, defaultEmitCallback, createSrcToOutPathMapper } from './util'; 10 | import { CliCompilerHost } from './cli-compiler-host'; 11 | import { inlineResources } from './transformers/inline-resources'; 12 | import { inlineMetadataBundle } from './inline-metadata'; 13 | 14 | export function createCliContext(config: NgcParsedConfiguration) { 15 | let sourceToOutMapper: (srcFileName: string, reverse?: boolean) => string; 16 | 17 | const compilerHost = new CliCompilerHost(config.options, new WebpackResourceLoader()); 18 | const getResource = (resourcePath: string): string | undefined => compilerHost.getResource(resourcePath); 19 | const realEmitCallback = createTsickleEmitCallback(config.options); // defaultEmitCallback; 20 | 21 | 22 | const inlineMetadataModule = (fileName: string, data: string): string => { 23 | const metadataBundle = JSON.parse(data); 24 | 25 | let relativeTo = Path.dirname(fileName); 26 | if (sourceToOutMapper) { 27 | relativeTo = sourceToOutMapper(relativeTo, true); 28 | } 29 | 30 | // process the metadata bundle and inline resources 31 | // we send the source location as the relative folder (not the dest) so matching resource paths 32 | // with compilerHost will work. 33 | metadataBundle.forEach( m => inlineMetadataBundle(relativeTo, m, getResource) ); 34 | 35 | return JSON.stringify(metadataBundle); 36 | }; 37 | 38 | const emitCallback = (emitArgs: TsEmitArguments) => { 39 | const writeFile = (...args: any[]) => { 40 | // we don't need to collect all source files mappings, we need only 1 so it's a bit different 41 | // from angular's code 42 | if (!sourceToOutMapper) { 43 | const outFileName: string = args[0]; 44 | const sourceFiles: ts.SourceFile[] = args[4]; 45 | if (sourceFiles && sourceFiles.length == 1) { 46 | sourceToOutMapper = createSrcToOutPathMapper( 47 | config.options.outDir, 48 | sourceFiles[0].fileName, 49 | outFileName 50 | ); 51 | } 52 | } 53 | return emitArgs.writeFile.apply(null, args); 54 | }; 55 | return realEmitCallback(Object.create(emitArgs, { writeFile: { value: writeFile } })); 56 | }; 57 | 58 | return { 59 | compilerHost, 60 | createCompilation(compiler) { 61 | const compilation = compiler.createCompilation(); 62 | compilerHost.resourceLoader.update(compilation); 63 | return compilation; 64 | }, 65 | getResource, 66 | createInlineResourcesTransformer() { 67 | return inlineResources( 68 | getResource, 69 | (fileName: string) => !fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts') 70 | ) 71 | }, 72 | emitCallback, 73 | 74 | /** 75 | * Returns a compilerHost instance that inline all resources (templateUrl, styleUrls) inside metadata files that was 76 | * created for a specific module (i.e. not a flat metadata bundle module) 77 | * 78 | */ 79 | resourceInliningCompilerHost() { 80 | return Object.create(compilerHost, { 81 | writeFile: { 82 | writable: true, 83 | value: (fileName: string, data: string, ...args: any[]): void => { 84 | if (/\.metadata\.json$/.test(fileName)) { 85 | data = inlineMetadataModule(fileName, data); 86 | } 87 | return compilerHost.writeFile(fileName, data, args[0], args[1], args[2]); 88 | } 89 | } 90 | }) 91 | }, 92 | 93 | inlineFlatModuleMetadataBundle(relativeTo: string, flatModuleOutFile: string): void { 94 | let metadataPath = Path.resolve(relativeTo, flatModuleOutFile.replace(/\.js$/, '.metadata.json')); 95 | 96 | if (sourceToOutMapper) { 97 | metadataPath = sourceToOutMapper(metadataPath); 98 | } 99 | 100 | if (!FS.existsSync(metadataPath)) { 101 | throw new Error(`Could not find flat module "metadata.json" output at ${metadataPath}`); 102 | } 103 | 104 | const metadataBundle = JSON.parse(FS.readFileSync(metadataPath, { encoding: 'utf8' })); 105 | 106 | // process the metadata bundle and inline resources 107 | // we send the source location as the relative folder (not the dest) so matching resource paths 108 | // with compilerHost will work. 109 | inlineMetadataBundle(relativeTo, metadataBundle, getResource); 110 | 111 | FS.writeFileSync(metadataPath, JSON.stringify(metadataBundle), { encoding: 'utf8' }); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/cli/cli-execution-host.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | 3 | import { NgcWebpackPluginOptions } from '../plugin-options'; 4 | import { NgcCompilerExecutionHost } from '../execution-models'; 5 | 6 | import { NgcParsedConfiguration } from './config'; 7 | import {ParsedDiagnostics, parseDiagnostics} from './util'; 8 | import { performCompilationAsync } from './perform_compile_async'; 9 | import { createCliContext } from './cli-context'; 10 | import { promiseWrapper } from '../utils'; 11 | 12 | export interface AsyncNgcCompilerExecutionHost extends NgcCompilerExecutionHost { 13 | executeAsync(compiler: any): Promise; 14 | } 15 | 16 | export function asyncCliExecutionHostFactory(config: NgcParsedConfiguration): { 17 | executionHostFactory: (options: NgcWebpackPluginOptions) => NgcCompilerExecutionHost, 18 | executeDone: Promise 19 | } { 20 | const executionHostFactory = cliExecutionHostFactory(config); 21 | const p = promiseWrapper(); 22 | 23 | const wrapper = (options: NgcWebpackPluginOptions): NgcCompilerExecutionHost => { 24 | const result = executionHostFactory(options); 25 | return Object.create(result, { 26 | execute: { 27 | value: (compiler: any) => result.executeAsync(compiler).then(p.resolve) .catch(p.reject) 28 | } 29 | }); 30 | }; 31 | 32 | return { 33 | executionHostFactory: wrapper, 34 | executeDone: p.promise 35 | } 36 | } 37 | 38 | export function cliExecutionHostFactory(config: NgcParsedConfiguration): (options: NgcWebpackPluginOptions) => AsyncNgcCompilerExecutionHost { 39 | return (options: NgcWebpackPluginOptions): AsyncNgcCompilerExecutionHost => { 40 | const inline = config.options.skipTemplateCodegen; 41 | if (config.options.skipTemplateCodegen && !config.options.fullTemplateTypeCheck) { 42 | /* 43 | Angular cli's compiler host will not generate metadata if skipping template codegen or no full template typescheck. 44 | See https://github.com/angular/angular/blob/master/packages/compiler-cli/src/transformers/compiler_host.ts#L440 45 | This is required if we want to inline the resources while compiling and not in post. 46 | 47 | To solve this we need to enforce `fullTemplateTypeCheck`: 48 | 49 | options.fullTemplateTypeCheck = true; 50 | 51 | but this has issues 52 | see issue: https://github.com/angular/angular/issues/19905 53 | which has pending PR to fix: https://github.com/angular/angular/pull/20490 54 | and also, dev's might want this off... 55 | 56 | current workaround will is to disable skipTemplateCodegen 57 | this looks weired because we want it on... 58 | but, at this point we have a config object (NgcParsedConfiguration) which is an angular-cli parsed config 59 | created by called `readNgcCommandLineAndConfiguration`. 60 | The config object has a property `emitFlags` which at this point has the flag `Codegen` OFF !!! 61 | OFF reflects config.options.skipTemplateCodegen = true. 62 | 63 | Setting `config.options.skipTemplateCodegen` to false, at this point, will not change the emitFlags. 64 | The compiler will NOT emit template code gen but the `isSourceFile` method in 65 | https://github.com/angular/angular/blob/master/packages/compiler-cli/src/transformers/compiler_host.ts#L440 66 | will return true! 67 | 68 | This is a weak workaround and a more solid one is required. 69 | 70 | TODO: refactor workaround to a writeFile wrapper that will not write generated files. 71 | */ 72 | // options.fullTemplateTypeCheck = true; 73 | config.options.skipTemplateCodegen = false; 74 | } 75 | 76 | const ctx = createCliContext(config); 77 | const { compilerHost } = ctx; 78 | 79 | return { 80 | execute(compiler: any): void { 81 | this.executeAsync(compiler); 82 | }, 83 | executeAsync(compiler: any): Promise { 84 | const compilation = ctx.createCompilation(compiler); 85 | const rootNames = config.rootNames.slice(); 86 | 87 | return performCompilationAsync({ 88 | rootNames, 89 | options: config.options, 90 | 91 | /* 92 | The compiler host "writeFile" is wrapped with a handler that will 93 | inline all resources into metadata modules (non flat bundle modules) 94 | */ 95 | host: (inline && !config.options.skipMetadataEmit && !config.options.flatModuleOutFile) 96 | ? ctx.resourceInliningCompilerHost() 97 | : compilerHost 98 | , 99 | emitFlags: config.emitFlags, 100 | // we use the compiler-cli emit callback but we wrap it so we can create a map of source file path to 101 | // output file path 102 | emitCallback: ctx.emitCallback, 103 | customTransformers: { 104 | beforeTs: inline ? [ ctx.createInlineResourcesTransformer() ] : [] 105 | } 106 | }) 107 | .then( result => { 108 | const parsedDiagnostics = parseDiagnostics(result.diagnostics, config.options); 109 | if (parsedDiagnostics.exitCode !== 0) { 110 | const error = parsedDiagnostics.error || new Error(parsedDiagnostics.exitCode.toString()); 111 | compilation.errors.push(error); 112 | } 113 | 114 | // inline resources into the flat metadata json file, if exists. 115 | if (compilation.errors.length === 0 && config.options.flatModuleOutFile) { 116 | // TODO: check that it exists, config.rootNames should not have it (i.e. it was added to rootNames) 117 | const flatModulePath = rootNames[rootNames.length - 1]; 118 | ctx.inlineFlatModuleMetadataBundle(Path.dirname(flatModulePath), config.options.flatModuleOutFile); 119 | } 120 | 121 | return parsedDiagnostics; 122 | } ); 123 | }, 124 | compilerHost, 125 | transformers: [] 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | import * as webpack from 'webpack'; 3 | import * as minimist from 'minimist'; 4 | import { AngularCompilerPlugin, AngularCompilerPluginOptions } from '@ngtools/webpack'; 5 | 6 | import { NgcWebpackPlugin } from '../plugin'; 7 | 8 | import { readNgcCommandLineAndConfiguration } from './config'; 9 | import { parseDiagnostics, ParsedDiagnostics } from './util'; 10 | import { asyncCliExecutionHostFactory } from './cli-execution-host'; 11 | 12 | /** 13 | * Resolve the config to an object. 14 | * If it's a fn, invoke. 15 | * 16 | * Also check if it's a mocked ES6 Module in cases where TS file is used that uses "export default" 17 | * @param config 18 | * @returns {any} 19 | */ 20 | function resolveConfig(config: any): any { 21 | if (typeof config === 'function') { 22 | return config(); 23 | } else if (config.__esModule === true && !!config.default) { 24 | return resolveConfig(config.default); 25 | } else { 26 | return config; 27 | } 28 | } 29 | 30 | export function findPluginIndex(plugins: any[], type: any): number { 31 | return plugins.findIndex( p => p instanceof type); 32 | } 33 | 34 | export function getPluginMeta(plugins: any[]): { idx: number, instance: AngularCompilerPlugin | NgcWebpackPlugin, options: AngularCompilerPluginOptions } { 35 | let idx = findPluginIndex(plugins, NgcWebpackPlugin); 36 | 37 | if (idx > -1) { 38 | return { 39 | idx, 40 | instance: plugins[idx], 41 | options: plugins[idx].ngcWebpackPluginOptions 42 | } 43 | } 44 | 45 | idx = findPluginIndex(plugins, AngularCompilerPlugin); 46 | if (idx > -1) { 47 | return { 48 | idx, 49 | instance: plugins[idx], 50 | options: plugins[idx].options 51 | } 52 | } 53 | 54 | // TODO: allow running without a plugin and create it here? 55 | throw new Error('Could not find an instance of NgcWebpackPlugin or AngularCompilerPlugin in the provided webpack configuration'); 56 | } 57 | 58 | 59 | function normalizeProjectParam(tsConfigPath: string, args: string[], parsedArgs: any): void { 60 | const [ pIdx, projectIdx ] = [args.indexOf('-p'), args.indexOf('--project')]; 61 | parsedArgs.p = tsConfigPath; 62 | if (pIdx > -1) { 63 | args[pIdx + 1] = tsConfigPath; 64 | } else { 65 | args.push('-p', tsConfigPath); 66 | } 67 | if (projectIdx > -1) { 68 | delete parsedArgs.project; 69 | args.splice(projectIdx, 1); 70 | } 71 | } 72 | 73 | /** 74 | * Run `ngc-webpack` in library mode. (i.e. run `ngc`) 75 | * In Library mode compilation and output is done per module and no bundling is done. 76 | * Webpack is used for resource compilation through it's loader chain but does not bundle anything. 77 | * The webpack configuration, excluding loaders, has no effect. 78 | * The webpack configuration must include a plugin instance (either NgcWebpackPlugin / AngularCompilerPlugin). 79 | * 80 | * Library mode configuration is done mainly from the `tsconfig` json file. 81 | * 82 | * `tsconfig` json path is taken from the options of NgcWebpackPlugin / AngularCompilerPlugin 83 | * 84 | * @param webpackConfig Webpack configuration module, object or string 85 | */ 86 | export function runCli(webpackConfig: string | webpack.Configuration): Promise; 87 | /** 88 | * Run `ngc-webpack` in library mode. (i.e. run `ngc`) 89 | * In Library mode compilation and output is done per module and no bundling is done. 90 | * Webpack is used for resource compilation through it's loader chain but does not bundle anything. 91 | * The webpack configuration, excluding loaders, has no effect. 92 | * The webpack configuration must include a plugin instance (either NgcWebpackPlugin / AngularCompilerPlugin). 93 | * 94 | * Library mode configuration is done mainly from the `tsconfig` json file. 95 | * 96 | * `tsconfig` json path is taken from cli parameters (-p or --project) or, if not exists the options of 97 | * NgcWebpackPlugin / AngularCompilerPlugin 98 | * 99 | * @param webpackConfig Webpack configuration module, object or string, 100 | * @param cliParams cli Parameters, parsedArgs is not mandatory 101 | */ 102 | export function runCli(webpackConfig: string | webpack.Configuration, 103 | cliParams: { args: string[], parsedArgs?: minimist.ParsedArgs }): Promise; 104 | /** 105 | * Run `ngc-webpack` in library mode. (i.e. run `ngc`) 106 | * In Library mode compilation and output is done per module and no bundling is done. 107 | * Webpack is used for resource compilation through it's loader chain but does not bundle anything. 108 | * The webpack configuration, excluding loaders, has no effect. 109 | * The webpack configuration must include a plugin instance (either NgcWebpackPlugin / AngularCompilerPlugin). 110 | * 111 | * Library mode configuration is done mainly from the `tsconfig` json file. 112 | * 113 | * `tsconfig` json path is taken from the supplied tsConfigPath parameter. 114 | * 115 | * @param webpackConfig Webpack configuration module, object or string, 116 | * @param tsConfigPath path to the tsconfig file, relative to process.cwd() 117 | * @param cliParams cli Parameters, parsedArgs is not mandatory 118 | */ 119 | export function runCli(webpackConfig: string | webpack.Configuration, 120 | tsConfigPath: string, 121 | cliParams?: { args: string[], parsedArgs?: minimist.ParsedArgs }): Promise; 122 | export function runCli(webpackConfig: string | webpack.Configuration, 123 | tsConfigPath?: any, 124 | cliParams?: { args: string[], parsedArgs?: minimist.ParsedArgs }): Promise { 125 | return Promise.resolve(null) 126 | .then( () => { 127 | // normalize params: 128 | if (tsConfigPath && typeof tsConfigPath !== 'string') { 129 | cliParams = tsConfigPath; 130 | tsConfigPath = undefined; 131 | } 132 | if (!cliParams) { 133 | cliParams = { args: [], parsedArgs: {} }; 134 | } else if (!cliParams.parsedArgs) { 135 | cliParams.parsedArgs = minimist(cliParams.args); 136 | } 137 | const { args, parsedArgs } = cliParams; 138 | 139 | if (typeof webpackConfig === 'string') { 140 | let configPath = Path.isAbsolute(webpackConfig) 141 | ? webpackConfig 142 | : Path.join(process.cwd(), webpackConfig) 143 | ; 144 | 145 | webpackConfig = require(configPath); 146 | } 147 | 148 | const configModule = resolveConfig(webpackConfig); 149 | const pluginMeta = getPluginMeta(configModule.plugins || []); 150 | 151 | if (!tsConfigPath) { 152 | tsConfigPath = parsedArgs.p || parsedArgs.project || pluginMeta.options.tsConfigPath; 153 | } 154 | if (!tsConfigPath) { 155 | throw new Error('Invalid configuration, please set tsconfig path in cli params -p or --project or in NgcWebpackPlugin configuration'); 156 | } 157 | pluginMeta.options.tsConfigPath = tsConfigPath; 158 | normalizeProjectParam(tsConfigPath, args, parsedArgs); 159 | 160 | const config = readNgcCommandLineAndConfiguration(args, parsedArgs); 161 | 162 | if (config.errors.length) { 163 | return parseDiagnostics(config.errors, /*options*/ undefined); 164 | } 165 | 166 | /* 167 | Because we are using webpack API to execute (`plugin.apply(compiler)`), the execution response is hidden. 168 | To play nice with webpack, instead of changing execute to return a promise we wrap the whole execute function 169 | and provide a notification 170 | */ 171 | const { executeDone, executionHostFactory } = asyncCliExecutionHostFactory(config); 172 | 173 | 174 | const plugin = new NgcWebpackPlugin(pluginMeta.options, executionHostFactory); 175 | configModule.plugins.splice(pluginMeta.idx, 1); 176 | 177 | const compiler = webpack(configModule); 178 | plugin.apply(compiler); 179 | 180 | return executeDone 181 | }); 182 | 183 | } 184 | 185 | 186 | if (require.main === module) { 187 | const args: string[] = process.argv.slice(2); 188 | const parsedArgs = minimist(args); 189 | 190 | const webpackConfig = parsedArgs.webpack; 191 | 192 | if (!webpackConfig) { 193 | throw new Error('Missing webpack argument'); 194 | } 195 | 196 | delete parsedArgs.webpack; 197 | args.splice(args.indexOf('--webpack'), 2); 198 | 199 | runCli(webpackConfig, { args, parsedArgs }) 200 | .then( parsedDiagnostics => { 201 | if (parsedDiagnostics.error) { 202 | console.error(parsedDiagnostics.error); 203 | } 204 | process.exit(parsedDiagnostics.exitCode); 205 | }); 206 | } 207 | -------------------------------------------------------------------------------- /src/cli/config.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { CompilerOptions, ParsedConfiguration, readConfiguration, EmitFlags } from '@angular/compiler-cli' ; 3 | import { ParsedArgs } from 'minimist'; 4 | 5 | export interface NgcParsedConfiguration extends ParsedConfiguration { watch?: boolean; } 6 | 7 | export function readNgcCommandLineAndConfiguration(args: any, 8 | parsedArgs?: ParsedArgs): NgcParsedConfiguration { 9 | const options: CompilerOptions = {}; 10 | parsedArgs = parsedArgs || require('minimist')(args); 11 | 12 | if (parsedArgs.i18nFile) options.i18nInFile = parsedArgs.i18nFile; 13 | if (parsedArgs.i18nFormat) options.i18nInFormat = parsedArgs.i18nFormat; 14 | if (parsedArgs.locale) options.i18nInLocale = parsedArgs.locale; 15 | const mt = parsedArgs.missingTranslation; 16 | if (mt === 'error' || mt === 'warning' || mt === 'ignore') { 17 | options.i18nInMissingTranslations = mt; 18 | } 19 | const config = readCommandLineAndConfiguration( 20 | args, 21 | options, 22 | ['i18nFile', 'i18nFormat', 'locale', 'missingTranslation', 'watch'] 23 | ); 24 | 25 | const watch = parsedArgs.w || parsedArgs.watch; 26 | return {...config, watch: !!watch}; 27 | } 28 | 29 | 30 | export function readCommandLineAndConfiguration(args: string[], 31 | existingOptions: CompilerOptions = {}, 32 | ngCmdLineOptions: string[] = []): ParsedConfiguration { 33 | let cmdConfig = ts.parseCommandLine(args); 34 | const project = cmdConfig.options.project || '.'; 35 | const cmdErrors = cmdConfig.errors.filter(e => { 36 | if (typeof e.messageText === 'string') { 37 | const msg = e.messageText; 38 | return !ngCmdLineOptions.some(o => msg.indexOf(o) >= 0); 39 | } 40 | return true; 41 | }); 42 | if (cmdErrors.length) { 43 | return { 44 | project, 45 | rootNames: [], 46 | options: cmdConfig.options, 47 | errors: cmdErrors, 48 | emitFlags: EmitFlags.Default 49 | }; 50 | } 51 | const config = readConfiguration(project, cmdConfig.options); 52 | const options = {...config.options, ...existingOptions}; 53 | if (options.locale) { 54 | options.i18nInLocale = options.locale; 55 | } 56 | return { 57 | project, 58 | rootNames: config.rootNames, 59 | options, 60 | errors: config.errors, 61 | emitFlags: config.emitFlags 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | export { runCli } from './cli'; 2 | export { runNgCli } from './ng-cli'; 3 | -------------------------------------------------------------------------------- /src/cli/inline-metadata.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | 3 | import { Component } from '@angular/core'; 4 | import { 5 | ModuleMetadata, 6 | MetadataSymbolicCallExpression, 7 | isClassMetadata, 8 | isMetadataSymbolicCallExpression, 9 | isMetadataImportedSymbolReferenceExpression 10 | } from '@angular/compiler-cli'; 11 | 12 | 13 | function hasResources(obj: any): obj is Component { 14 | return obj.hasOwnProperty('templateUrl') || obj.hasOwnProperty('styleUrls'); 15 | } 16 | 17 | function findComponentDecoratorMetadata(decorators: any[]): MetadataSymbolicCallExpression { 18 | return decorators 19 | .find( entry => { 20 | if (isMetadataSymbolicCallExpression(entry)) { 21 | const exp = entry.expression; 22 | if (isMetadataImportedSymbolReferenceExpression(exp) && exp.module === '@angular/core' && exp.name === 'Component') { 23 | return true; 24 | } 25 | } 26 | return false; 27 | }) 28 | } 29 | 30 | export function inlineMetadataBundle(relativeTo: string, 31 | metadataBundle: ModuleMetadata, 32 | getResource: (resourcePath: string) => string | undefined): void { 33 | const { metadata, origins } = metadataBundle; 34 | 35 | Object.keys(metadata).forEach( key => { 36 | const entry = metadata[key]; 37 | if (isClassMetadata(entry) && entry.decorators) { 38 | const exp = findComponentDecoratorMetadata(entry.decorators); 39 | const componentMetadata = exp && exp.arguments && exp.arguments[0]; 40 | 41 | if (componentMetadata && hasResources(componentMetadata)) { 42 | // when no "origins" it is metadata json for specific module, if origins it's flat mode. 43 | const origin = origins 44 | ? Path.dirname(Path.resolve(relativeTo, origins[key])) 45 | : relativeTo 46 | ; 47 | 48 | if (componentMetadata.templateUrl) { 49 | const template = getResource(Path.resolve(origin, componentMetadata.templateUrl)); 50 | if (template) { 51 | delete componentMetadata.templateUrl; 52 | componentMetadata.template = template; 53 | } 54 | } 55 | 56 | if (componentMetadata.styleUrls) { 57 | componentMetadata.styles = componentMetadata.styleUrls 58 | .map( stylePath => getResource(Path.resolve(origin, stylePath)) ); 59 | delete componentMetadata.styleUrls; 60 | } 61 | } 62 | } 63 | }); 64 | } -------------------------------------------------------------------------------- /src/cli/ng-cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as Path from 'path'; 4 | import * as FS from 'fs'; 5 | import * as minimist from 'minimist'; 6 | import * as resolve from 'resolve'; 7 | import { Configuration } from 'webpack'; 8 | import { NgCliWebpackConfig as _NgCliWebpackConfig } from '@angular/cli'; 9 | import { runCli } from './cli'; 10 | import { ParsedDiagnostics } from './util'; 11 | import { promisify, promiseWrapper } from '../utils'; 12 | 13 | function freezeStdout(): () => void { 14 | const old_stdout_write = process.stdout.write, 15 | old_stderr_write = process.stderr.write, 16 | old_console_error = console.error; 17 | 18 | process.stdout.write = (...args: any[]) => true; 19 | process.stderr.write = (...args: any[]) => true; 20 | console.error = (...args: any[]) => {}; 21 | 22 | return () => { 23 | process.stdout.write = old_stdout_write; 24 | process.stderr.write = old_stderr_write; 25 | console.error = old_console_error; 26 | }; 27 | } 28 | 29 | function tryFindNgScript(): Promise { 30 | return promisify(resolve)('@angular/cli', { basedir: process.cwd() }) 31 | .then( resolvedPath => { 32 | let value = resolvedPath; 33 | const root = Path.parse(value).root; 34 | while (value !== root) { 35 | const base = Path.basename(value); 36 | if (base === 'node_modules') { 37 | if (FS.existsSync(Path.resolve(value, '.bin/ng'))) { 38 | return Path.resolve(value, '.bin/ng'); 39 | } 40 | } else if (base === '') { 41 | break; 42 | } 43 | else { 44 | value = Path.dirname(value); 45 | } 46 | } 47 | throw new Error(`Could not find ng script (starting at: ${resolvedPath}`); 48 | }); 49 | } 50 | 51 | function hijackCliConfiguration(): Promise { 52 | const promise = promiseWrapper(); 53 | const state = { 54 | config: undefined as Configuration, 55 | err: undefined as Error, 56 | unfreeze: undefined as () => void 57 | }; 58 | 59 | const processExit = process.exit; 60 | process.exit = function (code?: number): void { 61 | process.exit = processExit; 62 | if (state.config) { 63 | if (state.unfreeze) { 64 | state.unfreeze(); 65 | delete state.unfreeze; 66 | } 67 | // error thrown to cancel cli work, suppress it and revert. 68 | promise.resolve(state.config); 69 | } else { 70 | const e = state.err || new Error('Invalid state, integration between ngc-webpack and @angular/cli failed.'); 71 | promise.reject(e); 72 | } 73 | }; 74 | 75 | promisify(resolve)('@angular/cli/models/webpack-config.js', { basedir: process.cwd() }) 76 | .then( value => { 77 | const NgCliWebpackConfig: typeof _NgCliWebpackConfig = require(value).NgCliWebpackConfig; 78 | 79 | const buildConfig = NgCliWebpackConfig.prototype.buildConfig; 80 | NgCliWebpackConfig.prototype.buildConfig = function(...args) { 81 | state.config = buildConfig.apply(this, args); 82 | state.unfreeze = freezeStdout(); 83 | throw new Error('suppressed error'); 84 | }; 85 | 86 | return tryFindNgScript().then( ngScriptPath => require(ngScriptPath) ); 87 | }) 88 | .catch( err => { 89 | state.err = err; 90 | process.exit(); 91 | }); 92 | 93 | return promise.promise; 94 | } 95 | 96 | /** 97 | * Run `ngc-webpack` in library mode (i.e. run `ngc`) using `@angular/cli` (ng) configuration. 98 | * The cli is used to create a live instance of the webpack configuration, from there it is the same process as [[runCli]] 99 | * 100 | * `tsconfig` json path is taken from the options of AngularCompilerPlugin 101 | * 102 | * > This is not recommended, you would normally want to provide your own tsconfig with proper `angularCompilerOptions`. 103 | */ 104 | export function runNgCli(): Promise; 105 | /** 106 | * Run `ngc-webpack` in library mode (i.e. run `ngc`) using `@angular/cli` (ng) configuration. 107 | * The cli is used to create a live instance of the webpack configuration, from there it is the same process as [[runCli]] 108 | * 109 | * `tsconfig` json path is taken from cli parameters (-p or --project) or, if not exists the options of 110 | * AngularCompilerPlugin 111 | * 112 | * @param cliParams cli Parameters, parsedArgs is not mandatory 113 | */ 114 | export function runNgCli(cliParams: { args: string[], parsedArgs?: minimist.ParsedArgs }): Promise; 115 | /** 116 | * Run `ngc-webpack` in library mode (i.e. run `ngc`) using `@angular/cli` (ng) configuration. 117 | * The cli is used to create a live instance of the webpack configuration, from there it is the same process as [[runCli]] 118 | * 119 | * `tsconfig` json path is taken from the supplied tsConfigPath parameter. 120 | * 121 | * @param {string} tsConfigPath 122 | * @param cliParams cli Parameters, parsedArgs is not mandatory 123 | */ 124 | export function runNgCli(tsConfigPath: string, 125 | cliParams?: { args: string[], parsedArgs?: minimist.ParsedArgs }): Promise; 126 | export function runNgCli(tsConfigPath?: any, 127 | cliParams?: { args: string[], parsedArgs?: minimist.ParsedArgs }): Promise { 128 | const p = hijackCliConfiguration(); 129 | return p.then( (webpackConfig) => runCli(webpackConfig, tsConfigPath, cliParams) ); 130 | } 131 | 132 | 133 | if (require.main === module) { 134 | const args: string[] = process.argv.slice(2); 135 | const parsedArgs = minimist(args); 136 | 137 | // p or project is not part of angular cli 138 | if ('p' in parsedArgs) { 139 | process.argv.splice(process.argv.indexOf('-p'), 2); 140 | } 141 | if ('project' in parsedArgs) { 142 | process.argv.splice(process.argv.indexOf('--project'), 2); 143 | } 144 | 145 | runNgCli({ args, parsedArgs }) 146 | .then( parsedDiagnostics => { 147 | if (parsedDiagnostics.error) { 148 | throw parsedDiagnostics.error; 149 | } 150 | }) 151 | .catch( err => { 152 | console.error(err); 153 | process.exit(1); 154 | }); 155 | } 156 | -------------------------------------------------------------------------------- /src/cli/perform_compile_async.ts: -------------------------------------------------------------------------------- 1 | /* Copied from https://github.com/angular/angular/blob/master/packages/compiler-cli/src/perform_compile.ts 2 | but witch async support for program.loadNgStructureAsync() 3 | */ 4 | 5 | import * as ts from 'typescript'; 6 | import { isSyntaxError } from '@angular/compiler'; 7 | import { Program, CompilerHost, CompilerOptions, TsEmitCallback, CustomTransformers, PerformCompilationResult, createCompilerHost, createProgram, Diagnostic, Diagnostics, EmitFlags, DEFAULT_ERROR_CODE, UNKNOWN_ERROR_CODE, SOURCE } from '@angular/compiler-cli'; 8 | 9 | export function performCompilationAsync({rootNames, options, host, oldProgram, emitCallback, 10 | gatherDiagnostics = asyncDiagnostics, 11 | customTransformers, emitFlags = EmitFlags.Default}: { 12 | rootNames: string[], 13 | options: CompilerOptions, 14 | host?: CompilerHost, 15 | oldProgram?: Program, 16 | emitCallback?: TsEmitCallback, 17 | gatherDiagnostics?: (program: Program) => Diagnostics, 18 | customTransformers?: CustomTransformers, 19 | emitFlags?: EmitFlags 20 | }): Promise { 21 | let program: Program | undefined; 22 | let emitResult: ts.EmitResult|undefined; 23 | let allDiagnostics: Diagnostics = []; 24 | 25 | return Promise.resolve() 26 | .then( () => { 27 | if (!host) { 28 | host = createCompilerHost({options}); 29 | } 30 | program = createProgram({rootNames, host, options, oldProgram}); 31 | return program.loadNgStructureAsync() 32 | }) 33 | .then( () => { 34 | const beforeDiags = Date.now(); 35 | allDiagnostics.push(...gatherDiagnostics(program !)); 36 | if (options.diagnostics) { 37 | const afterDiags = Date.now(); 38 | allDiagnostics.push( 39 | createMessageDiagnostic(`Time for diagnostics: ${afterDiags - beforeDiags}ms.`)); 40 | } 41 | 42 | if (!hasErrors(allDiagnostics)) { 43 | emitResult = program !.emit({emitCallback, customTransformers, emitFlags}); 44 | allDiagnostics.push(...emitResult.diagnostics); 45 | return {diagnostics: allDiagnostics, program, emitResult}; 46 | } 47 | return {diagnostics: allDiagnostics, program}; 48 | }) 49 | .catch( e => { 50 | let errMsg: string; 51 | let code: number; 52 | if (isSyntaxError(e)) { 53 | // don't report the stack for syntax errors as they are well known errors. 54 | errMsg = e.message; 55 | code = DEFAULT_ERROR_CODE; 56 | } else { 57 | errMsg = e.stack; 58 | // It is not a syntax error we might have a program with unknown state, discard it. 59 | program = undefined; 60 | code = UNKNOWN_ERROR_CODE; 61 | } 62 | allDiagnostics.push( 63 | {category: ts.DiagnosticCategory.Error, messageText: errMsg, code, source: SOURCE}); 64 | return {diagnostics: allDiagnostics, program}; 65 | }) 66 | } 67 | 68 | 69 | function asyncDiagnostics(angularProgram: Program): Diagnostics { 70 | const allDiagnostics: Diagnostics = []; 71 | 72 | // Check Angular structural diagnostics. 73 | allDiagnostics.push(...angularProgram.getNgStructuralDiagnostics()); 74 | 75 | // Check TypeScript parameter diagnostics. 76 | allDiagnostics.push(...angularProgram.getTsOptionDiagnostics()); 77 | 78 | // Check Angular parameter diagnostics. 79 | allDiagnostics.push(...angularProgram.getNgOptionDiagnostics()); 80 | 81 | 82 | function checkDiagnostics(diags: Diagnostics | undefined) { 83 | if (diags) { 84 | allDiagnostics.push(...diags); 85 | return !hasErrors(diags); 86 | } 87 | return true; 88 | } 89 | 90 | let checkOtherDiagnostics = true; 91 | // Check TypeScript syntactic diagnostics. 92 | checkOtherDiagnostics = checkOtherDiagnostics && 93 | checkDiagnostics(angularProgram.getTsSyntacticDiagnostics(undefined)); 94 | 95 | // Check TypeScript semantic and Angular structure diagnostics. 96 | checkOtherDiagnostics = checkOtherDiagnostics && 97 | checkDiagnostics(angularProgram.getTsSemanticDiagnostics(undefined)); 98 | 99 | // Check Angular semantic diagnostics 100 | checkOtherDiagnostics = checkOtherDiagnostics && 101 | checkDiagnostics(angularProgram.getNgSemanticDiagnostics(undefined)); 102 | 103 | return allDiagnostics; 104 | } 105 | 106 | function defaultGatherDiagnostics(program: Program): Diagnostics { 107 | const allDiagnostics: Diagnostics = []; 108 | 109 | function checkDiagnostics(diags: Diagnostics | undefined) { 110 | if (diags) { 111 | allDiagnostics.push(...diags); 112 | return !hasErrors(diags); 113 | } 114 | return true; 115 | } 116 | 117 | let checkOtherDiagnostics = true; 118 | // Check parameter diagnostics 119 | checkOtherDiagnostics = checkOtherDiagnostics && 120 | checkDiagnostics([...program.getTsOptionDiagnostics(), ...program.getNgOptionDiagnostics()]); 121 | 122 | // Check syntactic diagnostics 123 | checkOtherDiagnostics = 124 | checkOtherDiagnostics && checkDiagnostics(program.getTsSyntacticDiagnostics()); 125 | 126 | // Check TypeScript semantic and Angular structure diagnostics 127 | checkOtherDiagnostics = 128 | checkOtherDiagnostics && 129 | checkDiagnostics( 130 | [...program.getTsSemanticDiagnostics(), ...program.getNgStructuralDiagnostics()]); 131 | 132 | // Check Angular semantic diagnostics 133 | checkOtherDiagnostics = 134 | checkOtherDiagnostics && checkDiagnostics(program.getNgSemanticDiagnostics()); 135 | 136 | return allDiagnostics; 137 | } 138 | 139 | function hasErrors(diags: Diagnostics) { 140 | return diags.some(d => d.category === ts.DiagnosticCategory.Error); 141 | } 142 | 143 | function createMessageDiagnostic(messageText: string): ts.Diagnostic & Diagnostic { 144 | return { 145 | file: undefined, 146 | start: undefined, 147 | length: undefined, 148 | category: ts.DiagnosticCategory.Message, messageText, 149 | code: DEFAULT_ERROR_CODE, 150 | source: SOURCE, 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /src/cli/transformers/fw/ast_helpers.ts: -------------------------------------------------------------------------------- 1 | // @ignoreDep typescript 2 | import * as ts from 'typescript'; 3 | 4 | // Find all nodes from the AST in the subtree of node of SyntaxKind kind. 5 | export function collectDeepNodes(node: ts.Node, kind: ts.SyntaxKind): T[] { 6 | const nodes: T[] = []; 7 | const helper = (child: ts.Node) => { 8 | if (child.kind === kind) { 9 | nodes.push(child as T); 10 | } 11 | ts.forEachChild(child, helper); 12 | }; 13 | ts.forEachChild(node, helper); 14 | 15 | return nodes; 16 | } 17 | 18 | export function getFirstNode(sourceFile: ts.SourceFile): ts.Node | null { 19 | if (sourceFile.statements.length > 0) { 20 | return sourceFile.statements[0] || null; 21 | } 22 | return null; 23 | } 24 | 25 | export function getLastNode(sourceFile: ts.SourceFile): ts.Node | null { 26 | if (sourceFile.statements.length > 0) { 27 | return sourceFile.statements[sourceFile.statements.length - 1] || null; 28 | } 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /src/cli/transformers/fw/interfaces.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export enum OPERATION_KIND { 4 | Remove, 5 | Add, 6 | Replace 7 | } 8 | 9 | export interface StandardTransform { 10 | (sourceFile: ts.SourceFile): TransformOperation[]; 11 | } 12 | 13 | export abstract class TransformOperation { 14 | constructor( 15 | public kind: OPERATION_KIND, 16 | public sourceFile: ts.SourceFile, 17 | public target: ts.Node 18 | ) { } 19 | } 20 | 21 | export class RemoveNodeOperation extends TransformOperation { 22 | constructor(sourceFile: ts.SourceFile, target: ts.Node) { 23 | super(OPERATION_KIND.Remove, sourceFile, target); 24 | } 25 | } 26 | 27 | export class AddNodeOperation extends TransformOperation { 28 | constructor(sourceFile: ts.SourceFile, target: ts.Node, 29 | public before?: ts.Node, public after?: ts.Node) { 30 | super(OPERATION_KIND.Add, sourceFile, target); 31 | } 32 | } 33 | 34 | export class ReplaceNodeOperation extends TransformOperation { 35 | kind: OPERATION_KIND.Replace; 36 | constructor(sourceFile: ts.SourceFile, target: ts.Node, public replacement: ts.Node) { 37 | super(OPERATION_KIND.Replace, sourceFile, target); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/cli/transformers/fw/make_transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import { 4 | OPERATION_KIND, 5 | StandardTransform, 6 | TransformOperation, 7 | RemoveNodeOperation, 8 | AddNodeOperation, 9 | ReplaceNodeOperation, 10 | } from './interfaces'; 11 | 12 | 13 | // instead of adding `semver` package: 14 | function satisfies(tsVersion: string) { 15 | const version = tsVersion.split('.').map( v => Number(v) ); 16 | return version[0] > 2 || (version[0] === 2 && version[1] >= 5); 17 | } 18 | 19 | 20 | // Typescript below 2.5.0 needs a workaround. 21 | const visitEachChild = satisfies(ts.version) 22 | ? ts.visitEachChild 23 | : visitEachChildWorkaround; 24 | 25 | export function makeTransform( 26 | standardTransform: StandardTransform 27 | ): ts.TransformerFactory { 28 | 29 | return (context: ts.TransformationContext): ts.Transformer => { 30 | const transformer: ts.Transformer = (sf: ts.SourceFile) => { 31 | 32 | const ops: TransformOperation[] = standardTransform(sf); 33 | const removeOps = ops 34 | .filter((op) => op.kind === OPERATION_KIND.Remove) as RemoveNodeOperation[]; 35 | const addOps = ops.filter((op) => op.kind === OPERATION_KIND.Add) as AddNodeOperation[]; 36 | const replaceOps = ops 37 | .filter((op) => op.kind === OPERATION_KIND.Replace) as ReplaceNodeOperation[]; 38 | 39 | const visitor: ts.Visitor = (node) => { 40 | let modified = false; 41 | let modifiedNodes = [node]; 42 | // Check if node should be dropped. 43 | if (removeOps.find((op) => op.target === node)) { 44 | modifiedNodes = []; 45 | modified = true; 46 | } 47 | 48 | // Check if node should be replaced (only replaces with first op found). 49 | const replace = replaceOps.find((op) => op.target === node); 50 | if (replace) { 51 | modifiedNodes = [replace.replacement]; 52 | modified = true; 53 | } 54 | 55 | // Check if node should be added to. 56 | const add = addOps.filter((op) => op.target === node); 57 | if (add.length > 0) { 58 | modifiedNodes = [ 59 | ...add.filter((op) => op.before).map(((op) => op.before)), 60 | ...modifiedNodes, 61 | ...add.filter((op) => op.after).map(((op) => op.after)) 62 | ]; 63 | modified = true; 64 | } 65 | 66 | // If we changed anything, return modified nodes without visiting further. 67 | if (modified) { 68 | return modifiedNodes; 69 | } else { 70 | // Otherwise return node as is and visit children. 71 | return visitEachChild(node, visitor, context); 72 | } 73 | }; 74 | 75 | // Only visit source files we have ops for. 76 | return ops.length > 0 ? ts.visitNode(sf, visitor) : sf; 77 | }; 78 | 79 | return transformer; 80 | }; 81 | } 82 | 83 | /** 84 | * This is a version of `ts.visitEachChild` that works that calls our version 85 | * of `updateSourceFileNode`, so that typescript doesn't lose type information 86 | * for property decorators. 87 | * See https://github.com/Microsoft/TypeScript/issues/17384 and 88 | * https://github.com/Microsoft/TypeScript/issues/17551, fixed by 89 | * https://github.com/Microsoft/TypeScript/pull/18051 and released on TS 2.5.0. 90 | * 91 | * @param sf 92 | * @param statements 93 | */ 94 | function visitEachChildWorkaround(node: ts.Node, visitor: ts.Visitor, 95 | context: ts.TransformationContext) { 96 | 97 | if (node.kind === ts.SyntaxKind.SourceFile) { 98 | const sf = node as ts.SourceFile; 99 | const statements = ts.visitLexicalEnvironment(sf.statements, visitor, context); 100 | 101 | if (statements === sf.statements) { 102 | return sf; 103 | } 104 | // Note: Need to clone the original file (and not use `ts.updateSourceFileNode`) 105 | // as otherwise TS fails when resolving types for decorators. 106 | const sfClone = ts.getMutableClone(sf); 107 | sfClone.statements = statements; 108 | return sfClone; 109 | } 110 | 111 | return ts.visitEachChild(node, visitor, context); 112 | } 113 | -------------------------------------------------------------------------------- /src/cli/transformers/inline-resources.ts: -------------------------------------------------------------------------------- 1 | // @ignoreDep typescript 2 | import * as Path from 'path'; 3 | import * as ts from 'typescript'; 4 | 5 | import { collectDeepNodes } from './fw/ast_helpers'; 6 | import { makeTransform } from './fw/make_transform'; 7 | import { 8 | StandardTransform, 9 | ReplaceNodeOperation 10 | } from './fw/interfaces'; 11 | 12 | export function inlineResources(getResource: (resourcePath: string) => string | undefined, 13 | shouldTransform: (fileName: string) => boolean): ts.TransformerFactory { 14 | const createInlineLiteral = createInlineLiteralFactory(getResource); 15 | const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { 16 | if (!shouldTransform(sourceFile.fileName)) { 17 | return []; 18 | } 19 | return findResources(sourceFile, createInlineLiteral); 20 | }; 21 | 22 | return makeTransform(standardTransform); 23 | } 24 | 25 | export function findResources(sourceFile: ts.SourceFile, 26 | createInlineLiteral: (resourcePath: string) => ts.LiteralExpression | undefined): ReplaceNodeOperation[] { 27 | const replacements: ReplaceNodeOperation[] = []; 28 | 29 | // Find all object literals. 30 | collectDeepNodes(sourceFile, ts.SyntaxKind.ObjectLiteralExpression) 31 | // Get all their property assignments. 32 | .map(node => collectDeepNodes(node, ts.SyntaxKind.PropertyAssignment)) 33 | // Flatten into a single array (from an array of array). 34 | .reduce((prev, curr) => curr ? prev.concat(curr) : prev, []) 35 | // We only want property assignments for the templateUrl/styleUrls keys. 36 | .filter((node: ts.PropertyAssignment) => { 37 | const key = _getContentOfKeyLiteral(node.name); 38 | if (!key) { 39 | // key is an expression, can't do anything. 40 | return false; 41 | } 42 | return key == 'templateUrl' || key == 'styleUrls'; 43 | }) 44 | // Replace templateUrl/styleUrls key with template/styles, and and paths with require('path'). 45 | .forEach((node: ts.PropertyAssignment) => { 46 | const key = _getContentOfKeyLiteral(node.name); 47 | 48 | if (key == 'templateUrl') { 49 | const resourcePath = _getResourceRequest(node.initializer, sourceFile); 50 | const inlineLiteral = createInlineLiteral(resourcePath.resolved); 51 | 52 | if (!inlineLiteral) { 53 | const fileName = Path.relative(process.cwd(), sourceFile.fileName); 54 | throw new Error(`Could not find templateUrl expression "${resourcePath.raw}" in "${fileName}"`); 55 | } 56 | 57 | const propAssign = ts.createPropertyAssignment('template', inlineLiteral); 58 | 59 | replacements.push(new ReplaceNodeOperation(sourceFile, node, propAssign)); 60 | } else if (key == 'styleUrls') { 61 | const arr = collectDeepNodes(node, ts.SyntaxKind.ArrayLiteralExpression); 62 | 63 | if (!arr || arr.length == 0 || arr[0].elements.length == 0) { 64 | return; 65 | } 66 | 67 | const styleLiterals: ts.Expression[] = []; 68 | arr[0].elements.forEach((element: ts.Expression) => { 69 | const resourcePath = _getResourceRequest(element, sourceFile); 70 | const inlineLiteral = createInlineLiteral(resourcePath.resolved); 71 | 72 | if (!inlineLiteral) { 73 | const fileName = Path.relative(process.cwd(), sourceFile.fileName); 74 | throw new Error(`Could not find styleUrl expression "${resourcePath.raw}" in "${fileName}"`); 75 | } 76 | 77 | styleLiterals.push(inlineLiteral); 78 | }); 79 | 80 | const propAssign = ts.createPropertyAssignment('styles', ts.createArrayLiteral(styleLiterals)); 81 | replacements.push(new ReplaceNodeOperation(sourceFile, node, propAssign)); 82 | } 83 | }); 84 | 85 | return replacements; 86 | 87 | } 88 | 89 | function _getContentOfKeyLiteral(node?: ts.Node): string | null { 90 | if (!node) { 91 | return null; 92 | } else if (node.kind == ts.SyntaxKind.Identifier) { 93 | return (node as ts.Identifier).text; 94 | } else if (node.kind == ts.SyntaxKind.StringLiteral) { 95 | return (node as ts.StringLiteral).text; 96 | } else { 97 | return null; 98 | } 99 | } 100 | 101 | function _getResourceRequest(element: ts.Expression, sourceFile: ts.SourceFile): { raw: string, resolved: string } { 102 | if (element.kind == ts.SyntaxKind.StringLiteral) { 103 | let url = (element as ts.StringLiteral).text; 104 | // If the URL does not start with / OR ./ OR ../, prepends ./ to it. 105 | if (! (/(^\.?\.\/)|(^\/)/.test(url)) ) { 106 | url = './' + url; 107 | } 108 | return { 109 | raw: (element as ts.StringLiteral).text, 110 | resolved: resolveResourcePath(url, sourceFile) 111 | }; 112 | } else { 113 | throw new Error('Expressions are not supported when inlining resources.') 114 | } 115 | } 116 | 117 | function resolveResourcePath(fileName: string, sourceFile: ts.SourceFile): string { 118 | if (fileName[0] === '/') { 119 | return fileName; 120 | } else { 121 | const dir = Path.dirname(sourceFile.fileName); 122 | return Path.resolve(dir, fileName); 123 | } 124 | } 125 | 126 | function createInlineLiteralFactory(getResource: (resourcePath: string) => string | undefined) { 127 | return (resourcePath: string): ts.LiteralExpression | undefined => { 128 | const inlineContent = getResource(resourcePath); 129 | return inlineContent ? ts.createLiteral(inlineContent) : undefined; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/cli/util.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as ts from 'typescript'; 3 | 4 | import { 5 | CompilerOptions, 6 | exitCodeFromResult, 7 | formatDiagnostics, 8 | Diagnostics, 9 | filterErrorsAndWarnings, 10 | TsEmitCallback 11 | } from '@angular/compiler-cli'; 12 | 13 | import * as tsickle from 'tsickle'; 14 | 15 | export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/; 16 | export const DTS = /\.d\.ts$/; 17 | 18 | export interface ParsedDiagnostics { 19 | exitCode: number; 20 | error?: Error; 21 | } 22 | 23 | export function parseDiagnostics(allDiagnostics: Diagnostics, 24 | options?: CompilerOptions): ParsedDiagnostics { 25 | const result: ParsedDiagnostics = { exitCode: exitCodeFromResult(allDiagnostics) }; 26 | 27 | const errorsAndWarnings = filterErrorsAndWarnings(allDiagnostics); 28 | if (errorsAndWarnings.length) { 29 | let currentDir = options ? options.basePath : undefined; 30 | const formatHost: ts.FormatDiagnosticsHost = { 31 | getCurrentDirectory: () => currentDir || ts.sys.getCurrentDirectory(), 32 | getCanonicalFileName: fileName => fileName, 33 | getNewLine: () => ts.sys.newLine 34 | }; 35 | result.error = new Error(formatDiagnostics(errorsAndWarnings, formatHost)); 36 | } 37 | return result; 38 | } 39 | 40 | export const defaultEmitCallback: TsEmitCallback = 41 | ({program, targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers}) => 42 | program.emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); 43 | 44 | export function createTsickleEmitCallback(options: CompilerOptions): TsEmitCallback | undefined { 45 | const transformDecorators = options.annotationsAs !== 'decorators'; 46 | const transformTypesToClosure = options.annotateForClosureCompiler; 47 | if (!transformDecorators && !transformTypesToClosure) { 48 | return undefined; 49 | } 50 | if (transformDecorators) { 51 | // This is needed as a workaround for https://github.com/angular/tsickle/issues/635 52 | // Otherwise tsickle might emit references to non imported values 53 | // as TypeScript elided the import. 54 | options.emitDecoratorMetadata = true; 55 | } 56 | const tsickleHost: tsickle.TsickleHost = { 57 | shouldSkipTsickleProcessing: fileName => DTS.test(fileName) || GENERATED_FILES.test(fileName), 58 | pathToModuleName: (context, importPath) => '', 59 | shouldIgnoreWarningsForPath: (filePath) => false, 60 | fileNameToModuleId: (fileName) => fileName, 61 | googmodule: false, 62 | untyped: true, 63 | convertIndexImportShorthand: false, transformDecorators, transformTypesToClosure, 64 | }; 65 | 66 | return ({ 67 | program, 68 | targetSourceFile, 69 | writeFile, 70 | cancellationToken, 71 | emitOnlyDtsFiles, 72 | customTransformers = {}, 73 | host, 74 | options 75 | }) => 76 | tsickle.emitWithTsickle( 77 | program, 78 | tsickleHost, 79 | host, 80 | options, 81 | targetSourceFile, 82 | writeFile, 83 | cancellationToken, 84 | emitOnlyDtsFiles, 85 | { 86 | beforeTs: customTransformers.before, 87 | afterTs: customTransformers.after, 88 | } 89 | ); 90 | } 91 | 92 | /** 93 | * Returns a function that can adjust a path from source path to out path, 94 | * based on an existing mapping from source to out path. 95 | * 96 | * TODO(tbosch): talk to the TypeScript team to expose their logic for calculating the `rootDir` 97 | * if none was specified. 98 | * 99 | * Note: This function works on normalized paths from typescript. 100 | * 101 | * @param outDir 102 | * @param outSrcMappings 103 | */ 104 | export function createSrcToOutPathMapper(outDir: string | undefined, 105 | sampleSrcFileName: string | undefined, 106 | sampleOutFileName: string | undefined, 107 | host: { 108 | dirname: typeof path.dirname, 109 | resolve: typeof path.resolve, 110 | relative: typeof path.relative 111 | } = path): (srcFileName: string, reverse?: boolean) => string { 112 | let srcToOutPath: (srcFileName: string) => string; 113 | if (outDir) { 114 | let path: {} = {}; // Ensure we error if we use `path` instead of `host`. 115 | if (sampleSrcFileName == null || sampleOutFileName == null) { 116 | throw new Error(`Can't calculate the rootDir without a sample srcFileName / outFileName. `); 117 | } 118 | const srcFileDir = normalizeSeparators(host.dirname(sampleSrcFileName)); 119 | const outFileDir = normalizeSeparators(host.dirname(sampleOutFileName)); 120 | if (srcFileDir === outFileDir) { 121 | return (srcFileName) => srcFileName; 122 | } 123 | // calculate the common suffix, stopping 124 | // at `outDir`. 125 | const srcDirParts = srcFileDir.split('/'); 126 | const outDirParts = normalizeSeparators(host.relative(outDir, outFileDir)).split('/'); 127 | let i = 0; 128 | while (i < Math.min(srcDirParts.length, outDirParts.length) && 129 | srcDirParts[srcDirParts.length - 1 - i] === outDirParts[outDirParts.length - 1 - i]) 130 | i++; 131 | const rootDir = srcDirParts.slice(0, srcDirParts.length - i).join('/'); 132 | srcToOutPath = (srcFileName, reverse?) => reverse 133 | ? host.resolve(rootDir, host.relative(outDir, srcFileName)) 134 | : host.resolve(outDir, host.relative(rootDir, srcFileName)) 135 | ; 136 | } else { 137 | srcToOutPath = (srcFileName) => srcFileName; 138 | } 139 | return srcToOutPath; 140 | } 141 | 142 | function normalizeSeparators(path: string): string { 143 | return path.replace(/\\/g, '/'); 144 | } 145 | -------------------------------------------------------------------------------- /src/execution-models.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { AngularCompilerPlugin } from '@ngtools/webpack'; 3 | import { NgcWebpackPluginOptions } from './plugin-options' 4 | 5 | /** 6 | * An execution host is a logical unit that knows how to execute a compilation task. 7 | * 8 | * An execution host is the logic that drives an `NgcWebpackPlugin` instance, which makes `NgcWebpackPlugin` an extensible 9 | * shell. 10 | * 11 | * With this abstraction, `NgcWebpackPlugin` can be used for different tasks. 12 | * For example, a proxy to `AngularCompilerPlugin` in application mode or an `ngc` executer in library mode. 13 | * 14 | * The role of `NgcWebpackPlugin` is to configure the hooks `ngc-webpack` allow. 15 | */ 16 | export interface NgcCompilerExecutionHost { 17 | /** 18 | * Invoke the compilation process. 19 | */ 20 | execute(compiler: any): void; 21 | 22 | /** 23 | * The compiler host used in the execution. 24 | */ 25 | compilerHost: NgcCompilerHost; 26 | 27 | /** 28 | * Transformers to be used in the compilation, `NgcWebpackPlugin` can use this to push transformers. 29 | */ 30 | transformers: ts.TransformerFactory[]; 31 | 32 | /** 33 | * A List of `ngc-webpack` hook overrides this execution host implements internally. 34 | */ 35 | hookOverride?: { 36 | [K in keyof NgcWebpackPluginOptions]?: (opt: NgcWebpackPluginOptions[K]) => void 37 | } 38 | } 39 | 40 | export interface NgcCompilerHost extends ts.CompilerHost { 41 | resourceLoader?: { get(filePath: string): Promise }; 42 | readResource?(fileName: string): Promise | string; 43 | } 44 | 45 | 46 | /* 47 | We are hacking through private variables in `AngularCompilerPlugin` 48 | This is not optimal but a validation is done to be safe. 49 | */ 50 | 51 | 52 | export interface MonkeyWebpackResourceLoader { 53 | get(filePath: string): Promise; 54 | } 55 | 56 | export interface MonkeyWebpackCompilerHost extends ts.CompilerHost { 57 | _files: {[path: string]: any | null}; 58 | _resourceLoader?: MonkeyWebpackResourceLoader | undefined; 59 | readResource?(fileName: string): Promise | string; 60 | } 61 | 62 | export interface MonkeyAngularCompilerPlugin extends Pick { 63 | _compilerHost: MonkeyWebpackCompilerHost; 64 | _transformers: ts.TransformerFactory[]; 65 | } -------------------------------------------------------------------------------- /src/patch-angular-compiler-cli.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module will patch the `@angular/compiler-cli` so it will correctly lower expression to declarations in decorators. 3 | * See https://github.com/angular/angular/issues/20216 4 | */ 5 | import * as ts from 'typescript'; 6 | import '@angular/compiler-cli'; 7 | const lowerExpressions = require('@angular/compiler-cli/src/transformers/lower_expressions'); 8 | 9 | function touchNode(node: ts.Node) { 10 | if (!node.parent) { 11 | const original: ts.Node = ts.getOriginalNode(node); 12 | if (original !== node && original.parent) { 13 | node.parent = original.parent; 14 | ts.forEachChild(node, touchNode) 15 | } 16 | } 17 | } 18 | 19 | const getExpressionLoweringTransformFactory = lowerExpressions.getExpressionLoweringTransformFactory; 20 | lowerExpressions.getExpressionLoweringTransformFactory = function(requestsMap, program) { 21 | const fn = getExpressionLoweringTransformFactory(requestsMap, program); 22 | return context => sourceFile => { 23 | const result = fn(context)(sourceFile); 24 | if (result !== sourceFile) { 25 | ts.forEachChild(result, touchNode) 26 | } 27 | return result; 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/plugin-options.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { AngularCompilerPluginOptions } from '@ngtools/webpack'; 3 | 4 | export type BeforeRunHandler = ( resourceCompiler: { get(filename: string): Promise }) => void | Promise; 5 | export type ResourcePathTransformer = (path: string) => string; 6 | export type ResourceTransformer = (path: string, source: string) => string | Promise; 7 | export type ReadFileTransformer = { 8 | predicate: RegExp | ( (path: string) => boolean ), 9 | transform: (path: string, source: string) => string 10 | }; 11 | 12 | 13 | export interface NgcWebpackPluginOptions extends AngularCompilerPluginOptions { 14 | 15 | /** 16 | * An alias for `AngularCompilerPluginOptions.skipCodeGeneration` simply to make it more readable. 17 | * If `skipCodeGeneration` is set, this value is ignored. 18 | * If this value is not set, the default value is taken from `skipCodeGeneration` 19 | * (which means AOT = true) 20 | */ 21 | AOT?: boolean; 22 | 23 | /** 24 | * A hook that invokes before the plugin start the compilation process (compiler 'run' event). 25 | * ( resourceCompiler: { get(filename: string): Promise }) => Promise; 26 | * 27 | * The hook accepts a resource compiler which able (using webpack) to perform compilation on 28 | * files using webpack's loader chain and return the final content. 29 | * @param resourceCompiler 30 | */ 31 | beforeRun?: BeforeRunHandler 32 | 33 | /** 34 | * Transform a source file (ts, js, metadata.json, summery.json). 35 | * If `predicate` is true invokes `transform` 36 | * 37 | * > Run's in both AOT and JIT mode on all files, internal and external as well as resources. 38 | * 39 | * 40 | * - Do not apply changes to resource files using this hook when in AOT mode, it will not commit. 41 | * - Do not apply changes to resource files in watch mode. 42 | * 43 | * Note that source code transformation is sync, you can't return a promise (contrary to `resourcePathTransformer`). 44 | * This means that you can not use webpack compilation (or any other async process) to alter source code context. 45 | * If you know the files you need to transform, use the `beforeRun` hook. 46 | */ 47 | readFileTransformer?: ReadFileTransformer; 48 | 49 | 50 | /** 51 | * Transform the path of a resource (html, css, etc) 52 | * (path: string) => string; 53 | * 54 | * > Run's in AOT mode only and on metadata resource files (templateUrl, styleUrls) 55 | */ 56 | resourcePathTransformer?: ResourcePathTransformer; 57 | 58 | /** 59 | * Transform a resource (html, css etc) 60 | * (path: string, source: string) => string | Promise; 61 | * 62 | * > Run's in AOT mode only and on metadata resource files (templateUrl, styleUrls) 63 | */ 64 | resourceTransformer?: ResourceTransformer; 65 | 66 | /** 67 | * Add custom TypeScript transformers to the compilation process. 68 | * 69 | * Transformers are applied after the transforms added by `@angular/compiler-cli` and 70 | * `@ngtools/webpack`. 71 | * 72 | * > `after` transformers are currently not supported. 73 | */ 74 | tsTransformers?: ts.CustomTransformers; 75 | } -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | 2 | import { NgcWebpackPluginOptions } from './plugin-options' 3 | import { hasHook } from './utils'; 4 | import { WebpackResourceLoader } from './resource-loader'; 5 | import { NgcCompilerExecutionHost, MonkeyWebpackCompilerHost } from './execution-models'; 6 | import { createAngularCompilerPluginExecutionHost } from './angular-compiler-execution-host'; 7 | 8 | 9 | export class NgcWebpackPlugin { 10 | readonly ngcWebpackPluginOptions: NgcWebpackPluginOptions; 11 | private executionHostFactory: (options: NgcWebpackPluginOptions) => NgcCompilerExecutionHost; 12 | 13 | constructor(options: NgcWebpackPluginOptions, 14 | executionHostFactory: (options: NgcWebpackPluginOptions) => NgcCompilerExecutionHost = createAngularCompilerPluginExecutionHost) { 15 | if (options.hasOwnProperty('AOT')) { 16 | if (!options.hasOwnProperty('skipCodeGeneration')) { 17 | options.skipCodeGeneration = !options.AOT; 18 | } 19 | delete options.AOT; 20 | } 21 | 22 | this.ngcWebpackPluginOptions = options; 23 | this.executionHostFactory = executionHostFactory; 24 | } 25 | 26 | apply(compiler: any) { 27 | const ngcOptions = this.ngcWebpackPluginOptions; 28 | const executionHost = this.executionHostFactory(this.ngcWebpackPluginOptions); 29 | const compilerHost = executionHost.compilerHost; 30 | 31 | const executeHook = (key: K, defaultHook: (opt: NgcWebpackPluginOptions[K]) => void) => { 32 | if (ngcOptions[key]) { 33 | if (executionHost.hookOverride && executionHost.hookOverride[key]) { 34 | executionHost.hookOverride[key](ngcOptions[key]); 35 | } else { 36 | defaultHook(ngcOptions[key]); 37 | } 38 | } 39 | }; 40 | 41 | executeHook('beforeRun', beforeRun => { 42 | let ran = false; 43 | const run = (cmp, next) => { 44 | if (ran) { 45 | next(); 46 | return; 47 | } 48 | // for now, run once 49 | // TODO: add hook for watch mode to notify on watch-run 50 | ran = true; 51 | const webpackResourceLoader = new WebpackResourceLoader(); 52 | webpackResourceLoader.update(compiler.createCompilation()); 53 | Promise.resolve(beforeRun(webpackResourceLoader)).then(next).catch(next); 54 | }; 55 | compiler.plugin('run', run); 56 | compiler.plugin('watch-run', run); 57 | }); 58 | 59 | executeHook('readFileTransformer', opt => { 60 | const orgReadFile = compilerHost.readFile; 61 | const { predicate, transform } = ngcOptions.readFileTransformer; 62 | const predicateFn = typeof predicate === 'function' 63 | ? predicate 64 | : (fileName: string) => predicate.test(fileName) 65 | ; 66 | 67 | Object.defineProperty(compilerHost, 'readFile', { 68 | value: function(this: MonkeyWebpackCompilerHost, fileName: string): string { 69 | const readFileResponse = orgReadFile.call(compilerHost, fileName); 70 | return predicateFn(fileName) ? transform(fileName, readFileResponse) : readFileResponse; 71 | } 72 | }); 73 | }); 74 | 75 | if (ngcOptions.tsTransformers) { 76 | if (ngcOptions.tsTransformers.before) { 77 | executionHost.transformers.push(...ngcOptions.tsTransformers.before); 78 | } 79 | if (ngcOptions.tsTransformers.after) { 80 | 81 | } 82 | } 83 | 84 | if (hasHook(ngcOptions, ['resourcePathTransformer', 'resourceTransformer']).some( v => v) ) { 85 | const resourceGet = compilerHost.resourceLoader.get; 86 | compilerHost.resourceLoader.get = (filePath: string): Promise => { 87 | executeHook('resourcePathTransformer', pathTransformer => filePath = pathTransformer(filePath)); 88 | 89 | let p = resourceGet.call(compilerHost.resourceLoader, filePath); 90 | 91 | executeHook( 92 | 'resourceTransformer', 93 | resourceTransformer => p = p.then( content => Promise.resolve(resourceTransformer(filePath, content)) ) 94 | ); 95 | 96 | return p; 97 | } 98 | } 99 | 100 | executionHost.execute(compiler); 101 | } 102 | 103 | static clone(plugin: NgcWebpackPlugin, 104 | overwrite: { 105 | options?: Partial, 106 | executionHostFactory?: (options: NgcWebpackPluginOptions) => NgcCompilerExecutionHost 107 | }): NgcWebpackPlugin { 108 | const options = Object.assign({}, plugin.ngcWebpackPluginOptions, overwrite.options || {}); 109 | const executionHostFactory = overwrite.executionHostFactory || plugin.executionHostFactory; 110 | return new NgcWebpackPlugin(options, executionHostFactory); 111 | } 112 | } -------------------------------------------------------------------------------- /src/resource-loader.ts: -------------------------------------------------------------------------------- 1 | export let WebpackResourceLoader: { new (): any; get(filename: string): Promise; }; 2 | export type WebpackResourceLoader = { 3 | new (): any; get(filename: string): Promise; 4 | update(parentCompilation: any): void; 5 | getResourceDependencies(filePath: string): string[]; 6 | }; 7 | 8 | try { 9 | setFromNgTools(); 10 | if (!WebpackResourceLoader) { 11 | setLocal(); 12 | } 13 | } catch (e) { 14 | setLocal(); 15 | } 16 | 17 | function setFromNgTools() { 18 | const resourceLoader = require('@ngtools/webpack/src/resource_loader'); 19 | WebpackResourceLoader = resourceLoader.WebpackResourceLoader; 20 | } 21 | 22 | function setLocal() { 23 | const resourceLoader = require('./_resource-loader'); 24 | WebpackResourceLoader = resourceLoader.WebpackResourceLoader; 25 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { NgcWebpackPluginOptions as NgcOptions } from './plugin-options' 2 | import { MonkeyAngularCompilerPlugin } from './execution-models'; 3 | 4 | export function hasHook(options: NgcOptions, name: Array): boolean[]; 5 | export function hasHook(options: NgcOptions, name: keyof NgcOptions): boolean; 6 | export function hasHook(options: NgcOptions, name: keyof NgcOptions | Array): boolean | boolean[] { 7 | if (Array.isArray(name)) { 8 | return name.map( n => typeof options[n] === 'function' ); 9 | } else { 10 | return typeof options[name] === 'function'; 11 | } 12 | } 13 | 14 | export function isValidAngularCompilerPlugin(instance: MonkeyAngularCompilerPlugin): boolean { 15 | return validators.every( m => m(instance) ); 16 | } 17 | 18 | const validators: Array<(instance: MonkeyAngularCompilerPlugin) => boolean> = [ 19 | (instance: MonkeyAngularCompilerPlugin) => Array.isArray(instance._transformers), 20 | (instance: MonkeyAngularCompilerPlugin) => !!instance._compilerHost, 21 | (instance: MonkeyAngularCompilerPlugin) => !!instance._compilerHost._resourceLoader, 22 | (instance: MonkeyAngularCompilerPlugin) => typeof instance._compilerHost._resourceLoader.get === 'function' 23 | ]; 24 | 25 | 26 | export function promiseWrapper() { 27 | const wrapper: { promise: Promise; resolve: (value?: T) => void; reject: (reason?: any) => void } = {}; 28 | wrapper.promise = new Promise( (res, rej) => { wrapper.resolve = res; wrapper.reject = rej; }); 29 | return wrapper; 30 | } 31 | 32 | // taken from: 33 | // https://github.com/notenoughneon/typed-promisify/blob/master/index.ts 34 | export function promisify(f: (cb: (err: any, res: T) => void) => void, thisContext?: any): () => Promise; 35 | export function promisify(f: (arg: A, cb: (err: any, res: T) => void) => void, thisContext?: any): (arg: A) => Promise; 36 | export function promisify(f: (arg: A, arg2: A2, cb: (err: any, res: T) => void) => void, thisContext?: any): (arg: A, arg2: A2) => Promise; 37 | export function promisify(f: (arg: A, arg2: A2, arg3: A3, cb: (err: any, res: T) => void) => void, thisContext?: any): (arg: A, arg2: A2, arg3: A3) => Promise; 38 | export function promisify(f: (arg: A, arg2: A2, arg3: A3, arg4: A4, cb: (err: any, res: T) => void) => void, thisContext?: any): (arg: A, arg2: A2, arg3: A3, arg4: A4) => Promise; 39 | export function promisify(f: (arg: A, arg2: A2, arg3: A3, arg4: A4, arg5: A5, cb: (err: any, res: T) => void) => void, thisContext?: any): (arg: A, arg2: A2, arg3: A3, arg4: A4, arg5: A5) => Promise; 40 | 41 | export function promisify(f: any, thisContext?: any) { 42 | return function () { 43 | let args = Array.prototype.slice.call(arguments); 44 | return new Promise((resolve, reject) => { 45 | args.push((err: any, result: any) => err !== null ? reject(err) : resolve(result)); 46 | f.apply(thisContext, args); 47 | }); 48 | } 49 | } -------------------------------------------------------------------------------- /test/cli.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | import * as FS from 'fs'; 3 | import * as rimraf from 'rimraf'; 4 | import { expect } from 'chai'; 5 | 6 | import { ModuleMetadata } from '@angular/compiler-cli'; 7 | import { configs, readFile, writeFile } from './testing/utils'; 8 | import { runCli as _runCli, runNgCli as _runNgCli } from '../index'; 9 | 10 | 11 | let runCli: typeof _runCli; 12 | let runNgCli: typeof _runNgCli; 13 | try { 14 | runCli = require('../dist').runCli; 15 | runNgCli = require('../dist').runNgCli; 16 | } catch (e) { 17 | runCli = require('../index').runCli; 18 | runNgCli = require('../index').runNgCli; 19 | } 20 | 21 | const tsConfig = require(configs.pluginLib.ts); 22 | const outDir = Path.resolve(Path.dirname(configs.pluginLib.ts), tsConfig.compilerOptions.outDir || '.'); 23 | 24 | function delOutDir(outDir: string) { 25 | const root = Path.resolve(Path.dirname(configs.pluginLib.ts), '.'); 26 | const cwd = process.cwd(); 27 | if (outDir.length > root.length && outDir.length > cwd.length) { 28 | rimraf.sync(outDir); 29 | } 30 | } 31 | 32 | async function createTempTsConfig(transform: ((config) => any) = cfg => cfg): Promise { 33 | const tmpTsConfig = configs.pluginLib.ts.replace(/\.json$/, '.tmp.json'); 34 | const cfg = JSON.parse(await readFile(configs.pluginLib.ts)); 35 | await writeFile(tmpTsConfig, JSON.stringify(transform(cfg))); 36 | return tmpTsConfig; 37 | } 38 | 39 | describe('ngc-webpack CLI', function() { 40 | this.timeout(1000 * 60 * 3); // 3 minutes, should be enough to compile. 41 | 42 | const outDirs = { 43 | ngCliWrapper: outDir + '-ng-cli-flat', 44 | flatModule: outDir + '-flat', 45 | perModule: outDir + '-module', 46 | skipTemplateCodegen: outDir + '-skipTemplateCodegen' 47 | }; 48 | 49 | it('angular cli wrapper (ng-cli) should inline resources in flat file output', async () => { 50 | const localOutDir = outDirs.ngCliWrapper; 51 | delOutDir(localOutDir); 52 | 53 | const tmpTsConfig = await createTempTsConfig( config => { 54 | config.compilerOptions.outDir = localOutDir; 55 | return Object.assign(config, { 56 | angularCompilerOptions: { 57 | annotateForClosureCompiler: true, 58 | skipMetadataEmit: false, 59 | skipTemplateCodegen: true, 60 | strictMetadataEmit: true, 61 | flatModuleOutFile: 'my-lib.ng-flat.js', 62 | flatModuleId: 'my-lib' 63 | } 64 | }); 65 | }); 66 | 67 | process.argv.splice(2, process.argv.length - 2, 'build'); 68 | const parsedDiagnostics = await runNgCli(tmpTsConfig); 69 | 70 | rimraf.sync(tmpTsConfig); 71 | 72 | expect(parsedDiagnostics.error).to.be.undefined; 73 | 74 | const meta = await readFile(Path.resolve(localOutDir, 'ng-lib/src/my-lib.ng-flat.metadata.json')); 75 | const metadataBundle: ModuleMetadata = JSON.parse(meta); 76 | const LibComponentComponent: any = metadataBundle.metadata.LibComponentComponent; 77 | 78 | expect(LibComponentComponent.decorators[0].arguments[0]).to.eql({ 79 | "selector": "lib-component", 80 | "template": "

Hello World

", 81 | "styles": [ 82 | "h1 {\n border: 15px black solid; }\n" 83 | ] 84 | }); 85 | }); 86 | 87 | it('should inline resources in flat file output', async () => { 88 | const localOutDir = outDirs.flatModule; 89 | delOutDir(localOutDir); 90 | 91 | const tmpTsConfig = await createTempTsConfig( config => { 92 | config.compilerOptions.outDir = localOutDir; 93 | return Object.assign(config, { 94 | angularCompilerOptions: { 95 | annotateForClosureCompiler: true, 96 | skipMetadataEmit: false, 97 | skipTemplateCodegen: true, 98 | strictMetadataEmit: true, 99 | flatModuleOutFile: 'my-lib.ng-flat.js', 100 | flatModuleId: 'my-lib' 101 | } 102 | }); 103 | }); 104 | 105 | const config = require(configs.pluginLib.wp)(true); 106 | const parsedDiagnostics = await runCli(config, tmpTsConfig); 107 | 108 | rimraf.sync(tmpTsConfig); 109 | 110 | expect(parsedDiagnostics.error).to.be.undefined; 111 | 112 | const meta = await readFile(Path.resolve(localOutDir, 'ng-lib/src/my-lib.ng-flat.metadata.json')); 113 | const metadataBundle: ModuleMetadata = JSON.parse(meta); 114 | const LibComponentComponent: any = metadataBundle.metadata.LibComponentComponent; 115 | 116 | expect(LibComponentComponent.decorators[0].arguments[0]).to.eql({ 117 | "selector": "lib-component", 118 | "template": "

Hello World

", 119 | "styles": [ 120 | "h1 {\n border: 15px black solid; }\n" 121 | ] 122 | }); 123 | }); 124 | 125 | it('cli and ng-cli should match', async () => { 126 | const ngCliMeta = await readFile(Path.resolve(outDirs.ngCliWrapper, 'ng-lib/src/my-lib.ng-flat.metadata.json')); 127 | const cliMeta = await readFile(Path.resolve(outDirs.flatModule, 'ng-lib/src/my-lib.ng-flat.metadata.json')); 128 | expect(ngCliMeta).to.eq(cliMeta); 129 | }); 130 | 131 | it('should inline resources per module', async () => { 132 | const localOutDir = outDirs.perModule; 133 | delOutDir(localOutDir); 134 | 135 | const tmpTsConfig = await createTempTsConfig( config => { 136 | config.compilerOptions.outDir = localOutDir; 137 | return Object.assign(config, { 138 | angularCompilerOptions: { 139 | annotateForClosureCompiler: true, 140 | skipMetadataEmit: false, 141 | skipTemplateCodegen: true, 142 | strictMetadataEmit: true 143 | } 144 | }); 145 | }); 146 | 147 | const config = require(configs.pluginLib.wp)(true); 148 | const parsedDiagnostics = await runCli(config, tmpTsConfig); 149 | 150 | rimraf.sync(tmpTsConfig); 151 | 152 | expect(parsedDiagnostics.error).to.be.undefined; 153 | const meta = await readFile(Path.resolve(localOutDir, 'ng-lib/src/lib-component/lib-component.component.metadata.json')); 154 | const metadataBundle: ModuleMetadata = JSON.parse(meta)[0]; 155 | const LibComponentComponent: any = metadataBundle.metadata.LibComponentComponent; 156 | 157 | expect(LibComponentComponent.decorators[0].arguments[0]).to.eql({ 158 | "selector": "lib-component", 159 | "template": "

Hello World

", 160 | "styles": [ 161 | "h1 {\n border: 15px black solid; }\n" 162 | ] 163 | }); 164 | }); 165 | 166 | it('should emit AOT artifacts when skipTemplateCodegen is false and not inline resources.', async () => { 167 | const localOutDir = outDirs.skipTemplateCodegen; 168 | delOutDir(localOutDir); 169 | 170 | const tmpTsConfig = await createTempTsConfig( config => { 171 | config.compilerOptions.outDir = localOutDir; 172 | return Object.assign(config, { 173 | angularCompilerOptions: { 174 | annotateForClosureCompiler: true, 175 | skipMetadataEmit: false, 176 | skipTemplateCodegen: false, 177 | strictMetadataEmit: true 178 | } 179 | }); 180 | }); 181 | 182 | const config = require(configs.pluginLib.wp)(true); 183 | const parsedDiagnostics = await runCli(config, tmpTsConfig); 184 | 185 | rimraf.sync(tmpTsConfig); 186 | 187 | expect(parsedDiagnostics.error).to.be.undefined; 188 | const meta = await readFile(Path.resolve(localOutDir, 'ng-lib/src/lib-component/lib-component.component.metadata.json')); 189 | const metadataBundle: ModuleMetadata = JSON.parse(meta)[0]; 190 | const LibComponentComponent: any = metadataBundle.metadata.LibComponentComponent; 191 | 192 | expect(LibComponentComponent.decorators[0].arguments[0]).to.eql({ 193 | "selector": "lib-component", 194 | "styleUrls": [ 195 | "./lib-component.component.scss" 196 | ], 197 | "templateUrl": "./lib-component.component.html" 198 | }); 199 | 200 | const p = Path.resolve(localOutDir, 'ng-lib/src/lib-component/lib-component.component.'); 201 | ['ngfactory.js', 'ngsummary.json', 'scss.shim.ngstyle.js'].forEach(suffix => { 202 | expect(FS.existsSync(p + suffix)).to.eq(true, 'Expected AOT file ' + p + suffix) 203 | }); 204 | }); 205 | }); 206 | 207 | -------------------------------------------------------------------------------- /test/ng-app/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, SkipSelf, ViewContainerRef, Optional } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'base-about', 6 | template: `` 7 | }) 8 | export class BaseAboutComponent { 9 | constructor(public route: ActivatedRoute) { 10 | 11 | } 12 | } 13 | 14 | @Component({ 15 | selector: 'about', 16 | styles: [` 17 | `], 18 | template: ` 19 |

About

20 |
21 | For hot module reloading run 22 |
npm run start:hmr
23 |
24 |
25 |

26 | patrick@AngularClass.com 27 |

28 |
29 |
this.localState = {{ localState | json }}
30 | ` 31 | }) 32 | export class AboutComponent extends BaseAboutComponent { 33 | localState: any; 34 | constructor(route: ActivatedRoute, public vcRef: ViewContainerRef) { 35 | super(route); 36 | } 37 | 38 | ngOnInit() { 39 | this.route 40 | .data 41 | .subscribe((data: any) => { 42 | // your resolved data from route 43 | this.localState = data.yourData; 44 | }); 45 | 46 | console.log('hello `About` component'); 47 | console.log(this.vcRef); 48 | 49 | // static data that is bundled 50 | // var mockData = require('assets/mock-data/mock-data.json'); 51 | // console.log('mockData', mockData); 52 | // if you're working with mock data you can also use http.get('assets/mock-data/mock-data.json') 53 | this.asyncDataWithWebpack(); 54 | } 55 | asyncDataWithWebpack() { 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /test/ng-app/app/about/index.ts: -------------------------------------------------------------------------------- 1 | export * from './about.component'; 2 | -------------------------------------------------------------------------------- /test/ng-app/app/app.component.css: -------------------------------------------------------------------------------- 1 | html, body{ 2 | height: 100%; 3 | font-family: Arial, Helvetica, sans-serif; 4 | background-image: url('../assets/check-off.png'); 5 | } 6 | 7 | span.active { 8 | background-color: gray; 9 | } 10 | -------------------------------------------------------------------------------- /test/ng-app/app/app.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Angular 2 decorators and services 3 | */ 4 | import { Component, ViewEncapsulation } from '@angular/core'; 5 | 6 | 7 | /* 8 | * App Component 9 | * Top Level Component 10 | */ 11 | @Component({ 12 | selector: 'app', 13 | encapsulation: ViewEncapsulation.None, 14 | styleUrls: [ 15 | './app.component.css' 16 | ], 17 | template: ` 18 |
43 | 44 |
45 | 46 |
47 | 48 | 56 | ` 57 | }) 58 | export class AppComponent { 59 | angularclassLogo = 'assets/img/angularclass-avatar.png'; 60 | name = 'Angular 2 Webpack Starter'; 61 | url = 'https://twitter.com/AngularClass'; 62 | } 63 | 64 | /* 65 | * Please review the https://github.com/AngularClass/angular2-examples/ repo for 66 | * more angular app examples that you may copy/paste 67 | * (The examples may not be updated as quickly. Please open an issue on github for us to update it) 68 | * For help or questions please contact us at @AngularClass on twitter 69 | * or our chat on Slack at https://AngularClass.com/slack-join 70 | */ 71 | -------------------------------------------------------------------------------- /test/ng-app/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | import { RouterModule, PreloadAllModules } from '@angular/router'; 6 | 7 | import '../styles/main.scss'; 8 | import '../styles/additional.css'; 9 | 10 | /* 11 | * Platform and Environment providers/directives/pipes 12 | */ 13 | import { ROUTES } from './app.routes'; 14 | // App is our top level component 15 | import { AppComponent } from './app.component'; 16 | import { HomeComponent } from './home'; 17 | import { AboutComponent, BaseAboutComponent } from './about'; 18 | import { NoContentComponent } from './no-content'; 19 | import { XLarge } from './home/x-large'; 20 | 21 | // Application wide providers 22 | const APP_PROVIDERS = [ 23 | ]; 24 | 25 | /** 26 | * `AppModule` is the main entry point into Angular2's bootstraping process 27 | */ 28 | @NgModule({ 29 | bootstrap: [ AppComponent ], 30 | declarations: [ 31 | BaseAboutComponent, 32 | AppComponent, 33 | AboutComponent, 34 | HomeComponent, 35 | NoContentComponent, 36 | XLarge 37 | ], 38 | imports: [ // import Angular's modules 39 | BrowserModule, 40 | FormsModule, 41 | HttpModule, 42 | RouterModule.forRoot(ROUTES, { useHash: true, preloadingStrategy: PreloadAllModules }) 43 | ], 44 | providers: [ // expose our Services and Providers into Angular's dependency injection 45 | APP_PROVIDERS 46 | ] 47 | }) 48 | export class AppModule { 49 | constructor() {} 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /test/ng-app/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { HomeComponent } from './home'; 3 | import { AboutComponent } from './about'; 4 | import { NoContentComponent } from './no-content'; 5 | 6 | 7 | export const ROUTES: Routes = [ 8 | { path: '', component: HomeComponent }, 9 | { path: 'home', component: HomeComponent }, 10 | { path: 'about', component: AboutComponent }, 11 | { path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule'}, 12 | { path: '**', component: NoContentComponent }, 13 | ]; 14 | -------------------------------------------------------------------------------- /test/ng-app/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Your Content Here

3 | 4 |
5 | For material design components use the material2 branch 6 |
7 | 8 |
9 | 10 |
11 | For hot module reloading run 12 |
npm run start:hmr
13 |
14 | 15 |
16 | 17 |
18 |

Local State

19 | 20 |
21 | 22 | 27 | 28 | 29 |
30 | 36 | 37 |
this.localState = {{ localState | json }}
38 | 39 |
40 | 41 |
42 | -------------------------------------------------------------------------------- /test/ng-app/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | $size: 99px; 2 | 3 | .home-size { 4 | font-size: 99px; 5 | } -------------------------------------------------------------------------------- /test/ng-app/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { Title } from './title'; 4 | import { XLarge } from './x-large'; 5 | 6 | @Component({ 7 | // The selector is what angular internally uses 8 | // for `document.querySelectorAll(selector)` in our index.html 9 | // where, in this case, selector is the string 'home' 10 | selector: 'home', // 11 | // We need to tell Angular's Dependency Injection which providers are in our app. 12 | providers: [ 13 | Title 14 | ], 15 | // Our list of styles in our component. We may add more to compose many styles together 16 | styleUrls: [ './home.component.scss' ], 17 | // Every Angular template is first compiled by the browser before Angular runs it's compiler 18 | templateUrl: './home.component.html' 19 | }) 20 | export class HomeComponent { 21 | // Set our default values 22 | localState = { value: '' }; 23 | // TypeScript public modifiers 24 | constructor(public title: Title) { 25 | 26 | } 27 | 28 | submitState(value: string) { 29 | console.log('submitState', value); 30 | this.localState.value = ''; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /test/ng-app/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; 2 | -------------------------------------------------------------------------------- /test/ng-app/app/home/title/index.ts: -------------------------------------------------------------------------------- 1 | export * from './title.service'; 2 | -------------------------------------------------------------------------------- /test/ng-app/app/home/title/title.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http } from '@angular/http'; 3 | 4 | @Injectable() 5 | export class Title { 6 | value = 'Angular 2'; 7 | constructor(public http: Http) { 8 | 9 | } 10 | 11 | getData() { 12 | console.log('Title#getData(): Get Data'); 13 | // return this.http.get('/assets/data.json') 14 | // .map(res => res.json()); 15 | return { 16 | value: 'AngularClass' 17 | }; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /test/ng-app/app/home/x-large/index.ts: -------------------------------------------------------------------------------- 1 | export * from './x-large.directive'; 2 | -------------------------------------------------------------------------------- /test/ng-app/app/home/x-large/x-large.directive.ts: -------------------------------------------------------------------------------- 1 | import { Component, Directive, ElementRef, Renderer } from '@angular/core'; 2 | /* 3 | * Directive 4 | * XLarge is a simple directive to show how one is made 5 | */ 6 | @Directive({ 7 | selector: '[x-large]' // using [ ] means selecting attributes 8 | }) 9 | export class XLarge { 10 | constructor(element: ElementRef, renderer: Renderer) { 11 | // simple DOM manipulation to set font size to x-large 12 | // `nativeElement` is the direct reference to the DOM element 13 | // element.nativeElement.style.fontSize = 'x-large'; 14 | 15 | // for server/webworker support use the renderer 16 | renderer.setElementStyle(element.nativeElement, 'fontSize', 'x-large'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/ng-app/app/index.ts: -------------------------------------------------------------------------------- 1 | // App 2 | export * from './app.module'; 3 | -------------------------------------------------------------------------------- /test/ng-app/app/lazy/detail.component.html: -------------------------------------------------------------------------------- 1 |

Hello from Detail

2 | 3 | 4 | -------------------------------------------------------------------------------- /test/ng-app/app/lazy/detail.component.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: green; 3 | background-image: url('../../assets/on-off.png'); 4 | } 5 | -------------------------------------------------------------------------------- /test/ng-app/app/lazy/detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | /* 3 | * We're loading this component asynchronously 4 | * We are using some magic with es6-promise-loader that will wrap the module with a Promise 5 | * see https://github.com/gdi2290/es6-promise-loader for more info 6 | */ 7 | 8 | console.log('`Detail` component loaded asynchronously'); 9 | 10 | @Component({ 11 | selector: 'detail', 12 | styleUrls: [ 'detail.component.scss' ], 13 | templateUrl: 'detail.component.html' 14 | }) 15 | export class DetailComponent { 16 | constructor() { 17 | 18 | } 19 | 20 | ngOnInit() { 21 | console.log('hello `Detail` component'); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /test/ng-app/app/lazy/index.ts: -------------------------------------------------------------------------------- 1 | export { LazyModule } from './lazy.module'; 2 | console.log('`Lazy bundle loaded asynchronously'); 3 | -------------------------------------------------------------------------------- /test/ng-app/app/lazy/lazy.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { NgModule } from '@angular/core'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | import { DetailComponent } from './detail.component'; 7 | 8 | // async components must be named routes for WebpackAsyncRoute 9 | export const routes = [ 10 | { path: '', component: DetailComponent, pathMatch: 'full' } 11 | ]; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | // Components / Directives/ Pipes 16 | DetailComponent 17 | ], 18 | imports: [ 19 | CommonModule, 20 | FormsModule, 21 | RouterModule.forChild(routes), 22 | ] 23 | }) 24 | export class LazyModule { 25 | static routes = routes; 26 | } 27 | -------------------------------------------------------------------------------- /test/ng-app/app/no-content/index.ts: -------------------------------------------------------------------------------- 1 | export * from './no-content.component'; 2 | -------------------------------------------------------------------------------- /test/ng-app/app/no-content/no-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'no-content', 5 | template: ` 6 |
7 |

404: page missing

8 |
9 | ` 10 | }) 11 | export class NoContentComponent { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /test/ng-app/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shlomiassaf/ngc-webpack/2ef6d772f483eab10d06ec5854acf7a60d21287b/test/ng-app/assets/avatar.png -------------------------------------------------------------------------------- /test/ng-app/assets/check-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shlomiassaf/ngc-webpack/2ef6d772f483eab10d06ec5854acf7a60d21287b/test/ng-app/assets/check-off.png -------------------------------------------------------------------------------- /test/ng-app/assets/check-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shlomiassaf/ngc-webpack/2ef6d772f483eab10d06ec5854acf7a60d21287b/test/ng-app/assets/check-on.png -------------------------------------------------------------------------------- /test/ng-app/assets/on-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shlomiassaf/ngc-webpack/2ef6d772f483eab10d06ec5854acf7a60d21287b/test/ng-app/assets/on-off.png -------------------------------------------------------------------------------- /test/ng-app/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /test/ng-app/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /test/ng-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/ng-app/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Angular bootstraping 3 | */ 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | /* 6 | * App Module 7 | * our top level module that holds all of our components 8 | */ 9 | import { AppModule } from './app'; 10 | 11 | /* 12 | * Bootstrap our Angular app with a top level NgModule 13 | */ 14 | export function main(): Promise { 15 | return platformBrowserDynamic() 16 | .bootstrapModule(AppModule) 17 | .catch(err => console.error(err)); 18 | } 19 | 20 | 21 | export function bootstrapDomReady() { 22 | document.addEventListener('DOMContentLoaded', main); 23 | } 24 | 25 | bootstrapDomReady(); 26 | -------------------------------------------------------------------------------- /test/ng-app/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | 68 | /** 69 | * Date, currency, decimal and percent pipes. 70 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 71 | */ 72 | // import 'intl'; // Run `npm install --save intl`. 73 | /** 74 | * Need to import at least one locale-data with intl. 75 | */ 76 | // import 'intl/locale-data/jsonp/en'; 77 | -------------------------------------------------------------------------------- /test/ng-app/styles/additional.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | body { 7 | background-image: url('../assets/check-on.png'); 8 | } -------------------------------------------------------------------------------- /test/ng-app/styles/main.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | 5 | * { 6 | color: blue; 7 | } 8 | } -------------------------------------------------------------------------------- /test/ng-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "sourceMap": false, 11 | "outDir": "dist/test/ng-app-cli-full-tsc-out", 12 | "noEmitHelpers": true, 13 | "strictNullChecks": false, 14 | "baseUrl": ".", 15 | "types": [ 16 | "node" 17 | ], 18 | "lib": [ 19 | "es2015", 20 | "dom" 21 | ] 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "test/testing", 27 | "test/*.spec.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test/ng-lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib-service.service'; 2 | export * from './lib-component/lib-component.component'; 3 | export * from './lib-module.module'; 4 | -------------------------------------------------------------------------------- /test/ng-lib/src/lib-component/lib-component.component.html: -------------------------------------------------------------------------------- 1 |

Hello World

-------------------------------------------------------------------------------- /test/ng-lib/src/lib-component/lib-component.component.scss: -------------------------------------------------------------------------------- 1 | $scss-value: 15px; 2 | 3 | h1 { 4 | border: $scss-value black solid; 5 | } -------------------------------------------------------------------------------- /test/ng-lib/src/lib-component/lib-component.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { LibServiceService } from '../lib-service.service'; 3 | 4 | 5 | @Component({ 6 | selector: 'lib-component', 7 | templateUrl: './lib-component.component.html', 8 | styleUrls: [ 9 | './lib-component.component.scss' 10 | ] 11 | }) 12 | export class LibComponentComponent { 13 | constructor(public libService: LibServiceService) { } 14 | } 15 | -------------------------------------------------------------------------------- /test/ng-lib/src/lib-module.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { LibServiceService } from './lib-service.service'; 6 | import { LibComponentComponent } from './lib-component/lib-component.component'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | LibComponentComponent 11 | ], 12 | imports: [ // import Angular's modules 13 | CommonModule, 14 | FormsModule 15 | ], 16 | exports: [ LibComponentComponent ] 17 | }) 18 | export class MyLibraryModule { 19 | 20 | static fromRoot(): ModuleWithProviders { 21 | return { 22 | ngModule: MyLibraryModule, 23 | providers: [ 24 | LibServiceService 25 | ] 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /test/ng-lib/src/lib-service.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class LibServiceService { 5 | 6 | doSomething(): void { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/ngc-webpack.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | import { expect } from 'chai'; 3 | 4 | import { NgcWebpackPluginOptions } from 'ngc-webpack'; 5 | import { runWebpack, resolveWebpackConfig, configs, logWebpackStats, readFile, expectFileToMatch } from './testing/utils'; 6 | 7 | type UniqueNgcOptions = Partial>; 9 | 10 | describe('ngc-webpack features', function() { 11 | this.timeout(1000 * 60 * 3); // 3 minutes, should be enough to compile. 12 | 13 | const run = async (wpConfig) => { 14 | const stats = await runWebpack(resolveWebpackConfig(wpConfig)).done; 15 | logWebpackStats(stats); 16 | 17 | const compileErrors = stats['compilation'] && stats['compilation'].errors; 18 | if (compileErrors) { 19 | expect(compileErrors.length).to.be 20 | .lt(1, `Expected no TypeScript errors, found ${compileErrors.length}\n` + compileErrors.map(e => (e.message || e) + '\n')); 21 | } 22 | return stats.toJson().assets; 23 | }; 24 | 25 | let test: any; 26 | 27 | describe('resources', () => { 28 | it('should replace the path for a resource', async () => { 29 | const ngcOptions: UniqueNgcOptions = { 30 | resourcePathTransformer: (p) => p.endsWith('app.component.css') 31 | ? Path.resolve('test/testing/replaced-resource.scss') 32 | : p 33 | }; 34 | const config = require(configs.pluginFull.wp)(true, ngcOptions); 35 | await run(config); 36 | await expectFileToMatch( 37 | Path.join(config.output.path, 'main.bundle.js'), 38 | `var styles = [".this-replaced-app-component {\\n display: none; }\\n"];` 39 | ); 40 | }); 41 | 42 | it('should replace the content for a resource', async () => { 43 | const ngcOptions: UniqueNgcOptions = { 44 | resourceTransformer: (p, c) => p.endsWith('home.component.html') 45 | ? '

HTML WAS HIJACKED BY A TEST!!!

' 46 | : c 47 | }; 48 | const config = require(configs.pluginFull.wp)(true, ngcOptions); 49 | await run(config); 50 | await expectFileToMatch( 51 | Path.join(config.output.path, 'main.bundle.js'), 52 | `HTML WAS HIJACKED BY A TEST!!!` 53 | ); 54 | 55 | }); 56 | }); 57 | 58 | describe('beforeRun', () => { 59 | let assets: any[]; 60 | it('should invoke beforeRun with a working webpack resource compiler', async () => { 61 | let resourceCompiler: { get(fileName: string): Promise }; 62 | const compilations: Array<[string, string, string]> = []; 63 | 64 | const ngcOptions: UniqueNgcOptions = { 65 | beforeRun: rCompiler => { 66 | resourceCompiler = rCompiler; 67 | return; 68 | }, 69 | resourceTransformer: (p, c) => { 70 | if (p.endsWith('.scss')) { 71 | return resourceCompiler.get(p) 72 | .then( content => { 73 | compilations.push([p, c, content]); 74 | return c; 75 | }); 76 | } else { 77 | return c; 78 | } 79 | } 80 | }; 81 | 82 | const config = require(configs.pluginFull.wp)(true, ngcOptions); 83 | assets = await run(config); 84 | 85 | expect(compilations.length).to.be.greaterThan(0); 86 | 87 | compilations.forEach( comp => { 88 | expect(comp[1]).to.eq(comp[2], `beforeRun resourceCompiler compilation mismatch for file ${comp[0]}`) 89 | }); 90 | }); 91 | 92 | it('using resource compiler should not effect bundle', async () => { 93 | const config = require(configs.pluginFull.wp)(true); 94 | const assetsClean = await run(config); 95 | 96 | expect(assets).to.eql(assetsClean); 97 | for (let i = 0; i < assets.length; i++) { 98 | await expectFileToMatch( 99 | Path.join(config.output.path, assets[i].name), 100 | await readFile(Path.join(config.output.path, assetsClean[i].name)) 101 | ); 102 | } 103 | }); 104 | }); 105 | 106 | describe('readFile', () => { 107 | it('should replace the content for a file', async () => { 108 | let predicateCount = 0; 109 | let transformCount = 0; 110 | const ngcOptions: UniqueNgcOptions = { 111 | readFileTransformer: { 112 | predicate: fileName => { 113 | predicateCount++; 114 | return fileName.endsWith('home.component.ts'); 115 | }, 116 | transform: (fileName, content) => { 117 | transformCount++; 118 | return content.replace(`console.log('submitState', value);`, `// TEST CLEARED console.log('submitState', value);`); 119 | } 120 | } 121 | }; 122 | 123 | const config = require(configs.pluginFull.wp)(true, ngcOptions); 124 | await run(config); 125 | 126 | expect(transformCount).to.eq(1); 127 | expect(predicateCount).to.be.greaterThan(1); 128 | 129 | await expectFileToMatch( 130 | Path.join(config.output.path, 'main.bundle.js'), 131 | `// TEST CLEARED console.log('submitState', value);` 132 | ); 133 | }); 134 | }); 135 | 136 | }); 137 | 138 | -------------------------------------------------------------------------------- /test/ngtools-webpack-flat-module-support.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { runWebpack, resolveWebpackConfig, configs, logWebpackStats, readFile, writeFile } from './testing/utils'; 4 | import { NgcWebpackPlugin as _NgcWebpackPlugin } from '../index'; 5 | import { findPluginIndex as _findPluginIndex } from '../src/cli/cli'; 6 | 7 | let NgcWebpackPlugin: typeof _NgcWebpackPlugin; 8 | let findPluginIndex: typeof _findPluginIndex; 9 | try { 10 | NgcWebpackPlugin = require('../dist').NgcWebpackPlugin; 11 | findPluginIndex = require('../dist/src/cli/cli').findPluginIndex; 12 | } catch (e) { 13 | NgcWebpackPlugin = require('../index').NgcWebpackPlugin; 14 | findPluginIndex = require('../src/cli/cli').findPluginIndex; 15 | } 16 | 17 | 18 | async function createTempTsConfig(transform: ((config) => any) = cfg => cfg): Promise { 19 | const tmpTsConfig = configs.pluginLib.ts.replace(/\.json$/, '.tmp.json'); 20 | const cfg = JSON.parse(await readFile(configs.pluginLib.ts)); 21 | await writeFile(tmpTsConfig, JSON.stringify(transform(cfg))); 22 | return tmpTsConfig; 23 | } 24 | 25 | describe('@ngtools/webpack flat module support', function() { 26 | this.timeout(1000 * 60 * 3); // 3 minutes, should be enough to compile. 27 | 28 | const run = async (wpConfig) => { 29 | const stats = await runWebpack(resolveWebpackConfig(wpConfig)).done; 30 | logWebpackStats(stats); 31 | return stats; 32 | }; 33 | 34 | 35 | it('should throw when trying to output with flatModule', async () => { 36 | 37 | const tmpTsConfig = await createTempTsConfig( config => { 38 | return Object.assign(config, { 39 | angularCompilerOptions: { 40 | annotateForClosureCompiler: true, 41 | skipMetadataEmit: false, 42 | skipTemplateCodegen: true, 43 | strictMetadataEmit: true, 44 | flatModuleOutFile: 'my-lib.ng-flat.js', 45 | flatModuleId: 'my-lib' 46 | } 47 | }); 48 | }); 49 | 50 | const config = require(configs.pluginLib.wp)(true); 51 | const pluginIdx = findPluginIndex(config.plugins, NgcWebpackPlugin); 52 | const options = { 53 | skipCodeGeneration: false, 54 | tsConfigPath: tmpTsConfig 55 | }; 56 | 57 | const plugin = NgcWebpackPlugin.clone(config.plugins[pluginIdx], { options }); 58 | config.plugins.splice(pluginIdx, 1, plugin); 59 | 60 | const stats = await run(config); 61 | const compileErrors = stats['compilation'] && stats['compilation'].errors; 62 | 63 | expect(compileErrors).not.to.be.undefined; 64 | expect(compileErrors.length).to.eq(1); 65 | expect(compileErrors[0]).to.include( 66 | `TypeError: Cannot set property writeFile of # which has only a getter`, 67 | `No exception on flatModule without a patch, looks like https://github.com/angular/angular-cli/issues/8473 has been fixed.` 68 | ); 69 | }); 70 | 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /test/no-hooks.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { runWebpack, resolveWebpackConfig, configs, logWebpackStats } from './testing/utils'; 4 | 5 | describe('@ngtools baseline', function() { 6 | this.timeout(1000 * 60 * 3); // 3 minutes, should be enough to compile. 7 | 8 | describe('no-hooks (pass-through)', () => { 9 | 10 | const outputMap = [ 11 | ['inline.bundle.js', 6000], 12 | ['inline.bundle.js.map', 6000], 13 | ['main.bundle.js', 1580541], 14 | ['main.bundle.js.map', 3580541], 15 | ['bundle.avatar.png', 2096], 16 | ['bundle.on-off.png', 4622], 17 | ['bundle.check-on.png', 2836], 18 | ['0.chunk.js', 11913], 19 | ['0.chunk.js.map', 4913], 20 | ['bundle.check-off.png', 2894], 21 | ['bundle.css', 182], 22 | ['bundle.css.map', 98], 23 | ['index.html', 239] 24 | ]; 25 | 26 | const assetsStore = { 27 | jit: { 28 | ngcwebpack: null, 29 | ngtools: null 30 | }, 31 | aot: { 32 | ngcwebpack: null, 33 | ngtools: null 34 | } 35 | }; 36 | 37 | const run = async (wpConfig) => { 38 | const stats = await runWebpack(resolveWebpackConfig(wpConfig)).done; 39 | logWebpackStats(stats); 40 | 41 | const compileErrors = stats['compilation'] && stats['compilation'].errors; 42 | if (compileErrors) { 43 | expect(compileErrors.length).to.be 44 | .lt(1, `Expected no TypeScript errors, found ${compileErrors.length}\n` + compileErrors.map(e => (e.message || e) + '\n')); 45 | } 46 | const assets = stats.toJson().assets; 47 | expect(assets.length).to.equal(outputMap.length); 48 | return assets; 49 | }; 50 | 51 | it('should compile using webpack plugin-full JIT', async () => { 52 | assetsStore.jit.ngcwebpack = await run(require(configs.pluginFull.wp)(false)); 53 | }); 54 | 55 | it('should compile using webpack plugin-full AOT', async () => { 56 | const cfg = require(configs.pluginFull.wp)(true); 57 | cfg.output.path += '-aot'; 58 | assetsStore.aot.ngcwebpack = await run(cfg); 59 | }); 60 | 61 | it('should compile using webpack ngtools-full JIT', async () => { 62 | assetsStore.jit.ngtools = await run(require(configs.ngToolsFull.wp)(false)); 63 | }); 64 | 65 | it('should compile using webpack ngtools-full AOT', async () => { 66 | const cfg = require(configs.ngToolsFull.wp)(true); 67 | cfg.output.path += '-aot'; 68 | assetsStore.aot.ngtools = await run(cfg); 69 | }); 70 | 71 | it('should match JIT outputs', () => { 72 | for (let [key, size] of outputMap) { 73 | const assetNgt = assetsStore.jit.ngtools.find ( a => a.name === key ); 74 | const assetNgc = assetsStore.jit.ngcwebpack.find ( a => a.name === key ); 75 | expect(assetNgt.size).to.eql(assetNgc.size) 76 | } 77 | }); 78 | 79 | it('should match AOT outputs', () => { 80 | for (let [key, size] of outputMap) { 81 | const assetNgt = assetsStore.aot.ngtools.find ( a => a.name === key ); 82 | const assetNgc = assetsStore.aot.ngcwebpack.find ( a => a.name === key ); 83 | expect(assetNgt.size).to.eql(assetNgc.size) 84 | } 85 | }); 86 | }); 87 | }); 88 | 89 | -------------------------------------------------------------------------------- /test/patch-angular-compiler-cli.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { NgcWebpackPluginOptions } from '..'; 4 | import { runWebpack, resolveWebpackConfig, configs, logWebpackStats } from './testing/utils'; 5 | 6 | type UniqueNgcOptions = Partial>; 8 | 9 | 10 | describe('patch-angular-compiler-cli', function() { 11 | this.timeout(1000 * 60 * 3); // 3 minutes, should be enough to compile. 12 | 13 | const run = async (wpConfig) => { 14 | const stats = await runWebpack(resolveWebpackConfig(wpConfig)).done; 15 | logWebpackStats(stats); 16 | return stats; 17 | }; 18 | 19 | const code = ` 20 | export function MyPropDecorator(value: () => any) { 21 | return (target: Object, key: string) => { } 22 | } 23 | 24 | 25 | export class MyClass { 26 | @MyPropDecorator(() => 15) 27 | prop: string; 28 | } 29 | `; 30 | 31 | it('should throw when using specific expression with lowering expression on', async () => { 32 | 33 | const ngcOptions: UniqueNgcOptions = { 34 | readFileTransformer: { 35 | predicate: fileName => fileName.endsWith('app.module.ts'), 36 | transform: (fileName, content) => content + code 37 | } 38 | }; 39 | 40 | const config = require(configs.pluginFull.wp)(true, ngcOptions); 41 | const stats = await run(config); 42 | const compileErrors = stats['compilation'] && stats['compilation'].errors; 43 | 44 | expect(compileErrors).not.to.be.undefined; 45 | expect(compileErrors.length).to.eq(1); 46 | expect(compileErrors[0]).to.include( 47 | `TypeError: Cannot read property 'kind' of undefined`, 48 | `No exception on lowering expression, looks like https://github.com/angular/angular/issues/20216 has been fixed.` 49 | ); 50 | }); 51 | 52 | it('should not throw when using specific expression with lowering expression on and a patch in place', async () => { 53 | const lowerExpressions = require('@angular/compiler-cli/src/transformers/lower_expressions'); 54 | const getExpressionLoweringTransformFactory = lowerExpressions.getExpressionLoweringTransformFactory; 55 | 56 | try { 57 | require('../dist/patch-angular-compiler-cli'); 58 | } catch (e) { 59 | require('../src/patch-angular-compiler-cli'); 60 | } 61 | 62 | const ngcOptions: UniqueNgcOptions = { 63 | readFileTransformer: { 64 | predicate: fileName => fileName.endsWith('app.module.ts'), 65 | transform: (fileName, content) => content + code 66 | } 67 | }; 68 | 69 | const config = require(configs.pluginFull.wp)(true, ngcOptions); 70 | const stats = await run(config); 71 | const compileErrors = stats['compilation'] && stats['compilation'].errors; 72 | 73 | lowerExpressions.getExpressionLoweringTransformFactory = getExpressionLoweringTransformFactory; 74 | 75 | expect(compileErrors.length).to.eq(0); 76 | }); 77 | }); 78 | 79 | -------------------------------------------------------------------------------- /test/testing/buildConfig/base-webpack-config.js: -------------------------------------------------------------------------------- 1 | // THIS CONFIG RUNS THE PLUGIN WITH ADVANCED WEBPACK TOOLS (extract css, html plugin etc...) 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | const NormalModuleReplacementPlugin = require('webpack/lib/NormalModuleReplacementPlugin'); 6 | const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); 7 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 8 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 9 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | const ngcWebpack = require('../../../dist/index'); 11 | const PurifyPlugin = require('@angular-devkit/build-optimizer').PurifyPlugin; 12 | 13 | module.exports = function (aot, ngcWebpackUniqueOptions) { 14 | 15 | const tsLoader = { 16 | test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, 17 | use: [ '@ngtools/webpack' ] 18 | }; 19 | 20 | if (aot) { 21 | tsLoader.use.unshift({ 22 | loader: '@angular-devkit/build-optimizer/webpack-loader', 23 | options: { 24 | // sourceMap: ? 25 | } 26 | }); 27 | } 28 | 29 | return { 30 | entry: { 31 | 'main': 'test/ng-app/main.ts' 32 | }, 33 | 34 | output: { 35 | 36 | path: path.join(process.cwd(), `dist/test/ng-app-plugin-full`), 37 | 38 | filename: '[name].bundle.js', 39 | 40 | sourceMapFilename: '[file].map', 41 | 42 | chunkFilename: '[id].chunk.js', 43 | 44 | library: 'ac_[name]', 45 | libraryTarget: 'var', 46 | }, 47 | 48 | resolve: { 49 | extensions: ['.ts', '.js'], 50 | }, 51 | 52 | resolveLoader: { 53 | modules: ["src", "node_modules"], 54 | extensions: ['.ts', '.js'], 55 | }, 56 | 57 | module: { 58 | rules: [ 59 | tsLoader, 60 | { 61 | test: /\.html$/, 62 | use: 'html-loader' 63 | }, 64 | 65 | { 66 | test: /\.(css|scss)$/, 67 | exclude: /styles\/.+\.(css|scss)$/, 68 | use: ['to-string-loader', 'css-loader', 'sass-loader'], 69 | }, 70 | { 71 | test: /styles\/.+\.(css|scss)$/, 72 | loader: ExtractTextPlugin.extract({ 73 | use: ['css-loader', 'sass-loader'], 74 | }) 75 | }, 76 | {test: /\.(png|ico|gif)$/, loader: "file-loader?name=bundle.[name].[ext]"} 77 | ] 78 | }, 79 | 80 | plugins: [ 81 | new LoaderOptionsPlugin({}), 82 | new webpack.SourceMapDevToolPlugin({ 83 | filename: '[file].map[query]', 84 | moduleFilenameTemplate: '[resource-path]', 85 | fallbackModuleFilenameTemplate: '[resource-path]?[hash]', 86 | sourceRoot: 'webpack:///' 87 | }), 88 | new ExtractTextPlugin("bundle.css"), 89 | new HtmlWebpackPlugin({ 90 | template: './test/ng-app/index.html', 91 | inject: true, 92 | filename: "index.html", 93 | }), 94 | new PurifyPlugin(), 95 | new webpack.optimize.CommonsChunkPlugin({ 96 | minChunks: Infinity, 97 | name: 'inline' 98 | }), 99 | new webpack.optimize.CommonsChunkPlugin({ 100 | name: 'main', 101 | async: 'common', 102 | children: true, 103 | minChunks: 2 104 | }) 105 | ], 106 | 107 | node: { 108 | global: true, 109 | crypto: 'empty', 110 | process: true, 111 | module: false, 112 | clearImmediate: false, 113 | setImmediate: false 114 | } 115 | 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /test/testing/buildConfig/webpack.ngtools-full.js: -------------------------------------------------------------------------------- 1 | // THIS CONFIG RUNS THE NGTOOLS AOT WITH ADVANCED WEBPACK TOOLS (extract css, html plugin etc...) 2 | const path = require('path'); 3 | const ngToolsWebpack = require('@ngtools/webpack'); 4 | 5 | const base = require('./base-webpack-config'); 6 | 7 | module.exports = function (aot) { 8 | const config = base(aot); 9 | config.output.path = path.join(process.cwd(), `dist/test/ng-app-ngtools-full`); 10 | config.plugins.unshift( 11 | new ngToolsWebpack.AngularCompilerPlugin({ 12 | skipCodeGeneration: !aot, 13 | tsConfigPath: './tsconfig.ngtools-full.json', 14 | mainPath: 'test/ng-app/main.ts' 15 | }) 16 | ); 17 | return config; 18 | }; 19 | -------------------------------------------------------------------------------- /test/testing/buildConfig/webpack.plugin-full.js: -------------------------------------------------------------------------------- 1 | // THIS CONFIG RUNS THE PLUGIN WITH ADVANCED WEBPACK TOOLS (extract css, html plugin etc...) 2 | const path = require('path'); 3 | const ngcWebpack = require('../../../dist/index'); 4 | 5 | const base = require('./base-webpack-config'); 6 | 7 | module.exports = function (aot, ngcWebpackUniqueOptions) { 8 | const config = base(aot); 9 | config.output.path = path.join(process.cwd(), `dist/test/ng-app-plugin-full`); 10 | config.plugins.unshift( 11 | new ngcWebpack.NgcWebpackPlugin(Object.assign({}, ngcWebpackUniqueOptions || {}, { 12 | skipCodeGeneration: !aot, 13 | tsConfigPath: './tsconfig.plugin-full.json', 14 | mainPath: 'test/ng-app/main.ts' 15 | })) 16 | ); 17 | return config; 18 | }; 19 | -------------------------------------------------------------------------------- /test/testing/buildConfig/webpack.plugin-lib.js: -------------------------------------------------------------------------------- 1 | // THIS CONFIG RUNS THE PLUGIN WITH ADVANCED WEBPACK TOOLS (extract css, html plugin etc...) 2 | const path = require('path'); 3 | const ngcWebpack = require('../../../dist/index'); 4 | const PurifyPlugin = require('@angular-devkit/build-optimizer').PurifyPlugin; 5 | 6 | module.exports = function (aot, ngcWebpackUniqueOptions) { 7 | return { 8 | entry: 'test/ng-lib/src/index.ts', 9 | 10 | externals: [ 11 | /^@angular\//, 12 | /^rxjs$/, 13 | /^rxjs\/.+/, 14 | ], 15 | 16 | output: { 17 | path: path.join(process.cwd(), `dist/test/ng-lib-plugin/bundle`), 18 | filename: '[name].bundle.webpack.umd.js', 19 | libraryTarget: 'umd', 20 | library: 'ng-lib' 21 | }, 22 | 23 | resolve: { 24 | extensions: ['.ts', '.js'], 25 | }, 26 | 27 | resolveLoader: { 28 | modules: ["src", "node_modules"], 29 | extensions: ['.ts', '.js'], 30 | }, 31 | 32 | module: { 33 | rules: [ 34 | { 35 | test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, 36 | use: [ 37 | { 38 | loader: '@angular-devkit/build-optimizer/webpack-loader', 39 | options: { 40 | sourceMap: false 41 | } 42 | }, 43 | '@ngtools/webpack' 44 | ] 45 | }, 46 | { 47 | test: /\.html$/, 48 | use: 'html-loader' 49 | }, 50 | 51 | { 52 | test: /\.(css|scss)$/, 53 | exclude: /styles\/.+\.(css|scss)$/, 54 | use: ['to-string-loader', 'css-loader', 'sass-loader'], 55 | }, 56 | {test: /\.(png|ico|gif)$/, loader: "file-loader?name=bundle.[name].[ext]"} 57 | ] 58 | }, 59 | 60 | plugins: [ 61 | new ngcWebpack.NgcWebpackPlugin(Object.assign({}, ngcWebpackUniqueOptions || {}, { 62 | skipCodeGeneration: !aot, 63 | tsConfigPath: './tsconfig.plugin-lib.json', 64 | mainPath: 'test/ng-lib/src/index.ts', 65 | // we must pass an entry module because the main path has not bootstrap that compiler can detect. 66 | entryModule: 'test/ng-lib/src/lib-module.module.ts#MyLibraryModule' 67 | })), 68 | new PurifyPlugin(), 69 | ], 70 | 71 | node: { 72 | global: true, 73 | crypto: 'empty', 74 | process: true, 75 | module: false, 76 | clearImmediate: false, 77 | setImmediate: false 78 | } 79 | 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /test/testing/replaced-resource.scss: -------------------------------------------------------------------------------- 1 | .this-replaced-app-component { 2 | display: none; 3 | } -------------------------------------------------------------------------------- /test/testing/utils.ts: -------------------------------------------------------------------------------- 1 | const Table = require('cli-table'); 2 | import * as fs from 'fs-extra'; 3 | import * as webpack from 'webpack'; 4 | import { spawn as spawnFactory } from 'child_process'; 5 | import * as Path from 'path'; 6 | 7 | export type Compiler = webpack.Compiler; 8 | export type Stats = webpack.Stats; 9 | 10 | process.env.NODE_ENV = 'production'; 11 | 12 | export const configs = { 13 | pluginFull: { 14 | ts: Path.resolve('tsconfig.plugin-full.json'), 15 | wp: Path.resolve('test/testing/buildConfig/webpack.plugin-full.js') 16 | }, 17 | pluginLib: { 18 | ts: Path.resolve('tsconfig.plugin-lib.json'), 19 | wp: Path.resolve('test/testing/buildConfig/webpack.plugin-lib.js') 20 | }, 21 | ngToolsFull: { 22 | ts: Path.resolve('tsconfig.ngtools-full.json'), 23 | wp: Path.resolve('test/testing/buildConfig/webpack.ngtools-full.js') 24 | } 25 | }; 26 | 27 | /** 28 | * Returns a webpack configuration object. 29 | * You can supply args to be used if the config is a function (webpack config factory) 30 | * 31 | * Also support ES6 default exports. 32 | * @param config 33 | * @param args 34 | * @returns {any} 35 | */ 36 | export function resolveWebpackConfig(config: any, ...args: any[]): any { 37 | if (typeof config === 'function') { 38 | return config(...args); 39 | } else if (config.__esModule === true && !!config.default) { 40 | return resolveWebpackConfig(config.default, ...args); 41 | } else { 42 | return config; 43 | } 44 | } 45 | 46 | /** 47 | * Run webpack based on a webpack config 48 | * @param config a webpack config object, can be a function, es6 default exported function, or object. 49 | */ 50 | export function runWebpack(config: any): { compiler: Compiler, done: Promise } { 51 | const compiler = webpack(resolveWebpackConfig(config)); 52 | return { 53 | compiler, 54 | done: new Promise( (RSV, RJT) => compiler.run((err, stats) => err ? RJT(err) : RSV(stats)) ) 55 | } 56 | } 57 | 58 | /** 59 | * Simple spawn wrapper that accepts a raw command line (with args) and return a promise with the result. 60 | * All IO goes to the console. 61 | * @param cmd 62 | * @returns {Promise} 63 | */ 64 | export function spawn(cmd): Promise { 65 | return new Promise( (resolve, reject) => { 66 | const args = cmd.split(' '); 67 | const spawnInstance = spawnFactory(args.shift(), args, {stdio: "inherit"}); 68 | 69 | spawnInstance.on('exit', function (code) { 70 | if (code === 0) { 71 | resolve(); 72 | } else { 73 | reject(code); 74 | } 75 | }); 76 | }); 77 | } 78 | 79 | export function getTsConfigMeta(tsConfigPath: string): {tsConfig: any} { 80 | const tsConfig = JSON.parse(fs.readFileSync(tsConfigPath, 'utf8')); 81 | return { 82 | tsConfig 83 | } 84 | } 85 | 86 | export function occurrences(regex: RegExp, str: string): number { 87 | if (!regex.global || !regex.multiline) { 88 | throw new Error('Must use a multi & global regex'); 89 | } 90 | 91 | let count = 0; 92 | let match = regex.exec(str); 93 | 94 | while (match) { 95 | count++; 96 | match = regex.exec(str); 97 | } 98 | 99 | return count; 100 | } 101 | 102 | export function logWebpackStats(stats: Stats) { 103 | let table = new Table({ head: ['', 'Total Memory'] }); 104 | 105 | 106 | const memUse = process.memoryUsage(); 107 | ['rss', 'heapTotal', 'heapUsed', 'external'].forEach( k => table.push([k , pretty(memUse[k])]) ); 108 | console.log(table.toString()); 109 | 110 | console.log(` 111 | Total Time: ${stats['endTime'] - stats['startTime']} ms [${Math.ceil((stats['endTime'] - stats['startTime']) / 1000)} secs] 112 | `); 113 | 114 | table = new Table({ head: ['Asset', 'Size'] }); 115 | stats.toJson().assets.forEach( a => table.push([a.name , pretty(a.size)]) ); 116 | console.log(table.toString()); 117 | 118 | } 119 | 120 | //https://github.com/davglass/prettysize/blob/master/index.js 121 | export function pretty (size, nospace?, one?, places?) { 122 | const sizes = [ 'Bytes', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB' ]; 123 | 124 | let mysize, f; 125 | places = places || 1; 126 | 127 | sizes.forEach(function(f, id) { 128 | if (one) { 129 | f = f.slice(0, 1); 130 | } 131 | var s = Math.pow(1024, id), 132 | fixed; 133 | if (size >= s) { 134 | fixed = String((size / s).toFixed(places)); 135 | if (fixed.indexOf('.0') === fixed.length - 2) { 136 | fixed = fixed.slice(0, -2); 137 | } 138 | mysize = fixed + (nospace ? '' : ' ') + f; 139 | } 140 | }); 141 | 142 | // zero handling 143 | // always prints in Bytes 144 | if (!mysize) { 145 | f = (one ? sizes[0].slice(0, 1) : sizes[0]); 146 | mysize = '0' + (nospace ? '' : ' ') + f; 147 | } 148 | 149 | return mysize; 150 | } 151 | 152 | export function writeFile(fileName: string, data: string) { 153 | return new Promise((resolve, reject) => { 154 | fs.writeFile(fileName, data,{encoding: 'utf-8'}, (err: any) => { 155 | if (err) { 156 | reject(err); 157 | } else { 158 | resolve(); 159 | } 160 | }); 161 | }); 162 | } 163 | 164 | export function readFile(fileName: string) { 165 | return new Promise((resolve, reject) => { 166 | fs.readFile(fileName, 'utf-8', (err: any, data: string) => { 167 | if (err) { 168 | reject(err); 169 | } else { 170 | resolve(data); 171 | } 172 | }); 173 | }); 174 | } 175 | 176 | export function expectFileToMatch(fileName: string, regEx: RegExp | string) { 177 | return readFile(fileName) 178 | .then(content => { 179 | if (typeof regEx == 'string') { 180 | if (content.indexOf(regEx) == -1) { 181 | throw new Error(`File "${fileName}" did not contain "${regEx}"... 182 | Content: 183 | ${content} 184 | ------ 185 | `); 186 | } 187 | } else { 188 | if (!content.match(regEx)) { 189 | throw new Error(`File "${fileName}" did not contain "${regEx}"... 190 | Content: 191 | ${content} 192 | ------ 193 | `); 194 | } 195 | } 196 | }); 197 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es6"], 6 | "noImplicitAny": false, 7 | "sourceMap": true, 8 | "inlineSources": true, 9 | "declaration": true, 10 | "outDir": "dist", 11 | "noEmitOnError": true, 12 | "moduleResolution": "node", 13 | "skipLibCheck": true, 14 | "baseUrl": "./", 15 | "paths": { 16 | "ngc-webpack": [ 17 | "dist/index.js", 18 | "./index.ts" 19 | ], 20 | "ngc-webpack/*": [ 21 | "dist/*", 22 | "*" 23 | ] 24 | } 25 | }, 26 | "include": [ 27 | "index.ts", 28 | "src/**/*" 29 | ], 30 | "exclude": [ 31 | "node_modules", 32 | "dist", 33 | "test/ng-app", 34 | "test/ng-lib" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.ngtools-full.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "sourceMap": false, 10 | "outDir": "./dist/test/ng-app-ngtools-full-tsc-out", 11 | "noEmitHelpers": true, 12 | "strictNullChecks": false, 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "types": [ 18 | "node" 19 | ] 20 | }, 21 | "include": [ 22 | "test/ng-app/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist", 27 | "src", 28 | "test/testing", 29 | "test/*.spec.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.plugin-full.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "sourceMap": false, 10 | "outDir": "./dist/test/ng-app-plugin-full-tsc-out", 11 | "noEmitHelpers": true, 12 | "strictNullChecks": false, 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "types": [ 18 | "node" 19 | ] 20 | }, 21 | "include": [ 22 | "test/ng-app/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist", 27 | "src", 28 | "test/testing", 29 | "test/*.spec.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.plugin-lib-ngcli.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "sourceMap": false, 11 | "noEmitHelpers": true, 12 | "strictNullChecks": false, 13 | "baseUrl": ".", 14 | "rootDir": "./test", 15 | "outDir": "./dist/test/ng-lib-plugin-ngcli", 16 | "lib": [ 17 | "es2015", 18 | "dom" 19 | ], 20 | "types": [ 21 | "node" 22 | ] 23 | }, 24 | "angularCompilerOptions": { 25 | "annotateForClosureCompiler": true, 26 | "skipMetadataEmit": false, 27 | "skipTemplateCodegen": true, 28 | "strictMetadataEmit": true, 29 | "flatModuleOutFile": "my-lib.ng-flat.js", 30 | "flatModuleId": "my-lib" 31 | }, 32 | "files": [ 33 | "test/ng-lib/src/index.ts" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "dist", 38 | "src", 39 | "test/testing", 40 | "test/*.spec.ts" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.plugin-lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "sourceMap": false, 11 | "noEmitHelpers": true, 12 | "strictNullChecks": false, 13 | "baseUrl": ".", 14 | "rootDir": "./test", 15 | "outDir": "./dist/test/ng-lib-plugin", 16 | "listEmittedFiles": true, 17 | "lib": [ 18 | "es2015", 19 | "dom" 20 | ], 21 | "types": [ 22 | "node" 23 | ] 24 | }, 25 | "files": [ 26 | "test/ng-lib/src/index.ts" 27 | ], 28 | "exclude": [ 29 | "node_modules", 30 | "dist", 31 | "src", 32 | "test/testing", 33 | "test/*.spec.ts" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": ["es6"], 7 | "noImplicitAny": false, 8 | "sourceMap": true, 9 | "inlineSources": true, 10 | "declaration": true, 11 | "outDir": "dist", 12 | "noEmitOnError": true, 13 | "moduleResolution": "node", 14 | "skipLibCheck": true 15 | }, 16 | "include": [ 17 | "index.ts", 18 | "src/**/*", 19 | "test/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "dist", 24 | "test/ng-app", 25 | "test/ng-lib" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /webpack-debug.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | require('ts-node/register'); 3 | const Path = require('path'); 4 | 5 | const tUtils = require('./test/testing/utils'); 6 | 7 | const configs = [ 8 | 'pluginFull', 9 | 'ngToolsFull' 10 | ]; 11 | 12 | const run = async (wpConfig) => { 13 | const stats = await tUtils.runWebpack(tUtils.resolveWebpackConfig(wpConfig)).done; 14 | tUtils.logWebpackStats(stats); 15 | 16 | const compileErrors = stats['compilation'] && stats['compilation'].errors; 17 | if (compileErrors) { 18 | compileErrors.forEach(e => console.error(e) ); 19 | } 20 | }; 21 | 22 | // EDIT HERE TO REPLACE CONFIG 23 | const IDX = 0; 24 | const config = tUtils.configs[configs[IDX]]; 25 | 26 | const ngcOptions = { 27 | 28 | }; 29 | run(require(config.wp)(true, ngcOptions)).catch( err => console.log(err) ); 30 | 31 | 32 | --------------------------------------------------------------------------------