utils.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. // eslint-disable-next-line import/no-extraneous-dependencies
  2. const webpackPkg = require('webpack/package.json');
  3. // eslint-disable-next-line import/no-extraneous-dependencies
  4. const ruleMatcher = require('webpack/lib/ModuleFilenameHelpers').matchObject;
  5. const { parseQuery } = require('loader-utils');
  6. const defaults = require('./config');
  7. const loaderDefaults = defaults.loader;
  8. const spriteLoaderPath = require.resolve('./loader');
  9. const stringifiedRegexp = /^'|".*'|"$/;
  10. const webpackMajorVersion = parseInt(webpackPkg.version.split('.')[0], 10);
  11. module.exports.webpackMajorVersion = webpackMajorVersion;
  12. const webpack1 = webpackMajorVersion === 1;
  13. module.exports.webpack1 = webpack1;
  14. /**
  15. * If already stringified - return original content
  16. * @param {Object|Array} content
  17. * @return {string}
  18. */
  19. function stringify(content) {
  20. if (typeof content === 'string' && stringifiedRegexp.test(content)) {
  21. return content;
  22. }
  23. return JSON.stringify(content, null, 2);
  24. }
  25. module.exports.stringify = stringify;
  26. /**
  27. * @param {SpriteSymbol} symbol
  28. * @return {string}
  29. */
  30. function stringifySymbol(symbol) {
  31. return stringify({
  32. id: symbol.id,
  33. use: symbol.useId,
  34. viewBox: symbol.viewBox,
  35. content: symbol.render()
  36. });
  37. }
  38. module.exports.stringifySymbol = stringifySymbol;
  39. /**
  40. * @param {string} content
  41. * @param {Object<string, string>} replacements
  42. * @return {string}
  43. */
  44. function replaceSpritePlaceholder(content, replacements) {
  45. const regexp = defaults.SPRITE_PLACEHOLDER_PATTERN;
  46. return content.replace(regexp, (match, p1) => {
  47. return p1 ? replacements[p1] : match;
  48. });
  49. }
  50. module.exports.replaceSpritePlaceholder = replaceSpritePlaceholder;
  51. /**
  52. * @param {NormalModule|ExtractedModule} module
  53. * @param {Object<string, string>} replacements
  54. * @return {NormalModule|ExtractedModule}
  55. */
  56. function replaceInModuleSource(module, replacements) {
  57. const source = module._source;
  58. if (typeof source === 'string') {
  59. module._source = replaceSpritePlaceholder(source, replacements);
  60. } else if (typeof source === 'object' && typeof source._value === 'string') {
  61. source._value = replaceSpritePlaceholder(source._value, replacements);
  62. }
  63. return module;
  64. }
  65. module.exports.replaceInModuleSource = replaceInModuleSource;
  66. /**
  67. * @param {string} filepath
  68. * @return {string}
  69. */
  70. function generateSpritePlaceholder(filepath) {
  71. return `{{sprite-filename|${filepath}}}`;
  72. }
  73. module.exports.generateSpritePlaceholder = generateSpritePlaceholder;
  74. /**
  75. * @param {string} symbol - Symbol name
  76. * @param {string} module - Module name
  77. * @param {boolean} esModule
  78. * @return {string}
  79. */
  80. function generateImport(symbol, module, esModule = loaderDefaults.esModule) {
  81. return esModule ?
  82. `import ${symbol} from ${stringify(module)}` :
  83. `var ${symbol} = require(${stringify(module)})`;
  84. }
  85. module.exports.generateImport = generateImport;
  86. /**
  87. * @param {string} content
  88. * @param {boolean} [esModule=false]
  89. * @return {string}
  90. */
  91. function generateExport(content, esModule = defaults.esModule) {
  92. return esModule ?
  93. `export default ${content}` :
  94. `module.exports = ${content}`;
  95. }
  96. module.exports.generateExport = generateExport;
  97. /**
  98. * webpack 1 compat rule normalizer
  99. * @param {string|Rule} rule (string - webpack 1, Object - webpack 2)
  100. * @return {Object<loader: string, options: Object|null>}
  101. */
  102. function normalizeRule(rule) {
  103. if (!rule) {
  104. throw new Error('Rule should be string or object');
  105. }
  106. let data;
  107. if (typeof rule === 'string') {
  108. const parts = rule.split('?');
  109. data = {
  110. loader: parts[0],
  111. options: parts[1] ? parseQuery(`?${parts[1]}`) : null
  112. };
  113. } else {
  114. const options = webpack1 ? rule.query : rule.options;
  115. data = {
  116. loader: rule.loader,
  117. options: options || null
  118. };
  119. }
  120. return data;
  121. }
  122. module.exports.normalizeRule = normalizeRule;
  123. /**
  124. * @param {NormalModule} module
  125. * @return {boolean}
  126. */
  127. function isModuleShouldBeExtracted(module) {
  128. const { request, issuer, loaders } = module;
  129. let rule = null;
  130. if (Array.isArray(loaders) && loaders.length > 0) {
  131. // Find loader rule
  132. rule = loaders.map(normalizeRule).find(data => data.loader === spriteLoaderPath);
  133. }
  134. let issuerResource = null;
  135. if (issuer) {
  136. // webpack 1 compat
  137. issuerResource = typeof issuer === 'string' ? issuer : issuer.resource;
  138. }
  139. if (request && (!request.includes(spriteLoaderPath) || !rule)) {
  140. return false;
  141. }
  142. return !!(
  143. (issuer && defaults.EXTRACTABLE_MODULE_ISSUER_PATTERN.test(issuerResource)) ||
  144. (rule && rule.options && rule.options.extract)
  145. );
  146. }
  147. module.exports.isModuleShouldBeExtracted = isModuleShouldBeExtracted;
  148. /**
  149. * @param {Compiler} compiler
  150. * @return {Rule[]}
  151. */
  152. function getLoadersRules(compiler) {
  153. const moduleConfig = compiler.options.module;
  154. // webpack 1 compat
  155. return moduleConfig.rules || moduleConfig.loaders;
  156. }
  157. module.exports.getLoadersRules = getLoadersRules;
  158. /**
  159. * @param {string} request
  160. * @param {Rule[]} rules Webpack loaders config
  161. * @return {Rule[]}
  162. */
  163. function getMatchedRules(request, rules) {
  164. return rules.filter(rule => ruleMatcher(rule, request));
  165. }
  166. module.exports.getMatchedRules = getMatchedRules;
  167. /**
  168. * Always returns last matched rule
  169. * @param {string} request
  170. * @param {Rule[]} rules Webpack loaders config
  171. * @return {Rule} Webpack rule
  172. */
  173. function getMatchedRule(request, rules) {
  174. const matched = getMatchedRules(request, rules);
  175. return matched[matched.length - 1];
  176. }
  177. module.exports.getMatchedRule = getMatchedRule;
  178. /**
  179. * webpack 1 compat loader options finder
  180. * @param {string} loaderPath
  181. * @param {Object|Rule} rule
  182. * @return {Object|null}
  183. */
  184. function getLoaderOptions(loaderPath, rule) {
  185. const multiRuleProp = webpack1 ? 'loaders' : 'use';
  186. const multiRule = typeof rule === 'object' && Array.isArray(rule[multiRuleProp]) ? rule[multiRuleProp] : null;
  187. let options;
  188. if (multiRule) {
  189. options = multiRule.map(normalizeRule).find(r => r.loader === loaderPath).options;
  190. } else {
  191. options = normalizeRule(rule).options;
  192. }
  193. return options;
  194. }
  195. module.exports.getLoaderOptions = getLoaderOptions;
  196. /**
  197. * Find nearest module chunk (not sure that is reliable method, but who cares).
  198. * @see http://stackoverflow.com/questions/43202761/how-to-determine-all-module-chunks-in-webpack
  199. * @param {NormalModule} module
  200. * @param {NormalModule[]} modules - webpack 1 compat
  201. * @return {Chunk?}
  202. */
  203. function getModuleChunk(module, modules) {
  204. const { chunks } = module;
  205. // webpack 1 compat
  206. const issuer = typeof module.issuer === 'string' ?
  207. modules.find(m => m.request === module.issuer) :
  208. module.issuer;
  209. if (Array.isArray(chunks) && chunks.length > 0) {
  210. return chunks[chunks.length - 1];
  211. } else if (issuer) {
  212. return getModuleChunk(issuer);
  213. }
  214. return null;
  215. }
  216. module.exports.getModuleChunk = getModuleChunk;
  217. /**
  218. * // TODO implement [chunkhash]
  219. * @param {string} filename
  220. * @param {Object} options
  221. * @param {string} options.chunkName
  222. * @param {string} options.context
  223. * @param {string} options.content
  224. * @return {string}
  225. */
  226. function interpolateSpriteFilename(filename, options) {
  227. return filename.replace(/\[chunkname\]/g, options.chunkName);
  228. }
  229. module.exports.interpolateSpriteFilename = interpolateSpriteFilename;
  230. /**
  231. * @param {SpriteSymbol[]} symbols
  232. * @param {Compilation} compilation
  233. * @return {Object<string, Object<module: NormalModule[], spriteFilename: string>>[]}
  234. */
  235. function aggregate(symbols, compilation) {
  236. const { compiler } = compilation;
  237. const allModules = compilation.modules;
  238. const modules = allModules.filter(isModuleShouldBeExtracted);
  239. const publicPath = compiler.options.output.publicPath || '';
  240. const rules = getLoadersRules(compiler);
  241. const aggregated = [];
  242. return symbols.reduce((acc, symbol) => {
  243. const resource = symbol.request.toString();
  244. const module = modules.find(m => m.resource === resource);
  245. const rule = getMatchedRule(symbol.request.file, rules);
  246. if (module && rule) {
  247. // webpack 1 compat
  248. const options = getLoaderOptions(spriteLoaderPath, rule);
  249. const spriteFilenameOption = (options && options.spriteFilename) ?
  250. options.spriteFilename :
  251. loaderDefaults.spriteFilename;
  252. const chunk = getModuleChunk(module, allModules);
  253. const spriteFilename = interpolateSpriteFilename(spriteFilenameOption, {
  254. chunkName: chunk.name
  255. });
  256. acc.push({
  257. symbol,
  258. module,
  259. resource,
  260. spriteFilename,
  261. url: `${publicPath}${spriteFilename}#${symbol.id}`,
  262. useUrl: `${publicPath}${spriteFilename}#${symbol.useId}`
  263. });
  264. }
  265. return acc;
  266. }, aggregated);
  267. }
  268. module.exports.aggregate = aggregate;
  269. /**
  270. * @param {Object} aggregated {@see aggregate}
  271. * @return {Object<string, SpriteSymbol[]>}
  272. */
  273. function groupSymbolsBySprites(aggregated) {
  274. return aggregated.map(item => item.spriteFilename)
  275. .filter((value, index, self) => self.indexOf(value) === index)
  276. .reduce((acc, spriteFilename) => {
  277. acc[spriteFilename] = aggregated
  278. .filter(item => item.spriteFilename === spriteFilename)
  279. .map(item => item.symbol);
  280. return acc;
  281. }, {});
  282. }
  283. module.exports.groupSymbolsBySprites = groupSymbolsBySprites;