read-sources.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. var fs = require('fs');
  2. var path = require('path');
  3. var applySourceMaps = require('./apply-source-maps');
  4. var extractImportUrlAndMedia = require('./extract-import-url-and-media');
  5. var isAllowedResource = require('./is-allowed-resource');
  6. var loadOriginalSources = require('./load-original-sources');
  7. var normalizePath = require('./normalize-path');
  8. var rebase = require('./rebase');
  9. var rebaseLocalMap = require('./rebase-local-map');
  10. var rebaseRemoteMap = require('./rebase-remote-map');
  11. var restoreImport = require('./restore-import');
  12. var tokenize = require('../tokenizer/tokenize');
  13. var Token = require('../tokenizer/token');
  14. var Marker = require('../tokenizer/marker');
  15. var hasProtocol = require('../utils/has-protocol');
  16. var isImport = require('../utils/is-import');
  17. var isRemoteResource = require('../utils/is-remote-resource');
  18. var UNKNOWN_URI = 'uri:unknown';
  19. function readSources(input, context, callback) {
  20. return doReadSources(input, context, function (tokens) {
  21. return applySourceMaps(tokens, context, function () {
  22. return loadOriginalSources(context, function () { return callback(tokens); });
  23. });
  24. });
  25. }
  26. function doReadSources(input, context, callback) {
  27. if (typeof input == 'string') {
  28. return fromString(input, context, callback);
  29. } else if (Buffer.isBuffer(input)) {
  30. return fromString(input.toString(), context, callback);
  31. } else if (Array.isArray(input)) {
  32. return fromArray(input, context, callback);
  33. } else if (typeof input == 'object') {
  34. return fromHash(input, context, callback);
  35. }
  36. }
  37. function fromString(input, context, callback) {
  38. context.source = undefined;
  39. context.sourcesContent[undefined] = input;
  40. context.stats.originalSize += input.length;
  41. return fromStyles(input, context, { inline: context.options.inline }, callback);
  42. }
  43. function fromArray(input, context, callback) {
  44. var inputAsImports = input.reduce(function (accumulator, uriOrHash) {
  45. if (typeof uriOrHash === 'string') {
  46. return addStringSource(uriOrHash, accumulator);
  47. } else {
  48. return addHashSource(uriOrHash, context, accumulator);
  49. }
  50. }, []);
  51. return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback);
  52. }
  53. function fromHash(input, context, callback) {
  54. var inputAsImports = addHashSource(input, context, []);
  55. return fromStyles(inputAsImports.join(''), context, { inline: ['all'] }, callback);
  56. }
  57. function addStringSource(input, imports) {
  58. imports.push(restoreAsImport(normalizeUri(input)));
  59. return imports;
  60. }
  61. function addHashSource(input, context, imports) {
  62. var uri;
  63. var normalizedUri;
  64. var source;
  65. for (uri in input) {
  66. source = input[uri];
  67. normalizedUri = normalizeUri(uri);
  68. imports.push(restoreAsImport(normalizedUri));
  69. context.sourcesContent[normalizedUri] = source.styles;
  70. if (source.sourceMap) {
  71. trackSourceMap(source.sourceMap, normalizedUri, context);
  72. }
  73. }
  74. return imports;
  75. }
  76. function normalizeUri(uri) {
  77. var currentPath = path.resolve('');
  78. var absoluteUri;
  79. var relativeToCurrentPath;
  80. var normalizedUri;
  81. if (isRemoteResource(uri)) {
  82. return uri;
  83. }
  84. absoluteUri = path.isAbsolute(uri) ?
  85. uri :
  86. path.resolve(uri);
  87. relativeToCurrentPath = path.relative(currentPath, absoluteUri);
  88. normalizedUri = normalizePath(relativeToCurrentPath);
  89. return normalizedUri;
  90. }
  91. function trackSourceMap(sourceMap, uri, context) {
  92. var parsedMap = typeof sourceMap == 'string' ?
  93. JSON.parse(sourceMap) :
  94. sourceMap;
  95. var rebasedMap = isRemoteResource(uri) ?
  96. rebaseRemoteMap(parsedMap, uri) :
  97. rebaseLocalMap(parsedMap, uri || UNKNOWN_URI, context.options.rebaseTo);
  98. context.inputSourceMapTracker.track(uri, rebasedMap);
  99. }
  100. function restoreAsImport(uri) {
  101. return restoreImport('url(' + uri + ')', '') + Marker.SEMICOLON;
  102. }
  103. function fromStyles(styles, context, parentInlinerContext, callback) {
  104. var tokens;
  105. var rebaseConfig = {};
  106. if (!context.source) {
  107. rebaseConfig.fromBase = path.resolve('');
  108. rebaseConfig.toBase = context.options.rebaseTo;
  109. } else if (isRemoteResource(context.source)) {
  110. rebaseConfig.fromBase = context.source;
  111. rebaseConfig.toBase = context.source;
  112. } else if (path.isAbsolute(context.source)) {
  113. rebaseConfig.fromBase = path.dirname(context.source);
  114. rebaseConfig.toBase = context.options.rebaseTo;
  115. } else {
  116. rebaseConfig.fromBase = path.dirname(path.resolve(context.source));
  117. rebaseConfig.toBase = context.options.rebaseTo;
  118. }
  119. tokens = tokenize(styles, context);
  120. tokens = rebase(tokens, context.options.rebase, context.validator, rebaseConfig);
  121. return allowsAnyImports(parentInlinerContext.inline) ?
  122. inline(tokens, context, parentInlinerContext, callback) :
  123. callback(tokens);
  124. }
  125. function allowsAnyImports(inline) {
  126. return !(inline.length == 1 && inline[0] == 'none');
  127. }
  128. function inline(tokens, externalContext, parentInlinerContext, callback) {
  129. var inlinerContext = {
  130. afterContent: false,
  131. callback: callback,
  132. errors: externalContext.errors,
  133. externalContext: externalContext,
  134. fetch: externalContext.options.fetch,
  135. inlinedStylesheets: parentInlinerContext.inlinedStylesheets || externalContext.inlinedStylesheets,
  136. inline: parentInlinerContext.inline,
  137. inlineRequest: externalContext.options.inlineRequest,
  138. inlineTimeout: externalContext.options.inlineTimeout,
  139. isRemote: parentInlinerContext.isRemote || false,
  140. localOnly: externalContext.localOnly,
  141. outputTokens: [],
  142. rebaseTo: externalContext.options.rebaseTo,
  143. sourceTokens: tokens,
  144. warnings: externalContext.warnings
  145. };
  146. return doInlineImports(inlinerContext);
  147. }
  148. function doInlineImports(inlinerContext) {
  149. var token;
  150. var i, l;
  151. for (i = 0, l = inlinerContext.sourceTokens.length; i < l; i++) {
  152. token = inlinerContext.sourceTokens[i];
  153. if (token[0] == Token.AT_RULE && isImport(token[1])) {
  154. inlinerContext.sourceTokens.splice(0, i);
  155. return inlineStylesheet(token, inlinerContext);
  156. } else if (token[0] == Token.AT_RULE || token[0] == Token.COMMENT) {
  157. inlinerContext.outputTokens.push(token);
  158. } else {
  159. inlinerContext.outputTokens.push(token);
  160. inlinerContext.afterContent = true;
  161. }
  162. }
  163. inlinerContext.sourceTokens = [];
  164. return inlinerContext.callback(inlinerContext.outputTokens);
  165. }
  166. function inlineStylesheet(token, inlinerContext) {
  167. var uriAndMediaQuery = extractImportUrlAndMedia(token[1]);
  168. var uri = uriAndMediaQuery[0];
  169. var mediaQuery = uriAndMediaQuery[1];
  170. var metadata = token[2];
  171. return isRemoteResource(uri) ?
  172. inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) :
  173. inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext);
  174. }
  175. function inlineRemoteStylesheet(uri, mediaQuery, metadata, inlinerContext) {
  176. var isAllowed = isAllowedResource(uri, true, inlinerContext.inline);
  177. var originalUri = uri;
  178. var isLoaded = uri in inlinerContext.externalContext.sourcesContent;
  179. var isRuntimeResource = !hasProtocol(uri);
  180. if (inlinerContext.inlinedStylesheets.indexOf(uri) > -1) {
  181. inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as it has already been imported.');
  182. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  183. return doInlineImports(inlinerContext);
  184. } else if (inlinerContext.localOnly && inlinerContext.afterContent) {
  185. inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as no callback given and after other content.');
  186. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  187. return doInlineImports(inlinerContext);
  188. } else if (isRuntimeResource) {
  189. inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no protocol given.');
  190. inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
  191. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  192. return doInlineImports(inlinerContext);
  193. } else if (inlinerContext.localOnly && !isLoaded) {
  194. inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as no callback given.');
  195. inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
  196. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  197. return doInlineImports(inlinerContext);
  198. } else if (!isAllowed && inlinerContext.afterContent) {
  199. inlinerContext.warnings.push('Ignoring remote @import of "' + uri + '" as resource is not allowed and after other content.');
  200. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  201. return doInlineImports(inlinerContext);
  202. } else if (!isAllowed) {
  203. inlinerContext.warnings.push('Skipping remote @import of "' + uri + '" as resource is not allowed.');
  204. inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
  205. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  206. return doInlineImports(inlinerContext);
  207. }
  208. inlinerContext.inlinedStylesheets.push(uri);
  209. function whenLoaded(error, importedStyles) {
  210. if (error) {
  211. inlinerContext.errors.push('Broken @import declaration of "' + uri + '" - ' + error);
  212. return process.nextTick(function () {
  213. inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
  214. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  215. doInlineImports(inlinerContext);
  216. });
  217. }
  218. inlinerContext.inline = inlinerContext.externalContext.options.inline;
  219. inlinerContext.isRemote = true;
  220. inlinerContext.externalContext.source = originalUri;
  221. inlinerContext.externalContext.sourcesContent[uri] = importedStyles;
  222. inlinerContext.externalContext.stats.originalSize += importedStyles.length;
  223. return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (importedTokens) {
  224. importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata);
  225. inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
  226. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  227. return doInlineImports(inlinerContext);
  228. });
  229. }
  230. return isLoaded ?
  231. whenLoaded(null, inlinerContext.externalContext.sourcesContent[uri]) :
  232. inlinerContext.fetch(uri, inlinerContext.inlineRequest, inlinerContext.inlineTimeout, whenLoaded);
  233. }
  234. function inlineLocalStylesheet(uri, mediaQuery, metadata, inlinerContext) {
  235. var currentPath = path.resolve('');
  236. var absoluteUri = path.isAbsolute(uri) ?
  237. path.resolve(currentPath, uri[0] == '/' ? uri.substring(1) : uri) :
  238. path.resolve(inlinerContext.rebaseTo, uri);
  239. var relativeToCurrentPath = path.relative(currentPath, absoluteUri);
  240. var importedStyles;
  241. var isAllowed = isAllowedResource(uri, false, inlinerContext.inline);
  242. var normalizedPath = normalizePath(relativeToCurrentPath);
  243. var isLoaded = normalizedPath in inlinerContext.externalContext.sourcesContent;
  244. if (inlinerContext.inlinedStylesheets.indexOf(absoluteUri) > -1) {
  245. inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as it has already been imported.');
  246. } else if (!isLoaded && (!fs.existsSync(absoluteUri) || !fs.statSync(absoluteUri).isFile())) {
  247. inlinerContext.errors.push('Ignoring local @import of "' + uri + '" as resource is missing.');
  248. } else if (!isAllowed && inlinerContext.afterContent) {
  249. inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as resource is not allowed and after other content.');
  250. } else if (inlinerContext.afterContent) {
  251. inlinerContext.warnings.push('Ignoring local @import of "' + uri + '" as after other content.');
  252. } else if (!isAllowed) {
  253. inlinerContext.warnings.push('Skipping local @import of "' + uri + '" as resource is not allowed.');
  254. inlinerContext.outputTokens = inlinerContext.outputTokens.concat(inlinerContext.sourceTokens.slice(0, 1));
  255. } else {
  256. importedStyles = isLoaded ?
  257. inlinerContext.externalContext.sourcesContent[normalizedPath] :
  258. fs.readFileSync(absoluteUri, 'utf-8');
  259. inlinerContext.inlinedStylesheets.push(absoluteUri);
  260. inlinerContext.inline = inlinerContext.externalContext.options.inline;
  261. inlinerContext.externalContext.source = normalizedPath;
  262. inlinerContext.externalContext.sourcesContent[normalizedPath] = importedStyles;
  263. inlinerContext.externalContext.stats.originalSize += importedStyles.length;
  264. return fromStyles(importedStyles, inlinerContext.externalContext, inlinerContext, function (importedTokens) {
  265. importedTokens = wrapInMedia(importedTokens, mediaQuery, metadata);
  266. inlinerContext.outputTokens = inlinerContext.outputTokens.concat(importedTokens);
  267. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  268. return doInlineImports(inlinerContext);
  269. });
  270. }
  271. inlinerContext.sourceTokens = inlinerContext.sourceTokens.slice(1);
  272. return doInlineImports(inlinerContext);
  273. }
  274. function wrapInMedia(tokens, mediaQuery, metadata) {
  275. if (mediaQuery) {
  276. return [[Token.NESTED_BLOCK, [[Token.NESTED_BLOCK_SCOPE, '@media ' + mediaQuery, metadata]], tokens]];
  277. } else {
  278. return tokens;
  279. }
  280. }
  281. module.exports = readSources;