123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. var path_1 = require("path");
  4. var interfaces_1 = require("./util/interfaces");
  5. var errors_1 = require("./util/errors");
  6. var bundle_1 = require("./bundle");
  7. var fs_extra_1 = require("fs-extra");
  8. var config_1 = require("./util/config");
  9. var logger_1 = require("./logger/logger");
  10. var logger_sass_1 = require("./logger/logger-sass");
  11. var logger_diagnostics_1 = require("./logger/logger-diagnostics");
  12. var node_sass_1 = require("node-sass");
  13. var postcss = require("postcss");
  14. var autoprefixer = require("autoprefixer");
  15. function sass(context, configFile) {
  16. configFile = config_1.getUserConfigFile(context, taskInfo, configFile);
  17. var logger = new logger_1.Logger('sass');
  18. return sassWorker(context, configFile)
  19. .then(function (outFile) {
  20. context.sassState = interfaces_1.BuildState.SuccessfulBuild;
  21. logger.finish();
  22. return outFile;
  23. })
  24. .catch(function (err) {
  25. context.sassState = interfaces_1.BuildState.RequiresBuild;
  26. throw logger.fail(err);
  27. });
  28. }
  29. exports.sass = sass;
  30. function sassUpdate(changedFiles, context) {
  31. var configFile = config_1.getUserConfigFile(context, taskInfo, null);
  32. var logger = new logger_1.Logger('sass update');
  33. return sassWorker(context, configFile)
  34. .then(function (outFile) {
  35. context.sassState = interfaces_1.BuildState.SuccessfulBuild;
  36. logger.finish();
  37. return outFile;
  38. })
  39. .catch(function (err) {
  40. context.sassState = interfaces_1.BuildState.RequiresBuild;
  41. throw logger.fail(err);
  42. });
  43. }
  44. exports.sassUpdate = sassUpdate;
  45. function sassWorker(context, configFile) {
  46. var sassConfig = getSassConfig(context, configFile);
  47. var bundlePromise = [];
  48. if (!context.moduleFiles && !sassConfig.file) {
  49. // sass must always have a list of all the used module files
  50. // so ensure we bundle if moduleFiles are currently unknown
  51. bundlePromise.push(bundle_1.bundle(context));
  52. }
  53. return Promise.all(bundlePromise).then(function () {
  54. logger_diagnostics_1.clearDiagnostics(context, logger_diagnostics_1.DiagnosticsType.Sass);
  55. // where the final css output file is saved
  56. if (!sassConfig.outFile) {
  57. sassConfig.outFile = path_1.join(context.buildDir, sassConfig.outputFilename);
  58. }
  59. logger_1.Logger.debug("sass outFile: " + sassConfig.outFile);
  60. // import paths where the sass compiler will look for imports
  61. sassConfig.includePaths.unshift(path_1.join(context.srcDir));
  62. logger_1.Logger.debug("sass includePaths: " + sassConfig.includePaths);
  63. // sass import sorting algorithms incase there was something to tweak
  64. sassConfig.sortComponentPathsFn = (sassConfig.sortComponentPathsFn || defaultSortComponentPathsFn);
  65. sassConfig.sortComponentFilesFn = (sassConfig.sortComponentFilesFn || defaultSortComponentFilesFn);
  66. if (!sassConfig.file) {
  67. // if the sass config was not given an input file, then
  68. // we're going to dynamically generate the sass data by
  69. // scanning through all the components included in the bundle
  70. // and generate the sass on the fly
  71. generateSassData(context, sassConfig);
  72. }
  73. else {
  74. sassConfig.file = config_1.replacePathVars(context, sassConfig.file);
  75. }
  76. return render(context, sassConfig);
  77. });
  78. }
  79. exports.sassWorker = sassWorker;
  80. function getSassConfig(context, configFile) {
  81. configFile = config_1.getUserConfigFile(context, taskInfo, configFile);
  82. return config_1.fillConfigDefaults(configFile, taskInfo.defaultConfigFile);
  83. }
  84. exports.getSassConfig = getSassConfig;
  85. function generateSassData(context, sassConfig) {
  86. /**
  87. * 1) Import user sass variables first since user variables
  88. * should have precedence over default library variables.
  89. * 2) Import all library sass files next since library css should
  90. * be before user css, and potentially have library css easily
  91. * overridden by user css selectors which come after the
  92. * library's in the same file.
  93. * 3) Import the user's css last since we want the user's css to
  94. * potentially easily override library css with the same
  95. * css specificity.
  96. */
  97. var moduleDirectories = [];
  98. if (context.moduleFiles) {
  99. context.moduleFiles.forEach(function (moduleFile) {
  100. var moduleDirectory = path_1.dirname(moduleFile);
  101. if (moduleDirectories.indexOf(moduleDirectory) < 0) {
  102. moduleDirectories.push(moduleDirectory);
  103. }
  104. });
  105. }
  106. logger_1.Logger.debug("sass moduleDirectories: " + moduleDirectories.length);
  107. // gather a list of all the sass variable files that should be used
  108. // these variable files will be the first imports
  109. var userSassVariableFiles = sassConfig.variableSassFiles.map(function (f) {
  110. return config_1.replacePathVars(context, f);
  111. });
  112. // gather a list of all the sass files that are next to components we're bundling
  113. var componentSassFiles = getComponentSassFiles(moduleDirectories, context, sassConfig);
  114. logger_1.Logger.debug("sass userSassVariableFiles: " + userSassVariableFiles.length);
  115. logger_1.Logger.debug("sass componentSassFiles: " + componentSassFiles.length);
  116. var sassImports = userSassVariableFiles.concat(componentSassFiles).map(function (sassFile) { return '"' + sassFile.replace(/\\/g, '\\\\') + '"'; });
  117. if (sassImports.length) {
  118. sassConfig.data = "@charset \"UTF-8\"; @import " + sassImports.join(',') + ";";
  119. }
  120. }
  121. function getComponentSassFiles(moduleDirectories, context, sassConfig) {
  122. var collectedSassFiles = [];
  123. var componentDirectories = getComponentDirectories(moduleDirectories, sassConfig);
  124. // sort all components with the library components being first
  125. // and user components coming last, so it's easier for user css
  126. // to override library css with the same specificity
  127. var sortedComponentPaths = componentDirectories.sort(sassConfig.sortComponentPathsFn);
  128. sortedComponentPaths.forEach(function (componentPath) {
  129. addComponentSassFiles(componentPath, collectedSassFiles, context, sassConfig);
  130. });
  131. return collectedSassFiles;
  132. }
  133. function addComponentSassFiles(componentPath, collectedSassFiles, context, sassConfig) {
  134. var siblingFiles = getSiblingSassFiles(componentPath, sassConfig);
  135. if (!siblingFiles.length && componentPath.indexOf(path_1.sep + 'node_modules') === -1) {
  136. // if we didn't find anything, see if this module is mapped to another directory
  137. for (var k in sassConfig.directoryMaps) {
  138. if (sassConfig.directoryMaps.hasOwnProperty(k)) {
  139. var actualDirectory = config_1.replacePathVars(context, k);
  140. var mappedDirectory = config_1.replacePathVars(context, sassConfig.directoryMaps[k]);
  141. componentPath = componentPath.replace(actualDirectory, mappedDirectory);
  142. siblingFiles = getSiblingSassFiles(componentPath, sassConfig);
  143. if (siblingFiles.length) {
  144. break;
  145. }
  146. }
  147. }
  148. }
  149. if (siblingFiles.length) {
  150. siblingFiles = siblingFiles.sort(sassConfig.sortComponentFilesFn);
  151. siblingFiles.forEach(function (componentFile) {
  152. collectedSassFiles.push(componentFile);
  153. });
  154. }
  155. }
  156. function getSiblingSassFiles(componentPath, sassConfig) {
  157. try {
  158. return fs_extra_1.readdirSync(componentPath).filter(function (f) {
  159. return isValidSassFile(f, sassConfig);
  160. }).map(function (f) {
  161. return path_1.join(componentPath, f);
  162. });
  163. }
  164. catch (ex) {
  165. // it's an invalid path
  166. return [];
  167. }
  168. }
  169. function isValidSassFile(filename, sassConfig) {
  170. for (var i = 0; i < sassConfig.includeFiles.length; i++) {
  171. if (sassConfig.includeFiles[i].test(filename)) {
  172. // filename passes the test to be included
  173. for (var j = 0; j < sassConfig.excludeFiles.length; j++) {
  174. if (sassConfig.excludeFiles[j].test(filename)) {
  175. // however, it also passed the test that it should be excluded
  176. logger_1.Logger.debug("sass excluded: " + filename);
  177. return false;
  178. }
  179. }
  180. return true;
  181. }
  182. }
  183. return false;
  184. }
  185. function getComponentDirectories(moduleDirectories, sassConfig) {
  186. // filter out module directories we know wouldn't have sibling component sass file
  187. // just a way to reduce the amount of lookups to be done later
  188. return moduleDirectories.filter(function (moduleDirectory) {
  189. // normalize this directory is using / between directories
  190. moduleDirectory = moduleDirectory.replace(/\\/g, '/');
  191. for (var i = 0; i < sassConfig.excludeModules.length; i++) {
  192. if (moduleDirectory.indexOf('/node_modules/' + sassConfig.excludeModules[i] + '/') > -1) {
  193. return false;
  194. }
  195. }
  196. return true;
  197. });
  198. }
  199. function render(context, sassConfig) {
  200. return new Promise(function (resolve, reject) {
  201. sassConfig.omitSourceMapUrl = false;
  202. if (sassConfig.sourceMap) {
  203. sassConfig.sourceMapContents = true;
  204. }
  205. node_sass_1.render(sassConfig, function (sassError, sassResult) {
  206. var diagnostics = logger_sass_1.runSassDiagnostics(context, sassError);
  207. if (diagnostics.length) {
  208. logger_diagnostics_1.printDiagnostics(context, logger_diagnostics_1.DiagnosticsType.Sass, diagnostics, true, true);
  209. // sass render error :(
  210. reject(new errors_1.BuildError('Failed to render sass to css'));
  211. }
  212. else {
  213. // sass render success :)
  214. renderSassSuccess(context, sassResult, sassConfig).then(function (outFile) {
  215. resolve(outFile);
  216. }).catch(function (err) {
  217. reject(new errors_1.BuildError(err));
  218. });
  219. }
  220. });
  221. });
  222. }
  223. function renderSassSuccess(context, sassResult, sassConfig) {
  224. if (sassConfig.autoprefixer) {
  225. // with autoprefixer
  226. var autoPrefixerMapOptions = false;
  227. if (sassConfig.sourceMap) {
  228. autoPrefixerMapOptions = {
  229. inline: false,
  230. prev: generateSourceMaps(sassResult, sassConfig)
  231. };
  232. }
  233. var postcssOptions = {
  234. to: path_1.basename(sassConfig.outFile),
  235. map: autoPrefixerMapOptions
  236. };
  237. logger_1.Logger.debug("sass, start postcss/autoprefixer");
  238. var postCssPlugins = [autoprefixer(sassConfig.autoprefixer)];
  239. if (sassConfig.postCssPlugins) {
  240. postCssPlugins = sassConfig.postCssPlugins.concat(postCssPlugins);
  241. }
  242. return postcss(postCssPlugins)
  243. .process(sassResult.css, postcssOptions).then(function (postCssResult) {
  244. postCssResult.warnings().forEach(function (warn) {
  245. logger_1.Logger.warn(warn.toString());
  246. });
  247. var apMapResult = null;
  248. if (sassConfig.sourceMap && postCssResult.map) {
  249. logger_1.Logger.debug("sass, parse postCssResult.map");
  250. apMapResult = generateSourceMaps(postCssResult, sassConfig);
  251. }
  252. logger_1.Logger.debug("sass: postcss/autoprefixer completed");
  253. return writeOutput(context, sassConfig, postCssResult.css, apMapResult);
  254. });
  255. }
  256. // without autoprefixer
  257. var sassMapResult = generateSourceMaps(sassResult, sassConfig);
  258. return writeOutput(context, sassConfig, sassResult.css.toString(), sassMapResult);
  259. }
  260. function generateSourceMaps(sassResult, sassConfig) {
  261. // this can be async and nothing needs to wait on it
  262. // build Source Maps!
  263. if (sassResult.map) {
  264. logger_1.Logger.debug("sass, generateSourceMaps");
  265. // transform map into JSON
  266. var sassMap = JSON.parse(sassResult.map.toString());
  267. // grab the stdout and transform it into stdin
  268. var sassMapFile = sassMap.file.replace(/^stdout$/, 'stdin');
  269. // grab the base file name that's being worked on
  270. var sassFileSrc = sassConfig.outFile;
  271. // grab the path portion of the file that's being worked on
  272. var sassFileSrcPath_1 = path_1.dirname(sassFileSrc);
  273. if (sassFileSrcPath_1) {
  274. // prepend the path to all files in the sources array except the file that's being worked on
  275. var sourceFileIndex_1 = sassMap.sources.indexOf(sassMapFile);
  276. sassMap.sources = sassMap.sources.map(function (source, index) {
  277. return (index === sourceFileIndex_1) ? source : path_1.join(sassFileSrcPath_1, source);
  278. });
  279. }
  280. // remove 'stdin' from souces and replace with filenames!
  281. sassMap.sources = sassMap.sources.filter(function (src) {
  282. if (src !== 'stdin') {
  283. return src;
  284. }
  285. });
  286. return sassMap;
  287. }
  288. }
  289. function writeOutput(context, sassConfig, cssOutput, sourceMap) {
  290. var mappingsOutput = JSON.stringify(sourceMap);
  291. return new Promise(function (resolve, reject) {
  292. logger_1.Logger.debug("sass start write output: " + sassConfig.outFile);
  293. var buildDir = path_1.dirname(sassConfig.outFile);
  294. fs_extra_1.ensureDirSync(buildDir);
  295. fs_extra_1.writeFile(sassConfig.outFile, cssOutput, function (cssWriteErr) {
  296. if (cssWriteErr) {
  297. reject(new errors_1.BuildError("Error writing css file, " + sassConfig.outFile + ": " + cssWriteErr));
  298. }
  299. else {
  300. logger_1.Logger.debug("sass saved output: " + sassConfig.outFile);
  301. if (mappingsOutput) {
  302. // save the css map file too
  303. // this save completes async and does not hold up the resolve
  304. var sourceMapPath_1 = path_1.join(buildDir, path_1.basename(sassConfig.outFile) + '.map');
  305. logger_1.Logger.debug("sass start write css map: " + sourceMapPath_1);
  306. fs_extra_1.writeFile(sourceMapPath_1, mappingsOutput, function (mapWriteErr) {
  307. if (mapWriteErr) {
  308. logger_1.Logger.error("Error writing css map file, " + sourceMapPath_1 + ": " + mapWriteErr);
  309. }
  310. else {
  311. logger_1.Logger.debug("sass saved css map: " + sourceMapPath_1);
  312. }
  313. });
  314. }
  315. // css file all saved
  316. // note that we're not waiting on the css map to finish saving
  317. resolve(sassConfig.outFile);
  318. }
  319. });
  320. });
  321. }
  322. function defaultSortComponentPathsFn(a, b) {
  323. var aIndexOfNodeModules = a.indexOf('node_modules');
  324. var bIndexOfNodeModules = b.indexOf('node_modules');
  325. if (aIndexOfNodeModules > -1 && bIndexOfNodeModules > -1) {
  326. return (a > b) ? 1 : -1;
  327. }
  328. if (aIndexOfNodeModules > -1 && bIndexOfNodeModules === -1) {
  329. return -1;
  330. }
  331. if (aIndexOfNodeModules === -1 && bIndexOfNodeModules > -1) {
  332. return 1;
  333. }
  334. return (a > b) ? 1 : -1;
  335. }
  336. function defaultSortComponentFilesFn(a, b) {
  337. var aPeriods = a.split('.').length;
  338. var bPeriods = b.split('.').length;
  339. var aDashes = a.split('-').length;
  340. var bDashes = b.split('-').length;
  341. if (aPeriods > bPeriods) {
  342. return 1;
  343. }
  344. else if (aPeriods < bPeriods) {
  345. return -1;
  346. }
  347. if (aDashes > bDashes) {
  348. return 1;
  349. }
  350. else if (aDashes < bDashes) {
  351. return -1;
  352. }
  353. return (a > b) ? 1 : -1;
  354. }
  355. var taskInfo = {
  356. fullArg: '--sass',
  357. shortArg: '-s',
  358. envVar: 'IONIC_SASS',
  359. packageConfig: 'ionic_sass',
  360. defaultConfigFile: 'sass.config'
  361. };