diff --git a/README.md b/README.md
index 18fe734f..9e724dd4 100644
--- a/README.md
+++ b/README.md
@@ -57,9 +57,9 @@ In many cases, the defaults which Ionic provides covers most of the scenarios re
[Default Config Files](https://github.com/driftyco/ionic-app-scripts/tree/master/config)
-### NPM Config
+### package.json Config
-Within the `package.json` file, NPM also provides a handy [config](https://docs.npmjs.com/misc/config#per-package-config-settings) property. Below is an example of setting a custom config file using the `config` property in a project's `package.json`.
+Within the `package.json` file, there's also 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": {
@@ -88,7 +88,7 @@ npm run build --rollup ./config/rollup.config.js
### Overriding Config Files
-| Config File | NPM Config Property | Cmd-line Flag |
+| Config File | package.json Config | Cmd-line Flag |
|-------------|---------------------|-----------------------|
| CleanCss | `ionic_cleancss` | `--cleancss` or `-e` |
| Copy | `ionic_copy` | `--copy` or `-y` |
@@ -103,7 +103,7 @@ npm run build --rollup ./config/rollup.config.js
### Overriding Config Values
-| Config Values | NPM Config Property | Cmd-line Flag | Defaults |
+| Config Values | package.json Config | Cmd-line Flag | Defaults |
|-----------------|---------------------|---------------|-----------------|
| bundler | `ionic_bundler` | `--bundler` | `webpack` |
| source map type | `ionic_source_map` | `--sourceMap` | `eval` |
@@ -113,9 +113,21 @@ npm run build --rollup ./config/rollup.config.js
| build directory | `ionic_build_dir` | `--buildDir` | `build` |
-### Ionic Environment Variable
+### Ionic Environment Variables
-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` task is `prod`, and the `watch` task is `dev`. Note that `ionic serve` uses the `watch` task. Additionally, using the `--dev` command line flag will force the build to use `dev`.
+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_TMP_DIR` | The absolute path to the project's temporary 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_APP_SCRIPTS_DIR` | The absolute path to the `@ionic/app-scripts` node_module directory. |
+
+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` task is `prod`, and the `watch` and `serve` tasks are `dev`. Additionally, using the `--dev` command line flag will force the build to use `dev`.
Please take a look at the bottom of the [default Rollup config file](https://github.com/driftyco/ionic-app-scripts/blob/master/config/rollup.config.js) to see how the `IONIC_ENV` environment variable is being used to conditionally change config values for production builds.
diff --git a/bin/ion-dev.css b/bin/ion-dev.css
index a150441e..826e6a6d 100644
--- a/bin/ion-dev.css
+++ b/bin/ion-dev.css
@@ -1,3 +1,272 @@
+#ion-diagnostics * {
+ box-sizing: border-box;
+}
+
+#ion-diagnostics {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 99999;
+ margin: 0;
+ padding: 15px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ font-size: 14px;
+ line-height: 1.5;
+ color: #333;
+ background-color: #fff;
+ word-wrap: break-word;
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ text-size-adjust: none;
+ overflow: auto;
+}
+
+#ion-diagnostics .ion-diagnostic {
+ margin-bottom: 40px;
+ border: 1px solid #ddd;
+ border-radius: 3px;
+}
+
+#ion-diagnostics .ion-diagnostic-masthead {
+ padding: 8px 12px 0 12px;
+}
+
+#ion-diagnostics .ion-diagnostic-header {
+ margin: 0;
+ font-size: 18px;
+ color: #222222;
+}
+
+#ion-diagnostics .ion-diagnostic-message {
+ margin-top: 4px;
+ color: #666666;
+}
+
+#ion-diagnostics .ion-diagnostic-file {
+ position: relative;
+ margin-top: 16px;
+ border-top: 1px solid #ddd;
+}
+
+#ion-diagnostics .ion-diagnostic-file-header {
+ padding: 5px 10px;
+ border-bottom: 1px solid #d8d8d8;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ background-color: #f7f7f7;
+}
+
+#ion-diagnostics .ion-diagnostic-blob {
+ overflow-x: auto;
+ overflow-y: hidden;
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px;
+}
+
+#ion-diagnostics .ion-diagnostic-table {
+ border-spacing: 0;
+ border-collapse: collapse;
+ -moz-tab-size: 2;
+ -o-tab-size: 2;
+ tab-size: 2;
+}
+
+#ion-diagnostics .ion-diagnostic-table td,
+#ion-diagnostics .ion-diagnostic-table th {
+ padding: 0;
+}
+
+#ion-diagnostics .ion-diagnostic-blob-num {
+ padding-right: 10px;
+ padding-left: 10px;
+ width: 1%;
+ min-width: 50px;
+ font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ font-size: 12px;
+ line-height: 20px;
+ color: rgba(0,0,0,0.3);
+ text-align: right;
+ white-space: nowrap;
+ vertical-align: top;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ border: solid #eee;
+ border-width: 0 1px 0 0;
+}
+
+#ion-diagnostics .ion-diagnostic-blob-num::before {
+ content: attr(data-line-number);
+}
+
+#ion-diagnostics .ion-diagnostic-error-line .ion-diagnostic-blob-num {
+ background-color: #ffdddd;
+ border-color: #f1c0c0;
+}
+
+#ion-diagnostics .ion-diagnostic-error-line .ion-diagnostic-blob-code {
+ background: rgba(255, 221, 221, 0.3);
+ z-index: -1;
+}
+
+#ion-diagnostics .ion-diagnostics-error-chr {
+ position: relative;
+}
+
+#ion-diagnostics .ion-diagnostics-error-chr::before {
+ position: absolute;
+ z-index: -1;
+ top: -3px;
+ left: 0px;
+ width: 8px;
+ height: 20px;
+ background-color: #ffdddd;
+ content: "";
+}
+
+#ion-diagnostics .ion-diagnostic-blob-code {
+ position: relative;
+ padding-right: 10px;
+ padding-left: 10px;
+ line-height: 20px;
+ vertical-align: top;
+ overflow: visible;
+ font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ font-size: 12px;
+ color: #333;
+ word-wrap: normal;
+ white-space: pre;
+}
+
+#ion-diagnostics .ion-diagnostic-blob-code::before {
+ content: "";
+}
+
+#ion-diagnostics .js-keyword,
+#ion-diagnostics .css-prop {
+ color: #183691;
+}
+
+#ion-diagnostics .js-comment,
+#ion-diagnostics .sass-comment {
+ color: #969896;
+}
+
+#ion-diagnostics-system-info {
+ padding-bottom: 20px;
+ font-size: 10px;
+ color: #999;
+}
+
+#ion-diagnostics {
+ -webkit-transition: opacity 150ms ease-out;
+ transition: opacity 150ms ease-out;
+}
+
+#ion-diagnostics.ion-diagnostics-fade-out {
+ opacity: 0;
+}
+
+#ion-diagnostics-toast {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ left: 10px;
+ z-index: 999999;
+ display: block;
+ margin: auto;
+ max-width: 700px;
+ border-radius: 3px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ background: rgba(0,0,0,.9);
+ -webkit-transform: translate3d(0px, -60px, 0px);
+ transform: translate3d(0px, -60px, 0px);
+ -webkit-transition: -webkit-transform 75ms ease-out;
+ transition: transform 75ms ease-out;
+ pointer-events: none;
+}
+
+#ion-diagnostics-toast.ion-diagnostics-toast-active {
+ -webkit-transform: translate3d(0px, 0px, 0px);
+ transform: translate3d(0px, 0px, 0px);
+}
+
+#ion-diagnostics-toast .ion-diagnostics-toast-content {
+ display: flex;
+ -webkit-align-items: center;
+ -ms-flex-align: center;
+ align-items: center;
+ pointer-events: auto;
+}
+
+#ion-diagnostics-toast .ion-diagnostics-toast-message {
+ -webkit-flex: 1;
+ -ms-flex: 1;
+ flex: 1;
+ padding: 15px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ font-size: 14px;
+ color: #fff;
+}
+
+#ion-diagnostics-toast .ion-diagnostics-toast-spinner {
+ position: relative;
+ display: inline-block;
+ width: 56px;
+ height: 28px;
+}
+
+#ion-diagnostics-toast svg:not(:root) {
+ overflow: hidden;
+}
+
+#ion-diagnostics-toast svg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ -webkit-transform: translateZ(0);
+ transform: translateZ(0);
+ -webkit-animation: ion-diagnostics-spinner-rotate 600ms linear infinite;
+ animation: ion-diagnostics-spinner-rotate 600ms linear infinite;
+}
+
+@-webkit-keyframes ion-diagnostics-spinner-rotate {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes ion-diagnostics-spinner-rotate {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+#ion-diagnostics-toast circle {
+ fill: transparent;
+ stroke: white;
+ stroke-width: 4px;
+ stroke-dasharray: 128px;
+ stroke-dashoffset: 82px;
+}
+
._ionic-error-view {
position: fixed;
top: 0;
diff --git a/bin/ion-dev.js b/bin/ion-dev.js
index 13603b81..90829eac 100644
--- a/bin/ion-dev.js
+++ b/bin/ion-dev.js
@@ -1,3 +1,4 @@
+window.IonicDevServerConfig = window.IonicDevServerConfig || {};
window.IonicDevServer = {
start: function() {
this.msgQueue = [];
@@ -6,15 +7,28 @@ window.IonicDevServer = {
this.consoleError = console.error;
this.consoleWarn = console.warn;
- if (IonicDevServerConfig && IonicDevServerConfig.sendConsoleLogs) {
+ IonicDevServerConfig.systemInfo.push('Navigator Platform: ' + window.navigator.platform);
+ IonicDevServerConfig.systemInfo.push('User Agent: ' + window.navigator.userAgent);
+
+ if (IonicDevServerConfig.sendConsoleLogs) {
this.patchConsole();
}
- console.log('dev server enabled');
-
this.openConnection();
this.bindKeyboardEvents();
+
+ document.addEventListener("DOMContentLoaded", IonicDevServer.domReady);
+ },
+
+ domReady: function() {
+ document.removeEventListener("DOMContentLoaded", IonicDevServer.domReady);
+ var diagnosticsEle = document.getElementById('ion-diagnostics');
+ if (diagnosticsEle) {
+ IonicDevServer.buildStatus('error');
+ } else {
+ IonicDevServer.buildStatus('success');
+ }
},
handleError: function(err) {
@@ -141,8 +155,8 @@ window.IonicDevServer = {
try {
var msg = JSON.parse(ev.data);
switch (msg.category) {
- case 'taskEvent':
- self.receiveTaskEvent(msg);
+ case 'buildUpdate':
+ self.buildUpdate(msg);
break;
}
} catch (e) {
@@ -208,23 +222,108 @@ window.IonicDevServer = {
}
}
},
- receiveTaskEvent: function(taskEvent) {
- if (taskEvent.data && ['bundle', 'sass', 'transpile', 'template'].indexOf(taskEvent.data.scope) > -1) {
- this.consoleLog(taskEvent.data.msg);
+
+ buildUpdate: function(msg) {
+ var status = 'success';
+
+ if (msg.type === 'started') {
+ status = 'active';
+
+ var toastEle = document.getElementById('ion-diagnostics-toast');
+ 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);
+ }
+ IonicDevServer.toastTimerId = setTimeout(function() {
+ var toastEle = document.getElementById('ion-diagnostics-toast');
+ if (toastEle) {
+ toastEle.classList.add('ion-diagnostics-toast-active');
+ }
+ }, 50);
+
+ } else {
+ status = msg.data.diagnosticsHtml ? 'error' : 'success';
+
+ clearTimeout(IonicDevServer.toastTimerId);
+
+ var toastEle = document.getElementById('ion-diagnostics-toast');
+ if (toastEle) {
+ toastEle.classList.remove('ion-diagnostics-toast-active');
+ }
+
+ var diagnosticsEle = document.getElementById('ion-diagnostics');
+ if (diagnosticsEle && !msg.data.diagnosticsHtml) {
+ diagnosticsEle.classList.add('ion-diagnostics-fade-out');
+ IonicDevServer.diagnosticsTimerId = setTimeout(function() {
+ var diagnosticsEle = document.getElementById('ion-diagnostics');
+ if (diagnosticsEle) {
+ diagnosticsEle.parentElement.removeChild(diagnosticsEle);
+ }
+ }, 100);
+
+ } else if (msg.data.diagnosticsHtml) {
+ clearTimeout(IonicDevServer.diagnosticsTimerId);
+
+ if (!diagnosticsEle) {
+ diagnosticsEle = document.createElement('div');
+ diagnosticsEle.id = 'ion-diagnostics';
+ diagnosticsEle.className = 'ion-diagnostics-fade-out';
+ document.body.insertBefore(diagnosticsEle, document.body.firstChild);
+ IonicDevServer.diagnosticsTimerId = setTimeout(function() {
+ var diagnosticsEle = document.getElementById('ion-diagnostics');
+ if (diagnosticsEle) {
+ diagnosticsEle.classList.remove('ion-diagnostics-fade-out');
+ }
+ }, 24);
+ }
+ diagnosticsEle.innerHTML = msg.data.diagnosticsHtml;
+ }
}
- if (taskEvent.data && taskEvent.data.type === 'failed') {
- Notification.requestPermission().then(function(result) {
- var options = {
- body: taskEvent.data.msg,
- icon: IonicDevServerConfig.notificationIconPath
+
+ IonicDevServer.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 = IonicDevServer[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('pre');
+ systemInfoEle.id = 'ion-diagnostics-system-info';
+ systemInfoEle.innerHTML = IonicDevServerConfig.systemInfo.join('\n');
+ diagnosticsEle.appendChild(systemInfoEle);
}
- var notification = new Notification(taskEvent.data.scope, options);
- setTimeout(notification.close.bind(n), 5000);
- });
+ }
}
- }
-};
+ },
+ activeIcon: 'data:image/png;base64,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',
+ errorIcon: 'data:image/png;base64,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',
+
+ successIcon: 'data:image/png;base64,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'
+
+};
IonicDevServer.start();
diff --git a/bin/ionic.png b/bin/ionic.png
deleted file mode 100644
index d82dab01..00000000
Binary files a/bin/ionic.png and /dev/null differ
diff --git a/config/watch.config.js b/config/watch.config.js
index bde65851..8ef6ec8f 100644
--- a/config/watch.config.js
+++ b/config/watch.config.js
@@ -1,9 +1,6 @@
-var fullBuildUpdate = require('../dist/build').fullBuildUpdate;
-var buildUpdate = require('../dist/build').buildUpdate;
-var templateUpdate = require('../dist/template').templateUpdate;
-var copyUpdate = require('../dist/copy').copyUpdate;
-var sassUpdate = require('../dist/sass').sassUpdate;
-var copyConfig = require('./copy.config').include;
+var watch = require('../dist/watch');
+var copy = require('../dist/copy');
+var copyConfig = require('./copy.config');
// https://www.npmjs.com/package/chokidar
@@ -14,44 +11,15 @@ module.exports = {
{
paths: [
- '{{SRC}}/**/*.ts'
+ '{{SRC}}/**/*.(ts|html|scss)'
],
options: { ignored: '{{SRC}}/**/*.spec.ts' },
- eventName: 'all',
- callback: buildUpdate
+ callback: watch.buildUpdate
},
{
- paths: [
- '{{SRC}}/**/*.html'
- ],
- eventName: 'all',
- callback: templateUpdate
- },
-
- {
- paths: [
- '{{SRC}}/**/*.scss'
- ],
- eventName: 'all',
- callback: sassUpdate
- },
-
- {
- paths: copyConfig.map(f => f.src),
- eventName: 'all',
- callback: copyUpdate
- },
-
- {
- paths: [
- '{{SRC}}/**/*',
- ],
- options: {
- ignored: `{{SRC}}/assets/**/*`
- },
- eventName: 'unlinkDir',
- callback: fullBuildUpdate
+ paths: copyConfig.include.map(f => f.src),
+ callback: copy.copyUpdate
}
]
diff --git a/package.json b/package.json
index 2817f4e2..0aaa0bf6 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"dependencies": {
"autoprefixer": "6.4.1",
"chalk": "1.1.3",
- "chokidar": "1.6.0",
+ "chokidar": "1.6.1",
"clean-css": "3.4.19",
"conventional-changelog-cli": "^1.2.0",
"cross-spawn": "4.0.0",
@@ -39,6 +39,7 @@
"fs-extra": "0.30.0",
"json-loader": "^0.5.4",
"node-sass": "3.10.1",
+ "os-name": "^2.0.1",
"postcss": "5.2.0",
"proxy-middleware": "^0.15.0",
"rollup": "0.36.3",
diff --git a/src/build.ts b/src/build.ts
index cfcb1c17..caa1ce44 100644
--- a/src/build.ts
+++ b/src/build.ts
@@ -1,15 +1,16 @@
-import { BuildContext } from './util/interfaces';
-import { BuildError, IgnorableError, Logger } from './util/logger';
+import { BuildContext, BuildState } from './util/interfaces';
+import { BuildError, Logger } from './util/logger';
import { bundle, bundleUpdate } from './bundle';
import { clean } from './clean';
import { copy } from './copy';
import { emit, EventType } from './util/events';
import { generateContext } from './util/config';
-import { lint } from './lint';
+import { lint, lintUpdate } from './lint';
import { minifyCss, minifyJs } from './minify';
import { ngc } from './ngc';
import { sass, sassUpdate } from './sass';
-import { transpile, transpileUpdate } from './transpile';
+import { templateUpdate } from './template';
+import { transpile, transpileUpdate, transpileDiagnosticsOnly } from './transpile';
export function build(context: BuildContext) {
@@ -20,8 +21,6 @@ export function build(context: BuildContext) {
return buildWorker(context)
.then(() => {
// congrats, we did it! (•_•) / ( •_•)>⌐■-■ / (⌐■_■)
- context.fullBuildCompleted = true;
- emit(EventType.BuildFinished);
logger.finish();
})
.catch(err => {
@@ -48,7 +47,6 @@ function buildProd(context: BuildContext) {
// async tasks
// these can happen all while other tasks are running
const copyPromise = copy(context);
- const lintPromise = lint(context);
// kick off ngc to run the Ahead of Time compiler
return ngc(context)
@@ -72,10 +70,13 @@ function buildProd(context: BuildContext) {
]);
})
.then(() => {
+ // kick off the tslint after everything else
+ // nothing needs to wait on its completion
+ lint(context);
+
// ensure the async tasks have fully completed before resolving
return Promise.all([
- copyPromise,
- lintPromise
+ copyPromise
]);
})
.catch(err => {
@@ -91,7 +92,6 @@ function buildDev(context: BuildContext) {
// async tasks
// these can happen all while other tasks are running
const copyPromise = copy(context);
- const lintPromise = lint(context);
// just bundle, and if that passes then do the rest at the same time
return transpile(context)
@@ -101,55 +101,175 @@ function buildDev(context: BuildContext) {
.then(() => {
return Promise.all([
sass(context),
- copyPromise,
- lintPromise
+ copyPromise
]);
})
+ .then(() => {
+ // kick off the tslint after everything else
+ // nothing needs to wait on its completion
+ lint(context);
+ return Promise.resolve();
+ })
.catch(err => {
throw new BuildError(err);
});
}
-export function fullBuildUpdate(event: string, filePath: string, context: BuildContext) {
- return buildUpdateWorker(event, filePath, context, true).then(() => {
- context.fullBuildCompleted = true;
- });
-}
export function buildUpdate(event: string, filePath: string, context: BuildContext) {
- return buildUpdateWorker(event, filePath, context, false);
+ return new Promise(resolve => {
+ const logger = new Logger('build');
+
+ buildUpdateId++;
+ emit(EventType.BuildUpdateStarted, buildUpdateId);
+
+ 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
+ emit(EventType.BuildUpdateCompleted, buildUpdateId);
+
+ if (resolveValue.requiresRefresh) {
+ // emit that we need to do a full page refresh
+ emit(EventType.ReloadApp);
+
+ } else {
+ // 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.changedFile);
+ }
+
+ if (filePath.endsWith('.ts')) {
+ // a ts file changed, so let's lint it too, however
+ // this task should run as an after thought
+ lintUpdate(event, filePath, context);
+ }
+
+ 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(event, filePath, context);
+ const parallelTasksPromise = buildUpdateParallelTasks(event, filePath, context);
+
+ // whether it was resolved or rejected, we need to do the same thing
+ buildTasksPromise
+ .then(buildTasksDone)
+ .catch(() => {
+ buildTasksDone({
+ requiresRefresh: false,
+ changedFile: filePath
+ });
+ });
+ });
}
-function buildUpdateWorker(event: string, filePath: string, context: BuildContext, fullBuild: boolean) {
- const logger = new Logger(`build update`);
+/**
+ * Collection of all the build tasks than need to run
+ * Each task will only run if it's set with eacn BuildState.
+ */
+function buildUpdateTasks(event: string, filePath: string, context: BuildContext) {
+ const resolveValue: BuildTaskResolveValue = {
+ requiresRefresh: false,
+ changedFile: filePath
+ };
- let transpilePromise: Promise = null;
- if (fullBuild) {
- transpilePromise = transpile(context);
- } else {
- transpilePromise = transpileUpdate(event, filePath, context);
- }
- return transpilePromise
+ return Promise.resolve()
.then(() => {
- if (fullBuild) {
- return bundle(context);
+ // TEMPLATE
+ if (context.templateState === BuildState.RequiresUpdate) {
+ resolveValue.requiresRefresh = true;
+ return templateUpdate(event, filePath, context);
}
- return bundleUpdate(event, filePath, context);
- }).then(() => {
- if (fullBuild) {
- return sass(context);
- } else if (event !== 'change' || !context.successfulSass) {
- // if just the TS file changed, then there's no need to do a sass update
- // however, if a new TS file was added or was deleted, then we should do a sass update
- return sassUpdate(event, filePath, context);
+ // no template updates required
+ return Promise.resolve();
+
+ })
+ .then(() => {
+ // TRANSPILE
+ if (context.transpileState === BuildState.RequiresUpdate) {
+ resolveValue.requiresRefresh = 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(event, filePath, context);
+
+ } else if (context.transpileState === BuildState.RequiresBuild) {
+ // run the whole transpile
+ resolveValue.requiresRefresh = true;
+ return transpile(context);
}
- }).then(() => {
- emit(EventType.BuildFinished);
- logger.finish();
- }).catch(err => {
- if (err instanceof IgnorableError) {
- throw err;
+ // no transpiling required
+ return Promise.resolve();
+
+ })
+ .then(() => {
+ // BUNDLE
+ if (context.bundleState === BuildState.RequiresUpdate) {
+ // we need to do a bundle update
+ resolveValue.requiresRefresh = true;
+ return bundleUpdate(event, filePath, context);
+
+ } else if (context.bundleState === BuildState.RequiresBuild) {
+ // we need to do a full bundle build
+ resolveValue.requiresRefresh = true;
+ return bundle(context);
}
- throw logger.fail(err);
+ // no bundling required
+ return Promise.resolve();
+
+ })
+ .then(() => {
+ // SASS
+ if (context.sassState === BuildState.RequiresUpdate) {
+ // we need to do a sass update
+ return sassUpdate(event, filePath, context).then(outputCssFile => {
+ resolveValue.changedFile = outputCssFile;
+ });
+
+ } else if (context.sassState === BuildState.RequiresBuild) {
+ // we need to do a full sass build
+ return sass(context).then(outputCssFile => {
+ resolveValue.changedFile = outputCssFile;
+ });
+ }
+ // no sass build required
+ return Promise.resolve();
+ })
+ .then(() => {
+ return resolveValue;
});
}
+
+interface BuildTaskResolveValue {
+ requiresRefresh: boolean;
+ changedFile: string;
+}
+
+/**
+ * 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(event: string, filePath: string, context: BuildContext) {
+ const parallelTasks: Promise[] = [];
+
+ if (context.transpileState === BuildState.RequiresUpdate) {
+ parallelTasks.push(transpileDiagnosticsOnly(context));
+ }
+
+ return Promise.all(parallelTasks);
+}
+
+let buildUpdateId = 0;
diff --git a/src/declarations.d.ts b/src/declarations.d.ts
index 409df78a..b416dd1f 100644
--- a/src/declarations.d.ts
+++ b/src/declarations.d.ts
@@ -1,5 +1,6 @@
declare module 'autoprefixer';
declare module 'mime-types';
+declare module 'os-name';
declare module 'proxy-middleware';
declare module 'rollup-pluginutils';
declare module 'rollup';
diff --git a/src/dev-server/http-server.ts b/src/dev-server/http-server.ts
index 23c741be..2cc80c19 100644
--- a/src/dev-server/http-server.ts
+++ b/src/dev-server/http-server.ts
@@ -6,12 +6,10 @@ import * as fs from 'fs';
import * as url from 'url';
import { ServeConfig, LOGGER_DIR } from './serve-config';
import { Logger } from '../util/logger';
-import { promisify } from '../util/promisify';
import * as proxyMiddleware from 'proxy-middleware';
-import { readDiagnosticsHtmlSync } from '../util/logger-diagnostics';
+import { injectDiagnosticsHtml } from '../util/logger-diagnostics';
import { getProjectJson, IonicProject } from '../util/ionic-project';
-const readFilePromise = promisify(fs.readFile);
/**
* Create HTTP server
@@ -26,7 +24,7 @@ export function createHttpServer(config: ServeConfig): express.Application {
app.get('/', serveIndex);
app.use('/', express.static(config.wwwDir));
- app.use(`/${LOGGER_DIR}`, express.static(path.join(__dirname, '..', '..', 'bin')));
+ app.use(`/${LOGGER_DIR}`, express.static(path.join(__dirname, '..', '..', 'bin'), { maxAge: 31536000 }));
app.get('/cordova.js', serveCordovaJS);
if (config.useProxy) {
@@ -58,33 +56,27 @@ function setupProxies(app: express.Application) {
*/
function serveIndex(req: express.Request, res: express.Response) {
const config: ServeConfig = req.app.get('serveConfig');
- let htmlFile = path.join(config.wwwDir, 'index.html');
- let diagnosticsHtml = readDiagnosticsHtmlSync(config.buildDir);
- function httpResponse(content: any) {
+ // respond with the index.html file
+ const indexFileName = path.join(config.wwwDir, 'index.html');
+ fs.readFile(indexFileName, (err, indexHtml) => {
if (config.useLiveReload) {
- content = injectLiveReloadScript(content, config.host, config.liveReloadPort);
- }
- if (config.useNotifier) {
- content = injectNotificationScript(content, config.notifyOnConsoleLog, config.notificationPort);
+ indexHtml = injectLiveReloadScript(indexHtml, config.host, config.liveReloadPort);
}
- // File found so lets send it back to the response
- res.set('Content-Type', 'text/html');
- res.send(content);
- }
+ indexHtml = injectNotificationScript(config.rootDir, indexHtml, config.notifyOnConsoleLog, config.notificationPort);
- if (diagnosticsHtml) {
- httpResponse(diagnosticsHtml);
- } else {
- readFilePromise(htmlFile).then(httpResponse);
- }
+ indexHtml = injectDiagnosticsHtml(config.buildDir, indexHtml);
+
+ res.set('Content-Type', 'text/html');
+ res.send(indexHtml);
+ });
}
/**
- * http responder for cordova.js fiel
+ * http responder for cordova.js file
*/
function serveCordovaJS(req: express.Request, res: express.Response) {
res.set('Content-Type', 'application/javascript');
res.send('// mock cordova file during development');
-}
\ No newline at end of file
+}
diff --git a/src/dev-server/injector.ts b/src/dev-server/injector.ts
index 423052cf..4438c8d9 100644
--- a/src/dev-server/injector.ts
+++ b/src/dev-server/injector.ts
@@ -1,10 +1,12 @@
+import { getAppScriptsVersion, getSystemInfo } from '../util/helpers';
import { LOGGER_DIR } from './serve-config';
+
const LOGGER_HEADER = '';
-export function injectNotificationScript(content: any, notifyOnConsoleLog: boolean, notificationPort: Number): any {
+export function injectNotificationScript(rootDir: string, content: any, notifyOnConsoleLog: boolean, notificationPort: Number): any {
let contentStr = content.toString();
- const consoleLogScript = getConsoleLoggerScript(notifyOnConsoleLog, notificationPort);
+ const consoleLogScript = getDevLoggerScript(rootDir, notifyOnConsoleLog, notificationPort);
if (contentStr.indexOf(LOGGER_HEADER) > -1) {
// already added script somehow
@@ -24,17 +26,19 @@ export function injectNotificationScript(content: any, notifyOnConsoleLog: boole
return contentStr;
}
-function getConsoleLoggerScript(notifyOnConsoleLog: boolean, notificationPort: Number) {
+function getDevLoggerScript(rootDir: string, notifyOnConsoleLog: boolean, notificationPort: Number) {
+ const appScriptsVersion = getAppScriptsVersion();
const ionDevServer = JSON.stringify({
sendConsoleLogs: notifyOnConsoleLog,
wsPort: notificationPort,
- notificationIconPath: `${LOGGER_DIR}/ionic.png`
+ appScriptsVersion: appScriptsVersion,
+ systemInfo: getSystemInfo(rootDir)
});
return `
${LOGGER_HEADER}
-
-
+
+
`;
}
diff --git a/src/dev-server/live-reload.ts b/src/dev-server/live-reload.ts
index 1dcb21fb..5b8b73e1 100644
--- a/src/dev-server/live-reload.ts
+++ b/src/dev-server/live-reload.ts
@@ -1,3 +1,4 @@
+import { hasDiagnostics } from '../util/logger-diagnostics';
import * as path from 'path';
import * as tinylr from 'tiny-lr';
import { ServeConfig } from './serve-config';
@@ -8,59 +9,27 @@ export function createLiveReloadServer(config: ServeConfig) {
const liveReloadServer = tinylr();
liveReloadServer.listen(config.liveReloadPort, config.host);
- function broadcastChange(filePath: string | string[]) {
- const files = Array.isArray(filePath) ? filePath : [filePath];
- const msg = {
- body: {
- files: files.map(f => '/' + path.relative(config.wwwDir, f))
- }
- };
- liveReloadServer.changed(msg);
- }
-
- let hasFinishedSass = false;
- let hasFinishedBundle = false;
- let hasFinishedBuild = false;
- let hasDoneHardRefresh = false;
-
- events.on(events.EventType.SassFinished, (sassFile: string) => {
- hasFinishedSass = true;
- if (hasFinishedBuild) {
- // only livereload css if a bundle has finished
- // and a build has finished
- // css live reload does not refresh the index page
- broadcastChange(sassFile);
- }
- });
-
- events.on(events.EventType.BundleFinished, (jsFile: string) => {
- hasFinishedBundle = true;
- if (hasFinishedSass && hasFinishedBuild) {
- // only livereload js if sass has finished
- // and a build has finished
- // js live reload refreshes the index page
- hasDoneHardRefresh = true;
- broadcastChange(jsFile);
+ function fileChange(filePath: string | string[]) {
+ // only do a live reload if there are no diagnostics
+ // the notification server takes care of showing diagnostics
+ if (!hasDiagnostics(config.buildDir)) {
+ const files = Array.isArray(filePath) ? filePath : [filePath];
+ liveReloadServer.changed({
+ body: {
+ files: files.map(f => '/' + path.relative(config.wwwDir, f))
+ }
+ });
}
- });
+ }
- events.on(events.EventType.BuildFinished, () => {
- hasFinishedBuild = true;
- if (!hasDoneHardRefresh) {
- hasDoneHardRefresh = true;
- broadcastChange('index.html');
- }
- });
+ events.on(events.EventType.FileChange, fileChange);
- events.on(events.EventType.UpdatedDiagnostics, () => {
- // new diagnostics files have been saved
- // refresh the index file so they render
- broadcastChange('index.html');
+ events.on(events.EventType.ReloadApp, () => {
+ fileChange('index.html');
});
-
- events.on(events.EventType.FileChange, broadcastChange);
}
+
export function injectLiveReloadScript(content: any, host: string, port: Number): any {
let contentStr = content.toString();
const liveReloadScript = getLiveReloadScript(host, port);
diff --git a/src/dev-server/notification-server.ts b/src/dev-server/notification-server.ts
index cdddbf6b..a8a1542f 100644
--- a/src/dev-server/notification-server.ts
+++ b/src/dev-server/notification-server.ts
@@ -1,24 +1,55 @@
// Ionic Dev Server: Server Side Logger
-import { Diagnostic, Logger, TaskEvent } from '../util/logger';
+import { Logger } from '../util/logger';
+import { hasDiagnostics, getDiagnosticsHtmlContent } from '../util/logger-diagnostics';
import { on, EventType } from '../util/events';
import { Server as WebSocketServer } from 'ws';
import { ServeConfig } from './serve-config';
-let wsServer: any;
-const msgToClient: WsMessage[] = [];
-
-export interface WsMessage {
- category: string;
- type: string;
- data: any;
-}
export function createNotificationServer(config: ServeConfig) {
- on(EventType.TaskEvent, (taskEvent: TaskEvent) => {
+ let wsServer: any;
+
+ // queue up all messages to the client
+ function queueMessageSend(msg: WsMessage) {
+ msgToClient.push(msg);
+ drainMessageQueue();
+ }
+
+ // drain the queue messages when the server is ready
+ function drainMessageQueue() {
+ if (wsServer) {
+ let msg: any;
+ while (msg = msgToClient.shift()) {
+ try {
+ wsServer.send(JSON.stringify(msg));
+ } catch (e) {
+ Logger.error(`error sending client ws, ${e}`);
+ }
+ }
+ }
+ }
+
+ // a build update has started, notify the client
+ on(EventType.BuildUpdateStarted, (buildUpdateId) => {
+ const msg: WsMessage = {
+ category: 'buildUpdate',
+ type: 'started',
+ data: {
+ buildUpdateId: buildUpdateId
+ }
+ };
+ queueMessageSend(msg);
+ });
+
+ // a build update has completed, notify the client
+ on(EventType.BuildUpdateCompleted, (buildUpdateId) => {
const msg: WsMessage = {
- category: EventType.TaskEvent,
- type: taskEvent.scope,
- data: taskEvent
+ category: 'buildUpdate',
+ type: 'completed',
+ data: {
+ buildUpdateId: buildUpdateId,
+ diagnosticsHtml: hasDiagnostics(config.buildDir) ? getDiagnosticsHtmlContent(config.buildDir) : null
+ }
};
queueMessageSend(msg);
});
@@ -27,52 +58,36 @@ export function createNotificationServer(config: ServeConfig) {
const wss = new WebSocketServer({ port: config.notificationPort });
wss.on('connection', (ws: any) => {
+ // we've successfully connected
wsServer = ws;
wsServer.on('message', (incomingMessage: string) => {
// incoming message from the client
try {
- printClientMessage(JSON.parse(incomingMessage));
+ printMessageFromClient(JSON.parse(incomingMessage));
} catch (e) {
Logger.error(`error opening ws message: ${incomingMessage}`);
}
});
+ // now that we're connected, send off any messages
+ // we might has already queued up
drainMessageQueue();
});
}
-function queueMessageSend(msg: WsMessage) {
- msgToClient.push(msg);
- drainMessageQueue();
-}
-
-
-function drainMessageQueue() {
- if (wsServer) {
- var msg: any;
- while (msg = msgToClient.shift()) {
- try {
- wsServer.send(JSON.stringify(msg));
- } catch (e) {
- Logger.error(`error sending client ws, ${e}`);
- }
- }
- }
-}
-
-function printClientMessage(msg: WsMessage) {
+function printMessageFromClient(msg: WsMessage) {
if (msg.data) {
switch (msg.category) {
- case 'console':
- printConsole(msg);
- break;
+ case 'console':
+ printConsole(msg);
+ break;
- case 'exception':
- printException(msg);
- break;
+ case 'exception':
+ printException(msg);
+ break;
}
}
}
@@ -82,24 +97,32 @@ function printConsole(msg: WsMessage) {
args[0] = `console.${msg.type}: ${args[0]}`;
switch (msg.type) {
- case 'error':
- Logger.error.apply(this, args);
- break;
+ case 'error':
+ Logger.error.apply(this, args);
+ break;
- case 'warn':
- Logger.warn.apply(this, args);
- break;
+ case 'warn':
+ Logger.warn.apply(this, args);
+ break;
- case 'debug':
- Logger.debug.apply(this, args);
- break;
+ case 'debug':
+ Logger.debug.apply(this, args);
+ break;
- default:
- Logger.info.apply(this, args);
- break;
- }
+ default:
+ Logger.info.apply(this, args);
+ break;
+ }
}
function printException(msg: WsMessage) {
}
+
+const msgToClient: WsMessage[] = [];
+
+export interface WsMessage {
+ category: string;
+ type: string;
+ data: any;
+}
diff --git a/src/dev-server/serve-config.ts b/src/dev-server/serve-config.ts
index 53ebbed6..495572a9 100644
--- a/src/dev-server/serve-config.ts
+++ b/src/dev-server/serve-config.ts
@@ -1,6 +1,7 @@
export interface ServeConfig {
httpPort: number;
host: string;
+ rootDir: string;
wwwDir: string;
buildDir: string;
launchBrowser: boolean;
@@ -9,7 +10,6 @@ export interface ServeConfig {
useLiveReload: boolean;
liveReloadPort: Number;
notificationPort: Number;
- useNotifier: boolean;
useServerLogs: boolean;
notifyOnConsoleLog: boolean;
useProxy: boolean;
diff --git a/src/index.ts b/src/index.ts
index b6502086..394bd8ca 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
-export { build, buildUpdate, fullBuildUpdate } from './build';
+export { build, buildUpdate } from './build';
export { bundle, bundleUpdate } from './bundle';
export { clean } from './clean';
export { cleancss } from './cleancss';
@@ -8,20 +8,19 @@ export { minify } from './minify';
export { ngc } from './ngc';
export { sass, sassUpdate } from './sass';
export { transpile } from './transpile';
-export { templateUpdate } from './template';
export { uglifyjs } from './uglifyjs';
export { watch } from './watch';
export * from './util/config';
export * from './util/helpers';
export * from './util/interfaces';
-import { Logger, getAppScriptsVersion } from './util/logger';
-import * as chalk from 'chalk';
+import { getAppScriptsVersion } from './util/helpers';
+import { Logger } from './util/logger';
export function run(task: string) {
try {
- Logger.info(chalk.cyan(`ionic-app-scripts ${getAppScriptsVersion()}`));
+ Logger.info(`ionic-app-scripts ${getAppScriptsVersion()}`, 'cyan');
} catch (e) {}
try {
diff --git a/src/lint.ts b/src/lint.ts
index d6e28032..f353e5e5 100644
--- a/src/lint.ts
+++ b/src/lint.ts
@@ -4,7 +4,8 @@ import { BuildError, Logger } from './util/logger';
import { generateContext, getUserConfigFile } from './util/config';
import { join } from 'path';
import { createProgram, findConfiguration, getFileNames } from 'tslint';
-import { runDiagnostics } from './util/logger-tslint';
+import { runTsLintDiagnostics } from './util/logger-tslint';
+import { printDiagnostics, DiagnosticsType } from './util/logger-diagnostics';
import { runWorker } from './worker-client';
import * as Linter from 'tslint';
import * as fs from 'fs';
@@ -22,17 +23,10 @@ export function lint(context?: BuildContext, configFile?: string) {
export function lintWorker(context: BuildContext, configFile: string) {
- const logger = new Logger('lint');
return getLintConfig(context, configFile).then(configFile => {
// there's a valid tslint config, let's continue
return lintApp(context, configFile);
- }).then(() => {
- // always finish and resolve
- logger.finish();
- }).catch(() => {
- // always finish and resolve
- logger.finish();
- });
+ }).catch(() => {});
}
@@ -101,7 +95,8 @@ function lintFile(context: BuildContext, program: ts.Program, filePath: string)
const lintResult = linter.lint();
if (lintResult && lintResult.failures) {
- runDiagnostics(context, lintResult.failures);
+ const diagnostics = runTsLintDiagnostics(context, lintResult.failures);
+ printDiagnostics(context, DiagnosticsType.TsLint, diagnostics, true, false);
}
} catch (e) {
diff --git a/src/ngc.ts b/src/ngc.ts
index 985ccccc..4406b7b1 100644
--- a/src/ngc.ts
+++ b/src/ngc.ts
@@ -1,7 +1,7 @@
import { basename, join } from 'path';
import { BuildContext, TaskInfo } from './util/interfaces';
import { copy as fsCopy, emptyDirSync, outputJsonSync, readFileSync, statSync } from 'fs-extra';
-import { endsWith, objectAssign } from './util/helpers';
+import { objectAssign } from './util/helpers';
import { fillConfigDefaults, generateContext, getUserConfigFile, getNodeBinExecutable } from './util/config';
import { getTsConfigPath } from './transpile';
import { BuildError, Logger } from './util/logger';
@@ -171,7 +171,7 @@ function filterCopyFiles(filePath: any, hoop: any) {
shouldInclude = (EXCLUDE_DIRS.indexOf(basename(filePath)) < 0);
} else {
- shouldInclude = (endsWith(filePath, '.ts') || endsWith(filePath, '.html'));
+ shouldInclude = (filePath.endsWith('.ts') || filePath.endsWith('.html'));
}
} catch (e) {}
diff --git a/src/rollup.ts b/src/rollup.ts
index 7de0305c..fa7d7e49 100644
--- a/src/rollup.ts
+++ b/src/rollup.ts
@@ -1,7 +1,5 @@
-import { BuildContext, TaskInfo } from './util/interfaces';
+import { BuildContext, BuildState, TaskInfo } from './util/interfaces';
import { BuildError, Logger } from './util/logger';
-import { emit, EventType } from './util/events';
-import { endsWith, setModulePathsCache } from './util/helpers';
import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config';
import { ionCompiler } from './plugins/ion-compiler';
import { join, isAbsolute, normalize } from 'path';
@@ -16,9 +14,11 @@ export function rollup(context: BuildContext, configFile: string) {
return rollupWorker(context, configFile)
.then(() => {
+ context.bundleState = BuildState.SuccessfulBuild;
logger.finish();
})
.catch(err => {
+ context.bundleState = BuildState.RequiresBuild;
throw logger.fail(err);
});
}
@@ -31,9 +31,11 @@ export function rollupUpdate(event: string, filePath: string, context: BuildCont
return rollupWorker(context, configFile)
.then(() => {
+ context.bundleState = BuildState.SuccessfulBuild;
logger.finish();
})
.catch(err => {
+ context.bundleState = BuildState.RequiresBuild;
throw logger.fail(err);
});
}
@@ -60,10 +62,8 @@ export function rollupWorker(context: BuildContext, configFile: string): Promise
);
}
- if (context.useBundleCache) {
- // tell rollup to use a previous bundle as its starting point
- rollupConfig.cache = cachedBundle;
- }
+ // tell rollup to use a previous bundle as its starting point
+ rollupConfig.cache = cachedBundle;
if (!rollupConfig.onwarn) {
// use our own logger if one wasn't already provided
@@ -84,10 +84,6 @@ export function rollupWorker(context: BuildContext, configFile: string): Promise
// this reference can be used elsewhere in the build (sass)
context.moduleFiles = bundle.modules.map((m) => m.id);
- // async cache all the module paths so we don't need
- // to always bundle to know which modules are used
- setModulePathsCache(context.moduleFiles);
-
// cache our bundle for later use
if (context.isWatch) {
cachedBundle = bundle;
@@ -98,7 +94,6 @@ export function rollupWorker(context: BuildContext, configFile: string): Promise
})
.then(() => {
// clean up any references (overkill yes, but let's play it safe)
- emit(EventType.BundleFinished, rollupConfig.dest);
rollupConfig = rollupConfig.cache = rollupConfig.onwarn = rollupConfig.plugins = null;
resolve();
@@ -130,7 +125,7 @@ export function getOutputDest(context: BuildContext, rollupConfig: RollupConfig)
function checkDeprecations(context: BuildContext, rollupConfig: RollupConfig) {
if (!context.isProd) {
- if (rollupConfig.entry.indexOf('.tmp') > -1 || endsWith(rollupConfig.entry, '.js')) {
+ if (rollupConfig.entry.indexOf('.tmp') > -1 || rollupConfig.entry.endsWith('.js')) {
// warning added 2016-10-05, v0.0.29
throw new BuildError('\nDev builds no longer use the ".tmp" directory. Please update your rollup config\'s\n' +
'entry to use your "src" directory\'s "main.dev.ts" TypeScript file.\n' +
diff --git a/src/sass.ts b/src/sass.ts
index a2ccf5c2..f5574183 100755
--- a/src/sass.ts
+++ b/src/sass.ts
@@ -1,11 +1,11 @@
import { basename, dirname, join, sep } from 'path';
-import { BuildContext, TaskInfo } from './util/interfaces';
+import { BuildContext, BuildState, TaskInfo } from './util/interfaces';
import { BuildError, Logger } from './util/logger';
-import { emit, EventType } from './util/events';
+import { bundle } from './bundle';
import { ensureDirSync, readdirSync, writeFile } from 'fs-extra';
import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config';
-import { getModulePathsCache } from './util/helpers';
-import { runDiagnostics, clearSassDiagnostics } from './util/logger-sass';
+import { runSassDiagnostics } from './util/logger-sass';
+import { printDiagnostics, clearDiagnostics, DiagnosticsType } from './util/logger-diagnostics';
import { SassError, render as nodeSassRender, Result } from 'node-sass';
import * as postcss from 'postcss';
import * as autoprefixer from 'autoprefixer';
@@ -17,14 +17,14 @@ export function sass(context?: BuildContext, configFile?: string) {
const logger = new Logger('sass');
- context.successfulSass = false;
-
return sassWorker(context, configFile)
- .then(() => {
- context.successfulSass = true;
+ .then(outFile => {
+ context.sassState = BuildState.SuccessfulBuild;
logger.finish();
+ return outFile;
})
.catch(err => {
+ context.sassState = BuildState.RequiresBuild;
throw logger.fail(err);
});
}
@@ -36,29 +36,28 @@ export function sassUpdate(event: string, filePath: string, context: BuildContex
const logger = new Logger('sass update');
return sassWorker(context, configFile)
- .then(() => {
+ .then(outFile => {
+ context.sassState = BuildState.SuccessfulBuild;
logger.finish();
+ return outFile;
})
.catch(err => {
+ context.sassState = BuildState.RequiresBuild;
throw logger.fail(err);
});
}
export function sassWorker(context: BuildContext, configFile: string) {
- return new Promise((resolve, reject) => {
- if (!context.moduleFiles) {
- // we haven't already gotten the moduleFiles in this process
- // see if we have it cached
- context.moduleFiles = getModulePathsCache();
- if (!context.moduleFiles) {
- reject(new BuildError('Cannot generate Sass files without first bundling JavaScript ' +
- 'files in order to know all used modules. Please build JS files first.'));
- return;
- }
- }
+ const bundlePromise: Promise[] = [];
+ if (!context.moduleFiles) {
+ // sass must always have a list of all the used module files
+ // so ensure we bundle if moduleFiles are currently unknown
+ bundlePromise.push(bundle(context));
+ }
- clearSassDiagnostics(context);
+ return Promise.all(bundlePromise).then(() => {
+ clearDiagnostics(context, DiagnosticsType.Sass);
const sassConfig: SassConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile);
@@ -84,15 +83,7 @@ export function sassWorker(context: BuildContext, configFile: string) {
generateSassData(context, sassConfig);
}
- return render(context, sassConfig)
- .then(() => {
- resolve(true);
- }, (reason: any) => {
- reject(reason);
- })
- .catch(err => {
- reject(new BuildError(err));
- });
+ return render(context, sassConfig);
});
}
@@ -219,8 +210,11 @@ function getComponentDirectories(moduleDirectories: string[], sassConfig: SassCo
// filter out module directories we know wouldn't have sibling component sass file
// just a way to reduce the amount of lookups to be done later
return moduleDirectories.filter(moduleDirectory => {
+ // normalize this directory is using / between directories
+ moduleDirectory = moduleDirectory.replace(/\\/g, '/');
+
for (var i = 0; i < sassConfig.excludeModules.length; i++) {
- if (moduleDirectory.indexOf(sassConfig.excludeModules[i]) > -1) {
+ if (moduleDirectory.indexOf('/node_modules/' + sassConfig.excludeModules[i] + '/') > -1) {
return false;
}
}
@@ -229,18 +223,9 @@ function getComponentDirectories(moduleDirectories: string[], sassConfig: SassCo
}
-function render(context: BuildContext, sassConfig: SassConfig) {
+function render(context: BuildContext, sassConfig: SassConfig): Promise {
return new Promise((resolve, reject) => {
- if (context.useSassCache && lastRenderKey !== null) {
- // if the sass data imports are same, don't bother
- const renderKey = getRenderCacheKey(sassConfig);
- if (renderKey === lastRenderKey) {
- resolve();
- return;
- }
- }
-
sassConfig.omitSourceMapUrl = true;
if (sassConfig.sourceMap) {
@@ -249,20 +234,17 @@ function render(context: BuildContext, sassConfig: SassConfig) {
}
nodeSassRender(sassConfig, (sassError: SassError, sassResult: Result) => {
- const diagnostics = runDiagnostics(context, sassError);
+ const diagnostics = runSassDiagnostics(context, sassError);
if (diagnostics.length) {
+ printDiagnostics(context, DiagnosticsType.Sass, diagnostics, true, true);
// sass render error :(
- const buildError = new BuildError();
- buildError.updatedDiagnostics = true;
- emit(EventType.UpdatedDiagnostics);
- reject(buildError);
+ reject(new BuildError());
} else {
// sass render success :)
- renderSassSuccess(context, sassResult, sassConfig).then(() => {
- lastRenderKey = getRenderCacheKey(sassConfig);
- resolve();
+ renderSassSuccess(context, sassResult, sassConfig).then(outFile => {
+ resolve(outFile);
}).catch(err => {
reject(new BuildError(err));
@@ -273,7 +255,7 @@ function render(context: BuildContext, sassConfig: SassConfig) {
}
-function renderSassSuccess(context: BuildContext, sassResult: Result, sassConfig: SassConfig): Promise {
+function renderSassSuccess(context: BuildContext, sassResult: Result, sassConfig: SassConfig): Promise {
if (sassConfig.autoprefixer) {
// with autoprefixer
@@ -361,7 +343,7 @@ function generateSourceMaps(sassResult: Result, sassConfig: SassConfig) {
}
-function writeOutput(context: BuildContext, sassConfig: SassConfig, cssOutput: string, mappingsOutput: string) {
+function writeOutput(context: BuildContext, sassConfig: SassConfig, cssOutput: string, mappingsOutput: string): Promise {
return new Promise((resolve, reject) => {
Logger.debug(`sass start write output: ${sassConfig.outFile}`);
@@ -393,12 +375,9 @@ function writeOutput(context: BuildContext, sassConfig: SassConfig, cssOutput: s
});
}
- // notify a file has changed
- emit(EventType.SassFinished, sassConfig.outFile);
-
// css file all saved
// note that we're not waiting on the css map to finish saving
- resolve();
+ resolve(sassConfig.outFile);
}
});
});
@@ -447,17 +426,6 @@ function defaultSortComponentFilesFn(a: any, b: any): number {
}
-function getRenderCacheKey(sassConfig: SassConfig) {
- return [
- sassConfig.data,
- sassConfig.file,
- ].join('|');
-}
-
-
-let lastRenderKey: string = null;
-
-
const taskInfo: TaskInfo = {
fullArg: '--sass',
shortArg: '-s',
diff --git a/src/serve.ts b/src/serve.ts
index 07ae135c..33ea43ed 100644
--- a/src/serve.ts
+++ b/src/serve.ts
@@ -2,7 +2,6 @@ import { BuildContext } from './util/interfaces';
import { generateContext, getConfigValue, hasConfigValue } from './util/config';
import { Logger } from './util/logger';
import { watch } from './watch';
-import * as chalk from 'chalk';
import open from './util/open';
import { createNotificationServer } from './dev-server/notification-server';
import { createHttpServer } from './dev-server/http-server';
@@ -21,6 +20,7 @@ export function serve(context?: BuildContext) {
const config: ServeConfig = {
httpPort: getHttpServerPort(context),
host: getHttpServerHost(context),
+ rootDir: context.rootDir,
wwwDir: context.wwwDir,
buildDir: context.buildDir,
launchBrowser: launchBrowser(context),
@@ -30,14 +30,13 @@ export function serve(context?: BuildContext) {
liveReloadPort: getLiveReloadServerPort(context),
notificationPort: getNotificationPort(context),
useServerLogs: useServerLogs(context),
- useNotifier: true,
useProxy: useProxy(context),
notifyOnConsoleLog: sendClientConsoleLogs(context)
};
- createHttpServer(config);
- createLiveReloadServer(config);
createNotificationServer(config);
+ createLiveReloadServer(config);
+ createHttpServer(config);
return watch(context)
.then(() => {
@@ -59,7 +58,8 @@ function onReady(config: ServeConfig, context: BuildContext) {
open(openOptions.join(''), browserToLaunch(context));
}
- Logger.info(chalk.green(`dev server running: http://${config.host}:${config.httpPort}/`));
+ Logger.info(`dev server running: http://${config.host}:${config.httpPort}/`, 'green', true);
+ Logger.newLine();
}
function getHttpServerPort(context: BuildContext) {
diff --git a/src/spec/ion-compiler.spec.ts b/src/spec/ion-compiler.spec.ts
index 69bc12ba..2517d7a0 100644
--- a/src/spec/ion-compiler.spec.ts
+++ b/src/spec/ion-compiler.spec.ts
@@ -47,7 +47,7 @@ describe('ion-compiler', () => {
// arrange
let context: BuildContext = {};
context.fileCache = new FileCache();
- context.fileCache.put(importer, {
+ context.fileCache.set(importer, {
path: importer,
content: null
});
@@ -63,7 +63,7 @@ describe('ion-compiler', () => {
// arrange
let context: BuildContext = {};
context.fileCache = new FileCache();
- context.fileCache.put(importer, {
+ context.fileCache.set(importer, {
path: importer,
content: ''
});
@@ -79,7 +79,7 @@ describe('ion-compiler', () => {
// arrange
let context: BuildContext = {};
context.fileCache = new FileCache();
- context.fileCache.put(importer, {
+ context.fileCache.set(importer, {
path: importer,
content: 'fake irrelevant data'
});
@@ -88,7 +88,7 @@ describe('ion-compiler', () => {
const importerBasename = dirname(importer);
const importeeFullPath = resolve(join(importerBasename, importee)) + '.ts';
- context.fileCache.put(importeeFullPath, {
+ context.fileCache.set(importeeFullPath, {
path: importeeFullPath,
content: 'someContent'
});
@@ -105,7 +105,7 @@ describe('ion-compiler', () => {
// arrange
let context: BuildContext = {};
context.fileCache = new FileCache();
- context.fileCache.put(importer, {
+ context.fileCache.set(importer, {
path: importer,
content: 'fake irrelevant data'
});
@@ -114,7 +114,7 @@ describe('ion-compiler', () => {
const importerBasename = dirname(importer);
const importeeFullPath = resolve(join(importerBasename, importee)) + '.ts';
- context.fileCache.put(importeeFullPath, { path: importeeFullPath, content: null});
+ context.fileCache.set(importeeFullPath, { path: importeeFullPath, content: null});
// act
const result = resolveId(importee, importer, context);
@@ -128,7 +128,7 @@ describe('ion-compiler', () => {
// arrange
let context: BuildContext = {};
context.fileCache = new FileCache();
- context.fileCache.put(importer, {
+ context.fileCache.set(importer, {
path: importer,
content: 'fake irrelevant data'
});
@@ -137,7 +137,7 @@ describe('ion-compiler', () => {
const importerBasename = dirname(importer);
const importeeFullPath = join(resolve(join(importerBasename, importee)), 'index.ts');
- context.fileCache.put(importeeFullPath, { path: importeeFullPath, content: null });
+ context.fileCache.set(importeeFullPath, { path: importeeFullPath, content: null });
// act
const result = resolveId(importee, importer, context);
@@ -151,7 +151,7 @@ describe('ion-compiler', () => {
// arrange
let context: BuildContext = {};
context.fileCache = new FileCache();
- context.fileCache.put(importer, {
+ context.fileCache.set(importer, {
path: importer,
content: 'fake irrelevant data'
});
@@ -160,7 +160,7 @@ describe('ion-compiler', () => {
const importerBasename = dirname(importer);
const importeeFullPath = join(resolve(join(importerBasename, importee)), 'index.ts');
- context.fileCache.put(importeeFullPath, { path: importeeFullPath, content: null});
+ context.fileCache.set(importeeFullPath, { path: importeeFullPath, content: null});
// act
const result = resolveId(importee, importer, context);
@@ -173,7 +173,7 @@ describe('ion-compiler', () => {
// arrange
let context: BuildContext = {};
context.fileCache = new FileCache();
- context.fileCache.put(importer, {
+ context.fileCache.set(importer, {
path: importer,
content: 'fake irrelevant data'
});
diff --git a/src/spec/logger.spec.ts b/src/spec/logger.spec.ts
index b4c0c704..e65473c4 100644
--- a/src/spec/logger.spec.ts
+++ b/src/spec/logger.spec.ts
@@ -16,7 +16,6 @@ describe('Logger', () => {
const json = buildErrorCopy.toJson();
expect(json.hasBeenLogged).toEqual(buildError.hasBeenLogged);
- expect(json.updatedDiagnostics).toEqual(buildError.updatedDiagnostics);
expect(json.message).toEqual(buildError.message);
expect(json.name).toEqual(buildError.name);
expect(json.stack).toEqual(buildError.stack);
@@ -30,7 +29,6 @@ describe('Logger', () => {
buildError.stack = 'stack';
const json = buildError.toJson();
expect(json.hasBeenLogged).toEqual(buildError.hasBeenLogged);
- expect(json.updatedDiagnostics).toEqual(buildError.updatedDiagnostics);
expect(json.message).toEqual(buildError.message);
expect(json.name).toEqual(buildError.name);
expect(json.stack).toEqual(buildError.stack);
diff --git a/src/spec/watch.spec.ts b/src/spec/watch.spec.ts
new file mode 100644
index 00000000..f86817e7
--- /dev/null
+++ b/src/spec/watch.spec.ts
@@ -0,0 +1,333 @@
+import { BuildContext, BuildState } from '../util/interfaces';
+import { FileCache } from '../util/file-cache';
+import { runBuildUpdate, ChangedFile } from '../watch';
+import { Watcher, prepareWatcher } from '../watch';
+import * as path from 'path';
+
+
+describe('watch', () => {
+
+ describe('runBuildUpdate', () => {
+
+ it('should get the html file if thats the only one', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.html',
+ ext: '.html'
+ }];
+ const data = runBuildUpdate(context, files);
+ expect(data.filePath).toEqual('file1.html');
+ });
+
+ it('should get the scss file for the filePath over html', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.html',
+ ext: '.html'
+ }, {
+ event: 'change',
+ filePath: 'file1.scss',
+ ext: '.scss'
+ }];
+ const data = runBuildUpdate(context, files);
+ expect(data.filePath).toEqual('file1.scss');
+ });
+
+ it('should get the ts file for the filePath over the others', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.html',
+ ext: '.html'
+ }, {
+ event: 'change',
+ filePath: 'file1.scss',
+ ext: '.scss'
+ }, {
+ event: 'change',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }];
+ const data = runBuildUpdate(context, files);
+ expect(data.filePath).toEqual('file1.ts');
+ });
+
+ it('should require transpile full build for html file add', () => {
+ const files: ChangedFile[] = [{
+ event: 'add',
+ filePath: 'file1.html',
+ ext: '.html'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.transpileState).toEqual(BuildState.RequiresBuild);
+ });
+
+ it('should require transpile full build for html file change and not already successful bundle', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.html',
+ ext: '.html'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.transpileState).toEqual(BuildState.RequiresBuild);
+ });
+
+ it('should require template update for html file change and already successful bundle', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.html',
+ ext: '.html'
+ }];
+ context.bundleState = BuildState.SuccessfulBuild;
+ runBuildUpdate(context, files);
+ expect(context.templateState).toEqual(BuildState.RequiresUpdate);
+ });
+
+ it('should require sass update for ts file unlink', () => {
+ const files: ChangedFile[] = [{
+ event: 'unlink',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.sassState).toEqual(BuildState.RequiresUpdate);
+ });
+
+ it('should require sass update for ts file add', () => {
+ const files: ChangedFile[] = [{
+ event: 'add',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.sassState).toEqual(BuildState.RequiresUpdate);
+ });
+
+ it('should require sass update for scss file add', () => {
+ const files: ChangedFile[] = [{
+ event: 'add',
+ filePath: 'file1.scss',
+ ext: '.scss'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.sassState).toEqual(BuildState.RequiresUpdate);
+ });
+
+ it('should require sass update for scss file change', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.scss',
+ ext: '.scss'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.sassState).toEqual(BuildState.RequiresUpdate);
+ });
+
+ it('should require transpile full build for single ts add, but only bundle update when already successful bundle', () => {
+ const files: ChangedFile[] = [{
+ event: 'add',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }];
+ context.bundleState = BuildState.SuccessfulBuild;
+ runBuildUpdate(context, files);
+ expect(context.transpileState).toEqual(BuildState.RequiresBuild);
+ expect(context.bundleState).toEqual(BuildState.RequiresUpdate);
+ });
+
+ it('should require transpile full build for single ts add', () => {
+ const files: ChangedFile[] = [{
+ event: 'add',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.transpileState).toEqual(BuildState.RequiresBuild);
+ expect(context.bundleState).toEqual(BuildState.RequiresBuild);
+ });
+
+ it('should require transpile full build for single ts change and not in file cache', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.transpileState).toEqual(BuildState.RequiresBuild);
+ expect(context.bundleState).toEqual(BuildState.RequiresBuild);
+ });
+
+ it('should require transpile update only and full bundle build for single ts change and already in file cache and hasnt already had successful bundle', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }];
+ context.bundleState = BuildState.SuccessfulBuild;
+ const resolvedFilePath = path.resolve('file1.ts');
+ context.fileCache.set(resolvedFilePath, { path: 'file1.ts', content: 'content' });
+ runBuildUpdate(context, files);
+ expect(context.transpileState).toEqual(BuildState.RequiresUpdate);
+ expect(context.bundleState).toEqual(BuildState.RequiresUpdate);
+ });
+
+ it('should require transpile update only and bundle update for single ts change and already in file cache and bundle already successful', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }];
+ const resolvedFilePath = path.resolve('file1.ts');
+ context.fileCache.set(resolvedFilePath, { path: 'file1.ts', content: 'content' });
+ runBuildUpdate(context, files);
+ expect(context.transpileState).toEqual(BuildState.RequiresUpdate);
+ expect(context.bundleState).toEqual(BuildState.RequiresBuild);
+ });
+
+ it('should require transpile full build for multiple ts changes', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }, {
+ event: 'change',
+ filePath: 'file2.ts',
+ ext: '.ts'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.transpileState).toEqual(BuildState.RequiresBuild);
+ expect(context.bundleState).toEqual(BuildState.RequiresBuild);
+ });
+
+ it('should not update bundle state if no transpile changes', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.scss',
+ ext: '.scss'
+ }];
+ runBuildUpdate(context, files);
+ expect(context.bundleState).toEqual(undefined);
+ });
+
+ it('should set add event when add and changed files', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file1.ts',
+ ext: '.ts'
+ }, {
+ event: 'add',
+ filePath: 'file2.ts',
+ ext: '.ts'
+ }];
+ const data = runBuildUpdate(context, files);
+ expect(data.event).toEqual('add');
+ });
+
+ it('should set unlink event when only unlinked files', () => {
+ const files: ChangedFile[] = [{
+ event: 'unlink',
+ filePath: 'file.ts',
+ ext: '.ts'
+ }];
+ const data = runBuildUpdate(context, files);
+ expect(data.event).toEqual('unlink');
+ });
+
+ it('should set change event when only changed files', () => {
+ const files: ChangedFile[] = [{
+ event: 'change',
+ filePath: 'file.ts',
+ ext: '.ts'
+ }];
+ const data = runBuildUpdate(context, files);
+ expect(data.event).toEqual('change');
+ });
+
+ it('should do nothing if there are no changed files', () => {
+ expect(runBuildUpdate(context, [])).toEqual(null);
+ expect(runBuildUpdate(context, null)).toEqual(null);
+ });
+
+
+ let context: BuildContext;
+ beforeEach(() => {
+ context = {
+ fileCache: new FileCache()
+ };
+ });
+
+ });
+
+ describe('prepareWatcher', () => {
+
+ it('should do nothing when options.ignored is a function', () => {
+ const ignoreFn = function(){};
+ const watcher: Watcher = { options: { ignored: ignoreFn } };
+ const context: BuildContext = { srcDir: '/some/src/' };
+ prepareWatcher(context, watcher);
+ expect(watcher.options.ignored).toBe(ignoreFn);
+ });
+
+ it('should set replacePathVars when options.ignored is a string', () => {
+ const watcher: Watcher = { options: { ignored: '{{SRC}}/**/*.spec.ts' } };
+ const context: BuildContext = { srcDir: '/some/src/' };
+ prepareWatcher(context, watcher);
+ expect(watcher.options.ignored).toEqual('/some/src/**/*.spec.ts');
+ });
+
+ it('should set replacePathVars when paths is an array', () => {
+ const watcher: Watcher = { paths: [
+ '{{SRC}}/some/path1',
+ '{{SRC}}/some/path2'
+ ] };
+ const context: BuildContext = { srcDir: '/some/src/' };
+ prepareWatcher(context, watcher);
+ expect(watcher.paths.length).toEqual(2);
+ expect(watcher.paths[0]).toEqual('/some/src/some/path1');
+ expect(watcher.paths[1]).toEqual('/some/src/some/path2');
+ });
+
+ it('should set replacePathVars when paths is a string', () => {
+ const watcher: Watcher = { paths: '{{SRC}}/some/path' };
+ const context: BuildContext = { srcDir: '/some/src/' };
+ prepareWatcher(context, watcher);
+ expect(watcher.paths).toEqual('/some/src/some/path');
+ });
+
+ it('should not set options.ignoreInitial if it was provided', () => {
+ const watcher: Watcher = { options: { ignoreInitial: false } };
+ const context: BuildContext = {};
+ prepareWatcher(context, watcher);
+ expect(watcher.options.ignoreInitial).toEqual(false);
+ });
+
+ it('should set options.ignoreInitial to true if it wasnt provided', () => {
+ const watcher: Watcher = { options: {} };
+ const context: BuildContext = {};
+ prepareWatcher(context, watcher);
+ expect(watcher.options.ignoreInitial).toEqual(true);
+ });
+
+ it('should not set options.cwd from context.rootDir if it was provided', () => {
+ const watcher: Watcher = { options: { cwd: '/my/cwd/' } };
+ const context: BuildContext = { rootDir: '/my/root/dir/' };
+ prepareWatcher(context, watcher);
+ expect(watcher.options.cwd).toEqual('/my/cwd/');
+ });
+
+ it('should set options.cwd from context.rootDir if it wasnt provided', () => {
+ const watcher: Watcher = {};
+ const context: BuildContext = { rootDir: '/my/root/dir/' };
+ prepareWatcher(context, watcher);
+ expect(watcher.options.cwd).toEqual(context.rootDir);
+ });
+
+ it('should create watcher options when not provided', () => {
+ const watcher: Watcher = {};
+ const context: BuildContext = {};
+ prepareWatcher(context, watcher);
+ expect(watcher.options).toBeDefined();
+ });
+
+ });
+
+});
diff --git a/src/spec/watcher.spec.ts b/src/spec/watcher.spec.ts
deleted file mode 100644
index e8c30533..00000000
--- a/src/spec/watcher.spec.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { BuildContext } from '../util/interfaces';
-import { Watcher, prepareWatcher } from '../watch';
-
-
-describe('watch', () => {
-
- describe('prepareWatcher', () => {
-
- it('should do nothing when options.ignored is a function', () => {
- const ignoreFn = function(){};
- const watcher: Watcher = { options: { ignored: ignoreFn } };
- const context: BuildContext = { srcDir: '/some/src/' };
- prepareWatcher(context, watcher);
- expect(watcher.options.ignored).toBe(ignoreFn);
- });
-
- it('should set replacePathVars when options.ignored is a string', () => {
- const watcher: Watcher = { options: { ignored: '{{SRC}}/**/*.spec.ts' } };
- const context: BuildContext = { srcDir: '/some/src/' };
- prepareWatcher(context, watcher);
- expect(watcher.options.ignored).toEqual('/some/src/**/*.spec.ts');
- });
-
- it('should set replacePathVars when paths is an array', () => {
- const watcher: Watcher = { paths: [
- '{{SRC}}/some/path1',
- '{{SRC}}/some/path2'
- ] };
- const context: BuildContext = { srcDir: '/some/src/' };
- prepareWatcher(context, watcher);
- expect(watcher.paths.length).toEqual(2);
- expect(watcher.paths[0]).toEqual('/some/src/some/path1');
- expect(watcher.paths[1]).toEqual('/some/src/some/path2');
- });
-
- it('should set replacePathVars when paths is a string', () => {
- const watcher: Watcher = { paths: '{{SRC}}/some/path' };
- const context: BuildContext = { srcDir: '/some/src/' };
- prepareWatcher(context, watcher);
- expect(watcher.paths).toEqual('/some/src/some/path');
- });
-
- it('should not set options.ignoreInitial if it was provided', () => {
- const watcher: Watcher = { options: { ignoreInitial: false } };
- const context: BuildContext = {};
- prepareWatcher(context, watcher);
- expect(watcher.options.ignoreInitial).toEqual(false);
- });
-
- it('should set options.ignoreInitial to true if it wasnt provided', () => {
- const watcher: Watcher = { options: {} };
- const context: BuildContext = {};
- prepareWatcher(context, watcher);
- expect(watcher.options.ignoreInitial).toEqual(true);
- });
-
- it('should not set options.cwd from context.rootDir if it was provided', () => {
- const watcher: Watcher = { options: { cwd: '/my/cwd/' } };
- const context: BuildContext = { rootDir: '/my/root/dir/' };
- prepareWatcher(context, watcher);
- expect(watcher.options.cwd).toEqual('/my/cwd/');
- });
-
- it('should set options.cwd from context.rootDir if it wasnt provided', () => {
- const watcher: Watcher = {};
- const context: BuildContext = { rootDir: '/my/root/dir/' };
- prepareWatcher(context, watcher);
- expect(watcher.options.cwd).toEqual(context.rootDir);
- });
-
- it('should create watcher options when not provided', () => {
- const watcher: Watcher = {};
- const context: BuildContext = {};
- prepareWatcher(context, watcher);
- expect(watcher.options).toBeDefined();
- });
-
- });
-
-});
diff --git a/src/template.ts b/src/template.ts
index ef28ed8d..cd462c43 100644
--- a/src/template.ts
+++ b/src/template.ts
@@ -1,62 +1,56 @@
-import { emit, EventType } from './util/events';
-import { BUNDLER_WEBPACK } from './util/config';
-import { BuildContext } from './util/interfaces';
-import { BuildError, IgnorableError, Logger } from './util/logger';
-import { bundleUpdate, getJsOutputDest } from './bundle';
-import { dirname, extname, join, parse, resolve } from 'path';
-import { readdirSync, readFileSync, writeFileSync } from 'fs';
-import { sassUpdate } from './sass';
-import { buildUpdate } from './build';
-
-
-export function templateUpdate(event: string, filePath: string, context: BuildContext) {
- const logger = new Logger('template update');
-
- return templateUpdateWorker(event, filePath, context)
- .then(() => {
- logger.finish();
- })
- .catch(err => {
- if (err instanceof IgnorableError) {
- throw err;
- }
- throw logger.fail(err);
- });
-}
-
-
-function templateUpdateWorker(event: string, filePath: string, context: BuildContext) {
- Logger.debug(`templateUpdate, event: ${event}, path: ${filePath}`);
-
- if (event === 'change') {
- if (context.bundler === BUNDLER_WEBPACK) {
- Logger.debug(`templateUpdate: updating webpack file`);
- const typescriptFile = getTemplatesTypescriptFile(context, filePath);
- if (typescriptFile && typescriptFile.length > 0) {
- return buildUpdate(event, typescriptFile, context);
- }
- } else if (updateBundledJsTemplate(context, filePath)) {
- Logger.debug(`templateUpdate, updated js bundle, path: ${filePath}`);
- // technically, the bundle has changed so emit an event saying so
- emit(EventType.BundleFinished, getJsOutputDest(context));
- return Promise.resolve();
+import { BuildContext, BuildState } from './util/interfaces';
+import { Logger } from './util/logger';
+import { getJsOutputDest } from './bundle';
+import { join, parse, resolve } from 'path';
+import { readFileSync, writeFile } from 'fs';
+
+
+export function templateUpdate(event: string, htmlFilePath: string, context: BuildContext) {
+ return new Promise((resolve) => {
+ const start = Date.now();
+ const bundleOutputDest = getJsOutputDest(context);
+
+ function failed() {
+ context.transpileState = BuildState.RequiresBuild;
+ context.bundleState = BuildState.RequiresUpdate;
+ resolve();
}
- }
- Logger.debug('templateUpdateWorker: Can\'t update template, doing a full rebuild');
- // not sure how it changed, just do a full rebuild without the bundle cache
- context.useBundleCache = false;
- return bundleUpdate(event, filePath, context)
- .then(() => {
- context.useSassCache = true;
- return sassUpdate(event, filePath, context);
- })
- .catch(err => {
- if (err instanceof IgnorableError) {
- throw err;
+ try {
+ let bundleSourceText = readFileSync(bundleOutputDest, 'utf8');
+ let newTemplateContent = readFileSync(htmlFilePath, 'utf8');
+
+ bundleSourceText = replaceBundleJsTemplate(bundleSourceText, newTemplateContent, htmlFilePath);
+
+ if (bundleSourceText) {
+ // awesome, all good and template updated in the bundle file
+ const logger = new Logger(`template update`);
+ logger.setStartTime(start);
+
+ writeFile(bundleOutputDest, bundleSourceText, { encoding: 'utf8'}, (err) => {
+ if (err) {
+ // eww, error saving
+ logger.fail(err);
+ failed();
+
+ } else {
+ // congrats, all gud
+ Logger.debug(`updateBundledJsTemplate, updated: ${htmlFilePath}`);
+ context.templateState = BuildState.SuccessfulBuild;
+ logger.finish();
+ resolve();
+ }
+ });
+
+ } else {
+ failed();
}
- throw new BuildError(err);
- });
+
+ } catch (e) {
+ Logger.debug(`updateBundledJsTemplate error: ${e}`);
+ failed();
+ }
+ });
}
@@ -111,94 +105,38 @@ export function replaceTemplateUrl(match: TemplateUrlMatch, htmlFilePath: string
}
-function updateBundledJsTemplate(context: BuildContext, htmlFilePath: string) {
- Logger.debug(`updateBundledJsTemplate, start: ${htmlFilePath}`);
-
- const outputDest = getJsOutputDest(context);
-
- try {
- let bundleSourceText = readFileSync(outputDest, 'utf8');
- let newTemplateContent = readFileSync(htmlFilePath, 'utf8');
-
- bundleSourceText = replaceBundleJsTemplate(bundleSourceText, newTemplateContent, htmlFilePath);
+export function replaceBundleJsTemplate(bundleSourceText: string, newTemplateContent: string, htmlFilePath: string): string {
+ let prefix = getTemplatePrefix(htmlFilePath);
+ let startIndex = bundleSourceText.indexOf(prefix);
- if (bundleSourceText) {
- writeFileSync(outputDest, bundleSourceText, { encoding: 'utf8'});
- Logger.debug(`updateBundledJsTemplate, updated: ${htmlFilePath}`);
- return true;
- }
+ let isStringified = false;
- } catch (e) {
- Logger.debug(`updateBundledJsTemplate error: ${e}`);
+ if (startIndex === -1) {
+ prefix = stringify(prefix);
+ isStringified = true;
}
- return false;
-}
-
-function getTemplatesTypescriptFile(context: BuildContext, templatePath: string) {
- try {
- const srcDirName = dirname(templatePath);
- const files = readdirSync(srcDirName);
- const typescriptFilesNames = files.filter(file => {
- return extname(file) === '.ts';
- });
- for (const fileName of typescriptFilesNames) {
- const fullPath = join(srcDirName, fileName);
- const fileContent = readFileSync(fullPath).toString();
- const isMatch = tryToMatchTemplateUrl(fileContent, fullPath, srcDirName, templatePath);
- if (isMatch) {
- return fullPath;
- }
- }
- return null;
- } catch (ex) {
- Logger.debug('getTemplatesTypescriptFile: Error occurred - ', ex.message);
+ startIndex = bundleSourceText.indexOf(prefix);
+ if (startIndex === -1) {
return null;
}
-}
-function tryToMatchTemplateUrl(fileContent: string, filePath: string, componentDirPath: string, templateFilePath: string) {
- let lastMatch: string = null;
- let match: TemplateUrlMatch;
-
- while (match = getTemplateMatch(fileContent)) {
- if (match.component === lastMatch) {
- // panic! we don't want to melt any machines if there's a bug
- Logger.debug(`Error matching component: ${match.component}`);
- return false;
- }
- lastMatch = match.component;
-
- if (!match.templateUrl || match.templateUrl === '') {
- Logger.error(`Error @Component templateUrl missing in: "${filePath}"`);
- return false;
- }
-
- const templatUrlPath = resolve(join(componentDirPath, match.templateUrl));
- if (templatUrlPath === templateFilePath) {
- return true;
- }
- }
- return false;
-}
-
-export function replaceBundleJsTemplate(bundleSourceText: string, newTemplateContent: string, htmlFilePath: string): string {
- const prefix = getTemplatePrefix(htmlFilePath);
- const startIndex = bundleSourceText.indexOf(prefix);
-
- if (startIndex === -1) {
- return null;
+ let suffix = getTemplateSuffix(htmlFilePath);
+ if (isStringified) {
+ suffix = stringify(suffix);
}
- const suffix = getTemplateSuffix(htmlFilePath);
const endIndex = bundleSourceText.indexOf(suffix, startIndex + 1);
-
if (endIndex === -1) {
return null;
}
const oldTemplate = bundleSourceText.substring(startIndex, endIndex + suffix.length);
- const newTemplate = getTemplateFormat(htmlFilePath, newTemplateContent);
+ let newTemplate = getTemplateFormat(htmlFilePath, newTemplateContent);
+
+ if (isStringified) {
+ newTemplate = stringify(newTemplate);
+ }
let lastChange: string = null;
while (bundleSourceText.indexOf(oldTemplate) > -1 && bundleSourceText !== lastChange) {
@@ -208,6 +146,11 @@ export function replaceBundleJsTemplate(bundleSourceText: string, newTemplateCon
return bundleSourceText;
}
+function stringify(str: string) {
+ str = JSON.stringify(str);
+ return str.substr(1, str.length - 2);
+}
+
export function getTemplateFormat(htmlFilePath: string, content: string) {
// turn the template into one line and espcape single quotes
diff --git a/src/transpile-worker.ts b/src/transpile-worker.ts
new file mode 100644
index 00000000..1d999bbd
--- /dev/null
+++ b/src/transpile-worker.ts
@@ -0,0 +1,34 @@
+import { BuildContext } from './util/interfaces';
+import { transpileWorker, TranspileWorkerMessage, TranspileWorkerConfig } from './transpile';
+
+
+const context: BuildContext = {};
+
+process.on('message', (incomingMsg: TranspileWorkerMessage) => {
+ context.rootDir = incomingMsg.rootDir;
+ context.buildDir = incomingMsg.buildDir;
+ context.isProd = incomingMsg.isProd;
+
+ const workerConfig: TranspileWorkerConfig = {
+ configFile: incomingMsg.configFile,
+ writeInMemory: false,
+ sourceMaps: false,
+ cache: false,
+ inlineTemplate: false
+ };
+
+ transpileWorker(context, workerConfig)
+ .then(() => {
+ const outgoingMsg: TranspileWorkerMessage = {
+ transpileSuccess: true
+ };
+ process.send(outgoingMsg);
+ })
+ .catch(() => {
+ const outgoingMsg: TranspileWorkerMessage = {
+ transpileSuccess: false
+ };
+ process.send(outgoingMsg);
+ });
+
+});
diff --git a/src/transpile.ts b/src/transpile.ts
index 477a9207..806f2ed2 100644
--- a/src/transpile.ts
+++ b/src/transpile.ts
@@ -1,15 +1,16 @@
import { FileCache } from './util/file-cache';
-import { BuildContext, File } from './util/interfaces';
+import { BuildContext, BuildState } from './util/interfaces';
import { BuildError, Logger } from './util/logger';
import { buildJsSourceMaps } from './bundle';
-import { changeExtension, endsWith } from './util/helpers';
+import { changeExtension } from './util/helpers';
+import { EventEmitter } from 'events';
import { generateContext } from './util/config';
import { inlineTemplate } from './template';
-import { join, normalize, resolve } from 'path';
-import { lintUpdate } from './lint';
import { readFileSync } from 'fs';
-import { runDiagnostics, clearTypeScriptDiagnostics } from './util/logger-typescript';
-import { runWorker } from './worker-client';
+import { runTypeScriptDiagnostics } from './util/logger-typescript';
+import { printDiagnostics, clearDiagnostics, DiagnosticsType } from './util/logger-diagnostics';
+import { fork, ChildProcess } from 'child_process';
+import * as path from 'path';
import * as ts from 'typescript';
@@ -28,20 +29,17 @@ export function transpile(context?: BuildContext) {
return transpileWorker(context, workerConfig)
.then(() => {
+ context.transpileState = BuildState.SuccessfulBuild;
logger.finish();
})
.catch(err => {
+ context.transpileState = BuildState.RequiresBuild;
throw logger.fail(err);
});
}
export function transpileUpdate(event: string, filePath: string, context: BuildContext) {
- if (!filePath.endsWith('.ts') ) {
- // however this ran, the changed file wasn't a .ts file so carry on
- return Promise.resolve();
- }
-
const workerConfig: TranspileWorkerConfig = {
configFile: getTsConfigPath(context),
writeInMemory: true,
@@ -54,9 +52,11 @@ export function transpileUpdate(event: string, filePath: string, context: BuildC
return transpileUpdateWorker(event, filePath, context, workerConfig)
.then(tsFiles => {
+ context.transpileState = BuildState.SuccessfulBuild;
logger.finish();
})
.catch(err => {
+ context.transpileState = BuildState.RequiresBuild;
throw logger.fail(err);
});
}
@@ -70,7 +70,7 @@ export function transpileWorker(context: BuildContext, workerConfig: TranspileWo
// let's do this
return new Promise((resolve, reject) => {
- clearTypeScriptDiagnostics(context);
+ clearDiagnostics(context, DiagnosticsType.TypeScript);
// get the tsconfig data
const tsConfig = getTsConfig(context, workerConfig.configFile);
@@ -101,131 +101,152 @@ export function transpileWorker(context: BuildContext, workerConfig: TranspileWo
}
});
+ // cache the typescript program for later use
+ cachedProgram = program;
+
const tsDiagnostics = program.getSyntacticDiagnostics()
.concat(program.getSemanticDiagnostics())
.concat(program.getOptionsDiagnostics());
- const diagnostics = runDiagnostics(context, tsDiagnostics);
+ const diagnostics = runTypeScriptDiagnostics(context, tsDiagnostics);
if (diagnostics.length) {
- // transpile failed :(
- cachedProgram = null;
+ // darn, we've got some things wrong, transpile failed :(
+ printDiagnostics(context, DiagnosticsType.TypeScript, diagnostics, true, true);
- const buildError = new BuildError();
- buildError.updatedDiagnostics = true;
- reject(buildError);
+ reject(new BuildError());
} else {
// transpile success :)
- // cache the typescript program for later use
- cachedProgram = program;
-
resolve();
}
});
}
+export function canRunTranspileUpdate(event: string, filePath: string, context: BuildContext) {
+ if (event === 'change' && context.fileCache) {
+ return context.fileCache.has(path.resolve(filePath));
+ }
+ return false;
+}
+
+
/**
* Iterative build for one TS file. If it's not an existing file change, or
* something errors out then it falls back to do the full build.
*/
function transpileUpdateWorker(event: string, filePath: string, context: BuildContext, workerConfig: TranspileWorkerConfig) {
- clearTypeScriptDiagnostics(context);
+ return new Promise((resolve, reject) => {
+ clearDiagnostics(context, DiagnosticsType.TypeScript);
- filePath = resolve(filePath);
+ filePath = path.resolve(filePath);
- // let's run tslint on this one file too, but run it in another
- // processor core and don't let it's results hang anything up
- lintUpdate(event, filePath, context);
+ // an existing ts file we already know about has changed
+ // let's "TRY" to do a single module build for this one file
+ const tsConfig = getTsConfig(context, workerConfig.configFile);
- let file: File = null;
- if (context.fileCache) {
- file = context.fileCache.get(filePath);
- }
- if (event === 'change' && file) {
- try {
- // an existing ts file we already know about has changed
- // let's "TRY" to do a single module build for this one file
- const tsConfig = getTsConfig(context, workerConfig.configFile);
+ // build the ts source maps if the bundler is going to use source maps
+ tsConfig.options.sourceMap = buildJsSourceMaps(context);
- // build the ts source maps if the bundler is going to use source maps
- tsConfig.options.sourceMap = buildJsSourceMaps(context);
+ const transpileOptions: ts.TranspileOptions = {
+ compilerOptions: tsConfig.options,
+ fileName: filePath,
+ reportDiagnostics: true
+ };
+
+ // let's manually transpile just this one ts file
+ // load up the source text for this one module
+ const sourceText = readFileSync(filePath, 'utf8');
+
+ // transpile this one module
+ const transpileOutput = ts.transpileModule(sourceText, transpileOptions);
+
+ const diagnostics = runTypeScriptDiagnostics(context, transpileOutput.diagnostics);
+
+ if (diagnostics.length) {
+ printDiagnostics(context, DiagnosticsType.TypeScript, diagnostics, false, true);
- const transpileOptions: ts.TranspileOptions = {
- compilerOptions: tsConfig.options,
- fileName: filePath,
- reportDiagnostics: true
- };
-
- // let's manually transpile just this one ts file
- // load up the source text for this one module
- const sourceText = readFileSync(filePath, 'utf8');
-
- // transpile this one module
- const transpileOutput = ts.transpileModule(sourceText, transpileOptions);
-
- const diagnostics = runDiagnostics(context, transpileOutput.diagnostics);
-
- if (diagnostics.length) {
- // darn, we've got some errors with this transpiling :(
- // but at least we reported the errors like really really fast, so there's that
- Logger.debug(`transpileUpdateWorker: transpileModule, diagnostics: ${diagnostics.length}`);
-
- const buildError = new BuildError();
- buildError.updatedDiagnostics = true;
- return Promise.reject(buildError);
-
- } else if (!transpileOutput.outputText) {
- // derp, not sure how there's no output text, just do a full build
- Logger.debug(`transpileUpdateWorker: transpileModule, missing output text`);
-
- } else {
- // convert the path to have a .js file extension for consistency
- const newPath = changeExtension(filePath, '.js');
-
- const sourceMapFile = { path: newPath + '.map', content: transpileOutput.sourceMapText };
- let jsContent: string = transpileOutput.outputText;
- if (workerConfig.inlineTemplate) {
- // use original path for template inlining
- jsContent = inlineTemplate(transpileOutput.outputText, filePath);
- }
- const jsFile = { path: newPath, content: jsContent };
- const tsFile = { path: filePath, content: sourceText};
-
- context.fileCache.put(sourceMapFile.path, sourceMapFile);
- context.fileCache.put(jsFile.path, jsFile);
- context.fileCache.put(tsFile.path, tsFile);
-
- // cool, the lil transpiling went through, but
- // let's still do the big transpiling (on another processor core)
- // and if there's anything wrong it'll print out messages
- // however, it doesn't hang anything up
- // also make sure it does a little as possible
- const fullBuildWorkerConfig: TranspileWorkerConfig = {
- configFile: workerConfig.configFile,
- writeInMemory: false,
- sourceMaps: false,
- cache: false,
- inlineTemplate: false
- };
- runWorker('transpile', 'transpileWorker', context, fullBuildWorkerConfig);
-
- return Promise.resolve();
+ // darn, we've got some errors with this transpiling :(
+ // but at least we reported the errors like really really fast, so there's that
+ Logger.debug(`transpileUpdateWorker: transpileModule, diagnostics: ${diagnostics.length}`);
+
+ reject(new BuildError());
+
+ } else {
+ // convert the path to have a .js file extension for consistency
+ const newPath = changeExtension(filePath, '.js');
+
+ const sourceMapFile = { path: newPath + '.map', content: transpileOutput.sourceMapText };
+ let jsContent: string = transpileOutput.outputText;
+ if (workerConfig.inlineTemplate) {
+ // use original path for template inlining
+ jsContent = inlineTemplate(transpileOutput.outputText, filePath);
}
+ const jsFile = { path: newPath, content: jsContent };
+ const tsFile = { path: filePath, content: sourceText };
- } catch (e) {
- // umm, oops. Yeah let's just do a full build then
- Logger.debug(`transpileModule error: ${e}`);
- throw new BuildError(e);
+ context.fileCache.set(sourceMapFile.path, sourceMapFile);
+ context.fileCache.set(jsFile.path, jsFile);
+ context.fileCache.set(tsFile.path, tsFile);
+
+ resolve();
}
+ });
+}
+
+
+export function transpileDiagnosticsOnly(context: BuildContext) {
+ return new Promise(resolve => {
+ workerEvent.once('DiagnosticsWorkerDone', () => {
+ resolve();
+ });
+
+ runDiagnosticsWorker(context);
+ });
+}
+
+const workerEvent = new EventEmitter();
+let diagnosticsWorker: ChildProcess = null;
+
+function runDiagnosticsWorker(context: BuildContext) {
+ if (!diagnosticsWorker) {
+ const workerModule = path.join(__dirname, 'transpile-worker.js');
+ diagnosticsWorker = fork(workerModule, [], { env: { FORCE_COLOR: true } });
+
+ Logger.debug(`diagnosticsWorker created, pid: ${diagnosticsWorker.pid}`);
+
+ diagnosticsWorker.on('error', (err: any) => {
+ Logger.error(`diagnosticsWorker error, pid: ${diagnosticsWorker.pid}, error: ${err}`);
+ workerEvent.emit('DiagnosticsWorkerDone');
+ });
+
+ diagnosticsWorker.on('exit', (code: number) => {
+ Logger.debug(`diagnosticsWorker exited, pid: ${diagnosticsWorker.pid}`);
+ diagnosticsWorker = null;
+ });
+
+ diagnosticsWorker.on('message', (msg: TranspileWorkerMessage) => {
+ workerEvent.emit('DiagnosticsWorkerDone');
+ });
}
- // do a full build if it wasn't an existing file that changed
- // or we haven't transpiled the whole thing yet
- // or there were errors trying to transpile just the one module
- Logger.debug(`transpileUpdateWorker: full build, context.tsFiles ${!!context.fileCache}, event: ${event}, file: ${filePath}`);
- return transpileWorker(context, workerConfig);
+ const msg: TranspileWorkerMessage = {
+ rootDir: context.rootDir,
+ buildDir: context.buildDir,
+ isProd: context.isProd,
+ configFile: getTsConfigPath(context)
+ };
+ diagnosticsWorker.send(msg);
+}
+
+
+export interface TranspileWorkerMessage {
+ rootDir?: string;
+ buildDir?: string;
+ isProd?: boolean;
+ configFile?: string;
+ transpileSuccess?: boolean;
}
@@ -237,14 +258,14 @@ function cleanFileNames(context: BuildContext, fileNames: string[]) {
function writeSourceFiles(fileCache: FileCache, sourceFiles: ts.SourceFile[]) {
for (const sourceFile of sourceFiles) {
- fileCache.put(sourceFile.fileName, { path: sourceFile.fileName, content: sourceFile.text });
+ fileCache.set(sourceFile.fileName, { path: sourceFile.fileName, content: sourceFile.text });
}
}
function writeTranspiledFilesCallback(fileCache: FileCache, sourcePath: string, data: string, shouldInlineTemplate: boolean) {
- sourcePath = normalize(sourcePath);
+ sourcePath = path.normalize(sourcePath);
- if (endsWith(sourcePath, '.js')) {
+ if (sourcePath.endsWith('.js')) {
sourcePath = sourcePath.substring(0, sourcePath.length - 3) + '.js';
let file = fileCache.get(sourcePath);
@@ -258,9 +279,9 @@ function writeTranspiledFilesCallback(fileCache: FileCache, sourcePath: string,
file.content = data;
}
- fileCache.put(sourcePath, file);
+ fileCache.set(sourcePath, file);
- } else if (endsWith(sourcePath, '.js.map')) {
+ } else if (sourcePath.endsWith('.js.map')) {
sourcePath = sourcePath.substring(0, sourcePath.length - 7) + '.js.map';
let file = fileCache.get(sourcePath);
@@ -269,7 +290,7 @@ function writeTranspiledFilesCallback(fileCache: FileCache, sourcePath: string,
}
file.content = data;
- fileCache.put(sourcePath, file);
+ fileCache.set(sourcePath, file);
}
}
@@ -295,12 +316,11 @@ export function getTsConfig(context: BuildContext, tsConfigPath?: string): TsCon
ts.sys, context.rootDir,
{}, tsConfigPath);
- const diagnostics = runDiagnostics(context, parsedConfig.errors);
+ const diagnostics = runTypeScriptDiagnostics(context, parsedConfig.errors);
if (diagnostics.length) {
- const buildError = new BuildError();
- buildError.updatedDiagnostics = true;
- throw buildError;
+ printDiagnostics(context, DiagnosticsType.TypeScript, diagnostics, true, true);
+ throw new BuildError();
}
config = {
@@ -318,7 +338,7 @@ export function getTsConfig(context: BuildContext, tsConfigPath?: string): TsCon
let cachedProgram: ts.Program = null;
export function getTsConfigPath(context: BuildContext) {
- return join(context.rootDir, 'tsconfig.json');
+ return path.join(context.rootDir, 'tsconfig.json');
}
export interface TsConfig {
diff --git a/src/util/events.ts b/src/util/events.ts
index 9487de25..b30d59e1 100644
--- a/src/util/events.ts
+++ b/src/util/events.ts
@@ -17,16 +17,13 @@ export function emit(eventType: string, val?: any) {
export const EventType = {
- BuildFinished: 'BuildFinished',
- SassFinished: 'SassFinished',
- BundleFinished: 'BundleFinished',
- FileChange: 'FileChange',
+ BuildUpdateCompleted: 'BuildUpdateCompleted',
+ BuildUpdateStarted: 'BuildUpdateStarted',
FileAdd: 'FileAdd',
+ FileChange: 'FileChange',
FileDelete: 'FileDelete',
DirectoryAdd: 'DirectoryAdd',
DirectoryDelete: 'DirectoryDelete',
- TaskEvent: 'TaskEvent',
- UpdatedDiagnostics: 'UpdatedDiagnostics',
-
+ ReloadApp: 'ReloadApp',
WebpackFilesChanged: 'WebpackFilesChanged'
};
diff --git a/src/util/file-cache.ts b/src/util/file-cache.ts
index 33292d06..cc872048 100644
--- a/src/util/file-cache.ts
+++ b/src/util/file-cache.ts
@@ -1,5 +1,5 @@
import { File } from './interfaces';
-import { emit, EventType } from './events';
+
export class FileCache {
@@ -9,19 +9,21 @@ export class FileCache {
this.map = new Map();
}
- put(key: string, file: File) {
+ set(key: string, file: File) {
file.timestamp = Date.now();
this.map.set(key, file);
- // emit(EventType.DanFileChanged, key);
}
get(key: string): File {
return this.map.get(key);
}
+ has(key: string) {
+ return this.map.has(key);
+ }
+
remove(key: string): Boolean {
const result = this.map.delete(key);
- // emit(EventType.DanFileDeleted, key);
return result;
}
diff --git a/src/util/helpers.ts b/src/util/helpers.ts
index 2e8cee8d..474d3a4b 100644
--- a/src/util/helpers.ts
+++ b/src/util/helpers.ts
@@ -1,11 +1,72 @@
import { BuildContext } from './interfaces';
-import { outputJson, readFile, readJsonSync, writeFile } from 'fs-extra';
-import { BuildError, Logger } from './logger';
+import { readFile, readJsonSync, writeFile } from 'fs-extra';
+import { BuildError } from './logger';
import { basename, dirname, extname, join } from 'path';
-import { tmpdir } from 'os';
+import * as osName from 'os-name';
let _context: BuildContext;
+
+
+let cachedAppScriptsPackageJson: any;
+export function getAppScriptsPackageJson() {
+ if (!cachedAppScriptsPackageJson) {
+ try {
+ cachedAppScriptsPackageJson = readJsonSync(join(__dirname, '..', '..', 'package.json'));
+ } catch (e) {}
+ }
+ return cachedAppScriptsPackageJson;
+}
+
+
+export function getAppScriptsVersion() {
+ const appScriptsPackageJson = getAppScriptsPackageJson();
+ return (appScriptsPackageJson && appScriptsPackageJson.version) ? appScriptsPackageJson.version : '';
+}
+
+function getUserPackageJson(userRootDir: string) {
+ try {
+ return readJsonSync(join(userRootDir, 'package.json'));
+ } catch (e) {}
+ return null;
+}
+
+export function getSystemInfo(userRootDir: string) {
+ const d: string[] = [];
+
+ let ionicAppScripts = getAppScriptsVersion();
+ let ionicFramework: string = null;
+ let ionicNative: string = null;
+ let angularCore: string = null;
+ let angularCompilerCli: string = null;
+
+ try {
+ const userPackageJson = getUserPackageJson(userRootDir);
+ if (userPackageJson) {
+ const userDependencies = userPackageJson.dependencies;
+ if (userDependencies) {
+ ionicFramework = userDependencies['ionic-angular'];
+ ionicNative = userDependencies['ionic-native'];
+ angularCore = userDependencies['@angular/core'];
+ angularCompilerCli = userDependencies['@angular/compiler-cli'];
+ }
+ }
+ } catch (e) {}
+
+ d.push(`Ionic Framework: ${ionicFramework}`);
+ if (ionicNative) {
+ d.push(`Ionic Native: ${ionicNative}`);
+ }
+ d.push(`Ionic App Scripts: ${ionicAppScripts}`);
+ d.push(`Angular Core: ${angularCore}`);
+ d.push(`Angular Compiler CLI: ${angularCompilerCli}`);
+ d.push(`Node: ${process.version.replace('v', '')}`);
+ d.push(`OS Platform: ${osName()}`);
+
+ return d;
+}
+
+
export const objectAssign = (Object.assign) ? Object.assign : function (target: any, source: any) {
const output = Object(target);
@@ -24,14 +85,6 @@ export const objectAssign = (Object.assign) ? Object.assign : function (target:
};
-export function endsWith(str: string, tail: string) {
- if (str && tail) {
- return !tail.length || str.slice(-tail.length).toLowerCase() === tail.toLowerCase();
- }
- return false;
-}
-
-
export function titleCase(str: string) {
return str.charAt(0).toUpperCase() + str.substr(1);
}
@@ -62,42 +115,6 @@ export function readFileAsync(filePath: string): Promise {
});
}
-export function setModulePathsCache(modulePaths: string[]) {
- // async save the module paths for later lookup
- const modulesCachePath = getModulesPathsCachePath();
-
- Logger.debug(`Cached module paths: ${modulePaths && modulePaths.length}, ${modulesCachePath}`);
-
- outputJson(modulesCachePath, modulePaths, (err) => {
- if (err) {
- Logger.error(`Error writing module paths cache: ${err}`);
- }
- });
-}
-
-
-export function getModulesPathsCachePath(): string {
- // make a unique tmp directory for this project's module paths cache file
- let cwd = process.cwd().replace(/-|:|\/|\\|\.|~|;|\s/g, '').toLowerCase();
- if (cwd.length > 40) {
- cwd = cwd.substr(cwd.length - 40);
- }
- return join(tmpdir(), cwd, 'modulepaths.json');
-}
-
-export function getModulePathsCache(): string[] {
- // sync get the cached array of module paths (if they exist)
- let modulePaths: string[] = null;
- const modulesCachePath = getModulesPathsCachePath();
- try {
- modulePaths = readJsonSync(modulesCachePath, { throws: false });
- Logger.debug(`Cached module paths: ${modulePaths && modulePaths.length}, ${modulesCachePath}`);
- } catch (e) {
- Logger.debug(`Cached module paths not found: ${modulesCachePath}`);
- }
- return modulePaths;
-}
-
export function setContext(context: BuildContext) {
_context = context;
}
@@ -120,4 +137,4 @@ export function changeExtension(filePath: string, newExtension: string) {
const extensionlessfileName = basename(filePath, extension);
const newFileName = extensionlessfileName + newExtension;
return join(dir, newFileName);
-}
\ No newline at end of file
+}
diff --git a/src/util/interfaces.ts b/src/util/interfaces.ts
index 60121fe8..e24660e5 100644
--- a/src/util/interfaces.ts
+++ b/src/util/interfaces.ts
@@ -10,16 +10,23 @@ export interface BuildContext {
moduleFiles?: string[];
isProd?: boolean;
isWatch?: boolean;
- isUpdate?: boolean;
- fullBuildCompleted?: boolean;
+
bundler?: string;
- useTranspileCache?: boolean;
- useBundleCache?: boolean;
- useSassCache?: boolean;
fileCache?: FileCache;
- successfulSass?: boolean;
inlineTemplates?: boolean;
webpackWatch?: any;
+
+ sassState?: BuildState;
+ transpileState?: BuildState;
+ templateState?: BuildState;
+ bundleState?: BuildState;
+}
+
+
+export enum BuildState {
+ SuccessfulBuild,
+ RequiresUpdate,
+ RequiresBuild
}
@@ -49,6 +56,7 @@ export interface TaskInfo {
defaultConfigFile: string;
}
+
export interface File {
path: string;
content: string;
diff --git a/src/util/logger-diagnostics.ts b/src/util/logger-diagnostics.ts
index 47d73c4a..f7e0a377 100644
--- a/src/util/logger-diagnostics.ts
+++ b/src/util/logger-diagnostics.ts
@@ -1,21 +1,23 @@
import { BuildContext } from './interfaces';
import { Diagnostic, Logger, PrintLine } from './logger';
-import { join } from 'path';
-import { readFileSync, writeFileSync, unlinkSync } from 'fs';
import { titleCase } from './helpers';
+import { join } from 'path';
+import { readFileSync, unlinkSync, writeFileSync } from 'fs';
import * as chalk from 'chalk';
-export function printDiagnostics(context: BuildContext, type: string, diagnostics: Diagnostic[]) {
- if (diagnostics.length) {
- let content: string[] = [];
- diagnostics.forEach(d => {
- consoleLogDiagnostic(d);
- content.push(generateDiagnosticHtml(d));
- });
+export function printDiagnostics(context: BuildContext, diagnosticsType: string, diagnostics: Diagnostic[], consoleLogDiagnostics: boolean, writeHtmlDiagnostics: boolean) {
+ if (diagnostics && diagnostics.length) {
+
+ if (consoleLogDiagnostics) {
+ diagnostics.forEach(consoleLogDiagnostic);
+ }
- const fileName = getDiagnosticsFileName(context.buildDir, type);
- writeFileSync(fileName, content.join('\n'), { encoding: 'utf8' });
+ if (writeHtmlDiagnostics) {
+ const content = diagnostics.map(generateDiagnosticHtml);
+ const fileName = getDiagnosticsFileName(context.buildDir, diagnosticsType);
+ writeFileSync(fileName, content.join('\n'), { encoding: 'utf8' });
+ }
}
}
@@ -99,87 +101,144 @@ function consoleHighlightError(errorLine: string, errorCharStart: number, errorL
}
-export function clearDiagnosticsHtmlSync(context: BuildContext, type: string) {
+let diagnosticsHtmlCache: {[key: string]: any} = {};
+
+export function clearDiagnosticsCache() {
+ diagnosticsHtmlCache = {};
+}
+
+export function clearDiagnostics(context: BuildContext, type: string) {
try {
+ delete diagnosticsHtmlCache[type];
unlinkSync(getDiagnosticsFileName(context.buildDir, type));
} catch (e) {}
}
-export function readDiagnosticsHtmlSync(buildDir: string) {
- let content = '';
+export function hasDiagnostics(buildDir: string) {
+ loadDiagnosticsHtml(buildDir);
+ const keys = Object.keys(diagnosticsHtmlCache);
+ for (var i = 0; i < keys.length; i++) {
+ if (typeof diagnosticsHtmlCache[keys[i]] === 'string') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+function loadDiagnosticsHtml(buildDir: string) {
try {
- content = readFileSync(getDiagnosticsFileName(buildDir, 'typescript'), 'utf8') + '\n';
- } catch (e) {}
+ if (diagnosticsHtmlCache[DiagnosticsType.TypeScript] === undefined) {
+ diagnosticsHtmlCache[DiagnosticsType.TypeScript] = readFileSync(getDiagnosticsFileName(buildDir, DiagnosticsType.TypeScript), 'utf8');
+ }
+ } catch (e) {
+ diagnosticsHtmlCache[DiagnosticsType.TypeScript] = false;
+ }
try {
- content += readFileSync(getDiagnosticsFileName(buildDir, 'sass'), 'utf8');
- } catch (e) {}
+ if (diagnosticsHtmlCache[DiagnosticsType.Sass] === undefined) {
+ diagnosticsHtmlCache[DiagnosticsType.Sass] = readFileSync(getDiagnosticsFileName(buildDir, DiagnosticsType.Sass), 'utf8');
+ }
+ } catch (e) {
+ diagnosticsHtmlCache[DiagnosticsType.Sass] = false;
+ }
+}
+
+
+export function injectDiagnosticsHtml(buildDir: string, content: any) {
+ if (!hasDiagnostics(buildDir)) {
+ return content;
+ }
+
+ let contentStr = content.toString();
+
+ const diagnosticsHtml: string[] = [];
+ diagnosticsHtml.push(``);
+ diagnosticsHtml.push(getDiagnosticsHtmlContent(buildDir));
+ diagnosticsHtml.push(`
`);
+
+ let match = contentStr.match(/(?![\s\S]*)/i);
+ if (match) {
+ contentStr = contentStr.replace(match[0], match[0] + '\n' + diagnosticsHtml.join('\n'));
+ } else {
+ contentStr = diagnosticsHtml.join('\n') + contentStr;
+ }
+
+ return contentStr;
+}
+
+
+export function getDiagnosticsHtmlContent(buildDir: string) {
+ loadDiagnosticsHtml(buildDir);
- if (content.length) {
- return generateHtml('Build Error', content);
+ const diagnosticsHtml: string[] = [];
+
+ const keys = Object.keys(diagnosticsHtmlCache);
+ for (var i = 0; i < keys.length; i++) {
+ if (typeof diagnosticsHtmlCache[keys[i]] === 'string') {
+ diagnosticsHtml.push(diagnosticsHtmlCache[keys[i]]);
+ }
}
- return null;
+ return diagnosticsHtml.join('\n');
}
function generateDiagnosticHtml(d: Diagnostic) {
const c: string[] = [];
- c.push(``);
+ c.push(`
`);
- c.push(``); // .ion-diagnostic-masthead
- c.push(`
`);
+ c.push(`
`);
- c.push(``);
+ c.push(``);
if (d.lines && d.lines.length) {
- c.push(`
`);
+ c.push(`
`);
- c.push(`
`);
+ c.push(``);
const lines = removeWhitespaceIndent(d.lines);
lines.forEach(l => {
let trCssClass = '';
- let lineNumberCssClass = `blob-num ${d.syntax}-line-number`;
let code = l.text;
if (l.errorCharStart > -1) {
code = htmlHighlightError(code, l.errorCharStart, l.errorLength);
- trCssClass = ' class="error-line"';
+ trCssClass = ' class="ion-diagnostic-error-line"';
}
- code = jsHtmlSyntaxHighlight(code);
-
c.push(``);
- c.push(` | `);
+ c.push(` | `);
- c.push(`${code} | `);
+ c.push(`${code} | `);
c.push(`
`);
});
c.push(`
`);
- c.push(``); // .blob-wrapper
+ c.push(``); // .ion-diagnostic-blob
}
- c.push(``); // .file
+ c.push(``); // .ion-diagnostic-file
- c.push(``); // .diagnostic
+ c.push(``); // .ion-diagnostic
return c.join('\n');
}
@@ -191,7 +250,7 @@ function htmlHighlightError(errorLine: string, errorCharStart: number, errorLeng
for (var i = 0; i < lineLength; i++) {
var chr = errorLine.charAt(i);
if (i >= errorCharStart && i < errorCharStart + errorLength) {
- chr = `${chr === '' ? ' ' : chr}`;
+ chr = `${chr === '' ? ' ' : chr}`;
}
lineChars.push(chr);
}
@@ -200,195 +259,6 @@ function htmlHighlightError(errorLine: string, errorCharStart: number, errorLeng
}
-const DIAGNOSTICS_CSS = `
-
-* {
- box-sizing: border-box;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
- font-size: 14px;
- line-height: 1.5;
- color: #333;
- background-color: #fff;
- word-wrap: break-word;
- margin: 0;
- padding: 0;
-}
-
-main {
- margin: 0;
- padding: 15px;
-}
-
-h2 {
- margin: 0;
- font-size: 18px;
- color: #222222;
-}
-
-p {
- margin-top: 8px;
- color: #666666;
-}
-
-table {
- border-spacing: 0;
- border-collapse: collapse;
-}
-
-td, th {
- padding: 0;
-}
-
-.diagnostic {
- margin-bottom: 40px;
- border: 1px solid #ddd;
- border-radius: 3px;
-}
-
-.diagnostic-header {
- padding: 8px 12px 0 12px;
-}
-
-.file {
- position: relative;
- margin-top: 16px;
- border-top: 1px solid #ddd;
-}
-
-.file-header {
- padding: 5px 10px;
- background-color: #f7f7f7;
- border-bottom: 1px solid #d8d8d8;
- border-top-left-radius: 2px;
- border-top-right-radius: 2px;
-}
-
-.blob-wrapper {
- overflow-x: auto;
- overflow-y: hidden;
- border-bottom-right-radius: 3px;
- border-bottom-left-radius: 3px;
-}
-
-.tab-size {
- -moz-tab-size: 2;
- -o-tab-size: 2;
- tab-size: 2;
-}
-
-.blob-num {
- width: 1%;
- min-width: 50px;
- padding-right: 10px;
- padding-left: 10px;
- font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
- font-size: 12px;
- line-height: 20px;
- color: rgba(0,0,0,0.3);
- text-align: right;
- white-space: nowrap;
- vertical-align: top;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- border: solid #eee;
- border-width: 0 1px 0 0;
-}
-
-.blob-num::before {
- content: attr(data-line-number);
-}
-
-.error-line .blob-num {
- background-color: #ffdddd;
- border-color: #f1c0c0;
-}
-
-.error-chr {
- position: relative;
-}
-
-.error-chr:before {
- content: "";
- position: absolute;
- z-index: -1;
- top: -3px;
- left: 0px;
- width: 8px;
- height: 20px;
- background-color: #ffdddd;
-}
-
-.blob-code {
- position: relative;
- padding-right: 10px;
- padding-left: 10px;
- line-height: 20px;
- vertical-align: top;
-}
-
-.blob-code-inner {
- overflow: visible;
- font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
- font-size: 12px;
- color: #333;
- word-wrap: normal;
- white-space: pre;
-}
-
-.blob-code-inner::before {
- content: "";
-}
-
-.js-keyword,
-.css-prop {
- color: #183691;
-}
-
-.js-comment,
-.sass-comment {
- color: #969896;
-}
-
-.system-info {
- font-size: 10px;
- color: #999;
-}
-
-`;
-
-
-function generateHtml(title: string, content: string) {
- return `
-
-
-
-
-${escapeHtml(title)}
-
-
-
-
-
-
-
-
-${content}
-
-${getSystemInfo().join('\n')}
-
-
-
-`;
-}
-
-
function jsConsoleSyntaxHighlight(text: string) {
if (text.trim().startsWith('//')) {
return chalk.dim(text);
@@ -431,54 +301,6 @@ function cssConsoleSyntaxHighlight(text: string, errorCharStart: number) {
return chars.join('');
}
-
-function jsHtmlSyntaxHighlight(text: string) {
- if (text.trim().startsWith('//')) {
- return ``;
- }
-
- const words = text.split(' ').map(word => {
- if (JS_KEYWORDS.indexOf(word) > -1) {
- return `${word}`;
- }
- return word;
- });
-
- return words.join(' ');
-}
-
-
-function cssHtmlSyntaxHighlight(text: string, errorCharStart: number) {
- if (text.trim().startsWith('//')) {
- return ``;
- }
-
- let cssProp = true;
- const safeChars = 'abcdefghijklmnopqrstuvwxyz-_';
- const notProp = '.#,:}@$[]/*';
-
- const chars: string[] = [];
-
- for (var i = 0; i < text.length; i++) {
- var c = text.charAt(i);
-
- if (c === ';' || c === '{') {
- cssProp = true;
- } else if (notProp.indexOf(c) > -1) {
- cssProp = false;
- }
- if (cssProp && safeChars.indexOf(c.toLowerCase()) > -1) {
- chars.push(`${c}`);
- continue;
- }
-
- chars.push(c);
- }
-
- return chars.join('');
-}
-
-
function escapeHtml(unsafe: string) {
return unsafe
.replace(/&/g, '&')
@@ -527,7 +349,6 @@ function eachLineHasLeadingWhitespace(lines: PrintLine[]) {
}
-
const JS_KEYWORDS = [
'as',
'break',
@@ -570,18 +391,6 @@ function getDiagnosticsFileName(buildDir: string, type: string) {
}
-export function getSystemInfo() {
- const systemData: string[] = [];
-
- try {
- // const ionicFramework = '2.0.0';
- // systemData.push(`Ionic Framework: ${ionicFramework}`);
- } catch (e) {}
-
- return systemData;
-}
-
-
function isMeaningfulLine(line: string) {
if (line) {
line = line.trim();
@@ -594,4 +403,8 @@ function isMeaningfulLine(line: string) {
const MEH_LINES = [';', ':', '{', '}', '(', ')', '/**', '/*', '*/', '*', '({', '})'];
-const FAVICON = 'data:image/png;base64,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';
+export const DiagnosticsType = {
+ TypeScript: 'typescript',
+ Sass: 'sass',
+ TsLint: 'tslint'
+};
diff --git a/src/util/logger-sass.ts b/src/util/logger-sass.ts
index 940bb569..d1c068d1 100644
--- a/src/util/logger-sass.ts
+++ b/src/util/logger-sass.ts
@@ -1,25 +1,10 @@
import { BuildContext } from './interfaces';
import { Diagnostic, Logger, PrintLine } from './logger';
-import { printDiagnostics, clearDiagnosticsHtmlSync } from './logger-diagnostics';
import { readFileSync } from 'fs';
import { SassError } from 'node-sass';
-export function runDiagnostics(context: BuildContext, sassError: SassError) {
- const diagnostics = loadDiagnostic(context, sassError);
-
- printDiagnostics(context, 'sass', diagnostics);
-
- return diagnostics;
-}
-
-
-export function clearSassDiagnostics(context: BuildContext) {
- clearDiagnosticsHtmlSync(context, 'sass');
-}
-
-
-function loadDiagnostic(context: BuildContext, sassError: SassError) {
+export function runSassDiagnostics(context: BuildContext, sassError: SassError) {
if (!sassError) {
return [];
}
diff --git a/src/util/logger-tslint.ts b/src/util/logger-tslint.ts
index 5b702e4e..a71e46b9 100644
--- a/src/util/logger-tslint.ts
+++ b/src/util/logger-tslint.ts
@@ -1,16 +1,11 @@
import { BuildContext } from './interfaces';
-import { printDiagnostics } from './logger-diagnostics';
import { Diagnostic, Logger, PrintLine } from './logger';
-export function runDiagnostics(context: BuildContext, failures: RuleFailure[]) {
- const diagnostics = failures.map(failure => {
+export function runTsLintDiagnostics(context: BuildContext, failures: RuleFailure[]) {
+ return failures.map(failure => {
return loadDiagnostic(context, failure);
});
-
- printDiagnostics(context, 'tslint', diagnostics);
-
- return diagnostics;
}
diff --git a/src/util/logger-typescript.ts b/src/util/logger-typescript.ts
index c43f63c8..63f71c46 100644
--- a/src/util/logger-typescript.ts
+++ b/src/util/logger-typescript.ts
@@ -1,5 +1,4 @@
import { BuildContext } from './interfaces';
-import { printDiagnostics, clearDiagnosticsHtmlSync } from './logger-diagnostics';
import { Diagnostic, Logger, PrintLine } from './logger';
import * as ts from 'typescript';
@@ -9,19 +8,10 @@ import * as ts from 'typescript';
* error reporting within a terminal. So, yeah, let's code it up, shall we?
*/
-export function runDiagnostics(context: BuildContext, tsDiagnostics: ts.Diagnostic[]) {
- const diagnostics = tsDiagnostics.map(tsDiagnostic => {
+export function runTypeScriptDiagnostics(context: BuildContext, tsDiagnostics: ts.Diagnostic[]) {
+ return tsDiagnostics.map(tsDiagnostic => {
return loadDiagnostic(context, tsDiagnostic);
});
-
- printDiagnostics(context, 'typescript', diagnostics);
-
- return diagnostics;
-}
-
-
-export function clearTypeScriptDiagnostics(context: BuildContext) {
- clearDiagnosticsHtmlSync(context, 'typescript');
}
diff --git a/src/util/logger.ts b/src/util/logger.ts
index a1ee54b3..c4437c45 100644
--- a/src/util/logger.ts
+++ b/src/util/logger.ts
@@ -1,4 +1,3 @@
-import { emit, EventType } from './events';
import { join } from 'path';
import { isDebugMode } from './config';
import { readJSONSync } from 'fs-extra';
@@ -7,7 +6,6 @@ import * as chalk from 'chalk';
export class BuildError extends Error {
hasBeenLogged = false;
- updatedDiagnostics = false;
constructor(err?: any) {
super();
@@ -26,9 +24,6 @@ export class BuildError extends Error {
if (typeof err.hasBeenLogged === 'boolean') {
this.hasBeenLogged = err.hasBeenLogged;
}
- if (typeof err.updatedDiagnostics === 'boolean') {
- this.updatedDiagnostics = err.updatedDiagnostics;
- }
}
}
@@ -37,8 +32,7 @@ export class BuildError extends Error {
message: this.message,
name: this.name,
stack: this.stack,
- hasBeenLogged: this.hasBeenLogged,
- updatedDiagnostics: this.updatedDiagnostics
+ hasBeenLogged: this.hasBeenLogged
};
}
}
@@ -67,53 +61,41 @@ export class Logger {
msg += memoryUsage();
}
Logger.info(msg);
-
- const taskEvent: TaskEvent = {
- scope: this.scope.split(' ')[0],
- type: 'start',
- msg: `${scope} started ...`
- };
- emit(EventType.TaskEvent, taskEvent);
}
- ready(chalkColor?: Function) {
- this.completed('ready', chalkColor);
+ ready(color?: string, bold?: boolean) {
+ this.completed('ready', color, bold);
}
- finish(chalkColor?: Function) {
- this.completed('finished', chalkColor);
+ finish(color?: string, bold?: boolean) {
+ this.completed('finished', color, bold);
}
- private completed(type: string, chalkColor: Function) {
+ private completed(type: string, color: string, bold: boolean) {
+ const duration = Date.now() - this.start;
+ let time: string;
- const taskEvent: TaskEvent = {
- scope: this.scope.split(' ')[0],
- type: type
- };
-
- taskEvent.duration = Date.now() - this.start;
-
- if (taskEvent.duration > 1000) {
- taskEvent.time = 'in ' + (taskEvent.duration / 1000).toFixed(2) + ' s';
+ if (duration > 1000) {
+ time = 'in ' + (duration / 1000).toFixed(2) + ' s';
} else {
- let ms = parseFloat((taskEvent.duration).toFixed(3));
+ let ms = parseFloat((duration).toFixed(3));
if (ms > 0) {
- taskEvent.time = 'in ' + taskEvent.duration + ' ms';
+ time = 'in ' + duration + ' ms';
} else {
- taskEvent.time = 'in less than 1 ms';
+ time = 'in less than 1 ms';
}
}
- taskEvent.msg = `${this.scope} ${taskEvent.type} ${taskEvent.time}`;
- emit(EventType.TaskEvent, taskEvent);
-
let msg = `${this.scope} ${type}`;
- if (chalkColor) {
- msg = chalkColor(msg);
+ if (color) {
+ msg = (chalk)[color](msg);
+ }
+ if (bold) {
+ msg = chalk.bold(msg);
}
- msg += ' ' + chalk.dim(taskEvent.time);
+ msg += ' ' + chalk.dim(time);
if (isDebugMode()) {
msg += memoryUsage();
@@ -128,14 +110,6 @@ export class Logger {
return;
}
- // only emit the event if it's a valid error
- const taskEvent: TaskEvent = {
- scope: this.scope.split(' ')[0],
- type: 'failed',
- msg: this.scope + ' failed'
- };
- emit(EventType.TaskEvent, taskEvent);
-
if (err instanceof BuildError) {
let failedMsg = `${this.scope} failed`;
if (err.message) {
@@ -161,6 +135,10 @@ export class Logger {
return err;
}
+ setStartTime(startTime: number) {
+ this.start = startTime;
+ }
+
/**
* Does not print out a time prefix or color any text. Only prefix
* with whitespace so the message is lined up with timestamped logs.
@@ -172,15 +150,31 @@ export class Logger {
}
/**
- * Prints out a dim colored timestamp prefix.
+ * Prints out a dim colored timestamp prefix, with optional color
+ * and bold message.
*/
- static info(...msg: any[]) {
- const lines = Logger.wordWrap(msg);
+ static info(msg: string, color?: string, bold?: boolean) {
+ const lines = Logger.wordWrap([msg]);
if (lines.length) {
let prefix = timePrefix();
- lines[0] = chalk.dim(prefix) + lines[0].substr(prefix.length);
+ let lineOneMsg = lines[0].substr(prefix.length);
+ if (color) {
+ lineOneMsg = (chalk)[color](lineOneMsg);
+ }
+ if (bold) {
+ lineOneMsg = chalk.bold(lineOneMsg);
+ }
+ lines[0] = chalk.dim(prefix) + lineOneMsg;
}
- lines.forEach(line => {
+ lines.forEach((line, lineIndex) => {
+ if (lineIndex > 0) {
+ if (color) {
+ line = (chalk)[color](line);
+ }
+ if (bold) {
+ line = chalk.bold(line);
+ }
+ }
console.log(line);
});
}
@@ -355,25 +349,6 @@ function memoryUsage() {
}
-export function getAppScriptsVersion() {
- let rtn = '';
- try {
- const packageJson = readJSONSync(join(__dirname, '..', '..', 'package.json'));
- rtn = packageJson.version || '';
- } catch (e) {}
- return rtn;
-}
-
-
-export interface TaskEvent {
- scope: string;
- type: string;
- duration?: number;
- time?: string;
- msg?: string;
-}
-
-
export interface Diagnostic {
level: string;
syntax: string;
diff --git a/src/watch.ts b/src/watch.ts
index 431c0db2..d1c4ccb2 100644
--- a/src/watch.ts
+++ b/src/watch.ts
@@ -1,9 +1,9 @@
-import { build, fullBuildUpdate } from './build';
-import { BuildContext, TaskInfo } from './util/interfaces';
-import { BuildError, IgnorableError, Logger } from './util/logger';
+import * as buildTask from './build';
+import { BuildContext, BuildState, TaskInfo } from './util/interfaces';
+import { BuildError, Logger } from './util/logger';
import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars, setIonicEnvironment } from './util/config';
-import { join, normalize } from 'path';
-import * as chalk from 'chalk';
+import { join, normalize, extname } from 'path';
+import { canRunTranspileUpdate } from './transpile';
import * as chokidar from 'chokidar';
@@ -16,17 +16,20 @@ export function watch(context?: BuildContext, configFile?: string) {
// force watch options
context.isProd = false;
context.isWatch = true;
- context.fullBuildCompleted = false;
+
+ context.sassState = BuildState.RequiresBuild;
+ context.transpileState = BuildState.RequiresBuild;
+ context.bundleState = BuildState.RequiresBuild;
const logger = new Logger('watch');
function buildDone() {
return startWatchers(context, configFile).then(() => {
- logger.ready(chalk.green);
+ logger.ready();
});
}
- return build(context)
+ return buildTask.build(context)
.then(buildDone, buildDone)
.catch(err => {
throw logger.fail(err);
@@ -82,20 +85,9 @@ function startWatcher(index: number, watcher: Watcher, context: BuildContext, wa
filePath = join(context.rootDir, filePath);
- context.isUpdate = true;
-
Logger.debug(`watch callback start, id: ${watchCount}, isProd: ${context.isProd}, event: ${event}, path: ${filePath}`);
- function taskDone() {
- Logger.newLine();
- Logger.info(chalk.green.bold('watch ready'));
- Logger.newLine();
- }
-
const callbackToExecute = function(event: string, filePath: string, context: BuildContext, watcher: Watcher) {
- if (!context.fullBuildCompleted) {
- return fullBuildUpdate(event, filePath, context);
- }
return watcher.callback(event, filePath, context);
};
@@ -103,15 +95,11 @@ function startWatcher(index: number, watcher: Watcher, context: BuildContext, wa
.then(() => {
Logger.debug(`watch callback complete, id: ${watchCount}, isProd: ${context.isProd}, event: ${event}, path: ${filePath}`);
watchCount++;
- taskDone();
})
.catch(err => {
Logger.debug(`watch callback error, id: ${watchCount}, isProd: ${context.isProd}, event: ${event}, path: ${filePath}`);
Logger.debug(`${err}`);
watchCount++;
- if (!(err instanceof IgnorableError)) {
- taskDone();
- }
});
});
@@ -151,6 +139,127 @@ export function prepareWatcher(context: BuildContext, watcher: Watcher) {
}
+let queuedChangedFiles: ChangedFile[] = [];
+let queuedChangeFileTimerId: any;
+export interface ChangedFile {
+ event: string;
+ filePath: string;
+ ext: string;
+}
+
+export function buildUpdate(event: string, filePath: string, context: BuildContext) {
+ const changedFile: ChangedFile = {
+ event: event,
+ filePath: filePath,
+ ext: extname(filePath).toLowerCase()
+ };
+
+ // do not allow duplicates
+ if (!queuedChangedFiles.some(f => f.filePath === filePath)) {
+ queuedChangedFiles.push(changedFile);
+
+ // debounce our build update incase there are multiple files
+ clearTimeout(queuedChangeFileTimerId);
+
+ // run this code in a few milliseconds if another hasn't come in behind it
+ queuedChangeFileTimerId = setTimeout(() => {
+ // figure out what actually needs to be rebuilt
+ const buildData = runBuildUpdate(context, queuedChangedFiles);
+
+ // clear out all the files that are queued up for the build update
+ queuedChangedFiles.length = 0;
+
+ if (buildData) {
+ // cool, we've got some build updating to do ;)
+ buildTask.buildUpdate(buildData.event, buildData.filePath, context);
+ }
+ }, BUILD_UPDATE_DEBOUNCE_MS);
+ }
+
+ return Promise.resolve();
+}
+
+
+export function runBuildUpdate(context: BuildContext, changedFiles: ChangedFile[]) {
+ if (!changedFiles || !changedFiles.length) {
+ return null;
+ }
+
+ // create the data which will be returned
+ const data = {
+ event: changedFiles.map(f => f.event).find(ev => ev !== 'change') || 'change',
+ filePath: changedFiles[0].filePath,
+ changedFiles: changedFiles.map(f => f.filePath)
+ };
+
+ const tsFiles = changedFiles.filter(f => f.ext === '.ts');
+ if (tsFiles.length > 1) {
+ // multiple .ts file changes
+ // if there is more than one ts file changing then
+ // let's just do a full transpile build
+ context.transpileState = BuildState.RequiresBuild;
+
+ } else if (tsFiles.length) {
+ // only one .ts file changed
+ if (canRunTranspileUpdate(tsFiles[0].event, tsFiles[0].filePath, context)) {
+ // .ts file has only changed, it wasn't a file add/delete
+ // we can do the quick typescript update on this changed file
+ context.transpileState = BuildState.RequiresUpdate;
+
+ } else {
+ // .ts file was added or deleted, we need a full rebuild
+ context.transpileState = BuildState.RequiresBuild;
+ }
+ }
+
+ const sassFiles = changedFiles.filter(f => f.ext === '.scss');
+ if (sassFiles.length) {
+ // .scss file was changed/added/deleted, lets do a sass update
+ context.sassState = BuildState.RequiresUpdate;
+ }
+
+ const sassFilesNotChanges = changedFiles.filter(f => f.ext === '.ts' && f.event !== 'change');
+ if (sassFilesNotChanges.length) {
+ // .ts file was either added or deleted, so we'll have to
+ // run sass again to add/remove that .ts file's potential .scss file
+ context.sassState = BuildState.RequiresUpdate;
+ }
+
+ const htmlFiles = changedFiles.filter(f => f.ext === '.html');
+ if (htmlFiles.length) {
+ if (context.bundleState === BuildState.SuccessfulBuild && htmlFiles.every(f => f.event === 'change')) {
+ // .html file was changed
+ // just doing a template update is fine
+ context.templateState = BuildState.RequiresUpdate;
+
+ } else {
+ // .html file was added/deleted
+ // we should do a full transpile build because of this
+ context.transpileState = BuildState.RequiresBuild;
+ }
+ }
+
+ if (context.transpileState === BuildState.RequiresUpdate || context.transpileState === BuildState.RequiresBuild) {
+ if (context.bundleState === BuildState.SuccessfulBuild || context.bundleState === BuildState.RequiresUpdate) {
+ // transpiling needs to happen
+ // and there has already been a successful bundle before
+ // so let's just do a bundle update
+ context.bundleState = BuildState.RequiresUpdate;
+ } else {
+ // transpiling needs to happen
+ // but we've never successfully bundled before
+ // so let's do a full bundle build
+ context.bundleState = BuildState.RequiresBuild;
+ }
+ }
+
+ // guess which file is probably the most important here
+ data.filePath = tsFiles.concat(sassFiles, htmlFiles)[0].filePath;
+
+ return data;
+}
+
+
const taskInfo: TaskInfo = {
fullArg: '--watch',
shortArg: '-w',
@@ -181,3 +290,5 @@ export interface Watcher {
}
let watchCount = 0;
+
+const BUILD_UPDATE_DEBOUNCE_MS = 20;
diff --git a/src/webpack.ts b/src/webpack.ts
index f6a2058f..ae9a3fb2 100644
--- a/src/webpack.ts
+++ b/src/webpack.ts
@@ -1,7 +1,7 @@
import { FileCache } from './util/file-cache';
-import { BuildContext, File, TaskInfo } from './util/interfaces';
+import { BuildContext, BuildState, File, TaskInfo } from './util/interfaces';
import { BuildError, IgnorableError, Logger } from './util/logger';
-import { changeExtension, readFileAsync, setContext, setModulePathsCache } from './util/helpers';
+import { changeExtension, readFileAsync, setContext } from './util/helpers';
import { emit, EventType } from './util/events';
import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config';
import { extname, join } from 'path';
@@ -34,9 +34,11 @@ export function webpack(context: BuildContext, configFile: string) {
return webpackWorker(context, configFile)
.then(() => {
+ context.bundleState = BuildState.SuccessfulBuild;
logger.finish();
})
.catch(err => {
+ context.bundleState = BuildState.RequiresBuild;
throw logger.fail(err);
});
}
@@ -69,8 +71,10 @@ export function webpackUpdate(event: string, path: string, context: BuildContext
Logger.debug('webpackUpdate: Incremental Build Done, processing Data');
return webpackBuildComplete(stats, context, webpackConfig);
}).then(() => {
+ context.bundleState = BuildState.SuccessfulBuild;
return logger.finish();
}).catch(err => {
+ context.bundleState = BuildState.RequiresBuild;
if (err instanceof IgnorableError) {
throw err;
}
@@ -112,11 +116,6 @@ function webpackBuildComplete(stats: any, context: BuildContext, webpackConfig:
context.moduleFiles = files;
- // async cache all the module paths so we don't need
- // to always bundle to know which modules are used
- setModulePathsCache(context.moduleFiles);
-
- emit(EventType.BundleFinished, getOutputDest(context, webpackConfig));
return Promise.resolve();
}
diff --git a/src/webpack/ionic-webpack-factory.ts b/src/webpack/ionic-webpack-factory.ts
index ca5400cc..d7d6115f 100644
--- a/src/webpack/ionic-webpack-factory.ts
+++ b/src/webpack/ionic-webpack-factory.ts
@@ -1,7 +1,8 @@
import { IonicEnvironmentPlugin } from './ionic-environment-plugin';
import { getContext } from '../util/helpers';
+
export function getIonicEnvironmentPlugin() {
const context = getContext();
return new IonicEnvironmentPlugin(context.fileCache);
-}
\ No newline at end of file
+}
diff --git a/src/worker-client.ts b/src/worker-client.ts
index 028e44d8..4bb979e4 100644
--- a/src/worker-client.ts
+++ b/src/worker-client.ts
@@ -2,7 +2,6 @@ import { BuildContext, WorkerProcess, WorkerMessage } from './util/interfaces';
import { BuildError, Logger } from './util/logger';
import { fork, ChildProcess } from 'child_process';
import { join } from 'path';
-import { emit, EventType } from './util/events';
export function runWorker(taskModule: string, taskWorker: string, context: BuildContext, workerConfig: any) {
@@ -30,18 +29,10 @@ export function runWorker(taskModule: string, taskWorker: string, context: Build
worker.on('message', (msg: WorkerMessage) => {
if (msg.error) {
- const buildErrorError = new BuildError(msg.error);
- if (buildErrorError.updatedDiagnostics) {
- emit(EventType.UpdatedDiagnostics);
- }
- reject(buildErrorError);
+ reject(new BuildError(msg.error));
} else if (msg.reject) {
- const buildErrorReject = new BuildError(msg.reject);
- if (buildErrorReject.updatedDiagnostics) {
- emit(EventType.UpdatedDiagnostics);
- }
- reject(buildErrorReject);
+ reject(new BuildError(msg.reject));
} else {
resolve(msg.resolve);
diff --git a/src/worker-process.ts b/src/worker-process.ts
index f86c3583..91550905 100644
--- a/src/worker-process.ts
+++ b/src/worker-process.ts
@@ -11,7 +11,7 @@ process.on('message', (msg: WorkerMessage) => {
.then((val: any) => {
taskResolve(msg.taskModule, msg.taskWorker, val);
}, (val: any) => {
- taskReject(msg.taskModule, msg.taskWorker, val)
+ taskReject(msg.taskModule, msg.taskWorker, val);
})
.catch((err: any) => {
taskError(msg.taskModule, msg.taskWorker, err);