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(`
`); + c.push(`
`); const header = `${titleCase(d.type)} ${titleCase(d.level)}`; - c.push(`

${escapeHtml(header)}

`); + c.push(`
${escapeHtml(header)}
`); - c.push(`

${escapeHtml(d.messageText)}

`); + c.push(`
${escapeHtml(d.messageText)}
`); - c.push(`
`); + c.push(`
`); // .ion-diagnostic-masthead - c.push(`
`); + c.push(`
`); - c.push(`
${escapeHtml(d.relFileName)}
`); + c.push(`
${escapeHtml(d.relFileName)}
`); 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(``); + c.push(``); c.push(``); }); c.push(`
${code}${code}
`); - 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 `${text}`; - } - - 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 `${text}`; - } - - 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);