Repository: driftyco/ionic-app-scripts Branch: master Commit: 972fdeccefbb Files: 148 Total size: 814.5 KB Directory structure: gitextract_525rclba/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin/ │ ├── ion-dev.js │ └── ionic-app-scripts.js ├── circle.yml ├── config/ │ ├── cleancss.config.js │ ├── copy.config.js │ ├── sass.config.js │ ├── uglifyjs.config.js │ ├── watch.config.js │ └── webpack.config.js ├── lab/ │ ├── index.html │ └── static/ │ ├── css/ │ │ └── style.css │ └── js/ │ └── lab.js ├── package.json ├── preprocessor.js ├── scripts/ │ ├── commit-changelog.js │ ├── create-github-release.js │ └── publish-nightly.js ├── src/ │ ├── aot/ │ │ ├── aot-compiler.ts │ │ ├── compiler-host-factory.ts │ │ ├── compiler-host.ts │ │ └── utils.ts │ ├── build/ │ │ └── util.ts │ ├── build.spec.ts │ ├── build.ts │ ├── bundle.spec.ts │ ├── bundle.ts │ ├── clean.spec.ts │ ├── clean.ts │ ├── cleancss.spec.ts │ ├── cleancss.ts │ ├── copy.spec.ts │ ├── copy.ts │ ├── core/ │ │ ├── bundle-components.ts │ │ ├── inject-script.spec.ts │ │ ├── inject-scripts.ts │ │ ├── ionic-global.spec.ts │ │ └── ionic-global.ts │ ├── declarations.d.ts │ ├── deep-linking/ │ │ ├── util.spec.ts │ │ └── util.ts │ ├── deep-linking.spec.ts │ ├── deep-linking.ts │ ├── dev-client/ │ │ └── sass/ │ │ ├── _code-block.scss │ │ ├── _diagnostics.scss │ │ ├── _header.scss │ │ ├── _options-menu.scss │ │ ├── _stack-block.scss │ │ ├── _system-info.scss │ │ ├── _toast.scss │ │ └── ion-dev.scss │ ├── dev-server/ │ │ ├── http-server.ts │ │ ├── injector.ts │ │ ├── lab.ts │ │ ├── live-reload.ts │ │ ├── notification-server.ts │ │ └── serve-config.ts │ ├── generators/ │ │ ├── constants.ts │ │ ├── util.spec.ts │ │ └── util.ts │ ├── generators.ts │ ├── highlight/ │ │ ├── github-gist.scss │ │ ├── highlight.spec.ts │ │ └── highlight.ts │ ├── index.ts │ ├── lint/ │ │ ├── lint-factory.spec.ts │ │ ├── lint-factory.ts │ │ ├── lint-utils.spec.ts │ │ └── lint-utils.ts │ ├── lint.spec.ts │ ├── lint.ts │ ├── logger/ │ │ ├── logger-diagnostics.ts │ │ ├── logger-runtime.ts │ │ ├── logger-sass.ts │ │ ├── logger-tslint.ts │ │ ├── logger-typescript.ts │ │ └── logger.ts │ ├── minify.ts │ ├── mocks/ │ │ └── mock-helpers.ts │ ├── ngc.ts │ ├── optimization/ │ │ ├── remove-unused-fonts.spec.ts │ │ └── remove-unused-fonts.ts │ ├── postprocess.ts │ ├── preprocess.spec.ts │ ├── preprocess.ts │ ├── sass.ts │ ├── serve.spec.ts │ ├── serve.ts │ ├── template.spec.ts │ ├── template.ts │ ├── transpile-worker.ts │ ├── transpile.spec.ts │ ├── transpile.ts │ ├── uglifyjs.ts │ ├── upgrade-scripts/ │ │ ├── add-default-ngmodules.spec.ts │ │ └── add-default-ngmodules.ts │ ├── util/ │ │ ├── clean-css-factory.ts │ │ ├── config.spec.ts │ │ ├── config.ts │ │ ├── constants.ts │ │ ├── cordova-config.spec.ts │ │ ├── cordova-config.ts │ │ ├── errors.spec.ts │ │ ├── errors.ts │ │ ├── events.ts │ │ ├── file-cache.ts │ │ ├── glob-util.ts │ │ ├── helpers/ │ │ │ ├── camel-case-regexp.ts │ │ │ ├── camel-case-upper-regexp.ts │ │ │ └── non-word-regexp.ts │ │ ├── helpers.spec.ts │ │ ├── helpers.ts │ │ ├── hybrid-file-system-factory.ts │ │ ├── hybrid-file-system.ts │ │ ├── interfaces.ts │ │ ├── ionic-project.ts │ │ ├── network.ts │ │ ├── open.ts │ │ ├── promisify.ts │ │ ├── source-maps.spec.ts │ │ ├── source-maps.ts │ │ ├── typescript-utils.spec.ts │ │ ├── typescript-utils.ts │ │ └── virtual-file-utils.ts │ ├── watch.spec.ts │ ├── watch.ts │ ├── webpack/ │ │ ├── cache-loader-impl.ts │ │ ├── cache-loader.ts │ │ ├── common-chunks-plugins.ts │ │ ├── ionic-environment-plugin.ts │ │ ├── ionic-webpack-factory.ts │ │ ├── loader-impl.spec.ts │ │ ├── loader-impl.ts │ │ ├── loader.ts │ │ ├── source-mapper.ts │ │ └── watch-memory-system.ts │ ├── webpack.ts │ ├── worker-client.ts │ └── worker-process.ts ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ **Note: for support questions, please use one of these channels:** https://forum.ionicframework.com/ http://ionicworldwide.herokuapp.com/ #### Short description of the problem: #### What behavior are you expecting? **Steps to reproduce:** 1. 2. 3. ``` insert any relevant code between the above and below backticks ``` **Which @ionic/app-scripts version are you using?** **Other information:** (e.g. stacktraces, related issues, suggestions how to fix, stackoverflow links, forum links, etc) ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ #### Short description of what this resolves: #### Changes proposed in this pull request: - - - **Fixes**: # ================================================ FILE: .gitignore ================================================ .DS_Store # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules jspm_packages # Optional npm cache directory .npm # Optional REPL history .node_repl_history # various dist .vscode *.sw[mnpcod] bin/ion-dev.css # WebStorm .idea ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: CHANGELOG.md ================================================ ## [3.2.4](https://github.com/ionic-team/ionic-app-scripts/compare/v3.2.3...v3.2.4) (2019-05-24) ### Bug Fixes * **livereload:** always serve latest file changes ([#1521](https://github.com/ionic-team/ionic-app-scripts/issues/1521)) ([266a871](https://github.com/ionic-team/ionic-app-scripts/commit/266a871)) ## [3.2.3](https://github.com/ionic-team/ionic-app-scripts/compare/v3.2.2...v3.2.3) (2019-03-01) ### Bug Fixes * **livereload:** fix issue with files not reloading([8870a17](https://github.com/ionic-team/ionic-app-scripts/commit/8870a17)) ## [3.2.2](https://github.com/ionic-team/ionic-app-scripts/compare/v3.2.1...v3.2.2) (2019-01-22) * Added support for Node 10 ## [3.2.1](https://github.com/ionic-team/ionic-app-scripts/compare/v3.2.0...v3.2.1) (2018-11-26) * Security release for dependencies for `node-sass` # [3.2.0](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.11...v3.2.0) (2018-08-24) ### Bug Fixes * **sass:** remove PostCSS warning ([#1364](https://github.com/ionic-team/ionic-app-scripts/issues/1364)) ([1e2035e](https://github.com/ionic-team/ionic-app-scripts/commit/1e2035e)), closes [#1359](https://github.com/ionic-team/ionic-app-scripts/issues/1359) [#13763](https://github.com/ionic-team/ionic-app-scripts/issues/13763) * **serve:** use wss protocol for secure websocket when page is using https ([#1358](https://github.com/ionic-team/ionic-app-scripts/issues/1358)) ([29c3e23](https://github.com/ionic-team/ionic-app-scripts/commit/29c3e23)) ### Features * **environments:** configuration via process.env.VAR replacement ([#1471](https://github.com/ionic-team/ionic-app-scripts/issues/1471)) ([53fc341](https://github.com/ionic-team/ionic-app-scripts/commit/53fc341)) ## [3.1.11](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.10...v3.1.11) (2018-07-12) ### Bug Fixes * **serve:** fix EADDRINUSE issue with dev logger server ([271a6e1](https://github.com/ionic-team/ionic-app-scripts/commit/271a6e1)) ## [3.1.10](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.9...v3.1.10) (2018-06-11) ## [3.1.9](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.8...v3.1.9) (2018-04-18) ### Bug Fixes * **2889:** fix build error with --prod ([94e5f7c](https://github.com/ionic-team/ionic-app-scripts/commit/94e5f7c)) * **live-server:** update android platform path ([#1407](https://github.com/ionic-team/ionic-app-scripts/issues/1407)) ([1591c81](https://github.com/ionic-team/ionic-app-scripts/commit/1591c81)) * **serve:** start listening when watch is ready ([06bbd06](https://github.com/ionic-team/ionic-app-scripts/commit/06bbd06)) ## [3.1.8](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.7...v3.1.8) (2018-01-18) This release includes a bump in the version of node-sass. This adds support for node 9. ## [3.1.7](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.6...v3.1.7) (2017-12-27) ### Bug Fixes * pin uglify-es ([dacf080](https://github.com/ionic-team/ionic-app-scripts/commit/dacf080)), closes [#1353](https://github.com/ionic-team/ionic-app-scripts/issues/1353) ## [3.1.6](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.5...v3.1.6) (2017-12-18) ### Bug Fixes * pin ws version ([db0cc4d](https://github.com/ionic-team/ionic-app-scripts/commit/db0cc4d)) ## [3.1.5](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.4...v3.1.5) (2017-12-07) ### Bug Fixes * **dependencies:** update angular build optimizer for a source map fix ([a5df139](https://github.com/ionic-team/ionic-app-scripts/commit/a5df139)) ## [3.1.4](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.3...v3.1.4) (2017-11-30) ### Bug Fixes * **aot:** remove template validation until we can properly handle the error message format ([d7c7136](https://github.com/ionic-team/ionic-app-scripts/commit/d7c7136)) ## [3.1.3](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.2...v3.1.3) (2017-11-29) ### Bug Fixes * **aot:** fix error reporting with ng 5.0.1 or greater ([dece391](https://github.com/ionic-team/ionic-app-scripts/commit/dece391)) ## [3.1.2](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.1...v3.1.2) (2017-11-13) ### Bug Fixes * **webpack:** revert to 3.6.0 for faster builds ([2553ca6](https://github.com/ionic-team/ionic-app-scripts/commit/2553ca6)) ## [3.1.1](https://github.com/ionic-team/ionic-app-scripts/compare/v3.1.0...v3.1.1) (2017-11-13) ### Bug Fixes * **AoT:** properly check for ngmodule declaration errors from the AoT build ([a47f120](https://github.com/ionic-team/ionic-app-scripts/commit/a47f120)) * **template:** fix bug with using dollar sign within templates ([de09048](https://github.com/ionic-team/ionic-app-scripts/commit/de09048)) # [3.1.0](https://github.com/ionic-team/ionic-app-scripts/compare/v3.0.1...v3.1.0) (2017-11-08) ## Features Supports Angular 5 ### Bug Fixes * **aot:** pass genDir to ng4 ([7506764](https://github.com/ionic-team/ionic-app-scripts/commit/7506764)) * **config:** only read ionic-angular package json for version info in apps, not in the Ionic repo itself ([700ca04](https://github.com/ionic-team/ionic-app-scripts/commit/700ca04)) * **deep-linking:** use .ts file extension for lazy loading in dev mode, and .js in AoT mode since the AoT compiler no longer emits an ngfactory.ts file ([dd99f14](https://github.com/ionic-team/ionic-app-scripts/commit/dd99f14)) * **live-server:** content.toString() crash ([#1288](https://github.com/ionic-team/ionic-app-scripts/issues/1288)) ([07e7e05](https://github.com/ionic-team/ionic-app-scripts/commit/07e7e05)) * **templates:** escape strings in template ([484d90d](https://github.com/ionic-team/ionic-app-scripts/commit/484d90d)) ### Performance Improvements * **uglifyjs:** remove unused `readFileAsync` during uglify ([#1305](https://github.com/ionic-team/ionic-app-scripts/issues/1305)) ([e9217c2](https://github.com/ionic-team/ionic-app-scripts/commit/e9217c2)) ## [3.0.1](https://github.com/ionic-team/ionic-app-scripts/compare/v3.0.0...v3.0.1) (2017-10-20) ### Bug Fixes * **cleancss:** update to latest version of clean-css to mitigate issue with purging some css that should not be purged ([564bd61](https://github.com/ionic-team/ionic-app-scripts/commit/564bd61)) * **deep-linking:** ensure hasExistingDeepLinkConfig returns true where there is a config referenced by a variable ([2e40340](https://github.com/ionic-team/ionic-app-scripts/commit/2e40340)) * **deep-linking:** ensure the deepLinkDir ends in path.sep ([496af40](https://github.com/ionic-team/ionic-app-scripts/commit/496af40)) * set context right immediately ([802b329](https://github.com/ionic-team/ionic-app-scripts/commit/802b329)) * **dev-server:** fix for --nolivereload flag to stop reloading ([#1200](https://github.com/ionic-team/ionic-app-scripts/issues/1200)) ([d62f5da](https://github.com/ionic-team/ionic-app-scripts/commit/d62f5da)) * **html:** limit regex to only applicable script tags for replacing content ([93db0ef](https://github.com/ionic-team/ionic-app-scripts/commit/93db0ef)) * **proxy:** add a cookieRewrite option which is passed to proxy-middleware. ([#1226](https://github.com/ionic-team/ionic-app-scripts/issues/1226)) ([771ee63](https://github.com/ionic-team/ionic-app-scripts/commit/771ee63)) * **source-maps:** fix race condition between copying and purging source maps ([f5529b5](https://github.com/ionic-team/ionic-app-scripts/commit/f5529b5)) * **webpack:** always use modules output from webpack to form default basis of where to look for sass files ([c199ea4](https://github.com/ionic-team/ionic-app-scripts/commit/c199ea4)) # [3.0.0](https://github.com/ionic-team/ionic-app-scripts/compare/v2.1.4...v3.0.0) (2017-09-28) ### Breaking Changes The `webpack` config format changed from being a config that is exported to being a dictionary of configs. Basically, the default config now exports a `dev` and `prod` property with a config assigned to each. See an example of the change [here](https://github.com/ionic-team/ionic-app-scripts/blob/master/config/webpack.config.js#L143-L146). This change is setting the stage for adding multiple "environment" support for the next app-scripts release. ### New Features This release adds support for `ngo`, the Angular team's build optimizer tool. `ngo` is enabled by default on `--prod` builds. In the event that `ngo` is not working for your app or something goes wrong, it can be disabled by running the following build command. ``` ionic cordova build ios --aot --minifyjs --minifycss --optimizejs ``` Using the `--aot` flag enables the `AoT Compiler`. `--minifyjs` and `--minifycss` minify the outputted code. ### Notes Version `3.0.0` deprecated support for Rollup, Closure Compiler, and Babili. The support for these was poor and they were not used by many developers. `uglifyjs` was replaced with the newer `uglifyes`, which supports ES2015. ### Bug Fixes * **aot:** normalize paths to fix path issues on windows ([b766037](https://github.com/ionic-team/ionic-app-scripts/commit/b766037)) * **build:** scan deeplink dir too if different from srcDir ([8929265](https://github.com/ionic-team/ionic-app-scripts/commit/8929265)) * **deep-linking:** convert deep linking to use TS Transform. DeepLinking now works on TypeScript src instead of on transpiled JS code ([63c4c7f](https://github.com/ionic-team/ionic-app-scripts/commit/63c4c7f)) * **deep-linking:** remove IonicPage import statement in transform/non-transform approachs to work better with strict TS settings ([84d9ec7](https://github.com/ionic-team/ionic-app-scripts/commit/84d9ec7)) * **devapp:** do not enable shake ([#1215](https://github.com/ionic-team/ionic-app-scripts/issues/1215)) ([118189c](https://github.com/ionic-team/ionic-app-scripts/commit/118189c)) * **generators:** correct pipes default folder name ([f0ea0da](https://github.com/ionic-team/ionic-app-scripts/commit/f0ea0da)) * **ngc:** don't replace deeplink config if an existing one exists ([eeed98b](https://github.com/ionic-team/ionic-app-scripts/commit/eeed98b)) * **optimization:** removing optimizations in preparation for ngo, updating to latest deps ([90eb8b3](https://github.com/ionic-team/ionic-app-scripts/commit/90eb8b3)) * **postprocess:** fix and add tests for the logic surrounding purging fonts ([0dd1b22](https://github.com/ionic-team/ionic-app-scripts/commit/0dd1b22)) * **sass:** include the platforms dir by default ([0da47cb](https://github.com/ionic-team/ionic-app-scripts/commit/0da47cb)) * **transpile:** check for existing deep link config before using generated one ([c51ac93](https://github.com/ionic-team/ionic-app-scripts/commit/c51ac93)) * **webpack:** when analyzing stats, factor in new shape of ModuleConcatenation info ([00cf038](https://github.com/ionic-team/ionic-app-scripts/commit/00cf038)) ## [2.1.4](https://github.com/ionic-team/ionic-app-scripts/compare/v2.1.3...v2.1.4) (2017-08-16) ### Bug Fixes * make --lab respect --nobrowser ([8db3be5](https://github.com/ionic-team/ionic-app-scripts/commit/8db3be5)) * **serve:** allow multiple arguments in console.log ([5c00970](https://github.com/ionic-team/ionic-app-scripts/commit/5c00970)) * **serve:** fix --consolelogs/--serverlogs usage with Cordova console plugin ([8e64407](https://github.com/ionic-team/ionic-app-scripts/commit/8e64407)) * **serve:** fix 'launchBrowser' of undefined ([8f71e35](https://github.com/ionic-team/ionic-app-scripts/commit/8f71e35)) ### Features * **sourcemaps:** copy for prod and dev ([a1ccc17](https://github.com/ionic-team/ionic-app-scripts/commit/a1ccc17)) * **sourcemaps:** preserve prod sourcemaps out of code dir ([ee3e41b](https://github.com/ionic-team/ionic-app-scripts/commit/ee3e41b)) ## [2.1.3](https://github.com/ionic-team/ionic-app-scripts/compare/v2.1.2...v2.1.3) (2017-07-27) ### Bug Fixes * **lab:** remove es6 features from lab ([41a1335](https://github.com/ionic-team/ionic-app-scripts/commit/41a1335)) ## [2.1.2](https://github.com/ionic-team/ionic-app-scripts/compare/v2.1.1...v2.1.2) (2017-07-27) ### Bug Fixes * **generators:** handle old cli ([6fd622c](https://github.com/ionic-team/ionic-app-scripts/commit/6fd622c)) ## [2.1.1](https://github.com/ionic-team/ionic-app-scripts/compare/v2.1.0...v2.1.1) (2017-07-27) ### Bug Fixes * **generator:** write file sync ([b0bcb05](https://github.com/ionic-team/ionic-app-scripts/commit/b0bcb05)) * **generators:** add exception for providers ([db9c793](https://github.com/ionic-team/ionic-app-scripts/commit/db9c793)) ### Features * **webpack:** update to latest webpack ([67907b6](https://github.com/ionic-team/ionic-app-scripts/commit/67907b6)) # [2.1.0](https://github.com/ionic-team/ionic-app-scripts/compare/v2.0.2...v2.1.0) (2017-07-25) ### Bug Fixes * **generators:** handle no ngModule in tabs ([653d9f2](https://github.com/ionic-team/ionic-app-scripts/commit/653d9f2)) ### Features * **generators:** refactor generators ([beaf0d3](https://github.com/ionic-team/ionic-app-scripts/commit/beaf0d3)) ## [2.0.2](https://github.com/ionic-team/ionic-app-scripts/compare/v2.0.1...v2.0.2) (2017-07-13) ## Upgrading Make sure you follow the instructions below for upgrading from `1.x` to `2.x`. In the `2.0.2` release, we had to make a small change to the `optimization` config. If you override this config, please review the [change](https://github.com/ionic-team/ionic-app-scripts/commit/785e044) and update your config accordingly. ### Bug Fixes * **sass:** fix potential null pointer, though it really should never happen ([427e556](https://github.com/ionic-team/ionic-app-scripts/commit/427e556)) * **webpack:** don't output deptree.js, this requires a minor tweak to the optimization config if you have it customized ([785e044](https://github.com/ionic-team/ionic-app-scripts/commit/785e044)) * **webpack:** upgrade to webpack 3.2.0 to fix some bugs within Webpack surrounding the ModuleConcatenationPlugin ([f85ade0](https://github.com/ionic-team/ionic-app-scripts/commit/f85ade0)) ## [2.0.1](https://github.com/ionic-team/ionic-app-scripts/compare/v2.0.0...v2.0.1) (2017-07-11) ## Upgrading from 1.x If you're upgrading directly from `1.3.12` or earlier, make sure you review the changelog for `2.0.0` and follow the [instructions here](https://github.com/ionic-team/ionic-app-scripts/releases/tag/v2.0.0). There were some very minor updates you'll need to make to your app. If you're customizing the build process and have a dependency that utilized `webpack@2.x`, it may be best to add an explicit `devDependency` on `webpack@3.1.0` to the project's `package.json` file. There have been a couple reports of non-standard 3rd party dependencies causing trouble with the `webpack` version. ### Bug Fixes * **generators:** no module by default ([#1096](https://github.com/ionic-team/ionic-app-scripts/issues/1096)) ([dfcaefa](https://github.com/ionic-team/ionic-app-scripts/commit/dfcaefa)) * **http-server:** revert change for path-based routing since it broke proxies ([065912e](https://github.com/ionic-team/ionic-app-scripts/commit/065912e)) * **sass:** use webpack/rollup modules for non-optimized build, use optimization data for prod/optimized buids ([0554201](https://github.com/ionic-team/ionic-app-scripts/commit/0554201)) * **serve:** fix cached file issue by only using the webpack module concat plugin for prod builds, make sure you update custom configs ([feea7fe](https://github.com/ionic-team/ionic-app-scripts/commit/feea7fe)) * **webpack:** webpack in-memory output file system was breaking some plugins ([574da39](https://github.com/ionic-team/ionic-app-scripts/commit/574da39)) # [2.0.0](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.12...v2.0.0) (2017-07-07) ### Breaking Changes In order to speed up the bundling process, we have separated `node_modules` code into a new, generated file called `vendor.js`. This means that on every change, `ionic-angular`, `@angular`, etc won't need to be processed by `webpack` :tada: This means that `src/index.html` must be modified to include a new vendor script tag ``. This new script tag must be placed above the `main.js` script tag. For example, ``` ... ... ``` Another side effect of this change is if you are overriding the `webpack` configuration, you will want to update your custom configuration based on the [new default configuration](https://github.com/ionic-team/ionic-app-scripts/blob/master/config/webpack.config.js). The main changes to the config are adding the `ModuleConcatenationPlugin` for scope hoisting for significantly faster apps, and adding the common chunks plugin for the `vendor.js` bundle. See commits [e14f819](https://github.com/ionic-team/ionic-app-scripts/commit/e14f819) and [141cb23](https://github.com/ionic-team/ionic-app-scripts/commit/141cb23) for the specifics of the `webpack.config.js` change. ### Bug Fixes * **config:** updated polyname env variable to match convention and fix typo with it ([d64fcb1](https://github.com/ionic-team/ionic-app-scripts/commit/d64fcb1)) * **lint:** improve linting performance ([106d82c](https://github.com/ionic-team/ionic-app-scripts/commit/106d82c)) * **sass:** dont try to process invalid directories ([8af9430](https://github.com/ionic-team/ionic-app-scripts/commit/8af9430)) * **sass:** fix a bug when calling sass task in stand alone fashion ([54bf3f6](https://github.com/ionic-team/ionic-app-scripts/commit/54bf3f6)) ### Features * **dev-server:** add support for path-based routing ([2441591](https://github.com/ionic-team/ionic-app-scripts/commit/2441591)) * **webpack:** add scope hoisting to webpack, update sass to read scss files from disk ([e14f819](https://github.com/ionic-team/ionic-app-scripts/commit/e14f819)) * **webpack:** use a vendor bundle to minimize code that needs re-bundling and source map generation ([141cb23](https://github.com/ionic-team/ionic-app-scripts/commit/141cb23)) * **webpack:** webpack 3.1.0 holy speed upgrade! ([a3bde4a](https://github.com/ionic-team/ionic-app-scripts/commit/a3bde4a)) ## [1.3.12](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.11...v1.3.12) (2017-06-29) ## Bug Fixes * **dependencies:** Added `reflect-metadata` to the list of dependencies ([e6f8481](https://github.com/ionic-team/ionic-app-scripts/commit/e6f8481) ## [1.3.11](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.10...v1.3.11) (2017-06-28) ## Bug Fixes * **dependencies:** Removed `peerDependencies`. ([90cd59d](https://github.com/ionic-team/ionic-app-scripts/commit/90cd59d)) ## [1.3.10](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.9...v1.3.10) (2017-06-28) ## Notes Ionic updated to npm 5 across the board, so please update to npm 5 to utilize our lock file when contributing. ### Bug Fixes * **bonjour:** remove bonjour as its causing trouble for users on Windows without git ([e4b5c59](https://github.com/ionic-team/ionic-app-scripts/commit/e4b5c59)) ## [1.3.9](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.8...v1.3.9) (2017-06-28) ### Features * **lab:** first iteration of the new Ionic Lab design * **scripts:** push npm build to arbitrary tag ([#1060](https://github.com/ionic-team/ionic-app-scripts/issues/1060)) ([4e93f60](https://github.com/ionic-team/ionic-app-scripts/commit/4e93f60)) ## [1.3.8](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.7...v1.3.8) (2017-06-21) ### Bug Fixes * **sass:** fixes issue with Node 8 and node-sass * **bonjour:** updates dependency + better error handling ([#1040](https://github.com/ionic-team/ionic-app-scripts/issues/1040)) ([e2f73c7](https://github.com/ionic-team/ionic-app-scripts/commit/e2f73c7)) * **core:** use lower case attrs and not dash case ([0154791](https://github.com/ionic-team/ionic-app-scripts/commit/0154791)) * **diagnostics:** change direction to always be ltr ([#1004](https://github.com/ionic-team/ionic-app-scripts/issues/1004)) ([6d5ef3c](https://github.com/ionic-team/ionic-app-scripts/commit/6d5ef3c)) * **lab:** allow params to be passed to iframes ([dabfdd1](https://github.com/ionic-team/ionic-app-scripts/commit/dabfdd1)) * **sass:** fix .sass files not being watched ([#957](https://github.com/ionic-team/ionic-app-scripts/issues/957)) ([0803eca](https://github.com/ionic-team/ionic-app-scripts/commit/0803eca)) * **serve:** if a build error occurs, return config if non-fatal ([e5a4134](https://github.com/ionic-team/ionic-app-scripts/commit/e5a4134)) ## [1.3.7](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.6...v1.3.7) (2017-05-04) ### Bug Fixes * **config:** create new file cache if not defined, even on existing context object ([4359b3d](https://github.com/ionic-team/ionic-app-scripts/commit/4359b3d)) * **generators:** import paths correct on windows ([d778857](https://github.com/ionic-team/ionic-app-scripts/commit/d778857)) * **optimizations:** don't ever remove menu-types since it's not a side-effect in menu, it is used just for types ([d7a4d1e](https://github.com/ionic-team/ionic-app-scripts/commit/d7a4d1e)) * **optimizations:** fix multiple bugs (components not being purged, overlays not working from providers, etc) for manual tree shaking ([4b538c7](https://github.com/ionic-team/ionic-app-scripts/commit/4b538c7)) * **webpack:** fix issue where bundles output to build dir sub directo… ([#938](https://github.com/ionic-team/ionic-app-scripts/issues/938)) ([aaa9d3c](https://github.com/ionic-team/ionic-app-scripts/commit/aaa9d3c)) ### Features * **bonjour:** adds service auto-discovery ([c17e6df](https://github.com/ionic-team/ionic-app-scripts/commit/c17e6df)) ## [1.3.6](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.5...v1.3.6) (2017-04-27) ### Bug Fixes * **webpack:** fix issue PR introduced with lazy loaded modules and webpack throwing an invalid error ([fb8b69a](https://github.com/ionic-team/ionic-app-scripts/commit/fb8b69a)) ### Features * **optimization:** enable manual tree shaking by default ([1c57ee6](https://github.com/ionic-team/ionic-app-scripts/commit/1c57ee6)) ## [1.3.5](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.4...v1.3.5) (2017-04-26) ### Bug Fixes * **build:** fix `extends` in `ts-config.json` ([#910](https://github.com/ionic-team/ionic-app-scripts/issues/910)) ([0f01603](https://github.com/ionic-team/ionic-app-scripts/commit/0f01603)) * **deep-linking:** fix issue where deep link config ends up being null when full build is triggered via a change to a template file with the identical content ([68fc463](https://github.com/ionic-team/ionic-app-scripts/commit/68fc463)) * **serve:** Fix for browser not opening on linux, fixes [#425](https://github.com/ionic-team/ionic-app-scripts/issues/425) ([#909](https://github.com/ionic-team/ionic-app-scripts/issues/909)) ([77edbc6](https://github.com/ionic-team/ionic-app-scripts/commit/77edbc6)) ### Features * **sass:** add option to pass addition postcss plugins ([#369](https://github.com/ionic-team/ionic-app-scripts/issues/369)) ([be30a40](https://github.com/ionic-team/ionic-app-scripts/commit/be30a40)) ## [1.3.4](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.3...v1.3.4) (2017-04-18) ### Bug Fixes * **webpack:** make ionic-angular/util dir dynamic and use the environment variable of ionic angular ([d3346b3](https://github.com/ionic-team/ionic-app-scripts/commit/d3346b3)) ## [1.3.3](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.2...v1.3.3) (2017-04-14) ### Bug Fixes * **optimizations:** temporarily do not purge ctor params from any of angular ([212146c](https://github.com/ionic-team/ionic-app-scripts/commit/212146c)) ## [1.3.2](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.1...v1.3.2) (2017-04-12) ### Bug Fixes * **deep-linking:** fix bug with null deep link config when modifying the main NgModule file (app.module.ts) ([759bb4f](https://github.com/ionic-team/ionic-app-scripts/commit/759bb4f)) * **optimization:** don't purge ctorParams for angular core or angular platform browser ([9562181](https://github.com/ionic-team/ionic-app-scripts/commit/9562181)) * **uglifyjs:** only minify files processed by webpack or rollup ([30ecdd8](https://github.com/ionic-team/ionic-app-scripts/commit/30ecdd8)) ## [1.3.1](https://github.com/ionic-team/ionic-app-scripts/compare/v1.3.0...v1.3.1) (2017-04-06) ### Bug Fixes * **config:** revert change and once again transpile bundle by default. ([b558584](https://github.com/ionic-team/ionic-app-scripts/commit/b558584)) * **decorators:** don't remove third party transpiled (not static) decorators ([3a3259a](https://github.com/ionic-team/ionic-app-scripts/commit/3a3259a)) * **deep-linking:** don't force the main bundle to be re-built unless the deep link config changed ([02b8e97](https://github.com/ionic-team/ionic-app-scripts/commit/02b8e97)) * **errors:** better error msg reporting from worker threads ([d9d000a](https://github.com/ionic-team/ionic-app-scripts/commit/d9d000a)) * **uglifyjs:** better error msg reporting ([49c0afb](https://github.com/ionic-team/ionic-app-scripts/commit/49c0afb)) # [1.3.0](https://github.com/ionic-team/ionic-app-scripts/compare/v1.2.5...v1.3.0) (2017-04-05) ### Features * **optimization:** purge decorators enabled by default ([b626e00](https://github.com/ionic-team/ionic-app-scripts/commit/b626e00)) * **optimizations:** purge transpiled decorators ([ba5e0cd](https://github.com/ionic-team/ionic-app-scripts/commit/ba5e0cd)) ## [1.2.5](https://github.com/ionic-team/ionic-app-scripts/compare/v1.2.4...v1.2.5) (2017-03-31) ### Bug Fixes * **webpack:** fixes bugs where some third party libs didn't load correctly ([e7559e5](https://github.com/ionic-team/ionic-app-scripts/commit/e7559e5)) ## [1.2.4](https://github.com/ionic-team/ionic-app-scripts/compare/v1.2.3...v1.2.4) (2017-03-30) ### Refactor * **deep-linking:** set default segment value to filename without extension([5a97ba5](https://github.com/ionic-team/ionic-app-scripts/commit/5a97ba5)) ## [1.2.3](https://github.com/ionic-team/ionic-app-scripts/compare/v1.2.2...v1.2.3) (2017-03-29) ### Bug Fixes * **deep-linking:** Deep linking fixes for Windows and non-unix paths * **script:** linux only accepts one argument after shebang, so revert giving app-scripts more memory by default ([0999f23](https://github.com/ionic-team/ionic-app-scripts/commit/0999f23)), closes [#838](https://github.com/ionic-team/ionic-app-scripts/issues/838) ## [1.2.2](https://github.com/ionic-team/ionic-app-scripts/compare/v1.2.1...v1.2.2) (2017-03-27) ### Bug Fixes * **generators:** use correct path and handle providers correctly ([e82d5ff](https://github.com/ionic-team/ionic-app-scripts/commit/e82d5ff)) * **rollup:** pass all config options to generate ([3502360](https://github.com/ionic-team/ionic-app-scripts/commit/3502360)) ## [1.2.1](https://github.com/ionic-team/ionic-app-scripts/compare/v1.2.0...v1.2.1) (2017-03-26) ### Bug Fixes * **deep-linking:** only attempt to inject deep-link config if there isn't an existing config and the ([507f1a8](https://github.com/ionic-team/ionic-app-scripts/commit/507f1a8)) * **rollup:** fix bug with not generating source-map correctly ([3b1fd16](https://github.com/ionic-team/ionic-app-scripts/commit/3b1fd16)) # [1.2.0](https://github.com/ionic-team/ionic-app-scripts/compare/v1.1.4...v1.2.0) (2017-03-24) ### Bug Fixes * **deep-linking:** Fix issue with deep-linking when attempting to update a template and failing, resulting in a full build but not processing deep links ([6b158d3](https://github.com/ionic-team/ionic-app-scripts/commit/6b158d3)) * **optimization:** fix out of memory errors by providing more memory by default ([b4c287a](https://github.com/ionic-team/ionic-app-scripts/commit/b4c287a)) * **optimizations:** only store ionic and src files in memory ([f51314f](https://github.com/ionic-team/ionic-app-scripts/commit/f51314f)) * **uglify:** check for correct file extension ([d17f2e1](https://github.com/ionic-team/ionic-app-scripts/commit/d17f2e1)) * **uglify:** verify source maps are generated correctly for all bundles, tests ([fc44ca6](https://github.com/ionic-team/ionic-app-scripts/commit/fc44ca6)) * **utils:** assign correct type ([3c3666c](https://github.com/ionic-team/ionic-app-scripts/commit/3c3666c)) * **watch:** fixed bug where options.ignore was being ignored if it's an array ([7f1e54c](https://github.com/ionic-team/ionic-app-scripts/commit/7f1e54c)) * **watch:** queue builds ([06e4971](https://github.com/ionic-team/ionic-app-scripts/commit/06e4971)) * **watch:** queue buildUpdates events to avoid race conditions when bundling/building ([43caefa](https://github.com/ionic-team/ionic-app-scripts/commit/43caefa)) * **webpack:** don't overwrite css files when outputting webpack files ([a32649f](https://github.com/ionic-team/ionic-app-scripts/commit/a32649f)) ### Features * **serve:** change http-server to use request hostname instead of the configured hostname. ([8e1e81a](https://github.com/ionic-team/ionic-app-scripts/commit/8e1e81a)) * **deep-linking:** generate default NgModule when missing by default ([90138fa](https://github.com/ionic-team/ionic-app-scripts/commit/90138fa)) * **deep-linking:** parsing deeplink decorator is now enabled by default, no longer experimental ([e097d4e](https://github.com/ionic-team/ionic-app-scripts/commit/e097d4e)) * **deep-linking:** upgrade script to generate NgModules for pages with [@DeepLink](https://github.com/DeepLink) decorator ([2943188](https://github.com/ionic-team/ionic-app-scripts/commit/2943188)) * **generators:** generators for page, component, directive, pipe, provider ([e2a45e4](https://github.com/ionic-team/ionic-app-scripts/commit/e2a45e4)) * **minification:** code-split bundles will be minified ([#814](https://github.com/ionic-team/ionic-app-scripts/issues/814)) ([d8d9a4e](https://github.com/ionic-team/ionic-app-scripts/commit/d8d9a4e)) ## [1.1.4](https://github.com/ionic-team/ionic-app-scripts/compare/v1.1.3...v1.1.4) (2017-02-23) ### Bug Fixes * **optimizations:** comment out code instead of purge it so source-maps don't error out in some edge ([1dedc53](https://github.com/ionic-team/ionic-app-scripts/commit/1dedc53)) * **watch:** make default watch fail-to-start timeout configurable so it works more reliably on slow ([2e2a647](https://github.com/ionic-team/ionic-app-scripts/commit/2e2a647)), closes [#772](https://github.com/ionic-team/ionic-app-scripts/issues/772) ## [1.1.3](https://github.com/ionic-team/ionic-app-scripts/compare/v1.1.2...v1.1.3) (2017-02-17) ### Bug Fixes * **config:** Setting readConfigJson constant wrong ([#761](https://github.com/ionic-team/ionic-app-scripts/issues/761)) ([64bc17f](https://github.com/ionic-team/ionic-app-scripts/commit/64bc17f)) * **source-maps:** source map must correspond to .js file name with a .map at the end ([debd88b](https://github.com/ionic-team/ionic-app-scripts/commit/debd88b)) ## [1.1.2](https://github.com/ionic-team/ionic-app-scripts/compare/v1.1.1...v1.1.2) (2017-02-16) ### Bug Fixes * **deep-links:** handle configs with internal arrays ([a7df816](https://github.com/ionic-team/ionic-app-scripts/commit/a7df816)) * **deep-links:** only provide deep links to webpack that contain the import used in code and the abs ([fae4862](https://github.com/ionic-team/ionic-app-scripts/commit/fae4862)) * **optimizations:** remove the js file created by the optimizations bundling pass ([c0bb3f4](https://github.com/ionic-team/ionic-app-scripts/commit/c0bb3f4)) ## [1.1.1](https://github.com/ionic-team/ionic-app-scripts/compare/v1.1.0...v1.1.1) (2017-02-15) ### Bug Fixes * **config:** node_modules directory should not be configurable (users were finding it confusing) ([1f58aaa](https://github.com/ionic-team/ionic-app-scripts/commit/1f58aaa)) * **copy:** support overriding config entries with empty objects ([5879a8b](https://github.com/ionic-team/ionic-app-scripts/commit/5879a8b)) * **deeplinks:** make deep link config parsing support 2.x and 3.x deep link config ([1ac7116](https://github.com/ionic-team/ionic-app-scripts/commit/1ac7116)) * **deeplinks:** provide deep-links config to webpack as needed vs via the constructor ([a735e96](https://github.com/ionic-team/ionic-app-scripts/commit/a735e96)) * **http-server:** drive reading ionic.config.json based on config value ([e2d0d83](https://github.com/ionic-team/ionic-app-scripts/commit/e2d0d83)) * **optimizations:** throw error when ionic-angular index file isn't found ([6437005](https://github.com/ionic-team/ionic-app-scripts/commit/6437005)) * **transpile:** get tsconfig.json location from config value ([79b0eeb](https://github.com/ionic-team/ionic-app-scripts/commit/79b0eeb)) # [1.1.0](https://github.com/ionic-team/ionic-app-scripts/compare/v1.0.1...v1.1.0) (2017-02-11) ### Optimizations We are starting to introduce optimizations to improve the size of the `bundle` generated by the build process. The first set of optimizations are behind flags: `ionic_experimental_manual_treeshaking` will remove Ionic components and code that are not being used from the bundle. `ionic_experimental_purge_decorators` helps tree shaking by removing unnecessary `decorator` metadata from AoT code. Since these are experimental, we are looking for feedback on how the work. Please test them out and [let us know](https://github.com/ionic-team/ionic-app-scripts/issues) how it goes. See the instructions [here](https://github.com/ionic-team/ionic-app-scripts#custom-configuration). ### Features * **fonts:** remove used fonts for cordova builds ([967f784](https://github.com/ionic-team/ionic-app-scripts/commit/967f784)) ### Bug Fixes * **build:** fix test if linting should trigger on file change ([#719](https://github.com/ionic-team/ionic-app-scripts/issues/719)) ([e13b857](https://github.com/ionic-team/ionic-app-scripts/commit/e13b857)) * **lint:** capture results of all linted files ([eb4314e](https://github.com/ionic-team/ionic-app-scripts/commit/eb4314e)), closes [#725](https://github.com/ionic-team/ionic-app-scripts/issues/725) * **optimizations:** make optimizations work on windows and mac ([5fe21f3](https://github.com/ionic-team/ionic-app-scripts/commit/5fe21f3)) * **serve:** assign all ports dynamically ([#727](https://github.com/ionic-team/ionic-app-scripts/issues/727)) ([6b4115c](https://github.com/ionic-team/ionic-app-scripts/commit/6b4115c)) * **webpack:** fix bug with using [name] for output file name ([1128c9c](https://github.com/ionic-team/ionic-app-scripts/commit/1128c9c)) ## [1.0.1](https://github.com/ionic-team/ionic-app-scripts/compare/v1.0.0...v1.0.1) (2017-02-07) ### Breaking Changes This release was accidentally published with a breaking change for Deep Links. If you're using Deep Links, please don't upgrade to this version. We are in the process of changing the DeepLinks API slightly. ### Bug Fixes * **angular:** support angular 2.3+ ngc api ([13e930a](https://github.com/ionic-team/ionic-app-scripts/commit/13e930a)) * **deep-linking:** works when there isn't a valid deep link config ([62f05fc](https://github.com/ionic-team/ionic-app-scripts/commit/62f05fc)) * **deep-links:** adjust paths for AoT ([4055d73](https://github.com/ionic-team/ionic-app-scripts/commit/4055d73)) * **sass:** output valid source maps, that chrome can parse ([#306](https://github.com/ionic-team/ionic-app-scripts/issues/306)) ([6589550](https://github.com/ionic-team/ionic-app-scripts/commit/6589550)) * **source-maps:** always generate source map, then purge them if not needed in postprocess step ([d26b44c](https://github.com/ionic-team/ionic-app-scripts/commit/d26b44c)) ### Features * **createWorker:** pass argv and config_argv to spawned processes ([#487](https://github.com/ionic-team/ionic-app-scripts/issues/487)) ([02dfff8](https://github.com/ionic-team/ionic-app-scripts/commit/02dfff8)) * **lint:** new option to have stand alone lint bail ([b3bb906](https://github.com/ionic-team/ionic-app-scripts/commit/b3bb906)) # [1.0.0](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.48...v1.0.0) (2017-01-06) ### Upgrade Instructions Execute the following command from your ionic project. This installs a new peer dependency called `sw-toolbox` that is used to simplify implementing a service-worker. ``` npm install sw-toolbox --save --save-exact ``` ### Bug Fixes * **build:** check to ensure tsconfig contains sourcemaps true. ([e6bcf22](https://github.com/ionic-team/ionic-app-scripts/commit/e6bcf22)) * **config:** resolve any inputs that could be paths to absolute paths ([50876eb](https://github.com/ionic-team/ionic-app-scripts/commit/50876eb)) * **copy:** check for null object and src/dest ([eabd125](https://github.com/ionic-team/ionic-app-scripts/commit/eabd125)) * **ngc:** revert change to purge decorators (Angular CLI did too) ([8aae85c](https://github.com/ionic-team/ionic-app-scripts/commit/8aae85c)) * **webpack:** update environment plugin for webpack 2 RC3 ([be3aac1](https://github.com/ionic-team/ionic-app-scripts/commit/be3aac1)) * **websockets:** fix exception when no ws clients connected during rebuild ([#616](https://github.com/ionic-team/ionic-app-scripts/issues/616)) ([8685bf8](https://github.com/ionic-team/ionic-app-scripts/commit/8685bf8)) ## [0.0.48](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.47...v0.0.48) (2016-12-19) ### Upgrade Instructions `@ionic/app-scripts` version `0.0.47` had some breaking changes so please make sure you have performed those upgrade instructions. ### Bug Fixes * **diagnostics:** fix null pointers ([72adc86](https://github.com/ionic-team/ionic-app-scripts/commit/72adc86)) * **inline-templates:** check for existence of content ([#557](https://github.com/ionic-team/ionic-app-scripts/issues/557)) ([b68e125](https://github.com/ionic-team/ionic-app-scripts/commit/b68e125)) * **logging:** don't log msgs about websocket state ([18185fb](https://github.com/ionic-team/ionic-app-scripts/commit/18185fb)) * **optimization:** stop removing decorators ([45b0255](https://github.com/ionic-team/ionic-app-scripts/commit/45b0255)) * **serve:** find an open port for the notification server if port is used. ([d6de413](https://github.com/ionic-team/ionic-app-scripts/commit/d6de413)) * **copy:** generate project context if it doesn't exist ([26f6db8](https://github.com/ionic-team/ionic-app-scripts/commit/26f6db8a7d3398b940cfb4c4b3eb4a6f141e1be7#diff-b477061dcc036b7490cfc73741747819)) ### Features * **sass:** enable Sass indented files compilation ([#565](https://github.com/ionic-team/ionic-app-scripts/issues/565)) ([f632298](https://github.com/ionic-team/ionic-app-scripts/commit/f632298)) ## [0.0.47](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.46...v0.0.47) (2016-12-12) ### Upgrade Instructions #### Install latest Ionic CLI Install the latest ionic cli. `sudo` may be required depending upon your `npm` set-up. ``` npm install -g ionic@latest ``` #### Entry Point Changes Delete `main.dev.ts` and `main.prod.ts` and create a `main.ts` file with the following content: ``` import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule); ``` #### Dev Builds By Default Changes All builds are now development (non-AoT) builds by default. This allows for a better development experience when testing on a device. To get started, please follow the steps below. Make sure the `scripts` section of `package.json` looks like this: ``` "scripts": { "ionic:build": "ionic-app-scripts build", "ionic:serve": "ionic-app-scripts serve" } ``` `ionic run android --prod` will do a production build that utilizes AoT compiling and minifaction. `ionic emulate ios --prod` will do a production build that utilizes AoT compiling and minifaction. `ionic run android` will do a development build `ionic emulate ios` will do a development build If you wish to run AoT but disable minifaction, do the following `ionic run android --aot` `ionic emulate ios --aot` #### Source Map Changes Change `ionic_source_map` to `ionic_source_map_type` in package.json if it is overridden. #### Config Changes There were significant improvements/changes to most configs. Please review the changes and make sure any custom configs are up to date. #### Validate TSConfig settings Verify that `tsconfig.json` is up to date with recommended settings: ``` { "compilerOptions": { "allowSyntheticDefaultImports": true, "declaration": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, "lib": [ "dom", "es2015" ], "module": "es2015", "moduleResolution": "node", "sourceMap": true, "target": "es5" }, "include": [ "src/**/*.ts" ], "exclude": [ "node_modules" ], "compileOnSave": false, "atom": { "rewriteTsconfig": false } } ``` ### Breaking Changes 1. `main.dev.ts` and `main.prod.ts` have been deprecated in favor of `main.ts` with the content of `main.dev.ts`. The content of `main.ts` will be optimized at build time for production builds. 2. Builds are now always development (non-AoT) by default. To enable `prod` builds, use the `--prod` option. 3. `copy.config` and `watch.config` have breaking changes moving to an easier-to-extend configuration style. 4. `copy.config` uses `node-glob` instead of `fs-extra` to do the copy. Migrate from directory/files to globs in any custom configs. 5. `ionic_source_map` configuration has been changed to `ionic_source_map_type`. 6. Source maps now use `source-map` devtool option by default instead of `eval`. Change `ionic_source_map_type` option to return to the faster building `eval`. ### Bug Fixes * **AoT:** dynamically enable prod mode for AoT builds ([0594803](https://github.com/ionic-team/ionic-app-scripts/commit/0594803)) * **AoT:** use in-memory data store instead of .tmp directory for AoT codegen ([93106ff](https://github.com/ionic-team/ionic-app-scripts/commit/93106ff)) * **build:** every build should run clean sync and copy async. ([6d4eb6e](https://github.com/ionic-team/ionic-app-scripts/commit/6d4eb6e)) * **copy:** Resolve race condition in copy task, move to glob config ([cc99a73](https://github.com/ionic-team/ionic-app-scripts/commit/cc99a73)) * **lab:** add lab to files ([f42c980](https://github.com/ionic-team/ionic-app-scripts/commit/f42c980)) * **livereload:** livereload now correctly serves cordova plugins on run and emulate. ([a0c3f5d](https://github.com/ionic-team/ionic-app-scripts/commit/a0c3f5d)) * **livereload:** on project build all pages connected should reload. ([#513](https://github.com/ionic-team/ionic-app-scripts/issues/513)) ([62d6b23](https://github.com/ionic-team/ionic-app-scripts/commit/62d6b23)) * **livereload:** use localhost instead of 0.0.0.0 when injecting live reload script ([#450](https://github.com/ionic-team/ionic-app-scripts/issues/450)) ([7f8a0c3](https://github.com/ionic-team/ionic-app-scripts/commit/7f8a0c3)) * **logging:** remove unnecessary websocket error msg, clean up copy error msg ([1517b06](https://github.com/ionic-team/ionic-app-scripts/commit/1517b06)) * **ngc:** simpler AoT error reporting ([1b0f163](https://github.com/ionic-team/ionic-app-scripts/commit/1b0f163)) * **serve:** add flag to indicate to serve for a cordova app ([93782e7](https://github.com/ionic-team/ionic-app-scripts/commit/93782e7)) * **source-maps:** use detailed source-map as default, fix windows path issue ([19464b3](https://github.com/ionic-team/ionic-app-scripts/commit/19464b3)) * **workers:** generate context in worker threads ([af036ec](https://github.com/ionic-team/ionic-app-scripts/commit/af036ec)) ### Features * **build:** replace --dev flag with --prod and add flags --aot, --minifyJs, --minifyCss, --optimizeJs ([99922ce](https://github.com/ionic-team/ionic-app-scripts/commit/99922ce)) * **bundle:** pre and post bundle hooks ([4835550](https://github.com/ionic-team/ionic-app-scripts/commit/4835550)) * **copy:** update copy config to move web workers ([a909fc4](https://github.com/ionic-team/ionic-app-scripts/commit/a909fc4)) * **lab:** fresh coat of paint ([edb6f09](https://github.com/ionic-team/ionic-app-scripts/commit/edb6f09)) * **replacePathVars:** support interpolation of objects and arrays ([#449](https://github.com/ionic-team/ionic-app-scripts/issues/449)) ([e039d46](https://github.com/ionic-team/ionic-app-scripts/commit/e039d46)) * all arguments passed should be compared as case insensitive ([085c897](https://github.com/ionic-team/ionic-app-scripts/commit/085c897)) ## [0.0.46](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.44...v0.0.46) (2016-11-21) ### Bug Fixes * **build:** better support for saving multiple files at a time ([254bb6c](https://github.com/ionic-team/ionic-app-scripts/commit/254bb6c)) * **copy:** ionicons copied from ionicons ([69f89a8](https://github.com/ionic-team/ionic-app-scripts/commit/69f89a8)) * **errors:** skip HTTP errors ([5906167](https://github.com/ionic-team/ionic-app-scripts/commit/5906167)) * **proxies:** Wrong parameter in Logger.info, in setupProxies function causing proxies not to load ([#395](https://github.com/ionic-team/ionic-app-scripts/issues/395)) ([316b1de](https://github.com/ionic-team/ionic-app-scripts/commit/316b1de)) * **typescript:** lock typescript version to 2.0.x for now due to build error with 2.1.x ([ef7203b](https://github.com/ionic-team/ionic-app-scripts/commit/ef7203b)) * **webpack:** fix path resolution ([97c23f9](https://github.com/ionic-team/ionic-app-scripts/commit/97c23f9)) * **webpack:** reference json-loader to account for webpack breaking change ([d6fe709](https://github.com/ionic-team/ionic-app-scripts/commit/d6fe709)) * **webpack:** resolve modules to rootDir ([#365](https://github.com/ionic-team/ionic-app-scripts/issues/365)) ([64eb845](https://github.com/ionic-team/ionic-app-scripts/commit/64eb845)) ### Features * **options:** allow users to pass their own cleanCss Options ([#377](https://github.com/ionic-team/ionic-app-scripts/issues/377)) ([20df6d4](https://github.com/ionic-team/ionic-app-scripts/commit/20df6d4)) ## [0.0.45](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.44...v0.0.45) (2016-11-17) ### Bug Fixes * **errors:** runtime error immediately, selectable stack ([70f68da](https://github.com/ionic-team/ionic-app-scripts/commit/70f68da)) * **inline-templates:** update bundle and memory file representation on template change ([11a949d](https://github.com/ionic-team/ionic-app-scripts/commit/11a949d)) * **rollup:** invalidate cache on template change ([80c0eb6](https://github.com/ionic-team/ionic-app-scripts/commit/80c0eb6)) * **webpack:** invalidate cache by use of timestamps ([4d6bbd5](https://github.com/ionic-team/ionic-app-scripts/commit/4d6bbd5)) ### Features * **run-build-update:** handle linked npm modules ([#375](https://github.com/ionic-team/ionic-app-scripts/issues/375)) ([0f113c8](https://github.com/ionic-team/ionic-app-scripts/commit/0f113c8)) * **serve:** add '/ionic-lab' as an alias for the lab html file path. ([c319404](https://github.com/ionic-team/ionic-app-scripts/commit/c319404)) ## [0.0.44](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.43...v0.0.44) (2016-11-15) ### Bug Fixes * **debug:** cmd+shift+8 to show debug menu ([a26d729](https://github.com/ionic-team/ionic-app-scripts/commit/a26d729)) * **error:** (cmd/ctrl)+8 for debug menu ([89550af](https://github.com/ionic-team/ionic-app-scripts/commit/89550af)) * **error:** add header padding for cordova iOS ([5c4c547](https://github.com/ionic-team/ionic-app-scripts/commit/5c4c547)) * **error:** apply correct css for runtime error close ([81f1d75](https://github.com/ionic-team/ionic-app-scripts/commit/81f1d75)) * **error:** fix content scrolling ([3b82465](https://github.com/ionic-team/ionic-app-scripts/commit/3b82465)) * **error:** reload immediately after js/html update ([07f918e](https://github.com/ionic-team/ionic-app-scripts/commit/07f918e)) * **error:** safari css fixes ([7c2fb59](https://github.com/ionic-team/ionic-app-scripts/commit/7c2fb59)) * **serve:** correct paths so that --lab works ([1d99a98](https://github.com/ionic-team/ionic-app-scripts/commit/1d99a98)) * **serve:** open browser to localhost ([14275c7](https://github.com/ionic-team/ionic-app-scripts/commit/14275c7)) * **transpile:** normalize and resolve paths always for OS independence ([ca6c889](https://github.com/ionic-team/ionic-app-scripts/commit/ca6c889)) * **watch:** fallback for when chokidar watch ready/error don't fire (happens on windows when file is ([519cd7f](https://github.com/ionic-team/ionic-app-scripts/commit/519cd7f)), closes [#282](https://github.com/ionic-team/ionic-app-scripts/issues/282) * **watch:** watch now ignores Mac OS meta data files ([02d0b8d](https://github.com/ionic-team/ionic-app-scripts/commit/02d0b8d)), closes [#331](https://github.com/ionic-team/ionic-app-scripts/issues/331) * **webpack:** source maps link to original src for ide debugging ([39edd2e](https://github.com/ionic-team/ionic-app-scripts/commit/39edd2e)) ### Features * **debug:** debug menu options ([53d6e30](https://github.com/ionic-team/ionic-app-scripts/commit/53d6e30)) * **debug:** shake device to show debug menu ([770f4e3](https://github.com/ionic-team/ionic-app-scripts/commit/770f4e3)) * **error:** client runtime error reporting ([fc40b92](https://github.com/ionic-team/ionic-app-scripts/commit/fc40b92)) * **error:** syntax and error highlighting ([8836310](https://github.com/ionic-team/ionic-app-scripts/commit/8836310)) ## [0.0.43](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.42...v0.0.43) (2016-11-10) ### Bug Fixes * **rollup:** removing rollup metadata prefix for paths ([350a288](https://github.com/ionic-team/ionic-app-scripts/commit/350a288)) * **watch:** remove shorthand arg for watch ([0685c0b](https://github.com/ionic-team/ionic-app-scripts/commit/0685c0b)), closes [#290](https://github.com/ionic-team/ionic-app-scripts/issues/290) * **webpack:** typo in import, close [#326](https://github.com/ionic-team/ionic-app-scripts/issues/326) ([#341](https://github.com/ionic-team/ionic-app-scripts/issues/341)) ([6b89fa2](https://github.com/ionic-team/ionic-app-scripts/commit/6b89fa2)) ## [0.0.42](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.41...v0.0.42) (2016-11-09) ## Upgrade Steps To use this version of `@ionic/app-scripts`, follow these steps to upgrade: 1. Install the latest version of the ionic cli ``` npm install ionic@latest -g ``` Note: sudo may be required depending on your workstation set-up 2. Update the project's `package.json` file's `script` section to look like this: ``` ... "scripts" : { "ionic:build": "ionic-app-scripts build", "ionic:serve": "ionic-app-scripts serve" } ... ``` Note: This is removing several deprecated Ionic scripts. If you have any of your own custom scripts, don't remove them. 3. Install the latest version of `@ionic/app-scripts` ``` npm install @ionic/app-scripts@latest --save-dev ``` ### Bug Fixes * **bundling:** execute bundle updates if full bundle has completed at least once ([fbe56dc](https://github.com/ionic-team/ionic-app-scripts/commit/fbe56dc)) * **sass:** remove broken sass caching ([91faf0b](https://github.com/ionic-team/ionic-app-scripts/commit/91faf0b)) ### Features * **error:** use datauri for favicon build status ([892cf4a](https://github.com/ionic-team/ionic-app-scripts/commit/892cf4a)) * **errors:** overlay build errors during development ([87f7648](https://github.com/ionic-team/ionic-app-scripts/commit/87f7648)) ## [0.0.41](https://github.com/ionic-team/ionic-app-scripts/compare/v0.0.40...v0.0.41) (2016-11-07) ### Bug Fixes * **webpack:** use source-maps instead of eval for prod builds ([fdd86be](https://github.com/ionic-team/ionic-app-scripts/commit/fdd86be)) ## 0.0.40 (2016-11-07) ### Breaking Changes `ionic_source_map` variable is now used to drive the `devtool` (sourcemap) value for webpack. It now defaults to `eval` for faster builds. Set it to `source-map` for `typescript` sourcemaps. ### Bug Fixes * **sourcemaps:** fix source maps for all files ([066de6d](https://github.com/ionic-team/ionic-app-scripts/commit/066de6d)) * **sourcemaps:** webpack .ts sourcemaps ([bfca1be](https://github.com/ionic-team/ionic-app-scripts/commit/bfca1be)) * **webpack:** modify config to use IONIC_APP_SCRIPTS_DIR variable ([2b7c606](https://github.com/ionic-team/ionic-app-scripts/commit/2b7c606)) ### Features * **events:** emit bundler events ([8d73da9](https://github.com/ionic-team/ionic-app-scripts/commit/8d73da9)) * **exports:** add templateUpdate and fullBuildUpdate ([a31897d](https://github.com/ionic-team/ionic-app-scripts/commit/a31897d)) * **webpack source maps:** make it easy to configure source map type ([03565b7](https://github.com/ionic-team/ionic-app-scripts/commit/03565b7)) ### Performance Improvements * **webpack:** speed up webpack build by not using file-system and watches ([23ad195](https://github.com/ionic-team/ionic-app-scripts/commit/23ad195)) # 0.0.39 (2016-10-31) * Switch default bundler to Webpack # 0.0.36 (2016-10-15) * Fix handling multiple async template updates # 0.0.35 (2016-10-15) * Fix resolving index files correctly * Fix template rebuilds for multiple templates in one file * Fix ability to watchers to ignore paths # 0.0.34 (2016-10-15) * Fix silently failed bundles * Fix template path resolving issues # 0.0.33 (2016-10-14) * Improve build times for template changes * Fix bundle updates on template changes # 0.0.32 (2016-10-14) * Fix Windows entry path normalization # 0.0.31 (2016-10-13) * Add ability use multiple processor cores for various subtasks * Use typescript `createProgram` to transpile entire app * Add syntax highlighting and colors to typescript, sass and tslint errors * Improved error messages for typescript errors * `clean` task only cleans out the `www/build/` directory rather than all of `www/` * Add task to copy `src/service-worker.js` to `www/service-worker.js` * Add task to copy `src/manifest.json` to `www/manifest.json` # 0.0.30 (2016-10-06) * Fix JS source maps * Fix template inlining # 0.0.29 (2016-10-05) * Addressed memory usage error * Dev builds no longer use the `.tmp` directory * Dev build entry files should be the source `main.dev.ts` file * Custom rollup configs should remove the `ngTemplate()` plugin ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Drifty Co Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![npm version](https://img.shields.io/npm/v/@ionic/app-scripts.svg)](https://www.npmjs.com/package/@ionic/app-scripts) [![Circle CI](https://circleci.com/gh/ionic-team/ionic-app-scripts.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/ionic-team/ionic-app-scripts) # DISCLAIMER: NO LONGER MAINTAINED Oh, [hello there](https://media.giphy.com/media/8JTFsZmnTR1Rs1JFVP/giphy.gif)! Ionic App Scripts is a tool tied specifically to version 3.x of Ionic Framework. Since the release of version 4.0 of Ionic Framework, we are no longer using Ionic App Scripts for building in Ionic Framework. Version 3 of Ionic Framework is no longer actively maintained by us. For more information on which versions are active, see our [support policy](https://ionicframework.com/docs/reference/support#framework-maintenance-and-support-status). Due to this, we are no longer maintaining Ionic App Scripts and we recommend developers update their apps to the latest framework release. This provides several new features, bug fixes, performance improvements, as well as up to date tooling for Ionic apps. For more details on how to upgrade, check out our [migration guide](https://ionicframework.com/docs/reference/migration). # Ionic App Scripts Helper scripts to get [Ionic apps](https://ionicframework.com/) up and running quickly (minus the config overload). To get the latest `@ionic/app-scripts`, please run: ``` npm install @ionic/app-scripts@latest --save-dev ``` ### Config Defaults Out of the box, Ionic starters have been preconfigured with great defaults for building fast apps, including: - Multi-core processing tasks in parallel for faster builds - In-memory file transpiling and bundling - Transpiling source code to ES5 JavaScript - Ahead of Time (AoT) template compiling - Just in Time (JiT) template compiling - Template inlining for JiT builds - Bundling modules for faster runtime execution - Treeshaking unused components and dead-code removal - Generating CSS from bundled component Sass files - Autoprefixing vendor CSS prefixes - Minifying JavaScript files - Compressing CSS files - Copying `src` static assets to `www` - Linting source files - Watching source files for live-reloading Just the bullet list above is a little overwhelming, and each task requires quite a bit of development time just to get started. Ionic App Script's intention is to make it easier to complete common tasks so developers can focus on building their app, rather than building build scripts. Note that the [Ionic Framework's](https://github.com/ionic-team/ionic) source is made up of modules and can be packaged by any bundler or build process. However, this project's goal is provide simple scripts to make building Ionic apps easier, while also allowing developers to further configure their build process. ### npm Scripts Instead of depending on external task runners, Ionic App Scripts now prefers being executed from [npm scripts](https://docs.npmjs.com/misc/scripts). Ionic's npm scripts come preconfigured in the project's `package.json` file. For example, this is the default setup for npm scripts in each starter: ``` "scripts": { "ionic:build": "ionic-app-scripts build", "ionic:serve": "ionic-app-scripts serve" }, ``` To run the `build` script found in the `package.json` `scripts` property, execute: ``` npm run build ``` ## Environments You can use Node style `process.env.MY_VAR` syntax directly in your typescript and when the application is bundled it'll be replaced with the following order of precedence: * If the variable exists in the process environment it will be replaced with that value. * If the variable is not defined in the process environment it will be read from a `.env.dev` file for dev builds or `.env.prod` file for prod builds which are located in the root of the app * If the variable is not defined in either place it will be `undefined` In order to take advantage of this apps will need a `src/declarations.d.ts` file with the following declaration: ```typescript declare var process: { env: { [key: string]: string | undefined; } }; ``` *Note*: This declaration may conflict if `@types/node` is installed in your project. See [#3541](https://github.com/ionic-team/ionic-cli/issues/3541). ## Custom Configuration In many cases, the defaults which Ionic provides cover most of the scenarios required by developers; however, Ionic App Scripts does provide multiple ways to configure and override the defaults for each of the various tasks. Note that Ionic will always apply its defaults for any property that was not provided by custom configuration. [Default Config Files](https://github.com/ionic-team/ionic-app-scripts/tree/master/config) ### package.json Config Ionic projects use the `package.json` file for configuration. There's a handy [config](https://docs.npmjs.com/misc/config#per-package-config-settings) property which can be used. Below is an example of setting a custom config file using the `config` property in a project's `package.json`. ``` "config": { "ionic_cleancss": "./config/cleancss.config.js" }, ``` ### Command-line Flags Remember how we're actually running `ionic-app-scripts` from the `scripts` property of a project's `package.json` file? Well we can also add command-line flags to each script, or make new scripts with these custom flags. For example: ``` "scripts": { "build": "ionic-app-scripts build --webpack ./config/webpack.dev.config.js", "minify": "ionic-app-scripts minify --cleancss ./config/cleancss.config.js", }, ``` The same command-line flags can be also applied to `npm run` commands too, such as: ``` npm run build --webpack ./config/webpack.dev.config.js ``` ### Overriding Config Files | Config File | package.json Config | Cmd-line Flag | |-------------|---------------------|-----------------------| | CleanCss | `ionic_cleancss` | `--cleancss` or `-e` | | Copy | `ionic_copy` | `--copy` or `-y` | | Generator | `ionic_generator` | `--generator` or `-g` | | NGC | `ionic_ngc` | `--ngc` or `-n` | | Sass | `ionic_sass` | `--sass` or `-s` | | TSLint | `ionic_tslint` | `--tslint` or `-i` | | UglifyJS | `ionic_uglifyjs` | `--uglifyjs` or `-u` | | Watch | `ionic_watch` | `--watch` | | Webpack | `ionic_webpack` | `--webpack` or `-w` | ### Overriding Config Values | Config Values | package.json Config | Cmd-line Flag | Defaults | Details | |-----------------|---------------------|---------------|-----------------|----------------| | root directory | `ionic_root_dir` | `--rootDir` | `process.cwd()` | The directory path of the Ionic app | | src directory | `ionic_src_dir` | `--srcDir` | `src` | The directory holding the Ionic src code | | www directory | `ionic_www_dir` | `--wwwDir` | `www` | The deployable directory containing everything needed to run the app | | build directory | `ionic_build_dir` | `--buildDir` | `build` | The build process uses this directory to store generated files, etc | | temp directory | `ionic_tmp_dir` | `--tmpDir` | `.tmp` | Temporary directory for writing files for debugging and various build tasks | | ionic-angular directory | `ionic_angular_dir` | `--ionicAngularDir` | `ionic-angular` | ionic-angular directory | | ionic-angular entry point | `ionic_angular_entry_point` | `--ionicAngularEntryPoint` | `index.js` | entry point file for ionic-angular | | source map type | `ionic_source_map_type` | `--sourceMapType` | `source-map` | Chooses the webpack `devtool` option. `eval` and `source-map` are supported | | generate source map | `ionic_generate_source_map` | `--generateSourceMap` | `true` | Determines whether to generate a source map or not | | tsconfig path | `ionic_ts_config` | `--tsconfig` | `{{rootDir}}/tsconfig.json` | absolute path to tsconfig.json | | app entry point | `ionic_app_entry_point` | `--appEntryPoint` | `{{srcDir}}/app/main.ts` | absolute path to app's entrypoint bootstrap file | | app ng module path | `ionic_app_ng_module_path` | `--appNgModulePath` | `{{srcDir}}/app/app.module.ts` | absolute path to app's primary `NgModule` | | app ng module class | `ionic_app_ng_module_class` | `--appNgModuleClass` | `AppModule` | Exported class name for app's primary `NgModule` | | clean before copy | `ionic_clean_before_copy` | `--cleanBeforeCopy` | `false` | clean out existing files before copy task runs | | output js file | `ionic_output_js_file_name` | `--outputJsFileName` | `main.js` | name of js file generated in `buildDir` | | output css file | `ionic_output_css_file_name` | `--outputCssFileName` | `main.css` | name of css file generated in `buildDir` | | bail on lint error | `ionic_bail_on_lint_error` | `--bailOnLintError` | `null` | Set to `true` to make stand-alone lint commands fail with non-zero status code | | enable type checking during lint | `ionic_type_check_on_lint` | `--typeCheckOnLint` | `null` | Set to `true` to enable [type checking](https://palantir.github.io/tslint/usage/type-checking) during lint | | write AoT files to disk | `ionic_aot_write_to_disk` | `--aotWriteToDisk` | `null` | Set to `true` to write files to disk for debugging | | print webpack dependency tree | `ionic_print_webpack_dependency_tree` | `--printWebpackDependencyTree` | `null` | Set to `true` to print out a dependency tree after running Webpack | | parse deeplink config | `ionic_parse_deeplinks` | `--parseDeepLinks` | `true` | Parses and extracts data from the `@IonicPage` decorator | | convert bundle to ES5 | `ionic_build_to_es5` | `--buildToEs5` | `true` | Convert bundle to ES5 for production deployments | | default watch timeout | `ionic_start_watch_timeout` | `--startWatchTimeout` | `3000` | Milliseconds controlling the default watch timeout | | choose the polyfill | `ionic_polyfill_name` | `--polyfillName` | `polyfills` | Change with polyfills.modern or polyfills.ng (all options)[https://github.com/driftyco/ionic/tree/master/scripts/polyfill] | | enable linting | `ionic_enable_lint` | `--enableLint` | `true` | Set to `false` for skipping the linting after the build | ### Ionic Environment Variables These environment variables are automatically set to [Node's `process.env`](https://nodejs.org/api/process.html#process_process_env) property. These variables can be useful from within custom configuration files, such as custom `webpack.config.js` file. | Environment Variable | Description | |----------------------------|----------------------------------------------------------------------| | `IONIC_ENV` | Value can be either `prod` or `dev`. | | `IONIC_ROOT_DIR` | The absolute path to the project's root directory. | | `IONIC_SRC_DIR` | The absolute path to the app's source directory. | | `IONIC_WWW_DIR` | The absolute path to the app's public distribution directory. | | `IONIC_BUILD_DIR` | The absolute path to the app's bundled js and css files. | | `IONIC_TMP_DIR` | Temp directory for debugging generated/optimized code and various build tasks | | `IONIC_NODE_MODULES_DIR` | The absolute path to the `node_modules` directory. | | `IONIC_ANGULAR_DIR` | The absolute path to the `ionic-angular` node_module directory. | | `IONIC_APP_SCRIPTS_DIR` | The absolute path to the `@ionic/app-scripts` node_module directory. | | `IONIC_SOURCE_MAP_TYPE` | The Webpack `devtool` setting. `eval` and `source-map` are supported.| | `IONIC_GENERATE_SOURCE_MAP`| Determines whether to generate a sourcemap or not. | | `IONIC_TS_CONFIG` | The absolute path to the project's `tsconfig.json` file | | `IONIC_APP_ENTRY_POINT` | The absolute path to the project's `main.ts` entry point file | | `IONIC_APP_NG_MODULE_PATH` | The absolute path to app's primary `NgModule` | | `IONIC_APP_NG_MODULE_CLASS` | The exported class name for app's primary `NgModule` | | `IONIC_GLOB_UTIL` | The path to Ionic's `glob-util` script. Used within configs. | | `IONIC_CLEAN_BEFORE_COPY` | Attempt to clean existing directories before copying files. | | `IONIC_CLOSURE_JAR` | The absolute path ot the closure compiler jar file | | `IONIC_OUTPUT_JS_FILE_NAME` | The file name of the generated javascript file | | `IONIC_OUTPUT_CSS_FILE_NAME` | The file name of the generated css file | | `IONIC_WEBPACK_FACTORY` | The absolute path to Ionic's `webpack-factory` script | | `IONIC_WEBPACK_LOADER` | The absolute path to Ionic's custom webpack loader | | `IONIC_BAIL_ON_LINT_ERROR` | Boolean determining whether to exit with a non-zero status code on error | | `IONIC_TYPE_CHECK_ON_LINT` | Boolean determining whether to type check code during lint or not | | `IONIC_AOT_WRITE_TO_DISK` | `--aotWriteToDisk` | `null` | Set to `true` to write files to disk for debugging | | `IONIC_PRINT_WEBPACK_DEPENDENCY_TREE` | boolean to print out a dependency tree after running Webpack | | `IONIC_PARSE_DEEPLINKS` | boolean to enable parsing the Ionic 3.x deep links API for lazy loading | | `IONIC_BUILD_TO_ES5` | boolean to enable converting bundle to ES5 for production deployments | | `IONIC_START_WATCH_TIMEOUT` | Milliseconds controlling the default watch timeout | The `process.env.IONIC_ENV` environment variable can be used to test whether it is a `prod` or `dev` build, which automatically gets set by any command. By default the `build` and `serve` tasks produce `dev` builds (a build that does not include Ahead of Time (AoT) compilation or minification). To force a `prod` build you should use the `--prod` command line flag. `process.env.IONIC_ENV` environment variable is set to `prod` for `--prod` builds, otherwise `dev` for all other builds. ## All Available Tasks These tasks are available within `ionic-app-scripts` and can be added to npm scripts or any Node command. | Task | Description | |------------|-----------------------------------------------------------------------------------------------------| | `build` | A complete build of the application. It uses `development` settings by default. Use `--prod` to create an optimized build | | `clean` | Empty the `www/build` directory. | | `cleancss` | Compress the output CSS with [CleanCss](https://github.com/jakubpawlowicz/clean-css) | | `copy` | Run the copy tasks, which by defaults copies the `src/assets/` and `src/index.html` files to `www`. | | `lint` | Run the linter against the source `.ts` files, using the `tslint.json` config file at the root. | | `minify` | Minifies the output JS bundle and compresses the compiled CSS. | | `sass` | Sass compilation of used modules. Bundling must have at least ran once before Sass compilation. | | `watch` | Runs watch for dev builds. | Example NPM Script: ``` "scripts": { "minify": "ionic-app-scripts minify" }, ``` ## Tips 1. The Webpack `devtool` setting is driven by the `ionic_source_map_type` variable. It defaults to `source-map` for the best quality source map. Developers can enable significantly faster builds by setting `ionic_source_map_type` to `eval`. 2. By default, the `lint` command does not exit with a non-zero status code on error. To enable this, pass `--bailOnLintError true` to the command. ``` "scripts" : { ... "lint": "ionic-app-scripts lint" ... } ``` ``` npm run lint --bailOnLintError true ``` ## The Stack - [Ionic Framework](https://ionicframework.com/) - [TypeScript Compiler](https://www.typescriptlang.org/) - [Angular Compiler (NGC)](https://github.com/angular/angular/tree/master/modules/%40angular/compiler-cli) - [Webpack Module Bundler](https://webpack.js.org/) - Ionic Component Sass - [Node Sass](https://www.npmjs.com/package/node-sass) - [Autoprefixer](https://github.com/postcss/autoprefixer) - [UglifyJS](https://lisperator.net/uglifyjs/) - [CleanCss](https://github.com/jakubpawlowicz/clean-css) - [TSLint](https://palantir.github.io/tslint/) ## Contributing We welcome any PRs, issues, and feedback! Please be respectful and follow the [Code of Conduct](https://github.com/ionic-team/ionic/blob/master/CODE_OF_CONDUCT.md). We use Node 6, and NPM 5 for contributing. ### Publish a Nightly Build 1. Run `npm run build` to generate the `dist` directory 2. Run `npm run test` to validate the `dist` works 3. Tick the `package.json` version 4. Run `npm run nightly` to generate a nightly build on npm ### Publish a release Execute the following steps to publish a release: 1. Ensure your branch has been merged into `master` 2. Run `npm run build` to generate the `dist` directory 3. Run `npm run test` to validate the `dist` works 4. Temporarily tick the `package.json` version 5. Run `npm run changelog` to append the latest additions to the changelog 6. Manually verify and commit the changelog changes. Often times you'll want to manually add content/instructions 7. Revert the `package.json` version to the original version 8. Run `npm version patch` to tick the version and generate a git tag 9. Run `npm run github-release` to create the github release entry 10. Run `npm publish` to publish the package to npm 11. `git push origin master` - push changes to master ================================================ FILE: bin/ion-dev.js ================================================ window.IonicDevServerConfig = window.IonicDevServerConfig || {}; window.IonicDevServer = { start: function() { this.msgQueue = []; this.consoleLog = console.log; this.consoleError = console.error; this.consoleWarn = console.warn; IonicDevServerConfig.systemInfo.push('Navigator Platform: ' + window.navigator.platform); IonicDevServerConfig.systemInfo.push('User Agent: ' + window.navigator.userAgent); if (IonicDevServerConfig.sendConsoleLogs) { this.patchConsole(); } this.openConnection(); this.bindEvents(); var self = this; document.addEventListener("DOMContentLoaded", function() { var diagnosticsEle = document.getElementById('ion-diagnostics'); if (diagnosticsEle) { self.buildStatus('error'); } else { self.buildStatus('success'); } }); if (window.cordova && document.documentElement) { document.documentElement.classList.add('ion-diagnostics-cordova'); var ua = window.navigator.userAgent.toLowerCase(); if ((ua.indexOf('ipad') > -1 || ua.indexOf('iphone') > -1 || ua.indexOf('ipod') > -1) && ua.indexOf('windows phone') === -1) { document.documentElement.classList.add('ion-diagnostics-cordova-ios'); } } window.onerror = function(msg, url, lineNo, columnNo, error) { self.handleError(error); }; }, handleError: function(err) { if (!err) return; // Ignore HTTP errors if(err.url || err.headers) return; // Socket is ready so send this error to the server for prettifying if (this.socketReady) { var msg = { category: 'runtimeError', type: 'runtimeError', data: { message: err.message ? err.message.toString() : null, stack: err.stack ? err.stack.toString() : null } }; this.queueMessageSend(msg); } else { var c = []; c.push('
'); c.push('
'); c.push('
Error
'); c.push('
'); c.push(' '); c.push('
'); c.push('
'); c.push('
'); c.push('
'); c.push('
'); c.push('
'); c.push('
Runtime Error
'); c.push('
' + this.escapeHtml(err.message) + '
'); c.push('
'); c.push('
Stack
'); c.push('
' + this.escapeHtml(err.stack) + '
'); c.push('
'); c.push('
'); this.buildUpdate({ type: 'clientError', data: { diagnosticsHtml: c.join('') } }); } }, reloadApp: function() { window.location.reload(true); }, openConnection: function() { var self = this; var socketProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; this.socket = new WebSocket(socketProtocol + '//' + window.location.hostname + ':' + IonicDevServerConfig.wsPort); this.socket.onopen = function(ev) { self.socketReady = true; self.socket.onmessage = function(ev) { try { var msg = JSON.parse(ev.data); switch (msg.category) { case 'buildUpdate': self.buildUpdate(msg); break; } } catch (e) { self.consoleError('error receiving ws message', e); } }; self.socket.onclose = function() { self.consoleLog('Dev server logger closed'); self.socketReady = false; }; self.drainMessageQueue(); }; }, queueMessageSend: function(msg) { this.msgQueue.push(msg); this.drainMessageQueue(); }, drainMessageQueue: function() { if (this.socketReady) { var msg; while (msg = this.msgQueue.shift()) { try { this.socket.send(JSON.stringify(msg)); } catch(e) { if (e instanceof TypeError) { } else { this.consoleError('ws error: ' + e); } } } } }, patchConsole: function() { var self = this; function patchConsole(consoleType) { console[consoleType] = (function() { var orgConsole = console[consoleType]; return function() { orgConsole.apply(console, arguments); var msg = { category: 'console', type: consoleType, data: [] }; for (var i = 0; i < arguments.length; i++) { msg.data.push(arguments[i]); } if (msg.data.length) { self.queueMessageSend(msg); } }; })(); } // https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-console/#supported-methods var consoleFns = ['log', 'error', 'exception', 'warn', 'info', 'debug', 'assert', 'dir', 'dirxml', 'time', 'timeEnd', 'table']; for (var i in consoleFns) { patchConsole(consoleFns[i]); } }, /** * Process a build update message and display something to the friendly user. */ buildUpdate: function(msg) { var status = 'success'; if (msg.type === 'started') { status = 'active'; this.buildingNotification(true); } else { if (msg.data.reloadApp) { this.reloadApp(); return; } status = msg.data.diagnosticsHtml ? 'error' : 'success'; this.buildingNotification(false); var diagnosticsEle = document.getElementById('ion-diagnostics'); // If we have an element but no html created yet if (diagnosticsEle && !msg.data.diagnosticsHtml) { diagnosticsEle.classList.add('ion-diagnostics-fade-out'); this.diagnosticsTimerId = setTimeout(function() { var diagnosticsEle = document.getElementById('ion-diagnostics'); if (diagnosticsEle) { diagnosticsEle.parentElement.removeChild(diagnosticsEle); } }, 100); } else if (msg.data.diagnosticsHtml) { // We don't have an element but we have diagnostics HTML, so create the error if (!diagnosticsEle) { diagnosticsEle = document.createElement('div'); diagnosticsEle.id = 'ion-diagnostics'; diagnosticsEle.className = 'ion-diagnostics-fade-out'; document.body.insertBefore(diagnosticsEle, document.body.firstChild); } // Show the last error clearTimeout(this.diagnosticsTimerId); this.diagnosticsTimerId = setTimeout(function() { var diagnosticsEle = document.getElementById('ion-diagnostics'); if (diagnosticsEle) { diagnosticsEle.classList.remove('ion-diagnostics-fade-out'); } }, 24); diagnosticsEle.innerHTML = msg.data.diagnosticsHtml } } this.buildStatus(status); }, buildStatus: function (status) { var iconLinks = document.querySelectorAll('link[rel="icon"]'); for (var i = 0; i < iconLinks.length; i++) { iconLinks[i].parentElement.removeChild(iconLinks[i]); } var iconLink = document.createElement('link'); iconLink.rel = 'icon'; iconLink.type = 'image/png'; iconLink.href = this[status + 'Icon']; document.head.appendChild(iconLink); if (status === 'error') { var diagnosticsEle = document.getElementById('ion-diagnostics'); if (diagnosticsEle) { var systemInfoEle = diagnosticsEle.querySelector('#ion-diagnostics-system-info'); if (!systemInfoEle) { systemInfoEle = document.createElement('div'); systemInfoEle.id = 'ion-diagnostics-system-info'; systemInfoEle.innerHTML = IonicDevServerConfig.systemInfo.join('\n'); diagnosticsEle.querySelector('.ion-diagnostics-content').appendChild(systemInfoEle); } } } }, buildingNotification: function(showToast) { clearTimeout(this.toastTimerId); var toastEle = document.getElementById('ion-diagnostics-toast'); if (showToast) { if (!toastEle) { toastEle = document.createElement('div'); toastEle.id = 'ion-diagnostics-toast'; var c = [] c.push('
'); c.push('
Building...
'); c.push('
'); c.push(' '); c.push('
'); c.push('
'); toastEle.innerHTML = c.join(''); document.body.insertBefore(toastEle, document.body.firstChild); } this.toastTimerId = setTimeout(function() { var toastEle = document.getElementById('ion-diagnostics-toast'); if (toastEle) { toastEle.classList.add('ion-diagnostics-toast-active'); } }, 16); } else if (!showToast && toastEle) { toastEle.classList.remove('ion-diagnostics-toast-active'); } }, toggleOptionsMenu: function() { var optsEle = document.getElementById('ion-diagnostics-options'); this.optionsMenu(!optsEle); }, optionsMenu: function(showMenu) { clearTimeout(this.optionsMenuTimerId); var optsEle = document.getElementById('ion-diagnostics-options'); if (showMenu) { if (!optsEle) { var c = []; c.push('
'); c.push('
'); c.push( '
'); c.push( '
'); c.push( '
Ionic App Debugger
'); c.push( ''); c.push( '
'); c.push( '
'); c.push( ''); c.push( '
'); c.push( '
'); c.push('
'); optsEle = document.createElement('div'); optsEle.id = 'ion-diagnostics-options'; optsEle.innerHTML = c.join('\n'); document.body.insertBefore(optsEle, document.body.firstChild); } this.optionsMenuTimerId = setTimeout(function() { var optsEle = document.getElementById('ion-diagnostics-options'); optsEle.classList.add('ion-diagnostics-options-show'); }, 16); } else if (!showMenu && optsEle) { optsEle.classList.remove('ion-diagnostics-options-show'); this.optionsMenuTimerId = setTimeout(function() { optsEle.parentElement.removeChild(optsEle); }, 300); } }, bindEvents: function() { var self = this; document.addEventListener('keyup', function(ev) { var key = ev.keyCode || ev.charCode; if (key == 27) { // escape key self.optionsMenu(false); } }); document.addEventListener('keydown', function(ev) { var key = ev.keyCode || ev.charCode; if ((ev.metaKey || ev.ctrlKey) && ev.shiftKey && key == 56) { // mac: command + shift + 8 // win: ctrl + shift + 8 self.toggleOptionsMenu(); } }); document.addEventListener('click', function(ev) { if (!ev.target) return; switch (ev.target.id) { case 'ion-diagnostic-close': self.buildUpdate({ type: 'closeDiagnostics', data: { diagnosticsHtml: null } }); break; case 'ion-diagnostics-options-reload-app': self.reloadApp(); break; case 'ion-diagnostics-backdrop': self.optionsMenu(false); break; case 'ion-diagnostics-options-close': self.optionsMenu(false); break; } }); if (location.href.indexOf('devapp=true') < 0) { this.enableShake(); } }, enableShake: function() { /* * Author: Alex Gibson * https://github.com/alexgibson/shake.js * License: MIT license */ var self = this; var threshold = 15; var timeout = 1000; self.shakeTime = new Date(); self.shakeX = null; self.shakeY = null; self.shakeZ = null; window.addEventListener('devicemotion', function(ev) { var current = ev.accelerationIncludingGravity; var currentTime; var timeDifference; var deltaX = 0; var deltaY = 0; var deltaZ = 0; if (self.shakeX === null) { self.shakeX = current.x; self.shakeY = current.y; self.shakeZ = current.z; return; } deltaX = Math.abs(self.shakeX - current.x); deltaY = Math.abs(self.shakeY - current.y); deltaZ = Math.abs(self.shakeZ - current.z); if (((deltaX > threshold) && (deltaY > threshold)) || ((deltaX > threshold) && (deltaZ > threshold)) || ((deltaY > threshold) && (deltaZ > threshold))) { currentTime = new Date(); timeDifference = currentTime.getTime() - self.shakeTime.getTime(); if (timeDifference > timeout) { self.optionsMenu(true); self.shakeTime = new Date(); } } self.shakeX = current.x; self.shakeY = current.y; self.shakeZ = current.z; }); }, escapeHtml: function (unsafe) { return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, activeIcon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABlApw1AAACQFBMVEUAAAD/xET/zDX/xz7/xUL/xUL/vlD/vk//zDX/zDb/xkD/zDX/xUP/xUL/zDT/vlD/vlD/zDX/zDb/zDb/vlD/zDT/vk//v0//v07/yzf/vVH/vU//vlD/vlD/zDX/v0//vk//zDX/vVD/zDb/yzX/zDT/wE3/vlD/wE3/zDX/wkj/zDT/vVH/v07/vVH/yzb/v0z/wE3/vlD/////yzb/vVD/zDT/vk//vk3/wkn/wEv/yDr/yjr/xET/xkH/xz7/yDz/wUj/w0b/xzz/yjf/wkb/xkD/wE3/yzj/x0D/wUz/w0f/xkP//v3//vr/+/D/+u7/yj3/9d3/+Ob/+/L/9+T/8tn/xkb//Pb/+ev//vz//fj/6Lj/x17/xEr/7cj/7Lr/5bL/36H/+/T/+en/9uH/9d//7sv/wU//zUH/89z/89X/787/7sD/3Jr/2Xv/x1n/3ZL/3IT/0nv/wlL//fn/9Nn/8NP/4Kb/35z/2pX/2I7/1IT/ymD/y1r/xFn/yUL/8dD/6sD/6L7/46P/0nf/13H/0W3/02b/1GH/x1T/8cv/6bL/6q7/14H/zmf/ymf/xlH/zUX/+Oj/4qz/5Kj/5J3/4JT/4In/13b/123/w1X/z1P/zUz/xkz/8Mn/78X/7MP/67X/5q//5qX/5qD/45f/1on/3oH/znT/zG3/1mX/w07/zkn/ykn/ykb/6Kn/2nb/0V3/yk//xkn//vn/347/3Yn/2Yn/04D/0HL/z1j/5bb/01n/zDn/zD0Eb5b9AAAAM3RSTlMAC+EVHwjhSOvzT0cmApta3NnRuXd2buuSkvTx18fHvbinnGxY9vTo0VwTvainmHD57Z6HmSHnAAANwklEQVR42t2dZ1tTSRTHQwfdqmvb3nufQFzWXbe4iy2VJFKSQEIJoSUQkB6QDiJNei8K0gUEBHT9ahsQdSa5c5PMTNjc/T8PvIT55d4zc1rOiDwo5OhHZ44fi4y6cG5PP+//OtBvTv35p/PHqfMH+vvvS5f+durX57r6ywv9caC//rryl1M/OfX773s/zt97in2qy3u/vv/htc/fO30y4ksRuYKOnDl24cLFixcvOHXYALFxezp7VvL56YggktW/fPSDKLFY/J8D7OnTbyJe9vXD/y7YufpAAXAq/KQvjyHsRKRYHFgAZyWn3gjzdv0fOz/9wAOQSMJf9Wr5ocfF4sAEkEjeD/W8/i+ixIELIHnL00MIe0UsDmSA6OgfeS0h5F1xoANI3gnhef1fFwc+gOQ1rCG8FCwWAoDks5dw648RBoAknJMg5PUYoQBIXuOwg7B3Y4QDEP2O+170SoyQAKI/dDu/YoQFEO1yooVGCQ3gLXQzPR4jNIDo9xH/M0Z4APBLFBQsRIDwFyHOiRghAkS/8fwBRAoT4NSzR/BRjDABok8e5B+ChQoQ/jRXcTRGqADREfsAHwgX4Ot9E/5EuABv7pnxkRjGAFevrTdbN+vbanO1mkSFXBGffWNS11vQ3uQYM1xhCxB9ZM8NZQhwaWW9eat3PB1wSn7DWLDT2nWFIcC3ToDXmQGMrm1NXwcepNEVNLX+xArgNWcgFsMGYLS5IE8OvFKxuWzHxgYgOkR0lAXA+bXCVAXwQckp5R0GFgARohP0ADXN9VrgszKMDV30AG+IXqEFqGlsiwdEUtS1d9ECfCj6ihKgcSoRECtt8qGNDuA90etUAC1t6YBK8Z10AG+LIikAVvoyAJ0y7Yt0AJ+JosgBrDkyQCdtFa0NvCmKIQVYuiUHlMqoerELXe7cLs8vr5yP8w0gmhRA2XhdCvgkVzk9oOne+t7eaV1trioxiWMXanq+jVaZtOqE4uLiZHVmz/xhAChnkgFGUpkmpXeraf3Btee68mCsq+Nh/WSmDIGueHaQ2UrlL/ikaUa93wFGUjEf/4ZivK9xGedOjzWVTSQWgwPVGp4CGHa0rq+WPc6/AAvcLptMkVLg8BQPOAomE/dtP6HqwJXYdj9IEmbj/AigtHIeXeqc+hbvApoqU4kcgN4DX6jhNpe/V+k/AOWAjMtmJwprfIjI2lM0VU+90Sott5vR6S8AywzH+tMm2n0NKTsM+wCLPYBbKUX+AbD0ceyHNwsvkcbEOwpc0FDpFwAlx/o1/aPEQb3BBHAy+wVgZgO4KrWZIitRlY0FSOz0A8BAAnBR+tYKTVqlgcfVrmAPsKoGLhpvpssLVQC8epgDtLgZ3K1RusSWIZ8HYJI1wKjr+Zvcv/InJYCJB8DMGECZ6np2FSp/owUo4wFIYQwwI3Ux30ElfW60gAfgEVuAoWSX9Q+xSO5u86SO8pkCLKlc188kO92hwCdd7CwBzt2Sou9/I5v0epcOC5A9zBKgMRn11++xqg9UJuEATCxdiaUcNGrsZ1bgsJlxAb+eJUCfDD2/lOwqNA3cViCtYBkPPE5HveduliUm7qPAyDIi+6cNseD4FqY1slYjcFfdMEuAITSDNeB9ke+XB+vrY9c8FPlaTVK3z7+aZVbiyRTq/1u8Ahhpr9ellGhVKm2OWdf70MFTpVysRKOC2xXDTPNC6APIWPCizLpWNq5RQ4afpL6d29uBL7Pqy1Uvlm/qfJaZq67KYgDw5BaA1a/0BPCrNUXOEfjL1DcejuHqxJerG/J1WpW2rsyuz3qeWuxJ0TMAWIhHdqAlD3XiGmvJBsBIll1hwNaJL2cVFRXtL/4A4Gx5kqySHuDcXfQI5i90/9qcwp/zLWkweFnoHt6LFXTD1ACPkTAmT8kLsNyfBjxIVt/qFYD+0b5BzNECXLyHpA+tvJX63VTghXI7PAMM27UHHpGEEqAbWdMEb6W+6bqXdbFtDwDVczrps1dOTwnQokAsgA9g0Otqq3yWDyCusi7hxQa8TQfwzyaAlGPhARj0oV6pruAByNIiyZUiKoDuPAAdR3d5eiWafap2y9t5XqF8GRzX6KkAHqvhZN8aHmAk28f6cAcewAZ/FlI7DcA/91ATxgLU1AIfldOKN2IdEpllUQBYYDdCtonvVukHPmv6ChagshhOD1VTACzBG6NiBAvQkuY7gMyOBSjSwEneTgqAFimcyMX2C50fBwRSdWEPsjoAqVJCCoCagLQPC2CVAhLlYwEq4H2olBzgyR34kQ/hAM7nEHaqYBueOtVwlrqIGMAyAf87Cw7AukEGIMvH+kLwWaapJgZYSkc2URzAOCBUtgEH8AhAmicGaAGQ7uAAduWkAAl2HEA+YsXEAKtIMgIH0EfeNaTDAdilsLETAwzAm9ACDuAmIFa2AQOgV8P+HDEAHE3KlzAAoxpyAEUTBqAoE85yEQPAjoTKggGwqskBistwALlwsYwYAI7GbigxAHdl5ABSHQYgCz6LtcQA8NudhwOYAhQy4wDghGkmMQDsyk3hACZoALSL3ABxpfARSgwAd4XexwAob1D1jbZiAExwIEUMkAifYxiAlet0jZcYAPgkUxADwBmJuxiAZRVd5zEGoBzewYkB5EIH8P8rpPHvK6TxwohrcqiM2IYBKIONmMk22obbRscDeBuFD7JUHEBqAB9keXBlAwdwJ4kCwM+uRBvszOEACtMAsWQmnDNnhvPxTNzpxG4MwEg8OYC8AQeQzcSd3oQzuy24gIbCl1AtYgCqFXBAwyakvIcBoDACaR0upJwDTELKBQDpLg6gRU0c1LfjAMrZBPVLCjggwAGcJ36HMhZxAEY2aRXLTfi/YRNbhYQxWVIpNrFVAnceVJOnFu/DW94CDqCG0J9LtOEA9HKkEZ88uTsAf14z2OTuFllytxSb3J1llNwVL0Ark+ZhAEgduvhWHACaWZylAFiCg8r0UWyBYyiZYA+dxRc44HcymabAYWmDjWAAX2Kq9x2gDl9ishfDjgRFiQk1Amkevsi37PNWqnHgi3xGKWwCRTRl1pYE+B0awZdZ13z0iBQN+DJrdSaAtE1X6IbDFdkMT6F70Lfv1FfwFLorkpH+USqAczMA0g2+sQyFap9qMzwASMuRcZgKQLwAL0u9igdwEsi9X7+Bp1diLhF1hKgA0GYJkMfbbmO9DbxSWgXfYAzUD8rupG14GkAO/0behqdmr/aizAbehqf5DOS0zqJuOUMcnVT+2Sqj96Wew2AHf8tZDxKyzdE3/d1H/uAqf9Pf1cHr/AgaZ+slL8AcUvCpraZvu0T7XvMsHtoul/s1Mqz/fLu09Qr/eJ7Lj1B3g0HfqGUKWcOAx/lC6/05aVy+T5rK5PA4IKkS2Yxz9Sxaj1cTAKTrj71oPR5s0yYi/l2xQpXaPuZ5wpMtF9luy5n0TltS3b/94Ln5u6nfmJKjytBoMjK1Zl1ZwwNvRlRlmdAuWT2b9vtV5I3YsHrdfn9t3dHR4Wh94PWMLfTrHMXlcWwAnqCPQLXrryFh1SVoh2w1I4CLCwqA7kT+AYg1uo42YAVwweV8uusfgHL0v9RmsQMYVaHOcKE/AOxyNGKYZ/lFuHsbLl/kYw8wj7qCstI4lgBKl1kk8c2sAfTZAFHJMNuhAK5J9Mw1tgC2EheXe471YAyrSw5atcYMgGP90jL2o0nuABeCFnYAVXvrR3cg9gDdN10za41KRgDz2a4xj94f43l2XTMn8ns1LAAM9njgagD+GZA0pHYNzmdW6AEWC1yd74QKf014KnQb7DG1Swugn3YvXcb5bUSVe4el1qqkAmjIBa4yZsX6DUB53z3HdmeXHMBmcs8l1Q77c8qZ5RZwU0lhDSFAu5ljHIbNv3PmujkIklMbSQCaHqk51q/396C87vvAXeltTb4CdPRwNczW6mP9DOAk6JNy1YumBn0B2DFy9vsabbH+Bzin3OIsKClubq57B9D1MCWRs+hkGo49DICflavp3En/jCnriicAg31alYDJ+e7vn4cyLHIN07EuTc7UbTnwALaHxuwETO5R5fQfDgnAqe5bMoBhSFLntBU0juzPitxf9tNpka1NBdO5Chm2ZFCrh8qshzGuszCdt4YqV5lTp+vL+gsKCsrqe3TmbAV/ySA/9idCgE9IB6Y+zksGjFScu+NcPhkAzczdTQ1gotumRYqRtcEUQ4N32xIBtdR181RDg49RjW0ezEsAVJLlztKNbaYdnF1TOLEByJVb3kU5OJt+dPloIelTSDKX26hHl7MYHr88OJVO0HJZO+tgMDyezfj+leYyH7s9tKU7rUzG9zO7QGGksT7H65716e0qA6MLFBheYXF1ZGhmUu6x1dJcb3eMMbvCgvElIjXLDmtfbQbOv4ifLG3v6DIwvUSE/TUuNdeWmwc37+icJT5NukIhV6RrMnPMqXv3uHSNPWB9jcv/4SIdwV9lJPzLpAR/nZfgL1QT/pV2gr9UUPjXOgr+Yk3hX20q/MtlBX+9r/AvWBb+FdeCv2Rc+Ne8C+eifXj9qEKPCQHg7VARVl++G/gA73wp4lHYmUAHOB0m4teRyEAGeOuIyKNCjwcuwPuhIm/0cXBgAoS/KvJSYSciAw/g1BthIu8V9F1wYAGEnwwS+aaXj34QFSgAn34T8bKIQEFHzhz7zwEkn5+OCBKRK+ToR2eOH4uMOnyAT0+9/d7pkxEhIn79CxIosts8XZ0fAAAAAElFTkSuQmCC', errorIcon: 'data:image/png;base64,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', successIcon: 'data:image/png;base64,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' }; IonicDevServer.start(); ================================================ FILE: bin/ionic-app-scripts.js ================================================ #!/usr/bin/env node if (process.argv.length > 2) { if (process.env.npm_config_argv && process.env.npm_config_argv.length > 0 && process.env.npm_config_argv !== 'undefined') { try { var npmRunArgs = JSON.parse(process.env.npm_config_argv); if (npmRunArgs && npmRunArgs.original && npmRunArgs.original.length > 2) { // add flags from original "npm run" command for (var i = 2; i < npmRunArgs.original.length; i++) { process.argv.push(npmRunArgs.original[i]); } } } catch (e) { console.log(e) } } require('../dist/index').run(process.argv[2]); } else { console.error('Missing ionic app script task name'); } ================================================ FILE: circle.yml ================================================ machine: node: version: 6.9.5 post: - npm install -g npm@3.x.x test: override: - nvm use 6 && npm test ================================================ FILE: config/cleancss.config.js ================================================ // https://www.npmjs.com/package/clean-css module.exports = { /** * sourceFileName: the file name of the src css file */ sourceFileName: process.env.IONIC_OUTPUT_CSS_FILE_NAME, /** * destFileName: the file name for the generated minified file */ destFileName: process.env.IONIC_OUTPUT_CSS_FILE_NAME }; ================================================ FILE: config/copy.config.js ================================================ // this is a custom dictionary to make it easy to extend/override // provide a name for an entry, it can be anything such as 'copyAssets' or 'copyFonts' // then provide an object with a `src` array of globs and a `dest` string module.exports = { copyAssets: { src: ['{{SRC}}/assets/**/*'], dest: '{{WWW}}/assets' }, copyIndexContent: { src: ['{{SRC}}/index.html', '{{SRC}}/manifest.json', '{{SRC}}/service-worker.js'], dest: '{{WWW}}' }, copyFonts: { src: ['{{ROOT}}/node_modules/ionicons/dist/fonts/**/*', '{{ROOT}}/node_modules/ionic-angular/fonts/**/*'], dest: '{{WWW}}/assets/fonts' }, copyPolyfills: { src: [`{{ROOT}}/node_modules/ionic-angular/polyfills/${process.env.IONIC_POLYFILL_FILE_NAME}`], dest: '{{BUILD}}' }, copySwToolbox: { src: ['{{ROOT}}/node_modules/sw-toolbox/sw-toolbox.js'], dest: '{{BUILD}}' } } ================================================ FILE: config/sass.config.js ================================================ // https://www.npmjs.com/package/node-sass module.exports = { /** * outputFilename: The filename of the saved CSS file * from the sass build. The directory which it is saved in * is set within the "buildDir" config options. */ outputFilename: process.env.IONIC_OUTPUT_CSS_FILE_NAME, /** * sourceMap: If source map should be built or not. */ sourceMap: false, /** * outputStyle: How node-sass should output the css file. */ outputStyle: 'expanded', /** * autoprefixer: The config options for autoprefixer. * Excluding this config will skip applying autoprefixer. * https://www.npmjs.com/package/autoprefixer */ autoprefixer: { browsers: [ 'last 2 versions', 'iOS >= 8', 'Android >= 4.4', 'Explorer >= 11', 'ExplorerMobile >= 11' ], cascade: false }, /** * includePaths: Used by node-sass for additional * paths to search for sass imports by just name. */ includePaths: [ 'node_modules/ionic-angular/themes', 'node_modules/ionicons/dist/scss', 'node_modules/ionic-angular/fonts' ], /** * includeFiles: An array of regex patterns to search for * sass files in the same directory as the component module. * If a file matches both include and exclude patterns, then * the file will be excluded. */ includeFiles: [ /\.(s(c|a)ss)$/i ], /** * excludeFiles: An array of regex patterns for files which * should be excluded. If a file matches both include and exclude * patterns, then the file will be excluded. */ excludeFiles: [ /* /\.(wp).(scss)$/i */ ], /** * variableSassFiles: Lists out the files which include * only sass variables. These variables are the first sass files * to be imported so their values override default variables. */ variableSassFiles: [ '{{SRC}}/theme/variables.scss' ], /** * directoryMaps: Compiled JS modules may be within a different * directory than its source file and sibling component sass files. * For example, NGC places it's files within the .tmp directory * but doesn't copy over its sass files. This is useful so sass * also checks the JavaScript's source directory for sass files. */ directoryMaps: { '{{TMP}}': '{{SRC}}' }, /** * excludeModules: Used just as a way to skip over * modules which we know wouldn't have any sass to be * bundled. "excludeModules" isn't necessary, but is a * good way to speed up build times by skipping modules. */ excludeModules: [ '@angular', 'commonjs-proxy', 'core-js', 'ionic-native', 'rxjs', 'zone.js' ] }; ================================================ FILE: config/uglifyjs.config.js ================================================ // https://www.npmjs.com/package/uglify-es module.exports = { /** * mangle: uglify 2's mangle option */ mangle: true, /** * compress: uglify 2's compress option */ compress: { toplevel: true, pure_getters: true } }; ================================================ FILE: config/watch.config.js ================================================ var watch = require('../dist/watch'); var copy = require('../dist/copy'); var copyConfig = require('./copy.config'); // this is a custom dictionary to make it easy to extend/override // provide a name for an entry, it can be anything such as 'srcFiles' or 'copyConfig' // then provide an object with the paths, options, and callback fields populated per the Chokidar docs // https://www.npmjs.com/package/chokidar module.exports = { srcFiles: { paths: ['{{SRC}}/**/*.(ts|html|s(c|a)ss)'], options: { ignored: ['{{SRC}}/**/*.spec.ts', '{{SRC}}/**/*.e2e.ts', '**/*.DS_Store', '{{SRC}}/index.html'] }, callback: watch.buildUpdate }, copyConfig: copy.copyConfigToWatchConfig() }; ================================================ FILE: config/webpack.config.js ================================================ /* * The webpack config exports an object that has a valid webpack configuration * For each environment name. By default, there are two Ionic environments: * "dev" and "prod". As such, the webpack.config.js exports a dictionary object * with "keys" for "dev" and "prod", where the value is a valid webpack configuration * For details on configuring webpack, see their documentation here * https://webpack.js.org/configuration/ */ var path = require('path'); var webpack = require('webpack'); var ionicWebpackFactory = require(process.env.IONIC_WEBPACK_FACTORY); const Dotenv = require('dotenv-webpack'); var ModuleConcatPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); var PurifyPlugin = require('@angular-devkit/build-optimizer').PurifyPlugin; var optimizedProdLoaders = [ { test: /\.json$/, loader: 'json-loader' }, { test: /\.js$/, loader: [ { loader: process.env.IONIC_CACHE_LOADER }, { loader: '@angular-devkit/build-optimizer/webpack-loader', options: { sourceMap: true } }, ] }, { test: /\.ts$/, loader: [ { loader: process.env.IONIC_CACHE_LOADER }, { loader: '@angular-devkit/build-optimizer/webpack-loader', options: { sourceMap: true } }, { loader: process.env.IONIC_WEBPACK_LOADER } ] } ]; function getProdLoaders() { if (process.env.IONIC_OPTIMIZE_JS === 'true') { return optimizedProdLoaders; } return devConfig.module.loaders; } var devConfig = { entry: process.env.IONIC_APP_ENTRY_POINT, output: { path: '{{BUILD}}', publicPath: 'build/', filename: '[name].js', devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(), }, devtool: process.env.IONIC_SOURCE_MAP_TYPE, resolve: { extensions: ['.ts', '.js', '.json'], modules: [path.resolve('node_modules')] }, module: { loaders: [ { test: /\.json$/, loader: 'json-loader' }, { test: /\.ts$/, loader: process.env.IONIC_WEBPACK_LOADER } ] }, plugins: [ new Dotenv({ path: '.env.dev', // load this now instead of the ones in '.env' systemvars: true, // load all the predefined 'process.env' variables which will trump anything local per dotenv specs. silent: true // hide any errors }), ionicWebpackFactory.getIonicEnvironmentPlugin(), ionicWebpackFactory.getCommonChunksPlugin() ], // Some libraries import Node modules but don't use them in the browser. // Tell Webpack to provide empty mocks for them so importing them works. node: { fs: 'empty', net: 'empty', tls: 'empty' } }; var prodConfig = { entry: process.env.IONIC_APP_ENTRY_POINT, output: { path: '{{BUILD}}', publicPath: 'build/', filename: '[name].js', devtoolModuleFilenameTemplate: ionicWebpackFactory.getSourceMapperFunction(), }, devtool: process.env.IONIC_SOURCE_MAP_TYPE, resolve: { extensions: ['.ts', '.js', '.json'], modules: [path.resolve('node_modules')] }, module: { loaders: getProdLoaders() }, plugins: [ new Dotenv({ path: '.env.prod', // load this now instead of the ones in '.env' systemvars: true, // load all the predefined 'process.env' variables which will trump anything local per dotenv specs. silent: true // hide any errors }), ionicWebpackFactory.getIonicEnvironmentPlugin(), ionicWebpackFactory.getCommonChunksPlugin(), new ModuleConcatPlugin(), new PurifyPlugin() ], // Some libraries import Node modules but don't use them in the browser. // Tell Webpack to provide empty mocks for them so importing them works. node: { fs: 'empty', net: 'empty', tls: 'empty' } }; module.exports = { dev: devConfig, prod: prodConfig } ================================================ FILE: lab/index.html ================================================ Ionic Lab

Psssst...

You can test your app live on iOS and Android with the Ionic View app!

================================================ FILE: lab/static/css/style.css ================================================ @font-face {font-family: 'AvenirNextLTPro-Regular';src: url('http://code.ionicframework.com/assets/fonts/28882F_0_0.eot');src: url('http://code.ionicframework.com/assets/fonts/28882F_0_0.eot?#iefix') format('embedded-opentype'),url('http://code.ionicframework.com/assets/fonts/28882F_0_0.woff') format('woff'),url('http://code.ionicframework.com/assets/fonts/28882F_0_0.ttf') format('truetype');} @font-face {font-family: 'AvenirNextLTPro-Medium';src: url('http://code.ionicframework.com/assets/fonts/28882F_1_0.eot');src: url('http://code.ionicframework.com/assets/fonts/28882F_1_0.eot?#iefix') format('embedded-opentype'),url('http://code.ionicframework.com/assets/fonts/28882F_1_0.woff') format('woff'),url('http://code.ionicframework.com/assets/fonts/28882F_1_0.ttf') format('truetype');} @font-face {font-family: 'AvenirNextLTPro-UltLt';src: url('http://code.ionicframework.com/assets/fonts/29CC36_0_0.eot');src: url('http://code.ionicframework.com/assets/fonts/29CC36_0_0.eot?#iefix') format('embedded-opentype'),url('http://code.ionicframework.com/assets/fonts/29CC36_0_0.woff') format('woff'),url('http://code.ionicframework.com/assets/fonts/29CC36_0_0.ttf') format('truetype');} html, body { height: 100%; } body { background-color: #242A31; font-family: 'AvenirNextLTPro-Regular', 'Helvetica Neue', 'Helvetica', Arial, sans-serif; padding: 0; margin: 0; -webkit-font-smoothing: antialiased; } h2 { color: #fff; font-family: 'AvenirNextLTPro-Regular', 'Helvetica Neue', 'Helvetica', Arial, sans-serif; } h2 a { font-size: 14px; color: #727a87; text-decoration: none; } .dropdown { height: 100%; position: relative; } .dropdown-toggle { color: #858D9B; display: block; height: 100%; background: none; border: none; font-size: 13px; cursor: pointer; padding-left: 15px; font-weight: bold; outline: none; font-family: 'AvenirNextLTPro-Medium', 'Helvetica Neue', 'Helvetica', Arial, sans-serif; } .dropdown-menu { display: none; position: absolute; z-index: 999; width: 150px; right: -10px; background-color: white; box-shadow: 0 6px 10px rgba(0,0,0,.08), 0 0px 6px rgba(0,0,0,.05); } .dropdown-menu > li { padding: 6px 5px; } .dropdown:hover .dropdown-toggle { /* increase the hit box when hovering */ /*padding-left: 100px;*/ } .dropdown:hover .dropdown-menu { display: block; } .dropdown li { list-style: none; } .dropdown ul { margin: 0; padding: 0; } .dropdown-caret { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #858D9B; display: inline-block; } .dropdown-menu input { cursor: pointer; background-color: #4D82E9; } .dropdown-menu label { font-size: 13px; margin-left: 5px; color: #858D9B; cursor: pointer; user-select: none; -webkit-user-select: none; } #app { display: flex; flex-direction: column; height: 100%; } #header { width: 100%; height: 50px; background-color: #151A21; box-shadow: 0px 1px 3px rgba(0,0,0, 0.15); } #header a { color: #858D9B; font-family: 'AvenirNextLTPro-Medium', 'Helvetica Neue', 'Helvetica', Arial, sans-serif; font-size: 13px; text-decoration: none; display: inline-block; font-weight: bold; } #header .icon { display: inline-block; font-size: 22px; vertical-align: middle; margin-bottom: 3px; margin-left: 3px; } #header .dropdown { display: inline-block; } #header-left { float: left; line-height: 50px; } #header-left #menu-toggle { margin-left: 13px; } #header a:hover { opacity: 1; } #header-right { float: right; margin-right: 15px; height: 100%; } #footer { width: 100%; border-top: 1px solid rgba(0,0,0,0.06); background-color: #151A21; } #footer-left { float: left; padding: 14px 0 14px 15px; font-size: 13px; } #app-info { color: #828080; } #footer-right { float: right; padding: 14px 15px 14px 5px; font-size: 13px; } #footer-right a { margin-left: 10px; color: #a2a9b4; text-decoration: none; font-family: 'AvenirNextLTPro-Medium', 'Helvetica Neue', 'Helvetica', Arial, sans-serif; } #logo { display: inline-block; vertical-align: middle; width: 64px; height: 28px; margin-left: 15px; background-size: 100%; background-repeat: no-repeat; background-image: url(data:image/png;base64,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); } #logo img { max-width: 100%; } preview { flex: 1; width: 100%; display: flex; vertical-align: middle; overflow: auto; text-align: center; justify-content: center; align-items: center; flex-wrap: wrap; flex-direction: row; } .phone { display: inline-block; margin: 0px 20px 0 20px; vertical-align: middle; } .phone h2 { text-align: left; vertical-align: middle; margin-top: 0; } .phone-icon { display: inline-block; vertical-align: middle; background-repeat: no-repeat; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAABgCAYAAAB8InCYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABFlJREFUeNrsmmdoFEEUxzeagIXYomKNhUv8YMPoBzGRKBpEgxoFBSGRiKDYsWFMAuqHqCAW0OgHlSgaRcSICoqFKMEPSuyIiAVLbIgFUcESPf+PvNNh2Nub3Z05EtwHP27uZnbnvzuzb96bvYRwOGxFbHnJRsuHJYAZYD74CsZFa7i5rOhvuZmlxzqB86ASZLo5b6KGzltx54OF326rHqzjDqyROic7Ei8BHcBC6bczoDZeAnJ4CCL2Fsx1cwK/AgYI5ZsgC9S5OYHKJOwGJoCe4Bu4Bi6Bn+Axz/zj4ARIApPBQL4zz8Fp/nQtoD3YBApBc6nuI6gCp/BM58N/TEF5H8gDraW25GgOgmXgnaqAEDgL+jqIm00oOC9yUAUgG4wBj2LNgRRwzqFzr5bK520RS8B20MfSb3T7Z/I8ijoE/dmf67YfPJFrYz2G0y0zti2ac5IFjDYkYIeqI0o30PlDJ+ckC+hsQMArN644wYCAJDcCvhkQ0M9pzZEr6gwIIMc2QlXAfUNPQbGqgKuGBIwHc1QEnLPM2S6wVJ7osoDr4IUhAdTXFnDZScBvsMcya7dirYa7DT2OZL/A1lgCyHOVGxKwUyUgISsDHwzEA2tVo2KK+RZrFjDf7qKcwvJKRofRxD7qJSyfzStkjk0dheU3wBsOUodJSUrEToIFXhOT7yAXlILX/NsDsJLzheEIy/M44u0C5oE73O4loDx8Kodk9iEz7Q8ohNaFvE5cUbzllCH15nzActonUE3PKekgle3oqkEG6C4kLPWc/Vzj2C+b75q2/YH+vKSucmgzFEzh8nlObh7pSE5z+apGusyaaYKO8iuAruIQaMmPEY1rjUP7Gk5qjoFk/uzpRwCtXm2EYOUZZ8TRjOqegnvCBsYmrwLo6icK30v5EZzlcMwsblMqJTupXgTk2UTMaQrjnyYFHVSe5ChA3LcTbLDGdWCI42MIR9SLA0dar8sg6CXn9AU6VyP0Q76jhP3HeppTET9Qxc6FbBAaZvHOhk6joaDtu0z+TmvH0MgcyBAaZhoMx8RzZ+jYJdMSqQYCAgGBgEYhoEL4rdxgf+K5K8SQjMLvw7wWXDQoYBG7fVoLLogCwhzHmTbqp7rJTMJqjf1UexGwxGp4C+LXTvDWjHpewBHSXTEsQ4wQltokSMFG1HrKvOicdhlY3OZAtPSvSXnCL0K53kO9bwH7hfIBD/W+klNbL+ay3j5SXVa8wfQw09PQgzMtIl0ohxLj0YklvarzOgRiJ5R654OxKp24mQOqV0Kp1jTeCfFliZw+h/xeiR8BK4KQLBAQCAgE/NcCyBOu83AcvaTYZzX8n8i3gKcejvvBIrTcgQoPx9FqWKRjNQwmYSAgEBAIaBRrwWqr4T1PiD+7xluAvHOQLCUm6UK5owkBsn22Gv6ie9Omri0LecIpeJ0gsp0uAU72yfr3x8S9TMRSeAjT+VMkWZcAJ3vP2L3i7yLMM3FYQ38EGAB86eMs4sfk/QAAAABJRU5ErkJggg=='); width: 16px; height: 16px; background-size: 100%; } #iphone .phone-icon { background-position: 0px 0px; } #android .phone-icon { background-position: 0px -16px; } #windows .phone-icon { background-position: 0px -32px; } .frame { border: none; } .phone-frame-wrap { position: relative; width: 375px; height: 667px; border-radius: 3px; box-shadow: 0 8px 24px rgba(0,0,0,.08), 0 0px 6px rgba(0,0,0,.1); overflow: hidden; } .phone-frame-wrap iframe { width: 100%; height: 100%; } .statusbar { position: absolute; top: 0; width: 375px; height: 10px; padding: 5px 0; background-size: 100%; background-repeat: no-repeat; background-color: transparent; background-position: center; border-radius: 3px 3px 0 0; } #iphone-frame .statusbar { background-image: url(../img/ios-statusbar.png); } #android-frame .statusbar { background-image: url(../img/android-statusbar.png); } #windows-frame .statusbar { background-image: url(../img/wp-statusbar.png) } /* Android and windows don't make space for the statusbar like iOS does */ #android-frame iframe, #windows-frame iframe { height: 647px; margin-top: 20px; } #view-popup { display: none; opacity: 0; transition: 400ms linear opacity; position: fixed; bottom: 15px; left: 15px; } #view-popup .view-popup-wrapper { position: relative; } #view-popup .ionitron { background: url('../img/popup-ionitron.png') no-repeat transparent; width: 90px; height: 90px; background-size: 100%; } #view-popup .content { box-sizing: border-box; background: url('../img/popup-view-bubble.png') no-repeat transparent; width: 350px; height: 350px; background-size: 100%; margin-bottom: -50px; padding-top: 25px; text-align: center; color: white; } #view-popup .content a { color: #fff; font-weight: bold; } #view-popup .content h2 { margin-bottom: 5px; } #view-popup .content p { font-size: 13px; max-width: 220px; margin: auto; } #view-popup .close { cursor: pointer; background: url('../img/popup-close.png') no-repeat transparent; position: absolute; right: 35px; top: 20px; width: 60px; height: 60px; background-size: 100%; position: absolute; } #main { display: flex; flex-direction: row; flex: 1; } #sidebar { position: relative; z-index: 1; background-color: #141A21; height: 100%; width: 300px; box-sizing: border-box; padding: 15px; overflow: auto; } #sidebar.hidden { display: none; } #sidebar .close { cursor: pointer; position: absolute; top: 15px; right: 15px; color: #a2a9b4; font-size: 24px; } #sidebar .title { font-size: 20px; margin-top: 5px; color: white; } .menu { padding: 0; margin: 0; -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Safari */ -khtml-user-select: none; /* Konqueror HTML */ -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* Internet Explorer/Edge */ user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ } .menu li { list-style: none; margin: 15px 0; } .menu li a { color: #a2a9b4; text-decoration: none; cursor: pointer; } .menu > li > ul { display: none; padding: 0; margin: 0; } .menu > li > ul li { padding-left: 15px; } .menu > li.expanded > ul { display: block; } .menu hr { height: 1px; border: 0; background-color: #2b3642; } .menu .version { color: #a2a9b4; opacity: 0.5; font-size: 12px; } #menu { } .ad { background-color: #232A31; cursor: pointer; border-radius: 2px; border: 1px solid #3f4650; font-size: 13px; color: white; padding: 10px; display: flex; margin-top: 55px; } .ad .logo { display: block; margin-right: 15px; } .ad .content { flex: 1; } .ad a { color: #308EFD; } @media screen and (max-height: 800px) { #header { height: 40px; } #header-left { line-height: 40px; } #logo { width: 50px; height: 22px; } #footer-left { float: left; padding: 7px 0 7px 15px; font-size: 12px; } #app-info { color: #828080; } #footer-right { float: right; padding: 7px 15px 7px 5px; font-size: 13px; } #footer-right .view-link { color: #4D82E9; } .phone-frame h2 { display: none; } @media screen and (max-width: 500px) { #footer { display: none; } } } @media screen and (max-height: 780px) { .statusbar { width: 340px; } .phone-frame-wrap { width: 340px; height: 605px; } #android-frame iframe, #windows-frame iframe { height: 585px; margin-top: 20px; } } @media screen and (max-height: 680px) { .statusbar { width: 325px; } .phone-frame-wrap { width: 325px; height: 578px; } #android-frame iframe, #windows-frame iframe { height: 558px; margin-top: 20px; } } ================================================ FILE: lab/static/js/lab.js ================================================ var $ = document.querySelector.bind(document); var API_ROOT = '/ionic-lab/api/v1'; var APP_CONFIG = {}; function loadAppConfig() { var req = new XMLHttpRequest(); req.addEventListener('load', function(e) { setAppConfig(JSON.parse(req.response)); }); req.open('GET', API_ROOT + '/app-config', true); req.send(null); } function setAppConfig(data) { APP_CONFIG = data; } function buildMenu() { buildComponentsMenu(); var sidebar = $('#sidebar'); var topLevels = sidebar.querySelectorAll('#menu > li > a'); var lastMenuConfig = window.localStorage.getItem('ionic_labmenu'); if (lastMenuConfig === 'true' || lastMenuConfig === null) { sidebar.classList.remove('hidden'); } Array.prototype.map.call(topLevels, function(a) { if (!a.href) { a.addEventListener('click', function(e) { if (a.parentNode.classList.contains('expanded')) { a.parentNode.classList.remove('expanded'); } else { a.parentNode.classList.add('expanded'); } e.preventDefault(); }); } }); $('#view-ad').addEventListener('click', function(e) { var win = window.open('http://view.ionic.io/', '_blank'); win.focus(); }); var toggleMenu = function(e) { if (sidebar.classList.contains('hidden')) { sidebar.classList.remove('hidden'); window.localStorage.setItem('ionic_labmenu', 'true'); } else { sidebar.classList.add('hidden'); window.localStorage.setItem('ionic_labmenu', 'false'); } }; $('#menu-toggle').addEventListener('click', toggleMenu); $('#sidebar .close').addEventListener('click', toggleMenu); } function buildComponentsMenu() { var items = [{"href":"http://ionicframework.com/docs/components/#overview","title":"Overview"},{"href":"http://ionicframework.com/docs/components/#action-sheets","title":"Action Sheets"},{"href":"http://ionicframework.com/docs/components/#alert","title":"Alerts"},{"href":"http://ionicframework.com/docs/components/#badges","title":"Badges"},{"href":"http://ionicframework.com/docs/components/#buttons","title":"Buttons"},{"href":"http://ionicframework.com/docs/components/#cards","title":"Cards"},{"href":"http://ionicframework.com/docs/components/#checkbox","title":"Checkbox"},{"href":"http://ionicframework.com/docs/components/#datetime","title":"DateTime"},{"href":"http://ionicframework.com/docs/components/#fabs","title":"FABs"},{"href":"http://ionicframework.com/docs/components/#gestures","title":"Gestures"},{"href":"http://ionicframework.com/docs/components/#grid","title":"Grid"},{"href":"http://ionicframework.com/docs/components/#icons","title":"Icons"},{"href":"http://ionicframework.com/docs/components/#inputs","title":"Inputs"},{"href":"http://ionicframework.com/docs/components/#lists","title":"Lists"},{"href":"http://ionicframework.com/docs/components/#loading","title":"Loading"},{"href":"http://ionicframework.com/docs/components/#menus","title":"Menus"},{"href":"http://ionicframework.com/docs/components/#modals","title":"Modals"},{"href":"http://ionicframework.com/docs/components/#navigation","title":"Navigation"},{"href":"http://ionicframework.com/docs/components/#popovers","title":"Popover"},{"href":"http://ionicframework.com/docs/components/#radio","title":"Radio"},{"href":"http://ionicframework.com/docs/components/#range","title":"Range"},{"href":"http://ionicframework.com/docs/components/#searchbar","title":"Searchbar"},{"href":"http://ionicframework.com/docs/components/#segment","title":"Segment"},{"href":"http://ionicframework.com/docs/components/#select","title":"Select"},{"href":"http://ionicframework.com/docs/components/#slides","title":"Slides"},{"href":"http://ionicframework.com/docs/components/#tabs","title":"Tabs"},{"href":"http://ionicframework.com/docs/components/#toast","title":"Toast"},{"href":"http://ionicframework.com/docs/components/#toggle","title":"Toggle"},{"href":"http://ionicframework.com/docs/components/#toolbar","title":"Toolbar"}]; var componentsMenu = $('#components-menu'); items.map(function (i) { var l = document.createElement('li'); var a = document.createElement('a'); a.href = i.href; a.target = "_blank"; a.innerText = i.title; l.appendChild(a); componentsMenu.appendChild(l); }); } function tryShowViewPopup() { var view = window.localStorage.getItem('ionic_viewpop'); if (!view) { $('#view-popup').style.display = 'block'; $('#view-popup .close').addEventListener('click', function(e) { window.localStorage.setItem('ionic_viewpop', true); $('#view-popup').style.opacity = 0; setTimeout(function() { $('#view-popup').style.display = 'none'; }, 200); }); window.requestAnimationFrame(function() { $('#view-popup').style.opacity = 1; }); } } // Bind the dropdown platform toggles function bindToggles() { // Watch for changes on the checkboxes in the device dropdown var iphone = $('#device-iphone'); var android = $('#device-android'); var windows = $('#device-windows'); var devices = [iphone, android, windows]; for(var i in devices) { devices[i].addEventListener('change', function(e) { var device = this.name; console.log('Device changed', device, this.checked); showDevice(device, this.checked); saveLastDevices(device, this.checked); }); } } // Show one of the devices function showDevice(device, isShowing) { $('#device-' + device).checked = isShowing; var rendered = $('#' + device); if(!rendered) { var template = $('#' + device + '-frame-template'); var clone = document.importNode(template, true); $('preview').appendChild(clone.content); //check for extra params in location.url to pass on to iframes var params = document.location.href.split('?'); if (params) { var newparams = params[params.length - 1]; var oldsrc = $('preview .frame').getAttribute('src'); $('preview .frame').setAttribute('src', oldsrc + '&' + newparams); } } else { rendered.style.display = isShowing ? '' : 'none'; } } function saveLastDevices(newDevice, didAdd) { var last = window.localStorage.getItem('ionic_lastdevices'); if(!last && didAdd) { window.localStorage.setItem('ionic_lastdevices', newDevice); return; } var devices = last.split(','); var di = devices.indexOf(newDevice); if(di == -1 && didAdd) { window.localStorage.setItem('ionic_lastdevices', devices.join(',') + ',' + newDevice); } else if(di >= 0) { devices.splice(di, 1); window.localStorage.setItem('ionic_lastdevices', devices.join(',')); } } function showLastDevices() { var last = window.localStorage.getItem('ionic_lastdevices'); if(!last) { showDevice('iphone', true); return; } var devices = last.split(','); for(var i = 0; i < devices.length; i++) { showDevice(devices[i], true); } } function setCordovaInfo(data) { var el = $('#app-info'); el.innerHTML = data.name + ' - v' + data.version; if(data.name) { document.title = data.name + ' - Ionic Lab'; } } function loadCordova() { var req = new XMLHttpRequest(); req.addEventListener('load', function(e) { setCordovaInfo(JSON.parse(req.response)); }); req.open('GET', API_ROOT + '/cordova', true); req.send(null); } //loadSearchIndex(); loadAppConfig(); buildMenu(); showLastDevices(); loadCordova(); bindToggles(); //tryShowViewPopup(); ================================================ FILE: package.json ================================================ { "name": "@ionic/app-scripts", "version": "3.2.4", "description": "Scripts for Ionic Projects", "homepage": "https://ionicframework.com/", "author": "Ionic Team (https://ionic.io)", "license": "MIT", "files": [ "bin/", "config/", "dist/", "lab", "LICENSE", "README.md" ], "bin": { "ionic-app-scripts": "./bin/ionic-app-scripts.js" }, "scripts": { "build": "npm run clean && tsc && npm run sass", "build-and-test": "jest", "changelog": "./node_modules/.bin/conventional-changelog -p angular -i CHANGELOG.md -s", "clean": "rimraf ./dist", "github-release": "node ./scripts/create-github-release.js", "lint": "tslint -c ./tslint.json --project ./tsconfig.json --type-check -t stylish", "nightly": "npm run build && node ./scripts/publish-nightly.js", "sass": "node-sass ./src/dev-client/sass/ion-dev.scss --output ./bin/ --output-style compressed", "sass-watch": "npm run sass && node-sass ./src/dev-client/sass/ion-dev.scss --watch --output ./bin/ --output-style compressed", "test": "jest", "watch": "npm run clean && tsc --watch & npm run sass-watch" }, "main": "dist/index.js", "dependencies": { "@angular-devkit/build-optimizer": "0.0.35", "autoprefixer": "^7.2.6", "chalk": "^2.4.0", "chokidar": "^2.0.4", "clean-css": "^4.1.11", "cross-spawn": "^5.1.0", "dotenv-webpack": "^1.5.7", "express": "^4.16.3", "fs-extra": "^4.0.2", "glob": "^7.1.2", "json-loader": "^0.5.7", "node-sass": "^4.10.0", "os-name": "^2.0.1", "postcss": "^6.0.21", "proxy-middleware": "^0.15.0", "reflect-metadata": "^0.1.10", "rollup": "0.50.0", "rollup-plugin-commonjs": "8.2.6", "rollup-plugin-node-resolve": "3.0.0", "source-map": "^0.6.1", "tiny-lr": "^1.1.1", "tslint": "^5.8.0", "tslint-eslint-rules": "^4.1.1", "uglify-es": "3.2.2", "webpack": "3.12.0", "ws": "3.3.2", "xml2js": "^0.4.19" }, "devDependencies": { "@angular/animations": "5.0.3", "@angular/common": "5.0.3", "@angular/compiler": "5.0.3", "@angular/compiler-cli": "5.0.3", "@angular/core": "5.0.3", "@angular/forms": "5.0.3", "@angular/http": "5.0.3", "@angular/platform-browser": "5.0.3", "@angular/platform-browser-dynamic": "5.0.3", "@angular/platform-server": "5.0.3", "@types/chokidar": "^1.7.5", "@types/clean-css": "^3.4.29", "@types/express": "^4.11.1", "@types/fs-extra": "^4.0.8", "@types/glob": "^5.0.35", "@types/jest": "^21.1.5", "@types/mock-fs": "^3.6.30", "@types/node": "^8.10.9", "@types/node-sass": "^3.10.32", "@types/rewire": "^2.5.27", "@types/webpack": "^3.8.11", "@types/ws": "^3.2.0", "conventional-changelog-cli": "^1.3.22", "github": "0.2.4", "ionic-cz-conventional-changelog": "^1.0.0", "jest": "^21.2.1", "mock-fs": "^4.4.2", "rewire": "^2.5.2", "rimraf": "^2.6.1", "rxjs": "^5.5.10", "sw-toolbox": "^3.6.0", "tslint-ionic-rules": "0.0.8", "typescript": "~2.4.2", "zone.js": "^0.8.26" }, "repository": { "type": "git", "url": "git+https://github.com/ionic-team/ionic-app-scripts.git" }, "bugs": { "url": "https://github.com/ionic-team/ionic-app-scripts/issues" }, "config": { "commitizen": { "path": "node_modules/ionic-cz-conventional-changelog" } }, "typings": "dist/index.d.ts", "jest": { "testEnvironment": "node", "moduleFileExtensions": [ "ts", "js" ], "transform": { "^.+\\.(ts)$": "/preprocessor.js" }, "testRegex": "/src/.*\\.spec\\.(ts|js)$", "coverageDirectory": "coverage" } } ================================================ FILE: preprocessor.js ================================================ const tsc = require('typescript'); const tsConfig = require('./tsconfig.json'); module.exports = { process(src, path) { if (path.endsWith('.ts')) { return tsc.transpile( src, tsConfig.compilerOptions, path, [] ); } return src; }, }; ================================================ FILE: scripts/commit-changelog.js ================================================ var execSync = require('child_process').execSync; function main() { try { execSync('git add ./CHANGELOG.md'); execSync('git commit -m "chore(changelog): update changelog for release"'); } catch (ex) { console.log('Failed to complete commiting changelog - ', ex.message); process.exit(1); } } ================================================ FILE: scripts/create-github-release.js ================================================ var path = require('path'); var execSync = require('child_process').execSync; var GithubApi = require('github'); var changelogCommand = './node_modules/.bin/conventional-changelog -p angular'; var packageJsonPath = path.join(__dirname, '..', 'package.json'); var packageJson = require(packageJsonPath); var github = new GithubApi({ version: '3.0.0'}); github.authenticate({ type: 'oauth', token: process.env.GH_TOKEN }); var changelogContent = execSync(changelogCommand).toString(); github.releases.createRelease({ owner: 'ionic-team', repo: 'ionic-app-scripts', target_commitish: 'master', tag_name: 'v' + packageJson.version, name: packageJson.version, body: changelogContent, prerelease: false }, function(err, result) { if (err) { console.log('[create-github-release] An error occurred: ' + err.message); process.exit(1); } else { console.log('[create-github-release]: Process succeeded'); } }); ================================================ FILE: scripts/publish-nightly.js ================================================ var execSync = require('child_process').execSync; var fs = require('fs'); var path = require('path'); var packageJsonPath = path.join(__dirname, '..', 'package.json'); var tempPackageJsonPath = path.join(__dirname, '..', 'package-orig.json'); var originalPackageJson = require(packageJsonPath); /* * This script assumes the `build` step was run prior to it */ function backupOriginalPackageJson() { var originalContent = JSON.stringify(originalPackageJson, null, 2); fs.writeFileSync(tempPackageJsonPath, originalContent); } function createNightlyVersionInPackageJson() { var originalVersion = originalPackageJson.version; originalPackageJson.version = originalVersion + '-' + createTimestamp(); fs.writeFileSync(packageJsonPath, JSON.stringify(originalPackageJson, null, 2)); } function revertPackageJson() { var fileContent = fs.readFileSync(tempPackageJsonPath); fileContent = fileContent + '\n'; fs.writeFileSync(packageJsonPath, fileContent); fs.unlinkSync(tempPackageJsonPath); } function createTimestamp() { // YYYYMMDDHHMM var d = new Date(); return d.getUTCFullYear() + // YYYY ('0' + (d.getUTCMonth() + 1)).slice(-2) + // MM ('0' + (d.getUTCDate())).slice(-2) + // DD ('0' + (d.getUTCHours())).slice(-2) + // HH ('0' + (d.getUTCMinutes())).slice(-2); // MM } function publishToNpm(tagName) { var command = `npm publish --tag=${tagName} ${process.cwd()}`; execSync(command); } function mainFunction() { try { let tagName = 'nightly'; if (process.argv.length >= 3) { tagName = process.argv[2]; } console.log(`Building ${tagName} ... BEGIN`); console.log('Backing up the original package.json'); backupOriginalPackageJson(); console.log('Creating the nightly version of package.json'); createNightlyVersionInPackageJson(); console.log('Publishing to npm'); publishToNpm(tagName); console.log('Restoring original package.json'); revertPackageJson(); console.log(`Building ${tagName}... DONE`); } catch (ex) { console.log(`Something went wrong with publishing the nightly. This process modifies the package.json, so restore it before committing code! - ${ex.message}`); process.exit(1); } } mainFunction(); ================================================ FILE: src/aot/aot-compiler.ts ================================================ import { readFileSync } from 'fs-extra'; import { extname, normalize, resolve } from 'path'; import 'reflect-metadata'; import { CompilerHost, CompilerOptions, DiagnosticCategory, ParsedCommandLine, Program, transpileModule, TranspileOptions, TranspileOutput, createProgram } from 'typescript'; import { HybridFileSystem } from '../util/hybrid-file-system'; import { getInstance as getHybridFileSystem } from '../util/hybrid-file-system-factory'; import { getFileSystemCompilerHostInstance } from './compiler-host-factory'; import { FileSystemCompilerHost } from './compiler-host'; import { getFallbackMainContent, replaceBootstrapImpl } from './utils'; import { Logger } from '../logger/logger'; import { printDiagnostics, clearDiagnostics, DiagnosticsType } from '../logger/logger-diagnostics'; import { runTypeScriptDiagnostics } from '../logger/logger-typescript'; import { getTsConfig, TsConfig } from '../transpile'; import { BuildError } from '../util/errors'; import { changeExtension, readFileAsync } from '../util/helpers'; import { BuildContext, CodegenOptions, File, SemverVersion } from '../util/interfaces'; export async function runAot(context: BuildContext, options: AotOptions) { const tsConfig = getTsConfig(context); const angularCompilerOptions = Object.assign({}, { basePath: options.rootDir, genDir: options.rootDir, entryPoint: options.entryPoint }); const aggregateCompilerOption = Object.assign(tsConfig.options, angularCompilerOptions); const fileSystem = getHybridFileSystem(false); const compilerHost = getFileSystemCompilerHostInstance(tsConfig.options); // todo, consider refactoring at some point const tsProgram = createProgram(tsConfig.fileNames, tsConfig.options, compilerHost); clearDiagnostics(context, DiagnosticsType.TypeScript); if (isNg5(context.angularVersion)) { await runNg5Aot(context, tsConfig, aggregateCompilerOption, compilerHost); } else { await runNg4Aot({ angularCompilerOptions: aggregateCompilerOption, cliOptions: { i18nFile: undefined, i18nFormat: undefined, locale: undefined, basePath: options.rootDir, missingTranslation: null }, program: tsProgram, compilerHost: compilerHost, compilerOptions: tsConfig.options }); } errorCheckProgram(context, tsConfig, compilerHost, tsProgram); // update bootstrap in main.ts const mailFilePath = isNg5(context.angularVersion) ? changeExtension(options.entryPoint, '.js') : options.entryPoint; const mainFile = context.fileCache.get(mailFilePath); const modifiedBootstrapContent = replaceBootstrap(mainFile, options.appNgModulePath, options.appNgModuleClass, options); mainFile.content = modifiedBootstrapContent; if (isTranspileRequired(context.angularVersion)) { transpileFiles(context, tsConfig, fileSystem); } } function errorCheckProgram(context: BuildContext, tsConfig: TsConfig, compilerHost: FileSystemCompilerHost, cachedProgram: Program) { // Create a new Program, based on the old one. This will trigger a resolution of all // transitive modules, which include files that might just have been generated. const program = createProgram(tsConfig.fileNames, tsConfig.options, compilerHost, cachedProgram); const globalDiagnostics = program.getGlobalDiagnostics(); const tsDiagnostics = program.getSyntacticDiagnostics() .concat(program.getSemanticDiagnostics()) .concat(program.getOptionsDiagnostics()); if (globalDiagnostics.length) { const diagnostics = runTypeScriptDiagnostics(context, globalDiagnostics); printDiagnostics(context, DiagnosticsType.TypeScript, diagnostics, true, false); throw new BuildError(new Error('Failed to transpile TypeScript')); } if (tsDiagnostics.length) { const diagnostics = runTypeScriptDiagnostics(context, tsDiagnostics); printDiagnostics(context, DiagnosticsType.TypeScript, diagnostics, true, false); throw new BuildError(new Error('Failed to transpile TypeScript')); } return program; } function replaceBootstrap(mainFile: File, appNgModulePath: string, appNgModuleClass: string, options: AotOptions) { if (!mainFile) { throw new BuildError(new Error(`Could not find entry point (bootstrap file) ${options.entryPoint}`)); } let modifiedFileContent: string = null; try { Logger.debug('[AotCompiler] compile: Dynamically changing entry point content to AOT mode content'); modifiedFileContent = replaceBootstrapImpl(mainFile.path, mainFile.content, appNgModulePath, appNgModuleClass); } catch (ex) { Logger.debug(`Failed to parse bootstrap: `, ex.message); Logger.warn(`Failed to parse and update ${options.entryPoint} content for AoT compilation. For now, the default fallback content will be used instead. Please consider updating ${options.entryPoint} with the content from the following link: https://github.com/ionic-team/ionic2-app-base/tree/master/src/app/main.ts`); modifiedFileContent = getFallbackMainContent(); } return modifiedFileContent; } export function isTranspileRequired(angularVersion: SemverVersion) { return angularVersion.major <= 4; } export function transpileFiles(context: BuildContext, tsConfig: TsConfig, fileSystem: HybridFileSystem) { const tsFiles = context.fileCache.getAll().filter(file => extname(file.path) === '.ts' && file.path.indexOf('.d.ts') === -1); for (const tsFile of tsFiles) { Logger.debug(`[AotCompiler] transpileFiles: Transpiling file ${tsFile.path} ...`); const transpileOutput = transpileFileContent(tsFile.path, tsFile.content, tsConfig.options); const diagnostics = runTypeScriptDiagnostics(context, transpileOutput.diagnostics); if (diagnostics.length) { // darn, we've got some things wrong, transpile failed :( printDiagnostics(context, DiagnosticsType.TypeScript, diagnostics, true, true); throw new BuildError(new Error('Failed to transpile TypeScript')); } const jsFilePath = changeExtension(tsFile.path, '.js'); fileSystem.addVirtualFile(jsFilePath, transpileOutput.outputText); fileSystem.addVirtualFile(jsFilePath + '.map', transpileOutput.sourceMapText); Logger.debug(`[AotCompiler] transpileFiles: Transpiling file ${tsFile.path} ... DONE`); } } function transpileFileContent(fileName: string, sourceText: string, options: CompilerOptions): TranspileOutput { const transpileOptions: TranspileOptions = { compilerOptions: options, fileName: fileName, reportDiagnostics: true }; return transpileModule(sourceText, transpileOptions); } export function isNg5(version: SemverVersion) { return version.major >= 5; } export async function runNg4Aot(options: CodegenOptions) { const module = await import('@angular/compiler-cli'); return await module.__NGTOOLS_PRIVATE_API_2.codeGen({ angularCompilerOptions: options.angularCompilerOptions, basePath: options.cliOptions.basePath, program: options.program, host: options.compilerHost, compilerOptions: options.compilerOptions, i18nFile: options.cliOptions.i18nFile, i18nFormat: options.cliOptions.i18nFormat, locale: options.cliOptions.locale, readResource: (fileName: string) => { return readFileAsync(fileName); } }); } export async function runNg5Aot(context: BuildContext, tsConfig: TsConfig, aggregateCompilerOptions: CompilerOptions, compilerHost: CompilerHost) { const ngTools2 = await import('@angular/compiler-cli/ngtools2'); const angularCompilerHost = ngTools2.createCompilerHost({options: aggregateCompilerOptions, tsHost: compilerHost}); const program = ngTools2.createProgram({ rootNames: tsConfig.fileNames, options: aggregateCompilerOptions, host: angularCompilerHost, oldProgram: null }); await program.loadNgStructureAsync(); const transformations: any[] = []; const transformers = { beforeTs: transformations }; const result = program.emit({ emitFlags: ngTools2.EmitFlags.Default, customTransformers: transformers }); const tsDiagnostics = program.getTsSyntacticDiagnostics() .concat(program.getTsOptionDiagnostics()) .concat(program.getTsSemanticDiagnostics()); const angularDiagnostics = program.getNgStructuralDiagnostics() .concat(program.getNgOptionDiagnostics()); if (tsDiagnostics.length) { const diagnostics = runTypeScriptDiagnostics(context, tsDiagnostics); printDiagnostics(context, DiagnosticsType.TypeScript, diagnostics, true, false); throw new BuildError(new Error('The Angular AoT build failed. See the issues above')); } if (angularDiagnostics.length) { const diagnostics = runTypeScriptDiagnostics(context, angularDiagnostics as any[]); printDiagnostics(context, DiagnosticsType.TypeScript, diagnostics, true, false); throw new BuildError(new Error('The Angular AoT build failed. See the issues above')); } } export interface AotOptions { tsConfigPath: string; rootDir: string; entryPoint: string; appNgModulePath: string; appNgModuleClass: string; } ================================================ FILE: src/aot/compiler-host-factory.ts ================================================ import { CompilerOptions } from 'typescript'; import { FileSystemCompilerHost } from './compiler-host'; import { getInstance as getFileSystemInstance } from '../util/hybrid-file-system-factory'; let instance: FileSystemCompilerHost = null; export function getFileSystemCompilerHostInstance(options: CompilerOptions) { if (!instance) { instance = new FileSystemCompilerHost(options, getFileSystemInstance(false)); } return instance; } ================================================ FILE: src/aot/compiler-host.ts ================================================ import { normalize } from 'path'; import { CancellationToken, CompilerHost, CompilerOptions, createCompilerHost, ScriptTarget, SourceFile } from 'typescript'; import { VirtualFileSystem } from '../util/interfaces'; import { getTypescriptSourceFile } from '../util/typescript-utils'; import { Logger } from '../logger/logger'; export interface OnErrorFn { (message: string): void; } export class FileSystemCompilerHost implements CompilerHost { private diskCompilerHost: CompilerHost; constructor(private options: CompilerOptions, private fileSystem: VirtualFileSystem, private setParentNodes = true) { this.diskCompilerHost = createCompilerHost(this.options, this.setParentNodes); } fileExists(filePath: string): boolean { filePath = normalize(filePath); const fileContent = this.fileSystem.getFileContent(filePath); if (fileContent) { return true; } return this.diskCompilerHost.fileExists(filePath); } readFile(filePath: string): string { filePath = normalize(filePath); const fileContent = this.fileSystem.getFileContent(filePath); if (fileContent) { return fileContent; } return this.diskCompilerHost.readFile(filePath); } directoryExists(directoryPath: string): boolean { directoryPath = normalize(directoryPath); const stats = this.fileSystem.getDirectoryStats(directoryPath); if (stats) { return true; } return this.diskCompilerHost.directoryExists(directoryPath); } getFiles(directoryPath: string): string[] { directoryPath = normalize(directoryPath); return this.fileSystem.getFileNamesInDirectory(directoryPath); } getDirectories(directoryPath: string): string[] { directoryPath = normalize(directoryPath); const subdirs = this.fileSystem.getSubDirs(directoryPath); let delegated: string[]; try { delegated = this.diskCompilerHost.getDirectories(directoryPath); } catch (e) { delegated = []; } return delegated.concat(subdirs); } getSourceFile(filePath: string, languageVersion: ScriptTarget, onError?: OnErrorFn) { filePath = normalize(filePath); // we haven't created a source file for this yet, so try to use what's in memory const fileContentFromMemory = this.fileSystem.getFileContent(filePath); if (fileContentFromMemory) { const typescriptSourceFile = getTypescriptSourceFile(filePath, fileContentFromMemory, languageVersion, this.setParentNodes); return typescriptSourceFile; } const diskSourceFile = this.diskCompilerHost.getSourceFile(filePath, languageVersion, onError); return diskSourceFile; } getCancellationToken(): CancellationToken { return this.diskCompilerHost.getCancellationToken(); } getDefaultLibFileName(options: CompilerOptions) { return this.diskCompilerHost.getDefaultLibFileName(options); } writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: OnErrorFn) { fileName = normalize(fileName); Logger.debug(`[NgcCompilerHost] writeFile: adding ${fileName} to virtual file system`); this.fileSystem.addVirtualFile(fileName, data); } getCurrentDirectory(): string { return this.diskCompilerHost.getCurrentDirectory(); } getCanonicalFileName(fileName: string): string { return this.diskCompilerHost.getCanonicalFileName(fileName); } useCaseSensitiveFileNames(): boolean { return this.diskCompilerHost.useCaseSensitiveFileNames(); } getNewLine(): string { return this.diskCompilerHost.getNewLine(); } } ================================================ FILE: src/aot/utils.ts ================================================ import { basename, dirname, join, normalize, relative, resolve } from 'path'; import { CallExpression, Identifier, PropertyAccessExpression, SyntaxKind, ScriptTarget } from 'typescript'; import { appendBefore, checkIfFunctionIsCalled, getTypescriptSourceFile, findNodes, insertNamedImportIfNeeded, replaceImportModuleSpecifier, replaceNamedImport, replaceNode } from '../util/typescript-utils'; export function getFallbackMainContent() { return ` import { platformBrowser } from '@angular/platform-browser'; import { enableProdMode } from '@angular/core'; import { AppModuleNgFactory } from './app.module.ngfactory'; enableProdMode(); platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);`; } function getBootstrapNodes(allCalls: CallExpression[]) { return allCalls .filter(call => call.expression.kind === SyntaxKind.PropertyAccessExpression) .map(call => call.expression as PropertyAccessExpression) .filter(access => { return access.name.kind === SyntaxKind.Identifier && access.name.text === 'bootstrapModule'; }); } function replaceNgModuleClassName(filePath: string, fileContent: string, className: string) { const sourceFile = getTypescriptSourceFile(filePath, fileContent, ScriptTarget.Latest, false); const allCalls = findNodes(sourceFile, sourceFile, SyntaxKind.CallExpression, true) as CallExpression[]; const bootstraps = getBootstrapNodes(allCalls); let modifiedContent = fileContent; allCalls.filter(call => bootstraps.some(bs => bs === call.expression)).forEach((call: CallExpression) => { modifiedContent = replaceNode(filePath, modifiedContent, call.arguments[0], className + 'NgFactory'); }); return modifiedContent; } function replacePlatformBrowser(filePath: string, fileContent: string) { const sourceFile = getTypescriptSourceFile(filePath, fileContent, ScriptTarget.Latest, false); const allCalls = findNodes(sourceFile, sourceFile, SyntaxKind.CallExpression, true) as CallExpression[]; const bootstraps = getBootstrapNodes(allCalls); const calls: CallExpression[] = bootstraps.reduce((previous, access) => { const expressions = findNodes(sourceFile, access, SyntaxKind.CallExpression, true) as CallExpression[]; return previous.concat(expressions); }, []) .filter((call: CallExpression) => { return call.expression.kind === SyntaxKind.Identifier && (call.expression as Identifier).text === 'platformBrowserDynamic'; }); let modifiedContent = fileContent; calls.forEach(call => { modifiedContent = replaceNode(filePath, modifiedContent, call.expression, 'platformBrowser'); }); return modifiedContent; } function checkForPlatformDynamicBrowser(filePath: string, fileContent: string) { const sourceFile = getTypescriptSourceFile(filePath, fileContent, ScriptTarget.Latest, false); const allCalls = findNodes(sourceFile, sourceFile, SyntaxKind.CallExpression, true) as CallExpression[]; const bootstraps = getBootstrapNodes(allCalls); const calls: CallExpression[] = bootstraps.reduce((previous, access) => { const expressions = findNodes(sourceFile, access, SyntaxKind.CallExpression, true) as CallExpression[]; return previous.concat(expressions); }, []) .filter((call: CallExpression) => { return call.expression.kind === SyntaxKind.Identifier && (call.expression as Identifier).text === 'platformBrowserDynamic'; }); return calls && calls.length; } function replaceBootstrapModuleFactory(filePath: string, fileContent: string) { const sourceFile = getTypescriptSourceFile(filePath, fileContent, ScriptTarget.Latest, false); const allCalls = findNodes(sourceFile, sourceFile, SyntaxKind.CallExpression, true) as CallExpression[]; const bootstraps = getBootstrapNodes(allCalls); let modifiedContent = fileContent; bootstraps.forEach((bs: PropertyAccessExpression) => { modifiedContent = replaceNode(filePath, modifiedContent, bs.name, 'bootstrapModuleFactory'); }); return modifiedContent; } function getPlatformBrowserFunctionNode(filePath: string, fileContent: string) { let modifiedFileContent = fileContent; const sourceFile = getTypescriptSourceFile(filePath, modifiedFileContent, ScriptTarget.Latest, false); const allCalls = findNodes(sourceFile, sourceFile, SyntaxKind.CallExpression, true) as CallExpression[]; const callsToPlatformBrowser = allCalls.filter(call => call.expression && call.expression.kind === SyntaxKind.Identifier && (call.expression as Identifier).text === 'platformBrowser'); const toAppend = `enableProdMode();\n`; if (callsToPlatformBrowser.length) { modifiedFileContent = appendBefore(filePath, modifiedFileContent, callsToPlatformBrowser[0].expression, toAppend); } else { // just throw it at the bottom modifiedFileContent += toAppend; } return modifiedFileContent; } function importAndEnableProdMode(filePath: string, fileContent: string) { let modifiedFileContent = fileContent; modifiedFileContent = insertNamedImportIfNeeded(filePath, modifiedFileContent, 'enableProdMode', '@angular/core'); const isCalled = checkIfFunctionIsCalled(filePath, modifiedFileContent, 'enableProdMode'); if (!isCalled) { // go ahead and insert this modifiedFileContent = getPlatformBrowserFunctionNode(filePath, modifiedFileContent); } return modifiedFileContent; } export function replaceBootstrapImpl(filePath: string, fileContent: string, appNgModulePath: string, appNgModuleClassName: string) { if (!fileContent.match(/\bbootstrapModule\b/)) { throw new Error(`Could not find bootstrapModule in ${filePath}`); } const withoutExtension = join(dirname(appNgModulePath), basename(appNgModulePath, '.ts')); const appModuleAbsoluteFileName = normalize(resolve(withoutExtension)); const withNgFactory = appModuleAbsoluteFileName + '.ngfactory'; const originalImport = './' + relative(dirname(filePath), appModuleAbsoluteFileName); const ngFactryImport = './' + relative(dirname(filePath), withNgFactory); if (!checkForPlatformDynamicBrowser(filePath, fileContent)) { throw new Error(`Could not find any references to "platformBrowserDynamic" in ${filePath}`); } let modifiedFileContent = fileContent; modifiedFileContent = replaceNgModuleClassName(filePath, modifiedFileContent, appNgModuleClassName); modifiedFileContent = replacePlatformBrowser(filePath, modifiedFileContent); modifiedFileContent = replaceBootstrapModuleFactory(filePath, modifiedFileContent); modifiedFileContent = replaceNamedImport(filePath, modifiedFileContent, 'platformBrowserDynamic', 'platformBrowser'); modifiedFileContent = replaceNamedImport(filePath, modifiedFileContent, appNgModuleClassName, appNgModuleClassName + 'NgFactory'); modifiedFileContent = replaceImportModuleSpecifier(filePath, modifiedFileContent, '@angular/platform-browser-dynamic', '@angular/platform-browser'); modifiedFileContent = replaceImportModuleSpecifier(filePath, modifiedFileContent, originalImport, ngFactryImport); // check if prod mode is imported and enabled modifiedFileContent = importAndEnableProdMode(filePath, modifiedFileContent); return modifiedFileContent; } ================================================ FILE: src/build/util.ts ================================================ import { join } from 'path'; import { getTsConfigAsync, TsConfig } from '../transpile'; import * as Constants from '../util/constants'; import { BuildError } from '../util/errors'; import { GlobResult, globAll } from '../util/glob-util'; import { getBooleanPropertyValue, getStringPropertyValue, readFileAsync, readJsonAsync, semverStringToObject } from '../util/helpers'; import { BuildContext, } from '../util/interfaces'; export function scanSrcTsFiles(context: BuildContext) { const srcGlob = join(context.srcDir, '**', '*.ts'); const globs: string[] = [srcGlob]; const deepLinkDir = getStringPropertyValue(Constants.ENV_VAR_DEEPLINKS_DIR); // these two will only not be equal in some weird cases like for building Ionic's demos with our current repository set-up if (deepLinkDir !== context.srcDir) { globs.push(join(deepLinkDir, '**', '*.ts')); } return globAll(globs).then((results: GlobResult[]) => { const promises = results.map(result => { const promise = readFileAsync(result.absolutePath); promise.then((fileContent: string) => { context.fileCache.set(result.absolutePath, { path: result.absolutePath, content: fileContent}); }); return promise; }); return Promise.all(promises); }); } export function validateTsConfigSettings(tsConfigFileContents: TsConfig) { return new Promise((resolve, reject) => { try { const isValid = tsConfigFileContents.options && tsConfigFileContents.options.sourceMap === true; if (!isValid) { const error = new BuildError(['The "tsconfig.json" file must have compilerOptions.sourceMap set to true.', 'For more information please see the default Ionic project tsconfig.json file here:', 'https://github.com/ionic-team/ionic2-app-base/blob/master/tsconfig.json'].join('\n')); error.isFatal = true; return reject(error); } resolve(); } catch (e) { const error = new BuildError('The "tsconfig.json" file contains malformed JSON.'); error.isFatal = true; return reject(error); } }); } export function validateRequiredFilesExist(context: BuildContext) { return Promise.all([ readFileAsync(process.env[Constants.ENV_APP_ENTRY_POINT]), getTsConfigAsync(context, process.env[Constants.ENV_TS_CONFIG]) ]).catch((error) => { if (error.code === 'ENOENT' && error.path === process.env[Constants.ENV_APP_ENTRY_POINT]) { error = new BuildError(`${error.path} was not found. The "main.dev.ts" and "main.prod.ts" files have been deprecated. Please create a new file "main.ts" containing the content of "main.dev.ts", and then delete the deprecated files. For more information, please see the default Ionic project main.ts file here: https://github.com/ionic-team/ionic2-app-base/tree/master/src/app/main.ts`); error.isFatal = true; throw error; } if (error.code === 'ENOENT' && error.path === process.env[Constants.ENV_TS_CONFIG]) { error = new BuildError([`${error.path} was not found. The "tsconfig.json" file is missing. This file is required.`, 'For more information please see the default Ionic project tsconfig.json file here:', 'https://github.com/ionic-team/ionic2-app-base/blob/master/tsconfig.json'].join('\n')); error.isFatal = true; throw error; } error.isFatal = true; throw error; }); } export async function readVersionOfDependencies(context: BuildContext) { // read the package.json version from ionic, angular/core, and typescript const promises: Promise[] = []; promises.push(readPackageVersion(context.angularCoreDir)); if (!getBooleanPropertyValue(Constants.ENV_SKIP_IONIC_ANGULAR_VERSION)) { promises.push(readPackageVersion(context.ionicAngularDir)); } promises.push(readPackageVersion(context.typescriptDir)); const versions = await Promise.all(promises); context.angularVersion = semverStringToObject(versions[0]); if (!getBooleanPropertyValue(Constants.ENV_SKIP_IONIC_ANGULAR_VERSION)) { context.ionicAngularVersion = semverStringToObject(versions[1]); } // index could be 1 or 2 depending on if you read ionic-angular, its always the last one bro context.typescriptVersion = semverStringToObject(versions[versions.length - 1]); } export async function readPackageVersion(packageDir: string) { const packageJsonPath = join(packageDir, 'package.json'); const packageObject = await readJsonAsync(packageJsonPath); return packageObject['version']; } ================================================ FILE: src/build.spec.ts ================================================ import * as Constants from './util/constants'; import { BuildContext } from './util/interfaces'; import * as helpers from './util/helpers'; import * as build from './build'; import * as buildUtils from './build/util'; import * as bundle from './bundle'; import * as copy from './copy'; import * as clean from './clean'; import * as deepLinking from './deep-linking'; import * as lint from './lint'; import * as minify from './minify'; import * as ngc from './ngc'; import * as postprocess from './postprocess'; import * as preprocess from './preprocess'; import * as sass from './sass'; import * as transpile from './transpile'; describe('build', () => { beforeEach(() => { spyOn(clean, 'clean'); spyOn(helpers, helpers.readFileAsync.name).and.returnValue(Promise.resolve()); spyOn(transpile, transpile.getTsConfigAsync.name).and.callFake(() => { return Promise.resolve({ 'options': { 'sourceMap': true } }); }); spyOn(buildUtils, buildUtils.scanSrcTsFiles.name).and.returnValue(Promise.resolve()); spyOn(buildUtils, buildUtils.validateRequiredFilesExist.name).and.returnValue(Promise.resolve(['fileOneContent', 'fileTwoContent'])); spyOn(buildUtils, buildUtils.validateTsConfigSettings.name).and.returnValue(Promise.resolve()); spyOn(buildUtils, buildUtils.readVersionOfDependencies.name).and.returnValue(Promise.resolve()); spyOn(bundle, bundle.bundle.name).and.returnValue(Promise.resolve()); spyOn(copy, copy.copy.name).and.returnValue(Promise.resolve()); spyOn(deepLinking, deepLinking.deepLinking.name).and.returnValue(Promise.resolve()); spyOn(minify, minify.minifyCss.name).and.returnValue(Promise.resolve()); spyOn(minify, minify.minifyJs.name).and.returnValue(Promise.resolve()); spyOn(lint, lint.lint.name).and.returnValue(Promise.resolve()); spyOn(ngc, ngc.ngc.name).and.returnValue(Promise.resolve()); spyOn(postprocess, postprocess.postprocess.name).and.returnValue(Promise.resolve()); spyOn(preprocess, preprocess.preprocess.name).and.returnValue(Promise.resolve()); spyOn(sass, sass.sass.name).and.returnValue(Promise.resolve()); spyOn(transpile, transpile.transpile.name).and.returnValue(Promise.resolve()); }); it('should do a prod build', () => { let context: BuildContext = { isProd: true, optimizeJs: true, runMinifyJs: true, runMinifyCss: true, runAot: true }; const getBooleanPropertyValueSpy = spyOn(helpers, helpers.getBooleanPropertyValue.name).and.returnValue(true); return build.build(context).then(() => { expect(buildUtils.scanSrcTsFiles).toHaveBeenCalled(); expect(copy.copy).toHaveBeenCalled(); expect(deepLinking.deepLinking).toHaveBeenCalled(); expect(ngc.ngc).toHaveBeenCalled(); expect(bundle.bundle).toHaveBeenCalled(); expect(minify.minifyJs).toHaveBeenCalled(); expect(sass.sass).toHaveBeenCalled(); expect(minify.minifyCss).toHaveBeenCalled(); expect(lint.lint).toHaveBeenCalled(); expect(getBooleanPropertyValueSpy.calls.all()[1].args[0]).toEqual(Constants.ENV_ENABLE_LINT); expect(transpile.transpile).not.toHaveBeenCalled(); }); }); it('should do a dev build', () => { let context: BuildContext = { isProd: false, optimizeJs: false, runMinifyJs: false, runMinifyCss: false, runAot: false }; const getBooleanPropertyValueSpy = spyOn(helpers, helpers.getBooleanPropertyValue.name).and.returnValue(true); return build.build(context).then(() => { expect(buildUtils.scanSrcTsFiles).toHaveBeenCalled(); expect(copy.copy).toHaveBeenCalled(); expect(deepLinking.deepLinking).toHaveBeenCalled(); expect(transpile.transpile).toHaveBeenCalled(); expect(bundle.bundle).toHaveBeenCalled(); expect(sass.sass).toHaveBeenCalled(); expect(lint.lint).toHaveBeenCalled(); expect(getBooleanPropertyValueSpy.calls.all()[1].args[0]).toEqual(Constants.ENV_ENABLE_LINT); expect(postprocess.postprocess).toHaveBeenCalled(); expect(preprocess.preprocess).toHaveBeenCalled(); expect(ngc.ngc).not.toHaveBeenCalled(); expect(minify.minifyJs).not.toHaveBeenCalled(); expect(minify.minifyCss).not.toHaveBeenCalled(); }); }); it('should skip lint', () => { let context: BuildContext = { isProd: false, optimizeJs: false, runMinifyJs: false, runMinifyCss: false, runAot: false }; const getBooleanPropertyValueSpy = spyOn(helpers, helpers.getBooleanPropertyValue.name).and.returnValue(false); return build.build(context).then(() => { expect(buildUtils.scanSrcTsFiles).toHaveBeenCalled(); expect(copy.copy).toHaveBeenCalled(); expect(transpile.transpile).toHaveBeenCalled(); expect(bundle.bundle).toHaveBeenCalled(); expect(sass.sass).toHaveBeenCalled(); expect(lint.lint).not.toHaveBeenCalled(); expect(getBooleanPropertyValueSpy.calls.all()[1].args[0]).toEqual(Constants.ENV_ENABLE_LINT); expect(postprocess.postprocess).toHaveBeenCalled(); expect(preprocess.preprocess).toHaveBeenCalled(); expect(ngc.ngc).not.toHaveBeenCalled(); expect(minify.minifyJs).not.toHaveBeenCalled(); expect(minify.minifyCss).not.toHaveBeenCalled(); }); }); }); describe('test project requirements before building', () => { it('should fail if APP_ENTRY_POINT file does not exist', () => { process.env[Constants.ENV_APP_ENTRY_POINT] = 'src/app/main.ts'; process.env[Constants.ENV_TS_CONFIG] = 'tsConfig.js'; const error = new Error('App entry point was not found'); spyOn(helpers, 'readFileAsync').and.returnValue(Promise.reject(error)); return build.build({}).catch((e) => { expect(helpers.readFileAsync).toHaveBeenCalledTimes(1); expect(e).toEqual(error); }); }); it('should fail if IONIC_TS_CONFIG file does not exist', () => { process.env[Constants.ENV_APP_ENTRY_POINT] = 'src/app/main.ts'; process.env[Constants.ENV_TS_CONFIG] = 'tsConfig.js'; const error = new Error('Config was not found'); spyOn(helpers, helpers.readFileAsync.name).and.returnValues(Promise.resolve()); spyOn(transpile, transpile.getTsConfigAsync.name).and.returnValues(Promise.reject(error)); return build.build({}).catch((e) => { expect(transpile.getTsConfigAsync).toHaveBeenCalledTimes(1); expect(helpers.readFileAsync).toHaveBeenCalledTimes(1); expect(e).toEqual(error); }); }); it('should fail fataly if IONIC_TS_CONFIG file does not contain valid JSON', () => { process.env[Constants.ENV_APP_ENTRY_POINT] = 'src/app/main.ts'; process.env[Constants.ENV_TS_CONFIG] = 'tsConfig.js'; spyOn(transpile, transpile.getTsConfigAsync.name).and.callFake(() => { return Promise.resolve(`{ "options" { "sourceMap": false } } `); }); spyOn(buildUtils, buildUtils.scanSrcTsFiles.name).and.returnValue(Promise.resolve()); spyOn(buildUtils, buildUtils.readVersionOfDependencies.name).and.returnValue(Promise.resolve()); return build.build({}).catch((e) => { expect(transpile.getTsConfigAsync).toHaveBeenCalledTimes(1); expect(e.isFatal).toBeTruthy(); }); }); it('should fail fataly if IONIC_TS_CONFIG file does not contain compilerOptions.sourceMap === true', () => { process.env[Constants.ENV_APP_ENTRY_POINT] = 'src/app/main.ts'; process.env[Constants.ENV_TS_CONFIG] = 'tsConfig.js'; spyOn(transpile, transpile.getTsConfigAsync.name).and.callFake(() => { return Promise.resolve(`{ "options": { "sourceMap": false } } `); }); spyOn(buildUtils, buildUtils.scanSrcTsFiles.name).and.returnValue(Promise.resolve()); spyOn(buildUtils, buildUtils.readVersionOfDependencies.name).and.returnValue(Promise.resolve()); return build.build({}).catch((e) => { expect(transpile.getTsConfigAsync).toHaveBeenCalledTimes(1); expect(e.isFatal).toBeTruthy(); }); }); }); ================================================ FILE: src/build.ts ================================================ import { join } from 'path'; import { readVersionOfDependencies, scanSrcTsFiles, validateRequiredFilesExist, validateTsConfigSettings } from './build/util'; import { bundle, bundleUpdate } from './bundle'; import { clean } from './clean'; import { copy } from './copy'; import { deepLinking, deepLinkingUpdate } from './deep-linking'; import { lint, lintUpdate } from './lint'; import { Logger } from './logger/logger'; import { minifyCss, minifyJs } from './minify'; import { ngc } from './ngc'; import { getTsConfigAsync, TsConfig } from './transpile'; import { postprocess } from './postprocess'; import { preprocess, preprocessUpdate } from './preprocess'; import { sass, sassUpdate } from './sass'; import { templateUpdate } from './template'; import { transpile, transpileUpdate, transpileDiagnosticsOnly } from './transpile'; import * as Constants from './util/constants'; import { BuildError } from './util/errors'; import { emit, EventType } from './util/events'; import { getBooleanPropertyValue, readFileAsync, setContext } from './util/helpers'; import { BuildContext, BuildState, BuildUpdateMessage, ChangedFile } from './util/interfaces'; export function build(context: BuildContext) { setContext(context); const logger = new Logger(`build ${(context.isProd ? 'prod' : 'dev')}`); return buildWorker(context) .then(() => { // congrats, we did it! (•_•) / ( •_•)>⌐■-■ / (⌐■_■) logger.finish(); }) .catch(err => { if (err.isFatal) { throw err; } throw logger.fail(err); }); } async function buildWorker(context: BuildContext) { const promises: Promise[] = []; promises.push(validateRequiredFilesExist(context)); promises.push(readVersionOfDependencies(context)); const results = await Promise.all(promises); const tsConfigContents = results[0][1]; await validateTsConfigSettings(tsConfigContents); await buildProject(context); } function buildProject(context: BuildContext) { // sync empty the www/build directory clean(context); buildId++; const copyPromise = copy(context); return scanSrcTsFiles(context) .then(() => { if (getBooleanPropertyValue(Constants.ENV_PARSE_DEEPLINKS)) { return deepLinking(context); } }) .then(() => { const compilePromise = (context.runAot) ? ngc(context) : transpile(context); return compilePromise; }) .then(() => { return preprocess(context); }) .then(() => { return bundle(context); }) .then(() => { const minPromise = (context.runMinifyJs) ? minifyJs(context) : Promise.resolve(); const sassPromise = sass(context) .then(() => { return (context.runMinifyCss) ? minifyCss(context) : Promise.resolve(); }); return Promise.all([ minPromise, sassPromise, copyPromise ]); }) .then(() => { return postprocess(context); }) .then(() => { if (getBooleanPropertyValue(Constants.ENV_ENABLE_LINT)) { // kick off the tslint after everything else // nothing needs to wait on its completion unless bailing on lint error is enabled const result = lint(context, null, false); if (getBooleanPropertyValue(Constants.ENV_BAIL_ON_LINT_ERROR)) { return result; } } }) .catch(err => { throw new BuildError(err); }); } export function buildUpdate(changedFiles: ChangedFile[], context: BuildContext) { return new Promise(resolve => { const logger = new Logger('build'); buildId++; const buildUpdateMsg: BuildUpdateMessage = { buildId: buildId, reloadApp: false }; emit(EventType.BuildUpdateStarted, buildUpdateMsg); function buildTasksDone(resolveValue: BuildTaskResolveValue) { // all build tasks have been resolved or one of them // bailed early, stopping all others to not run parallelTasksPromise.then(() => { // all parallel tasks are also done // so now we're done done const buildUpdateMsg: BuildUpdateMessage = { buildId: buildId, reloadApp: resolveValue.requiresAppReload }; emit(EventType.BuildUpdateCompleted, buildUpdateMsg); if (!resolveValue.requiresAppReload) { // just emit that only a certain file changed // this one is useful when only a sass changed happened // and the webpack only needs to livereload the css // but does not need to do a full page refresh emit(EventType.FileChange, resolveValue.changedFiles); } let requiresLintUpdate = false; for (const changedFile of changedFiles) { if (changedFile.ext === '.ts') { if (changedFile.event === 'change' || changedFile.event === 'add') { requiresLintUpdate = true; break; } } } if (requiresLintUpdate) { // a ts file changed, so let's lint it too, however // this task should run as an after thought if (getBooleanPropertyValue(Constants.ENV_ENABLE_LINT)) { lintUpdate(changedFiles, context, false); } } logger.finish('green', true); Logger.newLine(); // we did it! resolve(); }); } // kick off all the build tasks // and the tasks that can run parallel to all the build tasks const buildTasksPromise = buildUpdateTasks(changedFiles, context); const parallelTasksPromise = buildUpdateParallelTasks(changedFiles, context); // whether it was resolved or rejected, we need to do the same thing buildTasksPromise .then(buildTasksDone) .catch(() => { buildTasksDone({ requiresAppReload: false, changedFiles: changedFiles }); }); }); } /** * Collection of all the build tasks than need to run * Each task will only run if it's set with eacn BuildState. */ function buildUpdateTasks(changedFiles: ChangedFile[], context: BuildContext) { const resolveValue: BuildTaskResolveValue = { requiresAppReload: false, changedFiles: [] }; return loadFiles(changedFiles, context) .then(() => { // DEEP LINKING if (getBooleanPropertyValue(Constants.ENV_PARSE_DEEPLINKS)) { return deepLinkingUpdate(changedFiles, context); } }) .then(() => { // TEMPLATE if (context.templateState === BuildState.RequiresUpdate) { resolveValue.requiresAppReload = true; return templateUpdate(changedFiles, context); } // no template updates required return Promise.resolve(); }) .then(() => { // TRANSPILE if (context.transpileState === BuildState.RequiresUpdate) { resolveValue.requiresAppReload = true; // we've already had a successful transpile once, only do an update // not that we've also already started a transpile diagnostics only // build that only needs to be completed by the end of buildUpdate return transpileUpdate(changedFiles, context); } else if (context.transpileState === BuildState.RequiresBuild) { // run the whole transpile resolveValue.requiresAppReload = true; return transpile(context); } // no transpiling required return Promise.resolve(); }) .then(() => { // PREPROCESS return preprocessUpdate(changedFiles, context); }) .then(() => { // BUNDLE if (context.bundleState === BuildState.RequiresUpdate) { // we need to do a bundle update resolveValue.requiresAppReload = true; return bundleUpdate(changedFiles, context); } else if (context.bundleState === BuildState.RequiresBuild) { // we need to do a full bundle build resolveValue.requiresAppReload = true; return bundle(context); } // no bundling required return Promise.resolve(); }) .then(() => { // SASS if (context.sassState === BuildState.RequiresUpdate) { // we need to do a sass update return sassUpdate(changedFiles, context).then(outputCssFile => { const changedFile: ChangedFile = { event: Constants.FILE_CHANGE_EVENT, ext: '.css', filePath: outputCssFile }; context.fileCache.set(outputCssFile, { path: outputCssFile, content: outputCssFile }); resolveValue.changedFiles.push(changedFile); }); } else if (context.sassState === BuildState.RequiresBuild) { // we need to do a full sass build return sass(context).then(outputCssFile => { const changedFile: ChangedFile = { event: Constants.FILE_CHANGE_EVENT, ext: '.css', filePath: outputCssFile }; context.fileCache.set(outputCssFile, { path: outputCssFile, content: outputCssFile }); resolveValue.changedFiles.push(changedFile); }); } // no sass build required return Promise.resolve(); }) .then(() => { return resolveValue; }); } function loadFiles(changedFiles: ChangedFile[], context: BuildContext) { // UPDATE IN-MEMORY FILE CACHE let promises: Promise[] = []; for (const changedFile of changedFiles) { if (changedFile.event === Constants.FILE_DELETE_EVENT) { // remove from the cache on delete context.fileCache.remove(changedFile.filePath); } else { // load the latest since the file changed const promise = readFileAsync(changedFile.filePath); promises.push(promise); promise.then((content: string) => { context.fileCache.set(changedFile.filePath, { path: changedFile.filePath, content: content }); }); } } return Promise.all(promises); } interface BuildTaskResolveValue { requiresAppReload: boolean; changedFiles: ChangedFile[]; } /** * parallelTasks are for any tasks that can run parallel to the entire * build, but we still need to make sure they've completed before we're * all done, it's also possible there are no parallelTasks at all */ function buildUpdateParallelTasks(changedFiles: ChangedFile[], context: BuildContext) { const parallelTasks: Promise[] = []; if (context.transpileState === BuildState.RequiresUpdate) { parallelTasks.push(transpileDiagnosticsOnly(context)); } return Promise.all(parallelTasks); } let buildId = 0; ================================================ FILE: src/bundle.spec.ts ================================================ import * as bundle from './bundle'; import * as webpack from './webpack'; import * as Constants from './util/constants'; import { ChangedFile } from './util/interfaces'; describe('bundle task', () => { describe('bundle', () => { it('should return the value webpack task returns', () => { // arrange spyOn(webpack, webpack.webpack.name).and.returnValue(Promise.resolve()); const context = { bundler: Constants.BUNDLER_WEBPACK}; // act return bundle.bundle(context).then(() => { // assert expect(webpack.webpack).toHaveBeenCalled(); }); }); it('should throw when webpack throws', () => { const errorText = 'simulating an error'; // arrange spyOn(webpack, webpack.webpack.name).and.returnValue(Promise.reject(new Error(errorText))); const context = { bundler: Constants.BUNDLER_WEBPACK}; // act return bundle.bundle(context).then(() => { throw new Error('Should never happen'); }).catch(err => { // assert expect(webpack.webpack).toHaveBeenCalled(); expect(err.message).toBe(errorText); }); }); }); describe('bundleUpdate', () => { it('should return the value webpack returns', () => { // arrange spyOn(webpack, webpack.webpackUpdate.name).and.returnValue(Promise.resolve()); const context = { bundler: Constants.BUNDLER_WEBPACK}; const changedFiles: ChangedFile[] = []; // act return bundle.bundleUpdate(changedFiles, context).then(() => { // assert expect(webpack.webpackUpdate).toHaveBeenCalledWith(changedFiles, context); }); }); it('should throw when webpack throws', () => { const errorText = 'simulating an error'; try { // arrange spyOn(webpack, webpack.webpackUpdate.name).and.returnValue(Promise.reject(new Error(errorText))); const context = { bundler: Constants.BUNDLER_WEBPACK}; const changedFiles: ChangedFile[] = []; // act return bundle.bundleUpdate(changedFiles, context).then(() => { throw new Error('Should never happen'); }).catch(err => { // assert expect(webpack.webpackUpdate).toHaveBeenCalled(); expect(err.message).toBe(errorText); }); } catch (ex) { } }); }); describe('buildJsSourceMaps', () => { it('should get false when devtool is null for webpack', () => { // arrange const config = { }; spyOn(webpack, webpack.getWebpackConfig.name).and.returnValue(config); const context = { bundler: Constants.BUNDLER_WEBPACK}; // act const result = bundle.buildJsSourceMaps(context); // assert expect(webpack.getWebpackConfig).toHaveBeenCalledWith(context, null); expect(result).toEqual(false); }); it('should get false when devtool is valid', () => { // arrange const config = { devtool: 'someValue'}; spyOn(webpack, webpack.getWebpackConfig.name).and.returnValue(config); const context = { bundler: Constants.BUNDLER_WEBPACK}; // act const result = bundle.buildJsSourceMaps(context); // assert expect(webpack.getWebpackConfig).toHaveBeenCalledWith(context, null); expect(result).toEqual(true); }); }); describe('getJsOutputDest', () => { it('should get the value from webpack', () => { // arrange const returnValue = 'someString'; spyOn(webpack, webpack.getOutputDest.name).and.returnValue(returnValue); const context = { bundler: Constants.BUNDLER_WEBPACK}; // act const result = bundle.getJsOutputDest(context); // assert expect(webpack.getOutputDest).toHaveBeenCalledWith(context); expect(result).toEqual(returnValue); }); }); }); ================================================ FILE: src/bundle.ts ================================================ import { BuildContext, ChangedFile } from './util/interfaces'; import { BuildError, IgnorableError } from './util/errors'; import * as Constants from './util/constants'; import { webpack, webpackUpdate, getWebpackConfig, getOutputDest as webpackGetOutputDest } from './webpack'; export function bundle(context: BuildContext, configFile?: string) { return bundleWorker(context, configFile) .catch((err: Error) => { throw new BuildError(err); }); } function bundleWorker(context: BuildContext, configFile: string) { return webpack(context, configFile); } export function bundleUpdate(changedFiles: ChangedFile[], context: BuildContext) { return webpackUpdate(changedFiles, context) .catch(err => { if (err instanceof IgnorableError) { throw err; } throw new BuildError(err); }); } export function buildJsSourceMaps(context: BuildContext) { const webpackConfig = getWebpackConfig(context, null); return !!(webpackConfig.devtool && webpackConfig.devtool.length > 0); } export function getJsOutputDest(context: BuildContext) { return webpackGetOutputDest(context); } ================================================ FILE: src/clean.spec.ts ================================================ import * as fs from 'fs-extra'; import * as clean from './clean'; describe('clean task', () => { describe('clean', () => { it('should empty the build directory', () => { // arrage spyOn(fs, fs.emptyDirSync.name).and.returnValue('things'); const context = { buildDir: 'something' }; // act return clean.clean(context).then(() => { // assert expect(fs.emptyDirSync).toHaveBeenCalledWith(context.buildDir); }); }); it('should throw when failing to empty dir', () => { // arrage spyOn(fs, fs.emptyDirSync.name).and.throwError('Simulating an error'); const context = { buildDir: 'something' }; // act return clean.clean(context).catch((ex) => { expect(ex instanceof Error).toBe(true); expect(typeof ex.message).toBe('string'); }); }); }); }); ================================================ FILE: src/clean.ts ================================================ import { BuildContext } from './util/interfaces'; import { BuildError } from './util/errors'; import { emptyDirSync } from 'fs-extra'; import { Logger } from './logger/logger'; export function clean(context: BuildContext) { return new Promise((resolve, reject) => { const logger = new Logger('clean'); try { Logger.debug(`[Clean] clean: cleaning ${context.buildDir}`); emptyDirSync(context.buildDir); logger.finish(); } catch (ex) { reject(logger.fail(new BuildError(`Failed to clean directory ${context.buildDir} - ${ex.message}`))); } resolve(); }); } ================================================ FILE: src/cleancss.spec.ts ================================================ import { join } from 'path'; import * as cleanCss from './cleancss'; import * as cleanCssFactory from './util/clean-css-factory'; import * as config from './util/config'; import * as helpers from './util/helpers'; import * as workerClient from './worker-client'; describe('clean css task', () => { describe('cleancss', () => { it('should return when the worker returns', () => { // arrange const context = { }; const configFile: any = null; const spy = spyOn(workerClient, workerClient.runWorker.name).and.returnValue(Promise.resolve()); // act return (cleanCss as any).cleancss(context, null).then(() => { // assert expect(spy).toHaveBeenCalledWith('cleancss', 'cleancssWorker', context, configFile); }); }); it('should throw when the worker throws', () => { // arrange const context = { }; const errorMessage = 'Simulating an error'; spyOn(workerClient, workerClient.runWorker.name).and.returnValue(Promise.reject(new Error(errorMessage))); // act return (cleanCss as any).cleancss(context, null).then(() => { throw new Error('Should never get here'); }).catch((err: Error) => { // assert expect(err.message).toEqual(errorMessage); }); }); }); describe('cleancssworker', () => { it('should throw when reading the file throws', () => { const errorMessage = 'simulating an error'; // arrange const context = { buildDir: 'www'}; const cleanCssConfig = { sourceFileName: 'sourceFileName', destFileName: 'destFileName'}; spyOn(config, config.generateContext.name).and.returnValue(context); spyOn(config, config.fillConfigDefaults.name).and.returnValue(cleanCssConfig); spyOn(helpers, helpers.readFileAsync.name).and.returnValue(Promise.reject(new Error(errorMessage))); // act return (cleanCss as any).cleancssWorker(context, null).then(() => { throw new Error('Should never get here'); }).catch((err: Error) => { expect(err.message).toEqual(errorMessage); }); }); it('should return what writeFileAsync returns', () => { // arrange const context = { buildDir: 'www'}; const cleanCssConfig = { sourceFileName: 'sourceFileName', destFileName: 'destFileName'}; const fileContent = 'content'; const minifiedContent = 'someContent'; spyOn(config, config.generateContext.name).and.returnValue(context); spyOn(config, config.fillConfigDefaults.name).and.returnValue(cleanCssConfig); spyOn(helpers, helpers.readFileAsync.name).and.returnValue(Promise.resolve(fileContent)); spyOn(helpers, helpers.writeFileAsync.name).and.returnValue(Promise.resolve()); spyOn(cleanCssFactory, cleanCssFactory.getCleanCssInstance.name).and.returnValue({ minify: (content: string, cb: Function) => { cb(null, { styles: minifiedContent }); } }); // act return (cleanCss as any).cleancssWorker(context, null).then(() => { // assert expect(config.generateContext).toHaveBeenCalledWith(context); expect(config.fillConfigDefaults).toHaveBeenCalledWith(null, (cleanCss as any).taskInfo.defaultConfigFile); expect(helpers.readFileAsync).toHaveBeenCalledWith(join(context.buildDir, cleanCssConfig.sourceFileName)); expect(helpers.writeFileAsync).toHaveBeenCalledWith(join(context.buildDir, cleanCssConfig.destFileName), minifiedContent); }); }); }); describe('runCleanCss', () => { it('should reject when minification errors out', () => { // arrange const errorMessage = 'simulating an error'; const configFile = { options: {} }; const fileContent = 'fileContent'; const destinationFilePath = 'filePath'; const mockMinifier = { minify: () => {} }; const minifySpy = spyOn(mockMinifier, mockMinifier.minify.name); spyOn(cleanCssFactory, cleanCssFactory.getCleanCssInstance.name).and.returnValue(mockMinifier); // act const promise = (cleanCss as any).runCleanCss(configFile, fileContent, destinationFilePath); // call the callback from the spy's args const callback = minifySpy.calls.mostRecent().args[1]; callback(new Error(errorMessage), null); return promise.then(() => { throw new Error('Should never get here'); }).catch((err: Error) => { // assert expect(err.message).toEqual(errorMessage); }); }); it('should reject when minification has one or more errors', () => { // arrange const configFile = { options: {} }; const fileContent = 'fileContent'; const minificationResponse = { errors: ['some error'] }; const destinationFilePath = 'filePath'; const mockMinifier = { minify: () => {} }; const minifySpy = spyOn(mockMinifier, mockMinifier.minify.name); spyOn(cleanCssFactory, cleanCssFactory.getCleanCssInstance.name).and.returnValue(mockMinifier); // act const promise = (cleanCss as any).runCleanCss(configFile, fileContent, destinationFilePath); // call the callback from the spy's args const callback = minifySpy.calls.mostRecent().args[1]; callback(null, minificationResponse); return promise.then(() => { throw new Error('Should never get here'); }).catch((err: Error) => { // assert expect(err.message).toEqual(minificationResponse.errors[0]); }); }); it('should return minified content', () => { const configFile = { options: {} }; const fileContent = 'fileContent'; let minifySpy: jasmine.Spy = null; const minificationResponse = { styles: 'minifiedContent' }; const destinationFilePath = 'filePath'; const mockMinifier = { minify: () => {} }; minifySpy = spyOn(mockMinifier, mockMinifier.minify.name); spyOn(cleanCssFactory, cleanCssFactory.getCleanCssInstance.name).and.returnValue(mockMinifier); // act const promise = (cleanCss as any).runCleanCss(configFile, fileContent, destinationFilePath); // call the callback from the spy's args const callback = minifySpy.calls.mostRecent().args[1]; callback(null, minificationResponse); return promise.then((result: string) => { expect(result).toEqual(minificationResponse.styles); expect(cleanCssFactory.getCleanCssInstance).toHaveBeenCalledWith(configFile.options); expect(minifySpy.calls.mostRecent().args[0]).toEqual(fileContent); }); }); }); }); ================================================ FILE: src/cleancss.ts ================================================ import { join } from 'path'; import { BuildContext, TaskInfo } from './util/interfaces'; import { BuildError } from './util/errors'; import { fillConfigDefaults, generateContext, getUserConfigFile } from './util/config'; import { Logger } from './logger/logger'; import { readFileAsync, writeFileAsync } from './util/helpers'; import * as workerClient from './worker-client'; import { CleanCssConfig, getCleanCssInstance } from './util/clean-css-factory'; export function cleancss(context: BuildContext, configFile?: string) { const logger = new Logger('cleancss'); configFile = getUserConfigFile(context, taskInfo, configFile); return workerClient.runWorker('cleancss', 'cleancssWorker', context, configFile).then(() => { logger.finish(); }).catch(err => { throw logger.fail(err); }); } export function cleancssWorker(context: BuildContext, configFile: string): Promise { context = generateContext(context); const config: CleanCssConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); const srcFile = join(context.buildDir, config.sourceFileName); const destFilePath = join(context.buildDir, config.destFileName); Logger.debug(`[Clean CSS] cleancssWorker: reading source file ${srcFile}`); return readFileAsync(srcFile).then(fileContent => { return runCleanCss(config, fileContent); }).then(minifiedContent => { Logger.debug(`[Clean CSS] runCleanCss: writing file to disk ${destFilePath}`); return writeFileAsync(destFilePath, minifiedContent); }); } // exporting for easier unit testing export function runCleanCss(cleanCssConfig: CleanCssConfig, fileContent: string): Promise { return new Promise((resolve, reject) => { const minifier = getCleanCssInstance(cleanCssConfig.options); minifier.minify(fileContent, (err, minified) => { if (err) { reject(new BuildError(err)); } else if (minified.errors && minified.errors.length > 0) { // just return the first error for now I guess minified.errors.forEach(e => { Logger.error(e); }); reject(new BuildError(minified.errors[0])); } else { resolve(minified.styles); } }); }); } // export for testing only export const taskInfo: TaskInfo = { fullArg: '--cleancss', shortArg: '-e', envVar: 'IONIC_CLEANCSS', packageConfig: 'ionic_cleancss', defaultConfigFile: 'cleancss.config' }; ================================================ FILE: src/copy.spec.ts ================================================ import * as copy from './copy'; import * as config from './util/config'; describe('copy task', () => { describe('copyConfigToWatchConfig', () => { it('should convert to watch config format', () => { // arrange const context = { }; const configFile = 'configFile'; const sampleConfig: copy.CopyConfig = { copyAssets: { src: ['{{SRC}}/assets/**/*'], dest: '{{WWW}}/assets' }, copyIndexContent: { src: ['{{SRC}}/index.html', '{{SRC}}/manifest.json', '{{SRC}}/service-worker.js'], dest: '{{WWW}}' }, copyFonts: { src: ['{{ROOT}}/node_modules/ionicons/dist/fonts/**/*', '{{ROOT}}/node_modules/ionic-angular/fonts/**/*'], dest: '{{WWW}}/assets/fonts' }, copyPolyfills: { src: [`{{ROOT}}/node_modules/ionic-angular/polyfills/${process.env.POLLYFILL_NAME}.js`], dest: '{{BUILD}}' }, someOtherOption: { src: ['{{ROOT}}/whatever'], dest: '{{BUILD}}' } }; let combinedSource: string[] = []; Object.keys(sampleConfig).forEach(entry => combinedSource = combinedSource.concat(sampleConfig[entry].src)); spyOn(config, config.generateContext.name).and.returnValue(context); spyOn(config, config.getUserConfigFile.name).and.returnValue(configFile); spyOn(config, config.fillConfigDefaults.name).and.returnValue(sampleConfig); // act const result = copy.copyConfigToWatchConfig(null); // assert expect(config.generateContext).toHaveBeenCalledWith(null); expect(config.getUserConfigFile).toHaveBeenCalledWith(context, copy.taskInfo, ''); expect(config.fillConfigDefaults).toHaveBeenCalledWith(configFile, copy.taskInfo.defaultConfigFile); (result.paths as string[]).forEach(glob => { expect(combinedSource.indexOf(glob)).not.toEqual(-1); }); expect(result.callback).toBeDefined(); expect(result.options).toBeDefined(); }); }); }); ================================================ FILE: src/copy.ts ================================================ import { mkdirpSync } from 'fs-extra'; import { dirname as pathDirname, join as pathJoin, relative as pathRelative, resolve as pathResolve } from 'path'; import { Logger } from './logger/logger'; import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config'; import * as Constants from './util/constants'; import { emit, EventType } from './util/events'; import { generateGlobTasks, globAll, GlobObject, GlobResult } from './util/glob-util'; import { copyFileAsync, getBooleanPropertyValue, rimRafAsync, unlinkAsync } from './util/helpers'; import { BuildContext, ChangedFile, TaskInfo } from './util/interfaces'; import { Watcher, copyUpdate as watchCopyUpdate } from './watch'; const copyFilePathCache = new Map(); const FILTER_OUT_DIRS_FOR_CLEAN = ['{{WWW}}', '{{BUILD}}']; export function copy(context: BuildContext, configFile?: string) { configFile = getUserConfigFile(context, taskInfo, configFile); const logger = new Logger('copy'); return copyWorker(context, configFile) .then(() => { logger.finish(); }) .catch(err => { throw logger.fail(err); }); } export function copyWorker(context: BuildContext, configFile: string) { const copyConfig: CopyConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); const keys = Object.keys(copyConfig); const directoriesToCreate = new Set(); const toCopyList: CopyToFrom[] = []; return Promise.resolve().then(() => { // for each entry, make sure each glob in the list of globs has had string replacement performed on it cleanConfigContent(keys, copyConfig, context); return getFilesPathsForConfig(keys, copyConfig); }).then((resultMap: Map) => { // sweet, we have the absolute path of the files in the glob, and the ability to get the relative path // basically, we've got a stew goin' return populateFileAndDirectoryInfo(resultMap, copyConfig, toCopyList, directoriesToCreate); }).then(() => { if (getBooleanPropertyValue(Constants.ENV_CLEAN_BEFORE_COPY)) { cleanDirectories(context, directoriesToCreate); } }).then(() => { // create the directories synchronously to avoid any disk locking issues const directoryPathList = Array.from(directoriesToCreate); for (const directoryPath of directoryPathList) { mkdirpSync(directoryPath); } }).then(() => { // sweet, the directories are created, so now let's stream the files const promises: Promise[] = []; for (const file of toCopyList) { cacheCopyData(file); const promise = copyFileAsync(file.absoluteSourcePath, file.absoluteDestPath); promise.then(() => { Logger.debug(`Successfully copied ${file.absoluteSourcePath} to ${file.absoluteDestPath}`); }).catch(err => { Logger.warn(`Failed to copy ${file.absoluteSourcePath} to ${file.absoluteDestPath}`); }); promises.push(promise); } return Promise.all(promises); }); } export function copyUpdate(changedFiles: ChangedFile[], context: BuildContext) { const logger = new Logger('copy update'); const configFile = getUserConfigFile(context, taskInfo, null); const copyConfig: CopyConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); const keys = Object.keys(copyConfig); const directoriesToCreate = new Set(); const toCopyList: CopyToFrom[] = []; return Promise.resolve().then(() => { changedFiles.forEach(changedFile => Logger.debug(`copyUpdate, event: ${changedFile.event}, path: ${changedFile.filePath}`)); // for each entry, make sure each glob in the list of globs has had string replacement performed on it cleanConfigContent(keys, copyConfig, context); return getFilesPathsForConfig(keys, copyConfig); }).then((resultMap: Map) => { // sweet, we have the absolute path of the files in the glob, and the ability to get the relative path // basically, we've got a stew goin' return populateFileAndDirectoryInfo(resultMap, copyConfig, toCopyList, directoriesToCreate); }).then(() => { // first, process any deleted directories const promises: Promise[] = []; const directoryDeletions = changedFiles.filter(changedFile => changedFile.event === 'unlinkDir'); directoryDeletions.forEach(changedFile => promises.push(processRemoveDir(changedFile))); return Promise.all(promises); }).then(() => { // process any deleted files const promises: Promise[] = []; const fileDeletions = changedFiles.filter(changedFile => changedFile.event === 'unlink'); fileDeletions.forEach(changedFile => promises.push(processRemoveFile(changedFile))); return Promise.all(promises); }).then(() => { const promises: Promise[] = []; const additions = changedFiles.filter(changedFile => changedFile.event === 'change' || changedFile.event === 'add' || changedFile.event === 'addDir'); additions.forEach(changedFile => { const matchingItems = toCopyList.filter(toCopyEntry => toCopyEntry.absoluteSourcePath === changedFile.filePath); matchingItems.forEach(matchingItem => { // create the directories first (if needed) mkdirpSync(pathDirname(matchingItem.absoluteDestPath)); // cache the data and copy the files cacheCopyData(matchingItem); promises.push(copyFileAsync(matchingItem.absoluteSourcePath, matchingItem.absoluteDestPath)); emit(EventType.FileChange, additions); }); }); return Promise.all(promises); }).then(() => { logger.finish('green', true); Logger.newLine(); }).catch(err => { throw logger.fail(err); }); } function cleanDirectories(context: BuildContext, directoriesToCreate: Set) { const filterOut = replacePathVars(context, FILTER_OUT_DIRS_FOR_CLEAN); const directoryPathList = Array.from(directoriesToCreate); // filter out any directories that we don't want to allow a clean on const cleanableDirectories = directoryPathList.filter(directoryPath => { for (const uncleanableDir of filterOut) { if (uncleanableDir === directoryPath) { return false; } } return true; }); return deleteDirectories(cleanableDirectories); } function deleteDirectories(directoryPaths: string[]) { const promises: Promise[] = []; for (const directoryPath of directoryPaths) { promises.push(rimRafAsync(directoryPath)); } return Promise.all(promises); } function processRemoveFile(changedFile: ChangedFile) { // delete any destination files that match the source file const list = copyFilePathCache.get(changedFile.filePath) || []; copyFilePathCache.delete(changedFile.filePath); const promises: Promise[] = []; const deletedFilePaths: string[] = []; list.forEach(copiedFile => { const promise = unlinkAsync(copiedFile.absoluteDestPath); promises.push(promise); promise.catch(err => { if (err && err.message && err.message.indexOf('ENOENT') >= 0) { Logger.warn(`Failed to delete ${copiedFile.absoluteDestPath} because it doesn't exist`); return; } throw err; }); deletedFilePaths.push(copiedFile.absoluteDestPath); }); emit(EventType.FileDelete, deletedFilePaths); return Promise.all(promises).catch(err => { }); } function processRemoveDir(changedFile: ChangedFile): Promise { // remove any files from the cache where the dirname equals the provided path const keysToRemove: string[] = []; const directoriesToRemove = new Set(); copyFilePathCache.forEach((copiedFiles: CopyToFrom[], filePath: string) => { if (pathDirname(filePath) === changedFile.filePath) { keysToRemove.push(filePath); copiedFiles.forEach(copiedFile => directoriesToRemove.add(pathDirname(copiedFile.absoluteDestPath))); } }); keysToRemove.forEach(keyToRemove => copyFilePathCache.delete(keyToRemove)); // the entries are removed from the cache, now just rim raf the directoryPath const promises: Promise[] = []; directoriesToRemove.forEach(directoryToRemove => { promises.push(rimRafAsync(directoryToRemove)); }); emit(EventType.DirectoryDelete, Array.from(directoriesToRemove)); return Promise.all(promises); } function cacheCopyData(copyObject: CopyToFrom) { let list = copyFilePathCache.get(copyObject.absoluteSourcePath); if (!list) { list = []; } list.push(copyObject); copyFilePathCache.set(copyObject.absoluteSourcePath, list); } function getFilesPathsForConfig(copyConfigKeys: string[], copyConfig: CopyConfig): Promise> { // execute the glob functions to determine what files match each glob const srcToResultsMap = new Map(); const promises: Promise[] = []; copyConfigKeys.forEach(key => { const copyOptions = copyConfig[key]; if (copyOptions && copyOptions.src) { const promise = globAll(copyOptions.src); promises.push(promise); promise.then(globResultList => { srcToResultsMap.set(key, globResultList); }); } }); return Promise.all(promises).then(() => { return srcToResultsMap; }); } function populateFileAndDirectoryInfo(resultMap: Map, copyConfig: CopyConfig, toCopyList: CopyToFrom[], directoriesToCreate: Set) { resultMap.forEach((globResults: GlobResult[], dictionaryKey: string) => { globResults.forEach(globResult => { // get the relative path from the of each file from the glob const relativePath = pathRelative(globResult.base, globResult.absolutePath); // append the relative path to the dest const destFileName = pathResolve(pathJoin(copyConfig[dictionaryKey].dest, relativePath)); // store the file information toCopyList.push({ absoluteSourcePath: globResult.absolutePath, absoluteDestPath: destFileName }); const directoryToCreate = pathDirname(destFileName); directoriesToCreate.add(directoryToCreate); }); }); } function cleanConfigContent(dictionaryKeys: string[], copyConfig: CopyConfig, context: BuildContext) { dictionaryKeys.forEach(key => { const copyOption = copyConfig[key]; if (copyOption && copyOption.src && copyOption.src.length) { const cleanedUpSrc = replacePathVars(context, copyOption.src); copyOption.src = cleanedUpSrc; const cleanedUpDest = replacePathVars(context, copyOption.dest); copyOption.dest = cleanedUpDest; } }); } export function copyConfigToWatchConfig(context: BuildContext): Watcher { if (!context) { context = generateContext(context); } const configFile = getUserConfigFile(context, taskInfo, ''); const copyConfig: CopyConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); let results: GlobObject[] = []; for (const key of Object.keys(copyConfig)) { if (copyConfig[key] && copyConfig[key].src) { const list = generateGlobTasks(copyConfig[key].src, {}); results = results.concat(list); } } const paths: string[] = []; let ignored: string[] = []; for (const result of results) { paths.push(result.pattern); if (result.opts && result.opts.ignore) { ignored = ignored.concat(result.opts.ignore); } } return { paths: paths, options: { ignored: ignored }, callback: watchCopyUpdate }; } export interface CopySrcToDestResult { success: boolean; src: string; dest: string; errorMessage: string; } export const taskInfo: TaskInfo = { fullArg: '--copy', shortArg: '-y', envVar: 'IONIC_COPY', packageConfig: 'ionic_copy', defaultConfigFile: 'copy.config' }; export interface CopyConfig { [index: string]: CopyOptions; } export interface CopyToFrom { absoluteSourcePath: string; absoluteDestPath: string; } export interface CopyOptions { // https://www.npmjs.com/package/fs-extra src: string[]; dest: string; } ================================================ FILE: src/core/bundle-components.ts ================================================ import { BuildContext, CoreCompiler } from '../util/interfaces'; import { Logger } from '../logger/logger'; import * as fs from 'fs'; import * as path from 'path'; import * as nodeSass from 'node-sass'; import * as rollup from 'rollup'; import * as typescript from 'typescript'; import * as uglify from 'uglify-es'; import * as cleanCss from 'clean-css'; export function bundleCoreComponents(context: BuildContext) { const compiler = getCoreCompiler(context); if (!compiler) { Logger.debug(`skipping core component bundling`); return Promise.resolve(); } const config = { srcDir: context.coreDir, destDir: context.buildDir, attrCase: 'lower', packages: { cleanCss: cleanCss, fs: fs, path: path, nodeSass: nodeSass, rollup: rollup, typescript: typescript, uglify: uglify }, watch: context.isWatch }; return compiler.bundle(config).then(results => { if (results.errors) { results.errors.forEach((err: string) => { Logger.error(`compiler.bundle, results: ${err}`); }); } else if (results.componentRegistry) { // add the component registry to the global window.Ionic context.ionicGlobal = context.ionicGlobal || {}; context.ionicGlobal['components'] = results.componentRegistry; } }).catch(err => { if (err) { if (err.stack) { Logger.error(`compiler.bundle: ${err.stack}`); } else { Logger.error(`compiler.bundle: ${err}`); } } else { Logger.error(`compiler.bundle error`); } }); } function getCoreCompiler(context: BuildContext): CoreCompiler { try { return require(context.coreCompilerFilePath); } catch (e) { Logger.debug(`error loading core compiler: ${context.coreCompilerFilePath}, ${e}`); } return null; } ================================================ FILE: src/core/inject-script.spec.ts ================================================ import { injectCoreHtml } from './inject-scripts'; describe('Inject Scripts', () => { describe('injectCoreHtml', () => { it('should replace an existed injected script tag', () => { const inputHtml = '' + '\n' + '\n' + ' \n' + '\n' + '\n' + '\n' + ''; const output = injectCoreHtml(inputHtml, ' '); expect(output).toEqual( '\n' + '\n' + ' \n' + '\n' + '\n' + '\n' + ''); }); it('should replace only one existed injected script tag', () => { const inputHtml = '' + '\n' + '\n' + ' \n' + ' \n' + '\n' + '\n' + '\n' + ''; const output = injectCoreHtml(inputHtml, ' '); expect(output).toEqual( '\n' + '\n' + ' \n' + ' \n' + '\n' + '\n' + '\n' + ''); }); it('should add script to top of file when no html tag', () => { const inputHtml = '' + '\n' + ''; const output = injectCoreHtml(inputHtml, ''); expect(output).toEqual( '\n' + '\n' + ''); }); it('should add script below with attributes', () => { const inputHtml = '' + '\n' + '\n' + '\n' + ''; const output = injectCoreHtml(inputHtml, ''); expect(output).toEqual( '\n' + '\n' + '\n' + '\n' + ''); }); it('should add script below when no head tag', () => { const inputHtml = '' + '\n' + '\n' + '\n' + ''; const output = injectCoreHtml(inputHtml, ''); expect(output).toEqual( '\n' + '\n' + '\n' + '\n' + ''); }); it('should add script below ', () => { const inputHtml = '' + '\n' + '\n' + '\n' + '\n' + '\n' + ''; const output = injectCoreHtml(inputHtml, ''); expect(output).toEqual( '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + ''); }); it('should add script below with attributes and all caps tag', () => { const inputHtml = '' + '\n' + '\n' + '\n' + '\n' + '\n' + ''; const output = injectCoreHtml(inputHtml, ''); expect(output).toEqual( '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + ''); }); }); }); ================================================ FILE: src/core/inject-scripts.ts ================================================ import { BuildContext } from '../util/interfaces'; import { buildIonicGlobal } from './ionic-global'; import { readFileAsync, writeFileAsync } from '../util/helpers'; import { join } from 'path'; export function updateIndexHtml(context: BuildContext) { const indexPath = join(context.wwwDir, context.wwwIndex); return readFileAsync(indexPath).then(indexHtml => { if (!indexHtml) { return Promise.resolve(null); } indexHtml = injectCoreScripts(context, indexHtml); return writeFileAsync(indexPath, indexHtml); }); } export function injectCoreScripts(context: BuildContext, indexHtml: string) { const inject = []; inject.push(` `); return injectCoreHtml(indexHtml, inject.join('\n')); } export function injectCoreHtml(indexHtml: string, inject: string) { // see if we can find an existing ionic script tag and replace it entirely const existingTag = indexHtml.match(/ `; } ================================================ FILE: src/dev-server/lab.ts ================================================ import * as path from 'path'; import { buildCordovaConfig, CordovaProject } from '../util/cordova-config'; /** * Main Lab app view */ export let LabAppView = (req: any, res: any) => { return res.sendFile('index.html', {root: path.join(__dirname, '..', '..', 'lab')}); }; export let ApiCordovaProject = (req: any, res: any) => { buildCordovaConfig((err: any) => { res.status(400).json({ status: 'error', message: 'Unable to load config.xml' }); }, (config: CordovaProject) => { res.json(config); }); }; export let ApiPackageJson = (req: any, res: any) => { res.sendFile(path.join(process.cwd(), 'package.json'), { headers: { 'content-type': 'application/json' } }) }; ================================================ FILE: src/dev-server/live-reload.ts ================================================ import { ChangedFile } from '../util/interfaces'; import { hasDiagnostics } from '../logger/logger-diagnostics'; import * as path from 'path'; import * as tinylr from 'tiny-lr'; import { ServeConfig } from './serve-config'; import * as events from '../util/events'; export function createLiveReloadServer(config: ServeConfig) { const liveReloadServer = tinylr(); liveReloadServer.listen(config.liveReloadPort, config.host); function fileChange(changedFiles: ChangedFile[]) { // only do a live reload if there are no diagnostics // the notification server takes care of showing diagnostics if (!hasDiagnostics(config.buildDir)) { liveReloadServer.changed({ body: { files: changedFiles.map(changedFile => '/' + path.relative(config.wwwDir, changedFile.filePath)) } }); } } events.on(events.EventType.FileChange, fileChange); events.on(events.EventType.ReloadApp, () => { fileChange([{ event: 'change', ext: '.html', filePath: 'index.html'}]); }); } export function injectLiveReloadScript(content: any, host: string, port: Number): any { let contentStr = content.toString(); const liveReloadScript = getLiveReloadScript(host, port); if (contentStr.indexOf('/livereload.js') > -1) { // already added script return content; } let match = contentStr.match(/<\/body>(?![\s\S]*<\/body>)/i); if (!match) { match = contentStr.match(/<\/html>(?![\s\S]*<\/html>)/i); } if (match) { contentStr = contentStr.replace(match[0], `${liveReloadScript}\n${match[0]}`); } else { contentStr += liveReloadScript; } return contentStr; } function getLiveReloadScript(host: string, port: Number) { var src = `//${host}:${port}/livereload.js?snipver=1`; return ` \n `; } ================================================ FILE: src/dev-server/notification-server.ts ================================================ // Ionic Dev Server: Server Side Logger import { BuildUpdateMessage, WsMessage } from '../util/interfaces'; import { Logger } from '../logger/logger'; import { generateRuntimeDiagnosticContent } from '../logger/logger-runtime'; import { hasDiagnostics, getDiagnosticsHtmlContent } from '../logger/logger-diagnostics'; import { on, EventType } from '../util/events'; import { Server as WebSocketServer } from 'ws'; import { ServeConfig } from './serve-config'; export function createNotificationServer(config: ServeConfig) { let wsServer: any; const msgToClient: WsMessage[] = []; // queue up all messages to the client function queueMessageSend(msg: WsMessage) { msgToClient.push(msg); drainMessageQueue({ broadcast: true }); } // drain the queue messages when the server is ready function drainMessageQueue(options = { broadcast: false }) { let sendMethod = wsServer && wsServer.send; if (options.hasOwnProperty('broadcast') && options.broadcast) { sendMethod = wss.broadcast; } if (sendMethod && wss.clients.size > 0) { let msg: any; while (msg = msgToClient.shift()) { try { sendMethod(JSON.stringify(msg)); } catch (e) { if (e.message !== 'not opened' && e.message !== `Cannot read property 'readyState' of undefined`) { Logger.error(`error sending client ws - ${e.message}`); } } } } } // a build update has started, notify the client on(EventType.BuildUpdateStarted, (buildUpdateMsg: BuildUpdateMessage) => { const msg: WsMessage = { category: 'buildUpdate', type: 'started', data: { buildId: buildUpdateMsg.buildId, reloadApp: buildUpdateMsg.reloadApp, diagnosticsHtml: null } }; queueMessageSend(msg); }); // a build update has completed, notify the client on(EventType.BuildUpdateCompleted, (buildUpdateMsg: BuildUpdateMessage) => { const msg: WsMessage = { category: 'buildUpdate', type: 'completed', data: { buildId: buildUpdateMsg.buildId, reloadApp: buildUpdateMsg.reloadApp, diagnosticsHtml: hasDiagnostics(config.buildDir) ? getDiagnosticsHtmlContent(config.buildDir) : null } }; queueMessageSend(msg); }); // create web socket server const wss = new WebSocketServer({ host: config.host, port: config.notificationPort }); wss.broadcast = function broadcast(data: any) { wss.clients.forEach(function each(client: any) { client.send(data); }); }; wss.on('connection', (ws: any) => { // we've successfully connected wsServer = ws; wsServer.on('message', (incomingMessage: any) => { // incoming message from the client try { printMessageFromClient(JSON.parse(incomingMessage)); } catch (e) { Logger.error(`error opening ws message: ${incomingMessage}`); Logger.error(e.stack ? e.stack : e); } }); // now that we're connected, send off any messages // we might has already queued up drainMessageQueue(); }); function printMessageFromClient(msg: WsMessage) { if (msg && msg.data) { switch (msg.category) { case 'console': printConsole(msg); break; case 'runtimeError': handleRuntimeError(msg); break; } } } function printConsole(msg: WsMessage) { const args = msg.data; args[0] = `console.${msg.type}: ${args[0]}`; const log = args.join(' '); switch (msg.type) { case 'error': Logger.error(log); break; case 'warn': Logger.warn(log); break; case 'debug': Logger.debug(log); break; default: Logger.info(log); break; } } function handleRuntimeError(clientMsg: WsMessage) { const msg: WsMessage = { category: 'buildUpdate', type: 'completed', data: { diagnosticsHtml: generateRuntimeDiagnosticContent(config.rootDir, config.buildDir, clientMsg.data.message, clientMsg.data.stack) } }; queueMessageSend(msg); } } ================================================ FILE: src/dev-server/serve-config.ts ================================================ import * as path from 'path'; export interface ServeConfig { httpPort: number; host: string; hostBaseUrl: string; rootDir: string; wwwDir: string; buildDir: string; isCordovaServe: boolean; launchBrowser: boolean; launchLab: boolean; browserToLaunch: string; useLiveReload: boolean; liveReloadPort: Number; notificationPort: Number; useServerLogs: boolean; notifyOnConsoleLog: boolean; useProxy: boolean; devapp: boolean; } export const LOGGER_DIR = '__ion-dev-server'; export const IONIC_LAB_URL = '/ionic-lab'; export const IOS_PLATFORM_PATHS = [path.join('platforms', 'ios', 'www')]; export const ANDROID_PLATFORM_PATHS = [ path.join('platforms', 'android', 'assets', 'www'), path.join('platforms', 'android', 'app', 'src', 'main', 'assets', 'www') ]; ================================================ FILE: src/generators/constants.ts ================================================ export const CLASSNAME_VARIABLE = '$CLASSNAME'; export const TAB_CONTENT_VARIABLE = '$TAB_CONTENT'; export const TAB_VARIABLES_VARIABLE = '$TAB_VARIABLES'; export const TABS_IMPORTSTATEMENT_VARIABLE = '$TABS_IMPORTSTATEMENT'; export const FILENAME_VARIABLE = '$FILENAME'; export const PIPENAME_VARIABLE = '$PIPENAME'; export const SUPPLIEDNAME_VARIABLE = '$SUPPLIEDNAME'; export const IMPORTSTATEMENT_VARIABLE = '$IMPORTSTATEMENT'; export const IONICPAGE_VARIABLE = '$IONICPAGE'; export const KNOWN_FILE_EXTENSION = '.tmpl'; export const SPEC_FILE_EXTENSION = 'spec.ts'; export const NG_MODULE_FILE_EXTENSION = 'module.ts'; ================================================ FILE: src/generators/util.spec.ts ================================================ import { BuildContext } from '../util/interfaces'; import { basename, join } from 'path'; import * as fs from 'fs'; import * as Constants from '../util/constants'; import * as helpers from '../util/helpers'; import * as globUtils from '../util/glob-util'; import * as util from './util'; import * as GeneratorConstants from './constants'; import * as TypeScriptUtils from '../util/typescript-utils'; describe('util', () => { describe('hydrateRequest', () => { it('should take a component request and return a hydrated component request', () => { // arrange const baseDir = join(process.cwd(), 'someDir', 'project'); const componentsDir = join(baseDir, 'src', 'components'); const context = { componentsDir: componentsDir }; const request = { type: Constants.COMPONENT, name: 'settings view', includeSpec: true, includeNgModule: true }; const templateDir = join( baseDir, 'node_modules', 'ionic-angular', 'templates' ); spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue( templateDir ); // act const hydratedRequest = util.hydrateRequest(context, request); // assert expect(hydratedRequest).toEqual({ className: 'SettingsViewComponent', dirToRead: join(templateDir, 'component'), dirToWrite: join(componentsDir, 'settings-view'), fileName: 'settings-view', importStatement: 'import { IonicPage, NavController, NavParams } from \'ionic-angular\';', includeNgModule: true, includeSpec: true, ionicPage: '\n@IonicPage()', name: 'settings view', type: 'component' }); expect(hydratedRequest.type).toEqual(Constants.COMPONENT); expect(hydratedRequest.name).toEqual(request.name); expect(hydratedRequest.includeNgModule).toBeTruthy(); expect(hydratedRequest.includeSpec).toBeTruthy(); expect(hydratedRequest.className).toEqual('SettingsViewComponent'); expect(hydratedRequest.fileName).toEqual('settings-view'); expect(hydratedRequest.dirToRead).toEqual( join(templateDir, Constants.COMPONENT) ); expect(hydratedRequest.dirToWrite).toEqual( join(componentsDir, hydratedRequest.fileName) ); }); it('should take a page request and return a hydrated page request', () => { // arrange const baseDir = join(process.cwd(), 'someDir', 'project'); const pagesDir = join(baseDir, 'src', 'pages'); const context = { pagesDir: pagesDir }; const request = { type: Constants.PAGE, name: 'settings view', includeSpec: true, includeNgModule: true }; const templateDir = join( baseDir, 'node_modules', 'ionic-angular', 'templates' ); spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue( templateDir ); // act const hydratedRequest = util.hydrateRequest(context, request); // assert expect(hydratedRequest).toEqual({ className: 'SettingsViewPage', dirToRead: join(templateDir, 'page'), dirToWrite: join(pagesDir, 'settings-view'), fileName: 'settings-view', importStatement: 'import { IonicPage, NavController, NavParams } from \'ionic-angular\';', includeNgModule: true, includeSpec: true, ionicPage: '\n@IonicPage()', name: 'settings view', type: 'page' }); expect(hydratedRequest.type).toEqual(Constants.PAGE); expect(hydratedRequest.name).toEqual(request.name); expect(hydratedRequest.includeNgModule).toBeTruthy(); expect(hydratedRequest.includeSpec).toBeTruthy(); expect(hydratedRequest.className).toEqual('SettingsViewPage'); expect(hydratedRequest.fileName).toEqual('settings-view'); expect(hydratedRequest.dirToRead).toEqual( join(templateDir, Constants.PAGE) ); expect(hydratedRequest.dirToWrite).toEqual( join(pagesDir, hydratedRequest.fileName) ); }); it('should take a page with no module request and return a hydrated page request', () => { // arrange const baseDir = join(process.cwd(), 'someDir', 'project'); const pagesDir = join(baseDir, 'src', 'pages'); const context = { pagesDir: pagesDir }; const includeNgModule = false; const request = { type: Constants.PAGE, name: 'about', includeSpec: true, includeNgModule: false }; const templateDir = join( baseDir, 'node_modules', 'ionic-angular', 'templates' ); spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue( templateDir ); // act const hydratedRequest = util.hydrateRequest(context, request); // assert expect(hydratedRequest).toEqual({ className: 'AboutPage', dirToRead: join(templateDir, 'page'), dirToWrite: join(pagesDir, 'about'), fileName: 'about', importStatement: 'import { NavController, NavParams } from \'ionic-angular\';', includeNgModule: false, includeSpec: true, ionicPage: null, name: 'about', type: 'page' }); expect(hydratedRequest.ionicPage).toEqual(null); expect(hydratedRequest.importStatement).toEqual( 'import { NavController, NavParams } from \'ionic-angular\';' ); expect(hydratedRequest.type).toEqual(Constants.PAGE); expect(hydratedRequest.name).toEqual(request.name); expect(hydratedRequest.includeNgModule).toBeFalsy(); expect(hydratedRequest.includeSpec).toBeTruthy(); expect(hydratedRequest.className).toEqual('AboutPage'); expect(hydratedRequest.fileName).toEqual('about'); expect(hydratedRequest.dirToRead).toEqual( join(templateDir, Constants.PAGE) ); expect(hydratedRequest.dirToWrite).toEqual( join(pagesDir, hydratedRequest.fileName) ); }); }); describe('hydrateTabRequest', () => { it('should take a lazy loaded page set the tab root to a string', () => { // arrange const baseDir = join(process.cwd(), 'someDir', 'project'); const pagesDir = join(baseDir, 'src', 'pages'); const templateDir = join( baseDir, 'node_modules', 'ionic-angular', 'templates' ); const context: BuildContext = { pagesDir }; const request = { type: 'tabs', name: 'stooges', includeNgModule: true, tabs: [ { includeNgModule: true, type: 'page', name: 'moe', className: 'MoePage', fileName: 'moe', dirToRead: join(templateDir, 'page'), dirToWrite: join(pagesDir, 'moe') } ] }; spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue( templateDir ); // act const hydatedTabRequest = util.hydrateTabRequest(context, request); // assert expect(hydatedTabRequest.tabVariables).toEqual(` moeRoot = 'MoePage'\n`); }); it('should take a page set the tab root to a component ref', () => { // arrange const baseDir = join(process.cwd(), 'someDir', 'project'); const pagesDir = join(baseDir, 'src', 'pages'); const templateDir = join( baseDir, 'node_modules', 'ionic-angular', 'templates' ); const context: BuildContext = { pagesDir }; const request = { type: 'tabs', name: 'stooges', includeNgModule: false, tabs: [ { includeNgModule: false, type: 'page', name: 'moe', className: 'MoePage', fileName: 'moe', dirToRead: join(templateDir, 'page'), dirToWrite: join(pagesDir, 'moe') } ] }; spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue( templateDir ); // act const hydatedTabRequest = util.hydrateTabRequest(context, request); // assert expect(hydatedTabRequest.tabVariables).toEqual(' moeRoot = MoePage\n'); }); }); describe('readTemplates', () => { it('should get a map of templates and their content back', () => { // arrange const templateDir = '/Users/noone/project/node_modules/ionic-angular/templates/component'; const knownValues = [ 'html.tmpl', 'scss.tmpl', 'spec.ts.tmpl', 'ts.tmpl', 'module.tmpl' ]; const fileContent = 'SomeContent'; spyOn(fs, 'readdirSync').and.returnValue(knownValues); spyOn(helpers, helpers.readFileAsync.name).and.returnValue( Promise.resolve(fileContent) ); // act const promise = util.readTemplates(templateDir); // assert return promise.then((map: Map) => { expect(map.get(join(templateDir, knownValues[0]))).toEqual(fileContent); expect(map.get(join(templateDir, knownValues[1]))).toEqual(fileContent); expect(map.get(join(templateDir, knownValues[2]))).toEqual(fileContent); expect(map.get(join(templateDir, knownValues[3]))).toEqual(fileContent); expect(map.get(join(templateDir, knownValues[4]))).toEqual(fileContent); }); }); }); describe('filterOutTemplates', () => { it('should preserve all templates', () => { const map = new Map(); const templateDir = '/Users/noone/project/node_modules/ionic-angular/templates/component'; const fileContent = 'SomeContent'; const knownValues = [ 'html.tmpl', 'scss.tmpl', 'spec.ts.tmpl', 'ts.tmpl', 'module.tmpl' ]; map.set(join(templateDir, knownValues[0]), fileContent); map.set(join(templateDir, knownValues[1]), fileContent); map.set(join(templateDir, knownValues[2]), fileContent); map.set(join(templateDir, knownValues[3]), fileContent); map.set(join(templateDir, knownValues[4]), fileContent); const newMap = util.filterOutTemplates( { includeNgModule: true, includeSpec: true }, map ); expect(newMap.size).toEqual(knownValues.length); }); it('should remove spec', () => { const map = new Map(); const templateDir = '/Users/noone/project/node_modules/ionic-angular/templates/component'; const fileContent = 'SomeContent'; const knownValues = [ 'html.tmpl', 'scss.tmpl', 'spec.ts.tmpl', 'ts.tmpl', 'module.tmpl' ]; map.set(join(templateDir, knownValues[0]), fileContent); map.set(join(templateDir, knownValues[1]), fileContent); map.set(join(templateDir, knownValues[2]), fileContent); map.set(join(templateDir, knownValues[3]), fileContent); map.set(join(templateDir, knownValues[4]), fileContent); const newMap = util.filterOutTemplates( { includeNgModule: true, includeSpec: false }, map ); expect(newMap.size).toEqual(4); expect(newMap.get(join(templateDir, knownValues[0]))).toBeTruthy(); expect(newMap.get(join(templateDir, knownValues[1]))).toBeTruthy(); expect(newMap.get(join(templateDir, knownValues[2]))).toBeFalsy(); expect(newMap.get(join(templateDir, knownValues[3]))).toBeTruthy(); expect(newMap.get(join(templateDir, knownValues[4]))).toBeTruthy(); }); it('should remove spec and module', () => { const map = new Map(); const templateDir = '/Users/noone/project/node_modules/ionic-angular/templates/component'; const fileContent = 'SomeContent'; const knownValues = [ 'html.tmpl', 'scss.tmpl', 'spec.ts.tmpl', 'ts.tmpl', 'module.ts.tmpl' ]; map.set(join(templateDir, knownValues[0]), fileContent); map.set(join(templateDir, knownValues[1]), fileContent); map.set(join(templateDir, knownValues[2]), fileContent); map.set(join(templateDir, knownValues[3]), fileContent); map.set(join(templateDir, knownValues[4]), fileContent); const newMap = util.filterOutTemplates( { includeNgModule: false, includeSpec: false }, map ); expect(newMap.size).toEqual(3); expect(newMap.get(join(templateDir, knownValues[0]))).toBeTruthy(); expect(newMap.get(join(templateDir, knownValues[1]))).toBeTruthy(); expect(newMap.get(join(templateDir, knownValues[2]))).toBeFalsy(); expect(newMap.get(join(templateDir, knownValues[3]))).toBeTruthy(); expect(newMap.get(join(templateDir, knownValues[4]))).toBeFalsy(); }); }); describe('applyTemplates', () => { it('should replace the template content', () => { const fileOne = '/Users/noone/fileOne'; const fileOneContent = ` {{text}} `; const fileTwo = '/Users/noone/fileTwo'; const fileTwoContent = ` $FILENAME { } `; const fileThree = '/Users/noone/fileThree'; const fileThreeContent = ` describe('$CLASSNAME', () => { it('should do something', () => { expect(true).toEqual(true); }); }); `; const fileFour = '/Users/noone/fileFour'; const fileFourContent = ` import { Component } from '@angular/core'; /* Generated class for the $CLASSNAME component. See https://angular.io/docs/ts/latest/api/core/index/ComponentMetadata-class.html for more info on Angular 2 Components. */ @Component({ selector: '$FILENAME', templateUrl: '$FILENAME.html' }) export class $CLASSNAMEComponent { text: string; constructor() { console.log('Hello $CLASSNAME Component'); this.text = 'Hello World'; } } `; const fileFive = '/Users/noone/fileFive'; const fileFiveContent = ` import { NgModule } from '@angular/core'; import { $CLASSNAME } from './$FILENAME'; import { IonicModule } from 'ionic-angular'; @NgModule({ declarations: [ $CLASSNAME, ], imports: [ IonicModule.forChild($CLASSNAME) ], entryComponents: [ $CLASSNAME ], providers: [] }) export class $CLASSNAMEModule {} `; const fileSix = '/Users/noone/fileSix'; const fileSixContent = ` $SUPPLIEDNAME `; const fileSeven = '/Users/noone/fileSeven'; const fileSevenContent = ` $TAB_CONTENT `; const map = new Map(); map.set(fileOne, fileOneContent); map.set(fileTwo, fileTwoContent); map.set(fileThree, fileThreeContent); map.set(fileFour, fileFourContent); map.set(fileFive, fileFiveContent); map.set(fileSix, fileSixContent); map.set(fileSeven, fileSevenContent); const className = 'SettingsView'; const fileName = 'settings-view'; const suppliedName = 'settings view'; const results = util.applyTemplates( { name: suppliedName, className: className, fileName: fileName }, map ); const modifiedContentOne = results.get(fileOne); const modifiedContentTwo = results.get(fileTwo); const modifiedContentThree = results.get(fileThree); const modifiedContentFour = results.get(fileFour); const modifiedContentFive = results.get(fileFive); const modifiedContentSix = results.get(fileSix); const modifiedContentSeven = results.get(fileSeven); const nonExistentVars = [ GeneratorConstants.CLASSNAME_VARIABLE, GeneratorConstants.FILENAME_VARIABLE, GeneratorConstants.SUPPLIEDNAME_VARIABLE, GeneratorConstants.TAB_CONTENT_VARIABLE, GeneratorConstants.TAB_VARIABLES_VARIABLE ]; for (let v of nonExistentVars) { expect(modifiedContentOne.indexOf(v)).toEqual(-1); expect(modifiedContentTwo.indexOf(v)).toEqual(-1); expect(modifiedContentThree.indexOf(v)).toEqual(-1); expect(modifiedContentFour.indexOf(v)).toEqual(-1); expect(modifiedContentFive.indexOf(v)).toEqual(-1); expect(modifiedContentSix.indexOf(v)).toEqual(-1); expect(modifiedContentSeven.indexOf(v)).toEqual(-1); } }); }); describe('writeGeneratedFiles', () => { it('should return the list of files generated', () => { const map = new Map(); const templateDir = '/Users/noone/project/node_modules/ionic-angular/templates/component'; const fileContent = 'SomeContent'; const knownValues = [ 'html.tmpl', 'scss.tmpl', 'spec.ts.tmpl', 'ts.tmpl', 'module.tmpl' ]; const fileName = 'settings-view'; const dirToWrite = join('/Users/noone/project/src/components', fileName); map.set(join(templateDir, knownValues[0]), fileContent); map.set(join(templateDir, knownValues[1]), fileContent); map.set(join(templateDir, knownValues[2]), fileContent); map.set(join(templateDir, knownValues[3]), fileContent); map.set(join(templateDir, knownValues[4]), fileContent); spyOn(helpers, helpers.mkDirpAsync.name).and.returnValue( Promise.resolve() ); spyOn(helpers, helpers.writeFileAsync.name).and.returnValue( Promise.resolve() ); const promise = util.writeGeneratedFiles( { dirToWrite: dirToWrite, fileName: fileName }, map ); return promise.then((filesCreated: string[]) => { const fileExtensions = knownValues.map(knownValue => basename(knownValue, GeneratorConstants.KNOWN_FILE_EXTENSION) ); expect(filesCreated[0]).toEqual( join(dirToWrite, `${fileName}.${fileExtensions[0]}`) ); expect(filesCreated[1]).toEqual( join(dirToWrite, `${fileName}.${fileExtensions[1]}`) ); expect(filesCreated[2]).toEqual( join(dirToWrite, `${fileName}.${fileExtensions[2]}`) ); expect(filesCreated[3]).toEqual( join(dirToWrite, `${fileName}.${fileExtensions[3]}`) ); expect(filesCreated[4]).toEqual( join(dirToWrite, `${fileName}.${fileExtensions[4]}`) ); }); }); }); describe('getDirToWriteToByType', () => { let context: any; const componentsDir = '/path/to/components'; const directivesDir = '/path/to/directives'; const pagesDir = '/path/to/pages'; const pipesDir = '/path/to/pipes'; const providersDir = '/path/to/providers'; beforeEach(() => { context = { componentsDir, directivesDir, pagesDir, pipesDir, providersDir }; }); it('should return the appropriate components directory', () => { expect(util.getDirToWriteToByType(context, 'component')).toEqual( componentsDir ); }); it('should return the appropriate directives directory', () => { expect(util.getDirToWriteToByType(context, 'directive')).toEqual( directivesDir ); }); it('should return the appropriate pages directory', () => { expect(util.getDirToWriteToByType(context, 'page')).toEqual(pagesDir); }); it('should return the appropriate pipes directory', () => { expect(util.getDirToWriteToByType(context, 'pipe')).toEqual(pipesDir); }); it('should return the appropriate providers directory', () => { expect(util.getDirToWriteToByType(context, 'provider')).toEqual( providersDir ); }); it('should throw error upon unknown generator type', () => { expect(() => util.getDirToWriteToByType(context, 'dan')).toThrowError( 'Unknown Generator Type: dan' ); }); }); describe('getNgModules', () => { let context: any; const componentsDir = join(process.cwd(), 'path', 'to', 'components'); const directivesDir = join(process.cwd(), 'path', 'to', 'directives'); const pagesDir = join(process.cwd(), 'path', 'to', 'pages'); const pipesDir = join(process.cwd(), 'path', 'to', 'pipes'); const providersDir = join(process.cwd(), 'path', 'to', 'providers'); beforeEach(() => { context = { componentsDir, directivesDir, pagesDir, pipesDir, providersDir }; }); it('should return an empty list of glob results', () => { const globAllSpy = spyOn(globUtils, globUtils.globAll.name); util.getNgModules(context, []); expect(globAllSpy).toHaveBeenCalledWith([]); }); it('should return a list of glob results for components', () => { const globAllSpy = spyOn(globUtils, globUtils.globAll.name); spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue( '.module.ts' ); util.getNgModules(context, ['component']); expect(globAllSpy).toHaveBeenCalledWith([ join(componentsDir, '**', '*.module.ts') ]); }); it('should return a list of glob results for pages and components', () => { const globAllSpy = spyOn(globUtils, globUtils.globAll.name); spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue( '.module.ts' ); util.getNgModules(context, ['page', 'component']); expect(globAllSpy).toHaveBeenCalledWith([ join(pagesDir, '**', '*.module.ts'), join(componentsDir, '**', '*.module.ts') ]); }); }); }); ================================================ FILE: src/generators/util.ts ================================================ import { basename, dirname, extname, join, relative, sep } from 'path'; import { readdirSync, existsSync, writeFileSync, openSync, closeSync } from 'fs'; import { Logger } from '../logger/logger'; import { toUnixPath } from '../util/helpers'; import * as Constants from '../util/constants'; import * as GeneratorConstants from './constants'; import { camelCase, constantCase, getStringPropertyValue, mkDirpAsync, paramCase, pascalCase, readFileAsync, replaceAll, sentenceCase, upperCaseFirst, writeFileAsync } from '../util/helpers'; import { BuildContext } from '../util/interfaces'; import { globAll, GlobResult } from '../util/glob-util'; import { changeExtension, ensureSuffix, removeSuffix } from '../util/helpers'; import { appendNgModuleDeclaration, appendNgModuleExports, appendNgModuleProvider, insertNamedImportIfNeeded } from '../util/typescript-utils'; export function hydrateRequest(context: BuildContext, request: GeneratorRequest) { const hydrated = request as HydratedGeneratorRequest; const suffix = getSuffixFromGeneratorType(context, request.type); hydrated.className = ensureSuffix(pascalCase(request.name), upperCaseFirst(suffix)); hydrated.fileName = removeSuffix(paramCase(request.name), `-${paramCase(suffix)}`); if (request.type === 'pipe') hydrated.pipeName = camelCase(request.name); if (!!hydrated.includeNgModule) { if (hydrated.type === 'tabs') { hydrated.importStatement = `import { IonicPage, NavController } from 'ionic-angular';`; } else { hydrated.importStatement = `import { IonicPage, NavController, NavParams } from 'ionic-angular';`; } hydrated.ionicPage = '\n@IonicPage()'; } else { hydrated.ionicPage = null; hydrated.importStatement = `import { NavController, NavParams } from 'ionic-angular';`; } hydrated.dirToRead = join(getStringPropertyValue(Constants.ENV_VAR_IONIC_ANGULAR_TEMPLATE_DIR), request.type); const baseDir = getDirToWriteToByType(context, request.type); hydrated.dirToWrite = join(baseDir, hydrated.fileName); return hydrated; } export function createCommonModule(envVar: string, requestType: string) { let className = requestType.charAt(0).toUpperCase() + requestType.slice(1) + 's'; let tmplt = `import { NgModule } from '@angular/core';\n@NgModule({\n\tdeclarations: [],\n\timports: [],\n\texports: []\n})\nexport class ${className}Module {}\n`; writeFileSync(envVar, tmplt); } export function hydrateTabRequest(context: BuildContext, request: GeneratorTabRequest) { const h = hydrateRequest(context, request); const hydrated = Object.assign({ tabs: request.tabs, tabContent: '', tabVariables: '', tabsImportStatement: '', }, h) as HydratedGeneratorRequest; if (hydrated.includeNgModule) { hydrated.tabsImportStatement += `import { IonicPage, NavController } from 'ionic-angular';`; } else { hydrated.tabsImportStatement += `import { NavController } from 'ionic-angular';`; } for (let i = 0; i < request.tabs.length; i++) { const tabVar = `${camelCase(request.tabs[i].name)}Root`; if (hydrated.includeNgModule) { hydrated.tabVariables += ` ${tabVar} = '${request.tabs[i].className}'\n`; } else { hydrated.tabVariables += ` ${tabVar} = ${request.tabs[i].className}\n`; } // If this is the last ion-tab to insert // then we do not want a new line if (i === request.tabs.length - 1) { hydrated.tabContent += ` `; } else { hydrated.tabContent += ` \n`; } } return hydrated; } export function readTemplates(pathToRead: string): Promise> { const fileNames = readdirSync(pathToRead); const absolutePaths = fileNames.map(fileName => { return join(pathToRead, fileName); }); const filePathToContent = new Map(); const promises = absolutePaths.map(absolutePath => { const promise = readFileAsync(absolutePath); promise.then((fileContent: string) => { filePathToContent.set(absolutePath, fileContent); }); return promise; }); return Promise.all(promises).then(() => { return filePathToContent; }); } export function filterOutTemplates(request: HydratedGeneratorRequest, templates: Map) { const templatesToUseMap = new Map(); templates.forEach((fileContent: string, filePath: string) => { const newFileExtension = basename(filePath, GeneratorConstants.KNOWN_FILE_EXTENSION); const shouldSkip = (!request.includeNgModule && newFileExtension === GeneratorConstants.NG_MODULE_FILE_EXTENSION) || (!request.includeSpec && newFileExtension === GeneratorConstants.SPEC_FILE_EXTENSION); if (!shouldSkip) { templatesToUseMap.set(filePath, fileContent); } }); return templatesToUseMap; } export function applyTemplates(request: HydratedGeneratorRequest, templates: Map) { const appliedTemplateMap = new Map(); templates.forEach((fileContent: string, filePath: string) => { fileContent = replaceAll(fileContent, GeneratorConstants.CLASSNAME_VARIABLE, request.className); fileContent = replaceAll(fileContent, GeneratorConstants.PIPENAME_VARIABLE, request.pipeName); fileContent = replaceAll(fileContent, GeneratorConstants.IMPORTSTATEMENT_VARIABLE, request.importStatement); fileContent = replaceAll(fileContent, GeneratorConstants.IONICPAGE_VARIABLE, request.ionicPage); fileContent = replaceAll(fileContent, GeneratorConstants.FILENAME_VARIABLE, request.fileName); fileContent = replaceAll(fileContent, GeneratorConstants.SUPPLIEDNAME_VARIABLE, request.name); fileContent = replaceAll(fileContent, GeneratorConstants.TAB_CONTENT_VARIABLE, request.tabContent); fileContent = replaceAll(fileContent, GeneratorConstants.TAB_VARIABLES_VARIABLE, request.tabVariables); fileContent = replaceAll(fileContent, GeneratorConstants.TABS_IMPORTSTATEMENT_VARIABLE, request.tabsImportStatement); appliedTemplateMap.set(filePath, fileContent); }); return appliedTemplateMap; } export function writeGeneratedFiles(request: HydratedGeneratorRequest, processedTemplates: Map): Promise { const promises: Promise[] = []; const createdFileList: string[] = []; processedTemplates.forEach((fileContent: string, filePath: string) => { const newFileExtension = basename(filePath, GeneratorConstants.KNOWN_FILE_EXTENSION); const newFileName = `${request.fileName}.${newFileExtension}`; const fileToWrite = join(request.dirToWrite, newFileName); createdFileList.push(fileToWrite); promises.push(createDirAndWriteFile(fileToWrite, fileContent)); }); return Promise.all(promises).then(() => { return createdFileList; }); } function createDirAndWriteFile(filePath: string, fileContent: string) { const directory = dirname(filePath); return mkDirpAsync(directory).then(() => { return writeFileAsync(filePath, fileContent); }); } export function getNgModules(context: BuildContext, types: string[]): Promise { const ngModuleSuffix = getStringPropertyValue(Constants.ENV_NG_MODULE_FILE_NAME_SUFFIX); const patterns = types.map((type) => join(getDirToWriteToByType(context, type), '**', `*${ngModuleSuffix}`)); return globAll(patterns); } function getSuffixFromGeneratorType(context: BuildContext, type: string) { if (type === Constants.COMPONENT) { return 'Component'; } else if (type === Constants.DIRECTIVE) { return 'Directive'; } else if (type === Constants.PAGE || type === Constants.TABS) { return 'Page'; } else if (type === Constants.PIPE) { return 'Pipe'; } else if (type === Constants.PROVIDER) { return 'Provider'; } throw new Error(`Unknown Generator Type: ${type}`); } export function getDirToWriteToByType(context: BuildContext, type: string) { if (type === Constants.COMPONENT) { return context.componentsDir; } else if (type === Constants.DIRECTIVE) { return context.directivesDir; } else if (type === Constants.PAGE || type === Constants.TABS) { return context.pagesDir; } else if (type === Constants.PIPE) { return context.pipesDir; } else if (type === Constants.PROVIDER) { return context.providersDir; } throw new Error(`Unknown Generator Type: ${type}`); } export async function nonPageFileManipulation(context: BuildContext, name: string, ngModulePath: string, type: string) { const hydratedRequest = hydrateRequest(context, { type, name }); const envVar = getStringPropertyValue(`IONIC_${hydratedRequest.type.toUpperCase()}S_NG_MODULE_PATH`); let importPath; let fileContent: string; let templatesArray: string[] = await generateTemplates(context, hydratedRequest, false); if (hydratedRequest.type === 'pipe' || hydratedRequest.type === 'component' || hydratedRequest.type === 'directive') { if (!existsSync(envVar)) createCommonModule(envVar, hydratedRequest.type); } const typescriptFilePath = changeExtension(templatesArray.filter(path => extname(path) === '.ts')[0], ''); readFileAsync(ngModulePath).then((content) => { importPath = type === 'pipe' || type === 'component' || type === 'directive' // Insert `./` if it's a pipe component or directive // Since these will go in a common module. ? toUnixPath(`./${relative(dirname(ngModulePath), hydratedRequest.dirToWrite)}${sep}${hydratedRequest.fileName}`) : toUnixPath(`${relative(dirname(ngModulePath), hydratedRequest.dirToWrite)}${sep}${hydratedRequest.fileName}`); content = insertNamedImportIfNeeded(ngModulePath, content, hydratedRequest.className, importPath); if (type === 'pipe' || type === 'component' || type === 'directive') { content = appendNgModuleDeclaration(ngModulePath, content, hydratedRequest.className); content = appendNgModuleExports(ngModulePath, content, hydratedRequest.className); } if (type === 'provider') { content = appendNgModuleProvider(ngModulePath, content, hydratedRequest.className); } return writeFileAsync(ngModulePath, content); }); } export function tabsModuleManipulation(tabs: string[][], hydratedRequest: HydratedGeneratorRequest, tabHydratedRequests: HydratedGeneratorRequest[]): Promise { tabHydratedRequests.forEach((tabRequest, index) => { tabRequest.generatedFileNames = tabs[index]; }); const ngModulePath = tabs[0].find((element: any): boolean => element.indexOf('module') !== -1); if (!ngModulePath) { // Static imports const tabsPath = join(hydratedRequest.dirToWrite, `${hydratedRequest.fileName}.ts`); let modifiedContent: string = null; return readFileAsync(tabsPath).then(content => { tabHydratedRequests.forEach((tabRequest) => { const typescriptFilePath = changeExtension(tabRequest.generatedFileNames.filter(path => extname(path) === '.ts')[0], ''); const importPath = toUnixPath(relative(dirname(tabsPath), typescriptFilePath)); modifiedContent = insertNamedImportIfNeeded(tabsPath, content, tabRequest.className, importPath); content = modifiedContent; }); return writeFileAsync(tabsPath, modifiedContent); }); } } export function generateTemplates(context: BuildContext, request: HydratedGeneratorRequest, includePageConstants?: boolean): Promise { Logger.debug('[Generators] generateTemplates: Reading templates ...'); let pageConstantFile = join(context.pagesDir, 'pages.constants.ts'); if (includePageConstants && !existsSync(pageConstantFile)) createPageConstants(context); return readTemplates(request.dirToRead).then((map: Map) => { Logger.debug('[Generators] generateTemplates: Filtering out NgModule and Specs if needed ...'); return filterOutTemplates(request, map); }).then((filteredMap: Map) => { Logger.debug('[Generators] generateTemplates: Applying templates ...'); const appliedTemplateMap = applyTemplates(request, filteredMap); Logger.debug('[Generators] generateTemplates: Writing generated files to disk ...'); // Adding const to gets some type completion if (includePageConstants) createConstStatments(pageConstantFile, request); return writeGeneratedFiles(request, appliedTemplateMap); }); } export function createConstStatments(pageConstantFile: string, request: HydratedGeneratorRequest) { readFileAsync(pageConstantFile).then((content) => { content += `\nexport const ${constantCase(request.className)} = '${request.className}';`; writeFileAsync(pageConstantFile, content); }); } export function createPageConstants(context: BuildContext) { let pageConstantFile = join(context.pagesDir, 'pages.constants.ts'); writeFileAsync(pageConstantFile, '//Constants for getting type references'); } export interface GeneratorOption { type: string; multiple: boolean; } export interface GeneratorRequest { type?: string; name?: string; includeSpec?: boolean; includeNgModule?: boolean; } export interface GeneratorTabRequest extends GeneratorRequest { tabs?: HydratedGeneratorRequest[]; } export interface HydratedGeneratorRequest extends GeneratorRequest { fileName?: string; importStatement?: string; ionicPage?: string; className?: string; tabContent?: string; tabVariables?: string; tabsImportStatement?: string; dirToRead?: string; dirToWrite?: string; generatedFileNames?: string[]; pipeName?: string; } ================================================ FILE: src/generators.ts ================================================ import * as Constants from './util/constants'; import { BuildContext } from './util/interfaces'; import { hydrateRequest, hydrateTabRequest, getNgModules, GeneratorOption, GeneratorRequest, nonPageFileManipulation, generateTemplates, tabsModuleManipulation } from './generators/util'; export { getNgModules, GeneratorOption, GeneratorRequest }; export function processPageRequest(context: BuildContext, name: string, commandOptions?: { module?: boolean; constants?: boolean; }) { if (commandOptions) { const hydratedRequest = hydrateRequest(context, { type: 'page', name, includeNgModule: commandOptions.module }); return generateTemplates(context, hydratedRequest, commandOptions.constants); }else { const hydratedRequest = hydrateRequest(context, { type: 'page', name, includeNgModule: false }); return generateTemplates(context, hydratedRequest); } } export function processPipeRequest(context: BuildContext, name: string, ngModulePath: string) { return nonPageFileManipulation(context, name, ngModulePath, 'pipe'); } export function processDirectiveRequest(context: BuildContext, name: string, ngModulePath: string) { return nonPageFileManipulation(context, name, ngModulePath, 'directive'); } export function processComponentRequest(context: BuildContext, name: string, ngModulePath: string) { return nonPageFileManipulation(context, name, ngModulePath, 'component'); } export function processProviderRequest(context: BuildContext, name: string, ngModulePath: string) { return nonPageFileManipulation(context, name, ngModulePath, 'provider'); } export function processTabsRequest(context: BuildContext, name: string, tabs: any[], commandOptions?: { module?: boolean; constants?: boolean; }) { const includePageConstants = commandOptions ? commandOptions.constants : false; const includeNgModule = commandOptions ? commandOptions.module : false; const tabHydratedRequests = tabs.map((tab) => hydrateRequest(context, { type: 'page', name: tab, includeNgModule})); const hydratedRequest = hydrateTabRequest(context, { type: 'tabs', name, includeNgModule, tabs: tabHydratedRequests }); return generateTemplates(context, hydratedRequest, includePageConstants).then(() => { const promises = tabHydratedRequests.map((hydratedRequest) => { return generateTemplates(context, hydratedRequest, includePageConstants); }); return Promise.all(promises); }).then((tabs) => { tabsModuleManipulation(tabs, hydratedRequest, tabHydratedRequests); }); } export function listOptions() { const list: GeneratorOption[] = []; list.push({type: Constants.COMPONENT, multiple: false}); list.push({type: Constants.DIRECTIVE, multiple: false}); list.push({type: Constants.PAGE, multiple: false}); list.push({type: Constants.PIPE, multiple: false}); list.push({type: Constants.PROVIDER, multiple: false}); list.push({type: Constants.TABS, multiple: true}); return list; } ================================================ FILE: src/highlight/github-gist.scss ================================================ /** * GitHub Gist Theme * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro * https://highlightjs.org/ */ .hljs-comment, .hljs-meta { color: #969896; } .hljs-string, .hljs-variable, .hljs-template-variable, .hljs-strong, .hljs-emphasis, .hljs-quote { color: #df5000; } .hljs-keyword, .hljs-selector-tag, .hljs-type { color: #a71d5d; } .hljs-literal, .hljs-symbol, .hljs-bullet, .hljs-attribute { color: #0086b3; } .hljs-section, .hljs-name { color: #63a35c; } .hljs-tag { color: #333333; } .hljs-title, .hljs-attr, .hljs-selector-id, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo { color: #795da3; } .hljs-addition { color: #55a532; background-color: #eaffea; } .hljs-deletion { color: #bd2c00; background-color: #ffecec; } .hljs-link { text-decoration: underline; } ================================================ FILE: src/highlight/highlight.spec.ts ================================================ import { highlight, highlightError } from './highlight'; describe('highlight.js', () => { describe('highlightError', () => { it('should error highlight unescaped', () => { const htmlInput = `x & y`; const errorCharStart = 2; const errorLength = 1; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(`x & y`); }); it('should error highlight escaped >', () => { const sourceText = `x > y`; const htmlInput = highlight('typescript', sourceText, true).value; const errorCharStart = 2; const errorLength = 1; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(`x > y`); }); it('should error highlight before escaped >', () => { const sourceText = `if (x > y) return;`; const htmlInput = highlight('typescript', sourceText, true).value; const errorCharStart = 4; const errorLength = 1; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(`if (x > y) return;`); }); it('should error highlight after escaped <', () => { const sourceText = `if (x < y) return;`; const htmlInput = highlight('typescript', sourceText, true).value; const errorCharStart = 8; const errorLength = 1; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(`if (x < y) return;`); }); it('should error highlight first 3 chars', () => { // var name: string = 'Ellie'; const htmlInput = `var name: string = 'Ellie';`; const errorCharStart = 0; const errorLength = 3; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(`var name: string = 'Ellie';`); }); it('should error highlight second char', () => { // var name: string = 'Ellie'; const htmlInput = `var name: string = 'Ellie';`; const errorCharStart = 1; const errorLength = 1; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(`var name: string = 'Ellie';`); }); it('should error highlight first char', () => { // var name: string = 'Ellie'; const htmlInput = `var name: string = 'Ellie';`; const errorCharStart = 0; const errorLength = 1; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(`var name: string = 'Ellie';`); }); it('should return the same if there are is no errorLength', () => { // textInput = `var name: string = 'Ellie';`; const htmlInput = `var name: string = 'Ellie';`; const errorCharStart = 10; const errorLength = 0; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(htmlInput); }); it('should return the same if there are is no errorCharStart', () => { // textInput = `var name: string = 'Ellie';`; const htmlInput = `var name: string = 'Ellie';`; const errorCharStart = -1; const errorLength = 10; const v = highlightError(htmlInput, errorCharStart, errorLength); expect(v).toEqual(htmlInput); }); }); describe('typescript', () => { it('should replace typescript with <', () => { const sourceText = `if (x < y) return;`; const v = highlight('typescript', sourceText, true).value; expect(v).toEqual(`if (x < y) return;`); }); it('should replace typescript', () => { const sourceText = `var name: string = 'Ellie';`; const v = highlight('typescript', sourceText, true).value; expect(v).toEqual(`var name: string = 'Ellie';`); }); }); describe('html', () => { it('should replace html', () => { const sourceText = `
Text
`; const v = highlight('html', sourceText, true).value; expect(v).toEqual(`<div key="value">Text</div>`); }); }); describe('scss', () => { it('should replace scss', () => { const sourceText = `.className { color: $red; }`; const v = highlight('scss', sourceText, true).value; expect(v).toEqual(`.className { color: $red; }`); }); }); }); ================================================ FILE: src/highlight/highlight.ts ================================================ /** * Ported from highlight.js * Syntax highlighting with language autodetection. * https://highlightjs.org/ * Copyright (c) 2006, Ivan Sagalaev * https://github.com/isagalaev/highlight.js/blob/master/LICENSE */ var hljs: any = {}; // Convenience variables for build-in objects var objectKeys: any = Object.keys; // Global internal variables used within the highlight.js library. var languages: any = {}, aliases: any = {}; var spanEndTag = ''; // Global options used when within external APIs. This is modified when // calling the `hljs.configure` function. var options: any = { classPrefix: 'hljs-', tabReplace: null, useBR: false, languages: undefined }; // Object map that is used to escape some common HTML characters. var escapeRegexMap: any = { '&': '&', '<': '<', '>': '>' }; /* Utility functions */ function escape(value: string) { return value.replace(/[&<>]/gm, function(character: any) { return escapeRegexMap[character]; }); } function testRe(re: any, lexeme: any) { var match = re && re.exec(lexeme); return match && match.index === 0; } function inherit(parent: any, obj: any) { var key: any; var result: any = {}; for (key in parent) result[key] = parent[key]; if (obj) for (key in obj) result[key] = obj[key]; return result; } /* Initialization */ function compileLanguage(language: any) { function reStr(re: any) { return (re && re.source) || re; } function langRe(value: any, global?: any) { return new RegExp( reStr(value), 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '') ); } function compileMode(mode: any, parent?: any): any { if (mode.compiled) return; mode.compiled = true; mode.keywords = mode.keywords || mode.beginKeywords; if (mode.keywords) { var compiled_keywords: any = {}; var flatten = function(className: any, str: any) { if (language.case_insensitive) { str = str.toLowerCase(); } str.split(' ').forEach(function(kw: any) { var pair = kw.split('|'); compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1]; }); }; if (typeof mode.keywords === 'string') { // string flatten('keyword', mode.keywords); } else { objectKeys(mode.keywords).forEach(function (className: any) { flatten(className, mode.keywords[className]); }); } mode.keywords = compiled_keywords; } mode.lexemesRe = langRe(mode.lexemes || /\w+/, true); if (parent) { if (mode.beginKeywords) { mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')\\b'; } if (!mode.begin) mode.begin = /\B|\b/; mode.beginRe = langRe(mode.begin); if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/; if (mode.end) mode.endRe = langRe(mode.end); mode.terminator_end = reStr(mode.end) || ''; if (mode.endsWithParent && parent.terminator_end) mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end; } if (mode.illegal) mode.illegalRe = langRe(mode.illegal); if (mode.relevance == null) mode.relevance = 1; if (!mode.contains) { mode.contains = []; } var expanded_contains: any = []; mode.contains.forEach(function(c: any) { if (c.variants) { c.variants.forEach(function(v: any) {expanded_contains.push(inherit(c, v)); }); } else { expanded_contains.push(c === 'self' ? mode : c); } }); mode.contains = expanded_contains; mode.contains.forEach(function(c: any) {compileMode(c, mode); }); if (mode.starts) { compileMode(mode.starts, parent); } var terminators = mode.contains.map(function(c: any) { return c.beginKeywords ? '\\.?(' + c.begin + ')\\.?' : c.begin; }) .concat([mode.terminator_end, mode.illegal]) .map(reStr) .filter(Boolean); mode.terminators = terminators.length ? langRe(terminators.join('|'), true) : {exec: function(/*s*/): any {return null; }}; } compileMode(language); } export function highlightError(htmlInput: string, errorCharStart: number, errorLength: number) { if (errorCharStart < 0 || errorLength < 1 || !htmlInput) return htmlInput; const chars = htmlInput.split(''); let inTag = false; let textIndex = -1; for (var htmlIndex = 0; htmlIndex < chars.length; htmlIndex++) { if (chars[htmlIndex] === '<') { inTag = true; continue; } else if (chars[htmlIndex] === '>') { inTag = false; continue; } else if (inTag) { continue; } else if (chars[htmlIndex] === '&') { var isValidEscape = true; var escapeChars = '&'; for (var i = htmlIndex + 1; i < chars.length; i++) { if (!chars[i] || chars[i] === ' ') { isValidEscape = false; break; } else if (chars[i] === ';') { escapeChars += ';'; break; } else { escapeChars += chars[i]; } } isValidEscape = (isValidEscape && escapeChars.length > 1 && escapeChars.length < 9 && escapeChars[escapeChars.length - 1] === ';'); if (isValidEscape) { chars[htmlIndex] = escapeChars; for (let i = 0; i < escapeChars.length - 1; i++) { chars.splice(htmlIndex + 1, 1); } } } textIndex++; if (textIndex < errorCharStart || textIndex >= errorCharStart + errorLength) { continue; } chars[htmlIndex] = `${chars[htmlIndex]}`; } return chars.join(''); } /* Core highlighting function. Accepts a language name, or an alias, and a string with the code to highlight. Returns an object with the following properties: - relevance (int) - value (an HTML string with highlighting markup) */ export function highlight(name: string, value: string, ignore_illegals?: boolean, continuation?: any): {value: string, relevance: number, language?: string, top?: any} { function subMode(lexeme: any, mode: any) { var i: any, length: any; for (i = 0, length = mode.contains.length; i < length; i++) { if (testRe(mode.contains[i].beginRe, lexeme)) { return mode.contains[i]; } } } function endOfMode(mode: any, lexeme: any): any { if (testRe(mode.endRe, lexeme)) { while (mode.endsParent && mode.parent) { mode = mode.parent; } return mode; } if (mode.endsWithParent) { return endOfMode(mode.parent, lexeme); } } function isIllegal(lexeme: any, mode: any) { return !ignore_illegals && testRe(mode.illegalRe, lexeme); } function keywordMatch(mode: any, match: any) { var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0]; return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str]; } function buildSpan(classname: any, insideSpan: any, leaveOpen?: any, noPrefix?: any): any { var classPrefix = noPrefix ? '' : options.classPrefix, openSpan = ''; return openSpan + insideSpan + closeSpan; } function processKeywords() { var keyword_match: any, last_index: any, match: any, result: any; if (!top.keywords) return escape(mode_buffer); result = ''; last_index = 0; top.lexemesRe.lastIndex = 0; match = top.lexemesRe.exec(mode_buffer); while (match) { result += escape(mode_buffer.substr(last_index, match.index - last_index)); keyword_match = keywordMatch(top, match); if (keyword_match) { relevance += keyword_match[1]; result += buildSpan(keyword_match[0], escape(match[0])); } else { result += escape(match[0]); } last_index = top.lexemesRe.lastIndex; match = top.lexemesRe.exec(mode_buffer); } return result + escape(mode_buffer.substr(last_index)); } function processSubLanguage() { var explicit = typeof top.subLanguage === 'string'; if (explicit && !languages[top.subLanguage]) { return escape(mode_buffer); } var result = explicit ? highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]) : highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : undefined); // Counting embedded language score towards the host language may be disabled // with zeroing the containing mode relevance. Usecase in point is Markdown that // allows XML everywhere and makes every XML snippet to have a much larger Markdown // score. if (top.relevance > 0) { relevance += result.relevance; } if (explicit) { continuations[top.subLanguage] = result.top; } return buildSpan(result.language, result.value, false, true); } function processBuffer() { result += (top.subLanguage != null ? processSubLanguage() : processKeywords()); mode_buffer = ''; } function startNewMode(mode: any, asdf?: any) { result += mode.className ? buildSpan(mode.className, '', true) : ''; top = Object.create(mode, {parent: {value: top}}); } function processLexeme(buffer: any, lexeme?: any): any { mode_buffer += buffer; if (lexeme == null) { processBuffer(); return 0; } var new_mode = subMode(lexeme, top); if (new_mode) { if (new_mode.skip) { mode_buffer += lexeme; } else { if (new_mode.excludeBegin) { mode_buffer += lexeme; } processBuffer(); if (!new_mode.returnBegin && !new_mode.excludeBegin) { mode_buffer = lexeme; } } startNewMode(new_mode, lexeme); return new_mode.returnBegin ? 0 : lexeme.length; } var end_mode = endOfMode(top, lexeme); if (end_mode) { var origin = top; if (origin.skip) { mode_buffer += lexeme; } else { if (!(origin.returnEnd || origin.excludeEnd)) { mode_buffer += lexeme; } processBuffer(); if (origin.excludeEnd) { mode_buffer = lexeme; } } do { if (top.className) { result += spanEndTag; } if (!top.skip) { relevance += top.relevance; } top = top.parent; } while (top !== end_mode.parent); if (end_mode.starts) { startNewMode(end_mode.starts, ''); } return origin.returnEnd ? 0 : lexeme.length; } if (isIllegal(lexeme, top)) throw new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '') + '"'); /* Parser should not reach this point as all types of lexemes should be caught earlier, but if it does due to some bug make sure it advances at least one character forward to prevent infinite looping. */ mode_buffer += lexeme; return lexeme.length || 1; } var language = getLanguage(name); if (!language) { throw new Error('Unknown language: "' + name + '"'); } compileLanguage(language); var top: any = continuation || language; var continuations: any = {}; // keep continuations for sub-languages var result = '', current: any; for (current = top; current !== language; current = current.parent) { if (current.className) { result = buildSpan(current.className, '', true) + result; } } var mode_buffer = ''; var relevance = 0; try { var match: any, count: any, index = 0; while (true) { top.terminators.lastIndex = index; match = top.terminators.exec(value); if (!match) break; count = processLexeme(value.substr(index, match.index - index), match[0]); index = match.index + count; } processLexeme(value.substr(index)); for (current = top; current.parent; current = current.parent) { // close dangling modes if (current.className) { result += spanEndTag; } } return { relevance: relevance, value: result, language: name, top: top }; } catch (e) { if (e.message && e.message.indexOf('Illegal') !== -1) { return { relevance: 0, value: escape(value) }; } else { throw e; } } } /* Highlighting with language detection. Accepts a string with the code to highlight. Returns an object with the following properties: - language (detected language) - relevance (int) - value (an HTML string with highlighting markup) - second_best (object with the same structure for second-best heuristically detected language, may be absent) */ function highlightAuto(text: any, languageSubset?: any) { languageSubset = languageSubset || options.languages || objectKeys(languages); var result: any = { relevance: 0, value: escape(text) }; var second_best = result; languageSubset.filter(getLanguage).forEach(function(name: any) { var current = highlight(name, text, false); current.language = name; if (current.relevance > second_best.relevance) { second_best = current; } if (current.relevance > result.relevance) { second_best = result; result = current; } }); if (second_best.language) { result.second_best = second_best; } return result; } /* Updates highlight.js global options with values passed in the form of an object. */ function configure(user_options: any) { options = inherit(options, user_options); } function registerLanguage(name: any, language: any) { var lang = languages[name] = language(hljs); if (lang.aliases) { lang.aliases.forEach(function(alias: any) {aliases[alias] = name; }); } } function listLanguages() { return objectKeys(languages); } function getLanguage(name: any) { name = (name || '').toLowerCase(); return languages[name] || languages[aliases[name]]; } /* Interface definition */ hljs.highlight = highlight; hljs.highlightAuto = highlightAuto; hljs.configure = configure; hljs.registerLanguage = registerLanguage; hljs.listLanguages = listLanguages; hljs.getLanguage = getLanguage; hljs.inherit = inherit; // Common regexps hljs.IDENT_RE = '[a-zA-Z]\\w*'; hljs.UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*'; hljs.NUMBER_RE = '\\b\\d+(\\.\\d+)?'; hljs.C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float hljs.BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... hljs.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; // Common modes hljs.BACKSLASH_ESCAPE = { begin: '\\\\[\\s\\S]', relevance: 0 }; hljs.APOS_STRING_MODE = { className: 'string', begin: '\'', end: '\'', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE] }; hljs.QUOTE_STRING_MODE = { className: 'string', begin: '"', end: '"', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE] }; hljs.PHRASAL_WORDS_MODE = { begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/ }; hljs.COMMENT = function (begin: any, end: any, inherits: any) { var mode = hljs.inherit( { className: 'comment', begin: begin, end: end, contains: [] }, inherits || {} ); mode.contains.push(hljs.PHRASAL_WORDS_MODE); mode.contains.push({ className: 'doctag', begin: '(?:TODO|FIXME|NOTE|BUG|XXX):', relevance: 0 }); return mode; }; hljs.C_LINE_COMMENT_MODE = hljs.COMMENT('//', '$'); hljs.C_BLOCK_COMMENT_MODE = hljs.COMMENT('/\\*', '\\*/'); hljs.HASH_COMMENT_MODE = hljs.COMMENT('#', '$'); hljs.NUMBER_MODE = { className: 'number', begin: hljs.NUMBER_RE, relevance: 0 }; hljs.C_NUMBER_MODE = { className: 'number', begin: hljs.C_NUMBER_RE, relevance: 0 }; hljs.BINARY_NUMBER_MODE = { className: 'number', begin: hljs.BINARY_NUMBER_RE, relevance: 0 }; hljs.CSS_NUMBER_MODE = { className: 'number', begin: hljs.NUMBER_RE + '(' + '%|em|ex|ch|rem' + '|vw|vh|vmin|vmax' + '|cm|mm|in|pt|pc|px' + '|deg|grad|rad|turn' + '|s|ms' + '|Hz|kHz' + '|dpi|dpcm|dppx' + ')?', relevance: 0 }; hljs.REGEXP_MODE = { className: 'regexp', begin: /\//, end: /\/[gimuy]*/, illegal: /\n/, contains: [ hljs.BACKSLASH_ESCAPE, { begin: /\[/, end: /\]/, relevance: 0, contains: [hljs.BACKSLASH_ESCAPE] } ] }; hljs.TITLE_MODE = { className: 'title', begin: hljs.IDENT_RE, relevance: 0 }; hljs.UNDERSCORE_TITLE_MODE = { className: 'title', begin: hljs.UNDERSCORE_IDENT_RE, relevance: 0 }; hljs.METHOD_GUARD = { // excludes method names from keyword processing begin: '\\.\\s*' + hljs.UNDERSCORE_IDENT_RE, relevance: 0 }; hljs.registerLanguage('typescript', typescript); function typescript(hljs: any) { var KEYWORDS = { keyword: 'in if for while finally var new function do return void else break catch ' + 'instanceof with throw case default try this switch continue typeof delete ' + 'let yield const class public private protected get set super ' + 'static implements enum export import declare type namespace abstract', literal: 'true false null undefined NaN Infinity', built_in: 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + 'module console window document any number boolean string void' }; return { aliases: ['ts'], keywords: KEYWORDS, contains: [ { className: 'meta', begin: /^\s*['"]use strict['"]/ }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { // template string className: 'string', begin: '`', end: '`', contains: [ hljs.BACKSLASH_ESCAPE, { className: 'subst', begin: '\\$\\{', end: '\\}' } ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'number', variants: [ { begin: '\\b(0[bB][01]+)' }, { begin: '\\b(0[oO][0-7]+)' }, { begin: hljs.C_NUMBER_RE } ], relevance: 0 }, { // "value" container begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', keywords: 'return throw case', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.REGEXP_MODE ], relevance: 0 }, { className: 'function', begin: 'function', end: /[\{;]/, excludeEnd: true, keywords: KEYWORDS, contains: [ 'self', hljs.inherit(hljs.TITLE_MODE, {begin: /[A-Za-z$_][0-9A-Za-z$_]*/}), { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ], illegal: /["'\(]/ } ], illegal: /%/, relevance: 0 // () => {} is more typical in TypeScript }, { beginKeywords: 'constructor', end: /\{/, excludeEnd: true }, { // prevent references like module.id from being higlighted as module definitions begin: /module\./, keywords: {built_in: 'module'}, relevance: 0 }, { beginKeywords: 'module', end: /\{/, excludeEnd: true }, { beginKeywords: 'interface', end: /\{/, excludeEnd: true, keywords: 'interface extends' }, { begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` }, { begin: '\\.' + hljs.IDENT_RE, relevance: 0 // hack: prevents detection of keywords after dots } ] }; } hljs.registerLanguage('scss', scss); function scss(hljs: any) { var IDENT_RE = '[a-zA-Z-][a-zA-Z0-9_-]*'; var VARIABLE = { className: 'variable', begin: '(\\$' + IDENT_RE + ')\\b' }; var HEXCOLOR = { className: 'number', begin: '#[0-9A-Fa-f]+' }; // var DEF_INTERNALS = { // className: 'attribute', // begin: '[A-Z\\_\\.\\-]+', end: ':', // excludeEnd: true, // illegal: '[^\\s]', // starts: { // endsWithParent: true, excludeEnd: true, // contains: [ // HEXCOLOR, // hljs.CSS_NUMBER_MODE, // hljs.QUOTE_STRING_MODE, // hljs.APOS_STRING_MODE, // hljs.C_BLOCK_COMMENT_MODE, // { // className: 'meta', begin: '!important' // } // ] // } // }; return { case_insensitive: true, illegal: '[=/|\']', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'selector-id', begin: '\\#[A-Za-z0-9_-]+', relevance: 0 }, { className: 'selector-class', begin: '\\.[A-Za-z0-9_-]+', relevance: 0 }, { className: 'selector-attr', begin: '\\[', end: '\\]', illegal: '$' }, { className: 'selector-tag', // begin: IDENT_RE, end: '[,|\\s]' begin: '\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b', relevance: 0 }, { begin: ':(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)' }, { begin: '::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)' }, VARIABLE, { className: 'attribute', begin: '\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b', illegal: '[^\\s]' }, { begin: '\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b' }, { begin: ':', end: ';', contains: [ VARIABLE, HEXCOLOR, hljs.CSS_NUMBER_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, { className: 'meta', begin: '!important' } ] }, { begin: '@', end: '[{;]', keywords: 'mixin include extend for if else each while charset import debug media page content font-face namespace warn', contains: [ VARIABLE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, HEXCOLOR, hljs.CSS_NUMBER_MODE, { begin: '\\s[A-Za-z0-9_.-]+', relevance: 0 } ] } ] }; } hljs.registerLanguage('xml', xml); function xml(hljs: any) { var XML_IDENT_RE = '[A-Za-z0-9\\._:-]+'; var TAG_INTERNALS = { endsWithParent: true, illegal: /`]+/} ] } ] } ] }; return { aliases: ['html', 'xhtml', 'rss', 'atom', 'xjb', 'xsd', 'xsl', 'plist'], case_insensitive: true, contains: [ { className: 'meta', begin: '', relevance: 10, contains: [{begin: '\\[', end: '\\]'}] }, hljs.COMMENT( '', { relevance: 10 } ), { begin: '<\\!\\[CDATA\\[', end: '\\]\\]>', relevance: 10 }, { begin: /<\?(php)?/, end: /\?>/, subLanguage: 'php', contains: [{begin: '/\\*', end: '\\*/', skip: true}] }, { className: 'tag', /* The lookahead pattern (?=...) ensures that 'begin' only matches '|$)', end: '>', keywords: {name: 'style'}, contains: [TAG_INTERNALS], starts: { end: '', returnEnd: true, subLanguage: ['css', 'xml'] } }, { className: 'tag', // See the comment in the