diff --git a/src/lib/after-watch.js b/src/lib/after-watch.js index e61f970..807ff14 100644 --- a/src/lib/after-watch.js +++ b/src/lib/after-watch.js @@ -1,9 +1,5 @@ var converter = require('./converter'); module.exports = function ($logger) { - var watcher = converter.getWatcher(); - if (watcher) { - $logger.info("Stopping nativescript-dev-sass watcher"); - watcher.close(); - } -} + converter.dispose(); +} \ No newline at end of file diff --git a/src/lib/before-prepare.js b/src/lib/before-prepare.js index d346195..1aa748c 100644 --- a/src/lib/before-prepare.js +++ b/src/lib/before-prepare.js @@ -8,5 +8,5 @@ module.exports = function ($logger, $projectData, $usbLiveSyncService) { return; } - return converter.convert($logger, $projectData.projectDir, $projectData.appDirectoryPath); -} + return converter.convert($logger, $projectData.projectDir, $projectData.appDirectoryPath, $projectData.appResourcesDirectoryPath); +} \ No newline at end of file diff --git a/src/lib/compiler.js b/src/lib/compiler.js new file mode 100644 index 0000000..0110333 --- /dev/null +++ b/src/lib/compiler.js @@ -0,0 +1,101 @@ +exports.compile = compile; + +var fs = require("fs"); +var path = require('path'); +var spawn = require("child_process").spawn; +var LogProvider = require('./log-provider'); + +var currentSassProcess = null; + +function compile(data) { + if (currentSassProcess) { + return Promise.resolve(); + } + + return new Promise((res, rej) => { + var projectDir = data.projectDir, + appDir = data.appDir; + + var logger = new LogProvider(data.logger); + + var sassPath = require.resolve('node-sass/bin/node-sass'); + if (fs.existsSync(sassPath)) { + logger.info("Found peer node-sass"); + } else { + isResolved = true; + rej(new Error('node-sass installation local to project was not found. Install by executing `npm install node-sass`.')); + } + + var isResolved = false; + var resolve = () => { + if (isResolved) { + return; + } + + isResolved = true; + res(currentSassProcess); + } + var reject = err => { + if (isResolved) { + return; + } + + isResolved = true; + + err.errorAsWarning = true; + err.stopExecution = false; + + rej(err); + } + + // Node SASS Command Line Args (https://github.com/sass/node-sass#command-line-interface) + // --ouput : Output directory + // --output-style : CSS output style (nested | expanded | compact | compresed) + // -q : Supress log output except on error + // --follow : Follow symlinked directories + // -r : Recursively watch directories or files + // --watch : Watch a directory or file + var nodeArgs = [sassPath, appDir, '--output', appDir, '--output-style', 'compressed', '-q', '--follow', '--importer', path.join(__dirname, "importer.js")]; + logger.trace(process.execPath, nodeArgs.join(' ')); + + var env = Object.create(process.env); + env.PROJECT_DIR = projectDir; + env.APP_DIR = appDir; + + currentSassProcess = spawn(process.execPath, nodeArgs, { env: env }); + + currentSassProcess.stdout.on('data', data => { + var stringData = data.toString(); + logger.info(stringData); + }); + + currentSassProcess.stderr.on('data', error => { + var message = ''; + var stringData = error.toString(); + + try { + var parsed = JSON.parse(stringData); + message = parsed.formatted || parsed.message || stringData; + } catch (e) { + message = error.toString(); + } + + logger.info(message); + }); + + currentSassProcess.on('error', error => { + logger.info(err.message); + reject(error); + }); + + currentSassProcess.on('exit', (code, signal) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`SASS compiler failed with exit code ${code}`)); + } + + currentSassProcess = null; + }); + }); +} \ No newline at end of file diff --git a/src/lib/converter.js b/src/lib/converter.js index efd8af9..74739fa 100644 --- a/src/lib/converter.js +++ b/src/lib/converter.js @@ -1,143 +1,52 @@ exports.convert = convert; -exports.getWatcher = getWatcher; +exports.dispose = dispose; +var sassCompiler = require('./compiler'); var spawn = require('child_process').spawn; -var fs = require('fs'); var path = require('path'); -var choki = require('chokidar'); -var watcher = null; -var watchPromisesChain = Promise.resolve(); -function convert(logger, projectDir, appDir, options) { +var watcherProcess = null; + +function convert(logger, projectDir, appDir, appResourcesDir, options) { options = options || {}; - var sassPath = getSassPath(logger); var data = { - sassPath, projectDir, appDir, - logger, - options + appResourcesDir, + logger }; if (options.watch) { createWatcher(data); + return; } - return spawnNodeSass(data); -} - -function getWatcher() { - return watcher; + return sassCompiler.compile(data); } function createWatcher(data) { - var appDir = data.appDir; - var watcherOptions = { - ignoreInitial: true, - cwd: appDir, - awaitWriteFinish: { - pollInterval: 100, - stabilityThreshold: 300 - }, - ignored: ['**/.*', '.*'] // hidden files - }; + if (watcherProcess) { + return; + } + + watcherProcess = spawn(process.execPath, [ path.join(__dirname, "./watcher.js"), JSON.stringify({appDir: data.appDir, appResourcesDir: data.appResourcesDir, projectDir: data.projectDir })], { stdio: ["ignore", "ignore", "ignore", "ipc"] }); - watcher = choki.watch(['**/*.scss', '**/*.sass'], watcherOptions) - .on('all', (event, filePath) => { - watchPromisesChain = watchPromisesChain - .then(() => spawnNodeSass(data)) - .catch(err => { - if (!err.stopExecution && err.errorAsWarning) { - data.logger.warn(err.message); - } else { - throw err; - } - }); - }); + watcherProcess.on('error', error => { + throw new Error(error); + }); + + watcherProcess.on('message', message => { + if (message && message.logLevel) { + data.logger[message.logLevel](message.message); + } + }); } -function getSassPath(logger) { - var sassPath = require.resolve('node-sass/bin/node-sass'); - if (fs.existsSync(sassPath)) { - logger.info('Found peer node-sass'); - } else { - throw new Error('node-sass installation local to project was not found. Install by executing `npm install node-sass`.'); +function dispose() { + if (watcherProcess && watcherProcess.connected) { + watcherProcess.disconnect(); + watcherProcess = null; } - - return sassPath; } -function spawnNodeSass(data) { - return new Promise(function (resolve, reject) { - var sassPath = data.sassPath, - projectDir = data.projectDir, - appDir = data.appDir, - logger = data.logger, - options = data.options; - - var importerPath = path.join(__dirname, "importer.js"); - // Node SASS Command Line Args (https://github.com/sass/node-sass#command-line-interface) - // --ouput : Output directory - // --output-style : CSS output style (nested | expanded | compact | compresed) - // -q : Supress log output except on error - // --follow : Follow symlinked directories - // -r : Recursively watch directories or files - // --watch : Watch a directory or file - var nodeArgs = [sassPath, appDir, '--output', appDir, '--output-style', 'compressed', '-q', '--follow', '--importer', importerPath]; - logger.trace(process.execPath, nodeArgs.join(' ')); - - var env = Object.create( process.env ); - env.PROJECT_DIR = projectDir; - env.APP_DIR = appDir; - - var currentSassProcess = spawn(process.execPath, nodeArgs, { env: env }); - - var isResolved = false; - - currentSassProcess.stdout.on('data', function (data) { - var stringData = data.toString(); - logger.info(stringData); - }); - - currentSassProcess.stderr.on('data', function (err) { - var message = ''; - var stringData = err.toString(); - - try { - var parsed = JSON.parse(stringData); - message = parsed.formatted || parsed.message || stringData; - } catch (e) { - message = err.toString(); - } - - logger.info(message); - }); - - currentSassProcess.on('error', function (err) { - logger.info(err.message); - if (!isResolved) { - isResolved = true; - err.errorAsWarning = true; - err.stopExecution = false; - reject(err); - } - }); - - // TODO: Consider using close event instead of exit - currentSassProcess.on('exit', function (code, signal) { - currentSassProcess = null; - if (!isResolved) { - isResolved = true; - if (code === 0) { - resolve(); - } else { - var error = new Error('SASS compiler failed with exit code ' + code); - error.errorAsWarning = true; - error.stopExecution = false; - reject(error); - } - } - }); - }); -} diff --git a/src/lib/log-provider.js b/src/lib/log-provider.js new file mode 100644 index 0000000..c4313d4 --- /dev/null +++ b/src/lib/log-provider.js @@ -0,0 +1,25 @@ +const INFO_LOG_LEVEL = "info"; +const TRACE_LOG_LEVEL = "trace"; +const WARN_LOG_LEVEL = "warn"; + +module.exports = function(logger) { + this.info = (message) => { + this.logData({logLevel: INFO_LOG_LEVEL, message}); + } + + this.trace = (message) => { + this.logData({logLevel: TRACE_LOG_LEVEL, message}); + } + + this.warn = (message) => { + this.logData({logLevel: WARN_LOG_LEVEL, message}); + } + + this.logData = (data) => { + if (logger) { + return logger[data.logLevel](data.message); + } + + process.send(data); + } +} diff --git a/src/lib/watch.js b/src/lib/watch.js index 5afcfb0..46eea35 100644 --- a/src/lib/watch.js +++ b/src/lib/watch.js @@ -9,5 +9,5 @@ module.exports = function (logger, projectData, usbLiveSyncService, hookArgs) { } } - return converter.convert(logger, projectData.projectDir, projectData.appDirectoryPath, { watch: true }); -} + return converter.convert(logger, projectData.projectDir, projectData.appDirectoryPath, projectData.appResourcesDirectoryPath, { watch: true }); +} \ No newline at end of file diff --git a/src/lib/watcher.js b/src/lib/watcher.js new file mode 100644 index 0000000..677f493 --- /dev/null +++ b/src/lib/watcher.js @@ -0,0 +1,34 @@ +var choki = require('chokidar'); +var path = require('path'); +var compiler = require('./compiler'); +var LogProvider = require('./log-provider'); + +var args = JSON.parse(process.argv[2]); +var appDir = args.appDir; +var projectDir = args.projectDir; +var appResourcesDir = args.appResourcesDir; +var watchPromisesChain = Promise.resolve(); + +var watcherOptions = { + ignoreInitial: true, + cwd: appDir, + awaitWriteFinish: { + pollInterval: 100, + stabilityThreshold: 300 + }, + ignored: ['**/.*', '.*', appResourcesDir] // hidden files and App_Resources folder +}; + +watcher = choki.watch('**/*.s[ac]ss', watcherOptions) + .on('all', (event, filePath) => { + watchPromisesChain = watchPromisesChain + .then(() => compiler.compile({appDir, projectDir})) + .catch(err => { + if (!err.stopExecution && err.errorAsWarning) { + var logger = new LogProvider(null); + logger.warn(err.message); + } else { + throw err; + } + }); + }); \ No newline at end of file