diff --git a/src/build.ts b/src/build.ts index 671ea82d..12a16b5a 100644 --- a/src/build.ts +++ b/src/build.ts @@ -1,5 +1,7 @@ -import { BuildContext, BuildState, BuildUpdateMessage } from './util/interfaces'; +import { FILE_CHANGE_EVENT, FILE_DELETE_EVENT } from './util/constants'; +import { BuildContext, BuildState, BuildUpdateMessage, ChangedFile } from './util/interfaces'; import { BuildError } from './util/errors'; +import { readFileAsync } from './util/helpers'; import { bundle, bundleUpdate } from './bundle'; import { clean } from './clean'; import { copy } from './copy'; @@ -121,7 +123,7 @@ function buildDev(context: BuildContext) { } -export function buildUpdate(event: string, filePath: string, context: BuildContext) { +export function buildUpdate(changedFiles: ChangedFile[], context: BuildContext) { return new Promise(resolve => { const logger = new Logger('build'); @@ -151,13 +153,20 @@ export function buildUpdate(event: string, filePath: string, context: BuildConte // 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); + emit(EventType.FileChange, resolveValue.changedFiles); } - if (filePath.endsWith('.ts')) { + let requiresLintUpdate = false; + for (const changedFile of changedFiles) { + if (changedFile.ext === '.ts' && changedFile.event === 'ch') { + requiresLintUpdate = true; + break; + } + } + if (requiresLintUpdate) { // a ts file changed, so let's lint it too, however // this task should run as an after thought - lintUpdate(event, filePath, context); + lintUpdate(changedFiles, context); } logger.finish('green', true); @@ -170,8 +179,8 @@ export function buildUpdate(event: string, filePath: string, context: BuildConte // 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); + const buildTasksPromise = buildUpdateTasks(changedFiles, context); + const parallelTasksPromise = buildUpdateParallelTasks(changedFiles, context); // whether it was resolved or rejected, we need to do the same thing buildTasksPromise @@ -179,7 +188,7 @@ export function buildUpdate(event: string, filePath: string, context: BuildConte .catch(() => { buildTasksDone({ requiresAppReload: false, - changedFile: filePath + changedFiles: changedFiles }); }); }); @@ -189,18 +198,21 @@ export function buildUpdate(event: string, filePath: string, context: BuildConte * 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) { +function buildUpdateTasks(changedFiles: ChangedFile[], context: BuildContext) { const resolveValue: BuildTaskResolveValue = { requiresAppReload: false, - changedFile: filePath + changedFiles: [] }; return Promise.resolve() + .then(() => { + return loadFiles(changedFiles, context); + }) .then(() => { // TEMPLATE if (context.templateState === BuildState.RequiresUpdate) { resolveValue.requiresAppReload = true; - return templateUpdate(event, filePath, context); + return templateUpdate(changedFiles, context); } // no template updates required return Promise.resolve(); @@ -213,7 +225,7 @@ function buildUpdateTasks(event: string, filePath: string, context: BuildContext // 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); + return transpileUpdate(changedFiles, context); } else if (context.transpileState === BuildState.RequiresBuild) { // run the whole transpile @@ -229,7 +241,7 @@ function buildUpdateTasks(event: string, filePath: string, context: BuildContext if (context.bundleState === BuildState.RequiresUpdate) { // we need to do a bundle update resolveValue.requiresAppReload = true; - return bundleUpdate(event, filePath, context); + return bundleUpdate(changedFiles, context); } else if (context.bundleState === BuildState.RequiresBuild) { // we need to do a full bundle build @@ -244,14 +256,30 @@ function buildUpdateTasks(event: string, filePath: string, context: BuildContext // SASS if (context.sassState === BuildState.RequiresUpdate) { // we need to do a sass update - return sassUpdate(event, filePath, context).then(outputCssFile => { - resolveValue.changedFile = outputCssFile; + return sassUpdate(changedFiles, context).then(outputCssFile => { + const changedFile: ChangedFile = { + event: FILE_CHANGE_EVENT, + ext: '.css', + filePath: outputCssFile + }; + + context.fileCache.set(outputCssFile, { path: outputCssFile, content: outputCssFile}); + + resolveValue.changedFiles.push(changedFile); }); } else if (context.sassState === BuildState.RequiresBuild) { // we need to do a full sass build return sass(context).then(outputCssFile => { - resolveValue.changedFile = outputCssFile; + const changedFile: ChangedFile = { + event: FILE_CHANGE_EVENT, + ext: '.css', + filePath: outputCssFile + }; + + context.fileCache.set(outputCssFile, { path: outputCssFile, content: outputCssFile}); + + resolveValue.changedFiles.push(changedFile); }); } // no sass build required @@ -262,9 +290,29 @@ function buildUpdateTasks(event: string, filePath: string, context: BuildContext }); } +function loadFiles(changedFiles: ChangedFile[], context: BuildContext) { + // UPDATE IN-MEMORY FILE CACHE + let promises: Promise[] = []; + for (const changedFile of changedFiles) { + if (changedFile.event === FILE_DELETE_EVENT) { + // remove from the cache on delete + context.fileCache.remove(changedFile.filePath); + } else { + // load the latest since the file changed + const promise = readFileAsync(changedFile.filePath); + promises.push(promise); + promise.then((content: string) => { + context.fileCache.set(changedFile.filePath, { path: changedFile.filePath, content: content}); + }); + } + } + + return Promise.all(promises); +} + interface BuildTaskResolveValue { requiresAppReload: boolean; - changedFile: string; + changedFiles: ChangedFile[]; } /** @@ -272,7 +320,7 @@ interface BuildTaskResolveValue { * 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) { +function buildUpdateParallelTasks(changedFiles: ChangedFile[], context: BuildContext) { const parallelTasks: Promise[] = []; if (context.transpileState === BuildState.RequiresUpdate) { diff --git a/src/bundle.ts b/src/bundle.ts index dc9b7b46..6cb45c06 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -1,4 +1,4 @@ -import { BuildContext } from './util/interfaces'; +import { BuildContext, ChangedFile } from './util/interfaces'; import { BuildError, IgnorableError } from './util/errors'; import { generateContext, BUNDLER_ROLLUP } from './util/config'; import { rollup, rollupUpdate, getRollupConfig, getOutputDest as rollupGetOutputDest } from './rollup'; @@ -24,15 +24,15 @@ function bundleWorker(context: BuildContext, configFile: string) { } -export function bundleUpdate(event: string, filePath: string, context: BuildContext) { +export function bundleUpdate(changedFiles: ChangedFile[], context: BuildContext) { if (context.bundler === BUNDLER_ROLLUP) { - return rollupUpdate(event, filePath, context) + return rollupUpdate(changedFiles, context) .catch(err => { throw new BuildError(err); }); } - return webpackUpdate(event, filePath, context, null) + return webpackUpdate(changedFiles, context, null) .catch(err => { if (err instanceof IgnorableError) { throw err; diff --git a/src/dev-server/live-reload.ts b/src/dev-server/live-reload.ts index 89b2deb6..c8540e40 100644 --- a/src/dev-server/live-reload.ts +++ b/src/dev-server/live-reload.ts @@ -1,3 +1,4 @@ +import { ChangedFile } from '../util/interfaces'; import { hasDiagnostics } from '../logger/logger-diagnostics'; import * as path from 'path'; import * as tinylr from 'tiny-lr'; @@ -9,14 +10,13 @@ export function createLiveReloadServer(config: ServeConfig) { const liveReloadServer = tinylr(); liveReloadServer.listen(config.liveReloadPort, config.host); - function fileChange(filePath: string | string[]) { + function fileChange(changedFiles: ChangedFile[]) { // only do a live reload if there are no diagnostics // the notification server takes care of showing diagnostics if (!hasDiagnostics(config.buildDir)) { - const files = Array.isArray(filePath) ? filePath : [filePath]; liveReloadServer.changed({ body: { - files: files.map(f => '/' + path.relative(config.wwwDir, f)) + files: changedFiles.map(changedFile => '/' + path.relative(config.wwwDir, changedFile.filePath)) } }); } @@ -25,7 +25,7 @@ export function createLiveReloadServer(config: ServeConfig) { events.on(events.EventType.FileChange, fileChange); events.on(events.EventType.ReloadApp, () => { - fileChange('index.html'); + fileChange([{ event: 'change', ext: '.html', filePath: 'index.html'}]); }); } diff --git a/src/lint.ts b/src/lint.ts index 91a5c7ae..e477ca6f 100644 --- a/src/lint.ts +++ b/src/lint.ts @@ -1,5 +1,5 @@ import { access } from 'fs'; -import { BuildContext, TaskInfo } from './util/interfaces'; +import { BuildContext, ChangedFile, TaskInfo } from './util/interfaces'; import { BuildError } from './util/errors'; import { createProgram, findConfiguration, getFileNames } from 'tslint'; import { generateContext, getUserConfigFile } from './util/config'; @@ -31,12 +31,13 @@ export function lintWorker(context: BuildContext, configFile: string) { } -export function lintUpdate(event: string, filePath: string, context: BuildContext) { +export function lintUpdate(changedFiles: ChangedFile[], context: BuildContext) { + const changedTypescriptFiles = changedFiles.filter(changedFile => changedFile.ext === '.ts'); return new Promise(resolve => { // throw this in a promise for async fun, but don't let it hang anything up const workerConfig: LintWorkerConfig = { configFile: getUserConfigFile(context, taskInfo, null), - filePath: filePath + filePaths: changedTypescriptFiles.map(changedTypescriptFile => changedTypescriptFile.filePath) }; runWorker('lint', 'lintUpdateWorker', context, workerConfig); @@ -49,7 +50,7 @@ export function lintUpdateWorker(context: BuildContext, workerConfig: LintWorker return getLintConfig(context, workerConfig.configFile).then(configFile => { // there's a valid tslint config, let's continue (but be quiet about it!) const program = createProgram(configFile, context.srcDir); - return lintFile(context, program, workerConfig.filePath); + return lintFiles(context, program, workerConfig.filePaths); }).catch(() => { }); } @@ -66,6 +67,13 @@ function lintApp(context: BuildContext, configFile: string) { return Promise.all(promises); } +function lintFiles(context: BuildContext, program: ts.Program, filePaths: string[]) { + const promises: Promise[] = []; + for (const filePath of filePaths) { + promises.push(lintFile(context, program, filePath)); + } + return Promise.all(promises); +} function lintFile(context: BuildContext, program: ts.Program, filePath: string) { return new Promise((resolve) => { @@ -162,5 +170,5 @@ const taskInfo: TaskInfo = { export interface LintWorkerConfig { configFile: string; - filePath: string; + filePaths: string[]; } diff --git a/src/rollup.ts b/src/rollup.ts index 7d5ea1be..39941f3a 100644 --- a/src/rollup.ts +++ b/src/rollup.ts @@ -1,4 +1,4 @@ -import { BuildContext, BuildState, TaskInfo } from './util/interfaces'; +import { BuildContext, BuildState, ChangedFile, TaskInfo } from './util/interfaces'; import { BuildError } from './util/errors'; import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars } from './util/config'; import { ionCompiler } from './plugins/ion-compiler'; @@ -25,7 +25,7 @@ export function rollup(context: BuildContext, configFile: string) { } -export function rollupUpdate(event: string, filePath: string, context: BuildContext) { +export function rollupUpdate(changedFiles: ChangedFile[], context: BuildContext) { const logger = new Logger('rollup update'); const configFile = getUserConfigFile(context, taskInfo, null); diff --git a/src/sass.ts b/src/sass.ts index 9a2ff511..9d5067e1 100755 --- a/src/sass.ts +++ b/src/sass.ts @@ -1,5 +1,5 @@ import { basename, dirname, join, sep } from 'path'; -import { BuildContext, BuildState, TaskInfo } from './util/interfaces'; +import { BuildContext, BuildState, ChangedFile, TaskInfo } from './util/interfaces'; import { BuildError } from './util/errors'; import { bundle } from './bundle'; import { ensureDirSync, readdirSync, writeFile } from 'fs-extra'; @@ -31,7 +31,7 @@ export function sass(context?: BuildContext, configFile?: string) { } -export function sassUpdate(event: string, filePath: string, context: BuildContext) { +export function sassUpdate(changedFiles: ChangedFile[], context: BuildContext) { const configFile = getUserConfigFile(context, taskInfo, null); const logger = new Logger('sass update'); @@ -60,7 +60,7 @@ export function sassWorker(context: BuildContext, configFile: string) { return Promise.all(bundlePromise).then(() => { clearDiagnostics(context, DiagnosticsType.Sass); - const sassConfig: SassConfig = fillConfigDefaults(configFile, taskInfo.defaultConfigFile); + const sassConfig: SassConfig = getSassConfig(context, configFile); // where the final css output file is saved if (!sassConfig.outFile) { @@ -88,6 +88,10 @@ export function sassWorker(context: BuildContext, configFile: string) { }); } +export function getSassConfig(context: BuildContext, configFile: string): SassConfig { + configFile = getUserConfigFile(context, taskInfo, configFile); + return fillConfigDefaults(configFile, taskInfo.defaultConfigFile); +} function generateSassData(context: BuildContext, sassConfig: SassConfig) { /** diff --git a/src/spec/watch.spec.ts b/src/spec/watch.spec.ts index f86817e7..969773f3 100644 --- a/src/spec/watch.spec.ts +++ b/src/spec/watch.spec.ts @@ -1,6 +1,6 @@ -import { BuildContext, BuildState } from '../util/interfaces'; +import { BuildContext, BuildState, ChangedFile } from '../util/interfaces'; import { FileCache } from '../util/file-cache'; -import { runBuildUpdate, ChangedFile } from '../watch'; +import { runBuildUpdate } from '../watch'; import { Watcher, prepareWatcher } from '../watch'; import * as path from 'path'; @@ -9,48 +9,6 @@ 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', @@ -208,40 +166,6 @@ describe('watch', () => { 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); diff --git a/src/template.ts b/src/template.ts index aa75ad90..466695e3 100644 --- a/src/template.ts +++ b/src/template.ts @@ -1,62 +1,53 @@ -import { BuildContext, BuildState, File } from './util/interfaces'; +import { BuildContext, BuildState, ChangedFile, File } from './util/interfaces'; import { changeExtension } from './util/helpers'; import { Logger } from './logger/logger'; import { getJsOutputDest } from './bundle'; import { invalidateCache } from './rollup'; import { dirname, extname, join, parse, resolve } from 'path'; -import { readFileSync, writeFile } from 'fs'; +import { readFileSync, writeFileSync } from 'fs'; -export function templateUpdate(event: string, htmlFilePath: string, context: BuildContext) { - return new Promise((resolve) => { +export function templateUpdate(changedFiles: ChangedFile[], context: BuildContext) { + try { + const changedTemplates = changedFiles.filter(changedFile => changedFile.ext === '.html'); const start = Date.now(); const bundleOutputDest = getJsOutputDest(context); - - function failed() { - context.transpileState = BuildState.RequiresBuild; - context.bundleState = BuildState.RequiresUpdate; - resolve(); + let bundleSourceText = readFileSync(bundleOutputDest, 'utf8'); + + // update the corresponding transpiled javascript file with the template changed (inline it) + // as well as the bundle + for (const changedTemplateFile of changedTemplates) { + const file = context.fileCache.get(changedTemplateFile.filePath); + if (! updateCorrespondingJsFile(context, file.content, changedTemplateFile.filePath)) { + throw new Error(`Failed to inline template ${changedTemplateFile.filePath}`); + } + bundleSourceText = replaceExistingJsTemplate(bundleSourceText, file.content, changedTemplateFile.filePath); } - try { - let bundleSourceText = readFileSync(bundleOutputDest, 'utf8'); - let newTemplateContent = readFileSync(htmlFilePath, 'utf8'); - - const successfullyUpdated = updateCorrespondingJsFile(context, newTemplateContent, htmlFilePath); - bundleSourceText = replaceExistingJsTemplate(bundleSourceText, newTemplateContent, htmlFilePath); - - // invaldiate any rollup bundles, if they're not using rollup no harm done - invalidateCache(); - - if (successfullyUpdated && 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(`templateUpdate, updated: ${htmlFilePath}`); - context.templateState = BuildState.SuccessfulBuild; - logger.finish(); - resolve(); - } - }); - - } else { - failed(); - } + // invaldiate any rollup bundles, if they're not using rollup no harm done + invalidateCache(); - } catch (e) { - Logger.debug(`templateUpdate error: ${e}`); - failed(); - } - }); + // awesome, all good and template updated in the bundle file + const logger = new Logger(`template update`); + logger.setStartTime(start); + + writeFileSync(bundleOutputDest, bundleSourceText, { encoding: 'utf8'}); + + // congrats, all gud + changedTemplates.forEach(changedTemplate => { + Logger.debug(`templateUpdate, updated: ${changedTemplate.filePath}`); + }); + + context.templateState = BuildState.SuccessfulBuild; + logger.finish(); + resolve(); + + } catch (ex) { + Logger.debug(`templateUpdate error: ${ex.message}`); + context.transpileState = BuildState.RequiresBuild; + context.bundleState = BuildState.RequiresUpdate; + return Promise.resolve(); + } } function updateCorrespondingJsFile(context: BuildContext, newTemplateContent: string, existingHtmlTemplatePath: string) { diff --git a/src/transpile.ts b/src/transpile.ts index b5ca554d..c1fa59d3 100644 --- a/src/transpile.ts +++ b/src/transpile.ts @@ -1,5 +1,5 @@ import { FileCache } from './util/file-cache'; -import { BuildContext, BuildState } from './util/interfaces'; +import { BuildContext, BuildState, ChangedFile } from './util/interfaces'; import { BuildError } from './util/errors'; import { buildJsSourceMaps } from './bundle'; import { changeExtension } from './util/helpers'; @@ -40,7 +40,7 @@ export function transpile(context?: BuildContext) { } -export function transpileUpdate(event: string, filePath: string, context: BuildContext) { +export function transpileUpdate(changedFiles: ChangedFile[], context: BuildContext) { const workerConfig: TranspileWorkerConfig = { configFile: getTsConfigPath(context), writeInMemory: true, @@ -51,8 +51,15 @@ export function transpileUpdate(event: string, filePath: string, context: BuildC const logger = new Logger('transpile update'); - return transpileUpdateWorker(event, filePath, context, workerConfig) - .then(tsFiles => { + const changedTypescriptFiles = changedFiles.filter(changedFile => changedFile.ext === '.ts'); + + const promises: Promise[] = []; + for (const changedTypescriptFile of changedTypescriptFiles) { + promises.push(transpileUpdateWorker(changedTypescriptFile.event, changedTypescriptFile.filePath, context, workerConfig)); + } + + return Promise.all(promises) + .then(() => { context.transpileState = BuildState.SuccessfulBuild; logger.finish(); }) @@ -60,6 +67,7 @@ export function transpileUpdate(event: string, filePath: string, context: BuildC context.transpileState = BuildState.RequiresBuild; throw logger.fail(err); }); + } @@ -138,27 +146,29 @@ export function canRunTranspileUpdate(event: string, filePath: string, context: * something errors out then it falls back to do the full build. */ function transpileUpdateWorker(event: string, filePath: string, context: BuildContext, workerConfig: TranspileWorkerConfig) { - return new Promise((resolve, reject) => { + try { clearDiagnostics(context, DiagnosticsType.TypeScript); filePath = path.normalize(path.resolve(filePath)); // 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); + if (!cachedTsConfig) { + cachedTsConfig = getTsConfig(context, workerConfig.configFile); + } // build the ts source maps if the bundler is going to use source maps - tsConfig.options.sourceMap = buildJsSourceMaps(context); + cachedTsConfig.options.sourceMap = buildJsSourceMaps(context); const transpileOptions: ts.TranspileOptions = { - compilerOptions: tsConfig.options, + compilerOptions: cachedTsConfig.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'); + // since it is an update, it's in memory already + const sourceText = context.fileCache.get(filePath).content; // transpile this one module const transpileOutput = ts.transpileModule(sourceText, transpileOptions); @@ -172,8 +182,7 @@ function transpileUpdateWorker(event: string, filePath: string, context: BuildCo // 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()); - + throw new BuildError(`Failed to transpile file - ${filePath}`); } else { // convert the path to have a .js file extension for consistency const newPath = changeExtension(filePath, '.js'); @@ -190,10 +199,12 @@ function transpileUpdateWorker(event: string, filePath: string, context: BuildCo context.fileCache.set(sourceMapFile.path, sourceMapFile); context.fileCache.set(jsFile.path, jsFile); context.fileCache.set(tsFile.path, tsFile); - - resolve(); } - }); + + return Promise.resolve(); + } catch (ex) { + return Promise.reject(ex); + } } @@ -335,6 +346,7 @@ export function getTsConfig(context: BuildContext, tsConfigPath?: string): TsCon let cachedProgram: ts.Program = null; +let cachedTsConfig: TsConfig = null; export function getTsConfigPath(context: BuildContext) { return path.join(context.rootDir, 'tsconfig.json'); diff --git a/src/util/constants.ts b/src/util/constants.ts new file mode 100644 index 00000000..24767382 --- /dev/null +++ b/src/util/constants.ts @@ -0,0 +1,6 @@ +export const FILE_CHANGE_EVENT = 'change'; +export const FILE_ADD_EVENT = 'add'; +export const FILE_DELETE_EVENT = 'unlink'; +export const DIRECTORY_ADD_EVENT = 'addDir'; +export const DIRECTORY_DELETE_EVENT = 'unlinkDir'; + diff --git a/src/util/helpers.ts b/src/util/helpers.ts index b0cf319e..c997ac37 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -1,13 +1,11 @@ import { basename, dirname, extname, join } from 'path'; -import { BuildContext } from './interfaces'; +import { BuildContext, File } from './interfaces'; import { BuildError } from './errors'; -import { readFile, readJsonSync, writeFile } from 'fs-extra'; +import { readFile, readFileSync, readJsonSync, writeFile } from 'fs-extra'; import * as osName from 'os-name'; - let _context: BuildContext; - let cachedAppScriptsPackageJson: any; export function getAppScriptsPackageJson() { if (!cachedAppScriptsPackageJson) { @@ -18,7 +16,6 @@ export function getAppScriptsPackageJson() { return cachedAppScriptsPackageJson; } - export function getAppScriptsVersion() { const appScriptsPackageJson = getAppScriptsPackageJson(); return (appScriptsPackageJson && appScriptsPackageJson.version) ? appScriptsPackageJson.version : ''; @@ -109,7 +106,6 @@ export function writeFileAsync(filePath: string, content: string): Promise }); } - export function readFileAsync(filePath: string): Promise { return new Promise((resolve, reject) => { readFile(filePath, 'utf-8', (err, buffer) => { @@ -122,6 +118,15 @@ export function readFileAsync(filePath: string): Promise { }); } +export function createFileObject(filePath: string): File { + const content = readFileSync(filePath).toString(); + return { + content: content, + path: filePath, + timestamp: Date.now() + }; +} + export function setContext(context: BuildContext) { _context = context; } diff --git a/src/util/interfaces.ts b/src/util/interfaces.ts index f36ae7ba..b5251787 100644 --- a/src/util/interfaces.ts +++ b/src/util/interfaces.ts @@ -99,3 +99,9 @@ export interface BuildUpdateMessage { buildId: number; reloadApp: boolean; } + +export interface ChangedFile { + event: string; + filePath: string; + ext: string; +} diff --git a/src/watch.ts b/src/watch.ts index ceba6db6..4cf8aa2e 100644 --- a/src/watch.ts +++ b/src/watch.ts @@ -1,5 +1,5 @@ import * as buildTask from './build'; -import { BuildContext, BuildState, TaskInfo } from './util/interfaces'; +import { BuildContext, BuildState, ChangedFile, TaskInfo } from './util/interfaces'; import { BuildError } from './util/errors'; import { canRunTranspileUpdate } from './transpile'; import { fillConfigDefaults, generateContext, getUserConfigFile, replacePathVars, setIonicEnvironment } from './util/config'; @@ -161,11 +161,6 @@ 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 = { @@ -184,14 +179,14 @@ export function buildUpdate(event: string, filePath: string, context: BuildConte // 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); + const changedFiles = runBuildUpdate(context, queuedChangedFiles); // clear out all the files that are queued up for the build update queuedChangedFiles.length = 0; - if (buildData) { + if (changedFiles && changedFiles.length) { // cool, we've got some build updating to do ;) - buildTask.buildUpdate(buildData.event, buildData.filePath, context); + buildTask.buildUpdate(changedFiles, context); } }, BUILD_UPDATE_DEBOUNCE_MS); } @@ -202,16 +197,9 @@ export function buildUpdate(event: string, filePath: string, context: BuildConte export function runBuildUpdate(context: BuildContext, changedFiles: ChangedFile[]) { if (!changedFiles || !changedFiles.length) { - return null; + return []; } - // 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 jsFiles = changedFiles.filter(f => f.ext === '.js'); if (jsFiles.length) { // this is mainly for linked modules @@ -221,25 +209,25 @@ export function runBuildUpdate(context: BuildContext, changedFiles: ChangedFile[ } 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; + if (tsFiles.length) { + let requiresFullBuild = false; + for (const tsFile of tsFiles) { + if (!canRunTranspileUpdate(tsFile.event, tsFiles[0].filePath, context)) { + requiresFullBuild = true; + break; + } + } - } else { + if (requiresFullBuild) { // .ts file was added or deleted, we need a full rebuild context.transpileState = BuildState.RequiresBuild; + } else { + // .ts files have changed, so we can get away with doing an update + context.transpileState = BuildState.RequiresUpdate; } } + const sassFiles = changedFiles.filter(f => f.ext === '.scss'); if (sassFiles.length) { // .scss file was changed/added/deleted, lets do a sass update @@ -280,11 +268,7 @@ export function runBuildUpdate(context: BuildContext, changedFiles: ChangedFile[ context.bundleState = BuildState.RequiresBuild; } } - - // guess which file is probably the most important here - data.filePath = tsFiles.concat(sassFiles, htmlFiles, jsFiles)[0].filePath; - - return data; + return changedFiles.concat(); } diff --git a/src/webpack.ts b/src/webpack.ts index 1f8091ff..bd9bde93 100644 --- a/src/webpack.ts +++ b/src/webpack.ts @@ -1,5 +1,5 @@ import { FileCache } from './util/file-cache'; -import { BuildContext, BuildState, File, TaskInfo } from './util/interfaces'; +import { BuildContext, BuildState, ChangedFile, File, TaskInfo } from './util/interfaces'; import { BuildError, IgnorableError } from './util/errors'; import { changeExtension, readFileAsync, setContext } from './util/helpers'; import { emit, EventType } from './util/events'; @@ -45,12 +45,12 @@ export function webpack(context: BuildContext, configFile: string) { } -export function webpackUpdate(event: string, path: string, context: BuildContext, configFile: string) { +export function webpackUpdate(changedFiles: ChangedFile[], context: BuildContext, configFile: string) { const logger = new Logger('webpack update'); const webpackConfig = getWebpackConfig(context, configFile); Logger.debug('webpackUpdate: Starting Incremental Build'); const promisetoReturn = runWebpackIncrementalBuild(false, context, webpackConfig); - emit(EventType.WebpackFilesChanged, [path]); + emit(EventType.WebpackFilesChanged, null); return promisetoReturn.then((stats: any) => { // the webpack incremental build finished, so reset the list of pending promises pendingPromises = []; @@ -195,20 +195,6 @@ export function getOutputDest(context: BuildContext, webpackConfig: WebpackConfi return join(webpackConfig.output.path, webpackConfig.output.filename); } -function typescriptFileChanged(fileChangedPath: string, fileCache: FileCache): File[] { - // convert to the .js file because those are the transpiled files in memory - const jsFilePath = changeExtension(fileChangedPath, '.js'); - const sourceFile = fileCache.get(jsFilePath); - const mapFile = fileCache.get(jsFilePath + '.map'); - return [sourceFile, mapFile]; -} - -function otherFileChanged(fileChangedPath: string) { - return readFileAsync(fileChangedPath).then((content: string) => { - return { path: fileChangedPath, content: content}; - }); -} - const taskInfo: TaskInfo = { fullArg: '--webpack', shortArg: '-w',