index.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. 'use strict';
  2. const fancyLog = require('fancy-log');
  3. const PluginError = require('plugin-error');
  4. const supportsColor = require('supports-color');
  5. const File = require('vinyl');
  6. const MemoryFileSystem = require('memory-fs');
  7. const nodePath = require('path');
  8. const through = require('through');
  9. const ProgressPlugin = require('webpack/lib/ProgressPlugin');
  10. const clone = require('lodash.clone');
  11. const defaultStatsOptions = {
  12. colors: supportsColor.stdout.hasBasic,
  13. hash: false,
  14. timings: false,
  15. chunks: false,
  16. chunkModules: false,
  17. modules: false,
  18. children: true,
  19. version: true,
  20. cached: false,
  21. cachedAssets: false,
  22. reasons: false,
  23. source: false,
  24. errorDetails: false
  25. };
  26. module.exports = function (options, wp, done) {
  27. const cache = {
  28. options: options,
  29. wp: wp
  30. };
  31. options = clone(options) || {};
  32. let config = options.config || options;
  33. const isInWatchMode = !!options.watch;
  34. delete options.watch;
  35. if (typeof config === 'string') {
  36. config = require(config);
  37. }
  38. // Webpack 4 doesn't support the `quiet` attribute, however supports
  39. // setting `stats` to a string within an array of configurations
  40. // (errors-only|minimal|none|normal|verbose) or an object with an absurd
  41. // amount of config
  42. const isSilent = options.quiet || (typeof options.stats === 'string' && (options.stats.match(/^(errors-only|minimal|none)$/)));
  43. if (typeof done !== 'function') {
  44. let callingDone = false;
  45. done = function (err, stats) {
  46. if (err) {
  47. // The err is here just to match the API but isnt used
  48. return;
  49. }
  50. stats = stats || {};
  51. if (isSilent || callingDone) {
  52. return;
  53. }
  54. // Debounce output a little for when in watch mode
  55. if (isInWatchMode) {
  56. callingDone = true;
  57. setTimeout(function () {
  58. callingDone = false;
  59. }, 500);
  60. }
  61. if (options.verbose) {
  62. fancyLog(stats.toString({
  63. colors: supportsColor.stdout.hasBasic
  64. }));
  65. } else {
  66. const statsOptions = (options && options.stats) || {};
  67. if (typeof statsOptions === 'object') {
  68. Object.keys(defaultStatsOptions).forEach(function (key) {
  69. if (typeof statsOptions[key] === 'undefined') {
  70. statsOptions[key] = defaultStatsOptions[key];
  71. }
  72. });
  73. }
  74. const statusLog = stats.toString(statsOptions);
  75. if (statusLog) {
  76. fancyLog(statusLog);
  77. }
  78. }
  79. };
  80. }
  81. const webpack = wp || require('webpack');
  82. let entry = [];
  83. const entries = Object.create(null);
  84. const stream = through(function (file) {
  85. if (file.isNull()) {
  86. return;
  87. }
  88. if ('named' in file) {
  89. if (!Array.isArray(entries[file.named])) {
  90. entries[file.named] = [];
  91. }
  92. entries[file.named].push(file.path);
  93. } else {
  94. entry = entry || [];
  95. entry.push(file.path);
  96. }
  97. }, function () {
  98. const self = this;
  99. const handleConfig = function (config) {
  100. config.output = config.output || {};
  101. // Determine pipe'd in entry
  102. if (Object.keys(entries).length > 0) {
  103. entry = entries;
  104. if (!config.output.filename) {
  105. // Better output default for multiple chunks
  106. config.output.filename = '[name].js';
  107. }
  108. } else if (entry.length < 2) {
  109. entry = entry[0] || entry;
  110. }
  111. config.entry = config.entry || entry;
  112. config.output.path = config.output.path || process.cwd();
  113. entry = [];
  114. if (!config.entry || config.entry.length < 1) {
  115. fancyLog('webpack-stream - No files given; aborting compilation');
  116. self.emit('end');
  117. return false;
  118. }
  119. return true;
  120. };
  121. let succeeded;
  122. if (Array.isArray(config)) {
  123. for (let i = 0; i < config.length; i++) {
  124. succeeded = handleConfig(config[i]);
  125. if (!succeeded) {
  126. return false;
  127. }
  128. }
  129. } else {
  130. succeeded = handleConfig(config);
  131. if (!succeeded) {
  132. return false;
  133. }
  134. }
  135. // Cache compiler for future use
  136. const compiler = cache.compiler || webpack(config);
  137. cache.compiler = compiler;
  138. const callback = function (err, stats) {
  139. if (err) {
  140. self.emit('error', new PluginError('webpack-stream', err));
  141. return;
  142. }
  143. const jsonStats = stats ? stats.toJson() || {} : {};
  144. const errors = jsonStats.errors || [];
  145. if (errors.length) {
  146. const resolveErrorMessage = (err) => {
  147. if (
  148. typeof err === 'object' &&
  149. err !== null &&
  150. Object.prototype.hasOwnProperty.call(err, 'message')
  151. ) {
  152. return err.message;
  153. } else if (
  154. typeof err === 'object' &&
  155. err !== null &&
  156. 'toString' in err &&
  157. err.toString() !== '[object Object]'
  158. ) {
  159. return err.toString();
  160. } else if (Array.isArray(err)) {
  161. return err.map(resolveErrorMessage).join('\n');
  162. } else {
  163. return err;
  164. }
  165. };
  166. const errorMessage = errors.map(resolveErrorMessage).join('\n');
  167. const compilationError = new PluginError('webpack-stream', errorMessage);
  168. if (!isInWatchMode) {
  169. self.emit('error', compilationError);
  170. }
  171. self.emit('compilation-error', compilationError);
  172. }
  173. if (!isInWatchMode) {
  174. self.queue(null);
  175. }
  176. done(err, stats);
  177. if (isInWatchMode && !isSilent) {
  178. fancyLog('webpack is watching for changes');
  179. }
  180. };
  181. if (isInWatchMode) {
  182. const watchOptions = options.watchOptions || {};
  183. compiler.watch(watchOptions, callback);
  184. } else {
  185. compiler.run(callback);
  186. }
  187. const handleCompiler = function (compiler) {
  188. if (options.progress) {
  189. (new ProgressPlugin(function (percentage, msg) {
  190. percentage = Math.floor(percentage * 100);
  191. msg = percentage + '% ' + msg;
  192. if (percentage < 10) msg = ' ' + msg;
  193. fancyLog('webpack', msg);
  194. })).apply(compiler);
  195. }
  196. cache.mfs = cache.mfs || new MemoryFileSystem();
  197. const fs = compiler.outputFileSystem = cache.mfs;
  198. const assetEmittedPlugin = compiler.hooks
  199. // Webpack 4/5
  200. ? function (callback) { compiler.hooks.assetEmitted.tapAsync('WebpackStream', callback); }
  201. // Webpack 2/3
  202. : function (callback) { compiler.plugin('asset-emitted', callback); };
  203. assetEmittedPlugin(function (outname, _, callback) {
  204. const file = prepareFile(fs, compiler, outname);
  205. self.queue(file);
  206. callback();
  207. });
  208. };
  209. if (Array.isArray(options.config)) {
  210. compiler.compilers.forEach(function (compiler) {
  211. handleCompiler(compiler);
  212. });
  213. } else {
  214. handleCompiler(compiler);
  215. }
  216. if (options.watch && !isSilent) {
  217. const watchRunPlugin = compiler.hooks
  218. // Webpack 4/5
  219. ? callback => compiler.hooks.watchRun.tapAsync('WebpackInfo', callback)
  220. // Webpack 2/3
  221. : callback => compiler.plugin('watch-run', callback);
  222. watchRunPlugin((compilation, callback) => {
  223. fancyLog('webpack compilation starting...');
  224. callback();
  225. });
  226. }
  227. });
  228. // If entry point manually specified, trigger that
  229. const hasEntry = Array.isArray(config)
  230. ? config.some(function (c) { return c.entry; })
  231. : config.entry;
  232. if (hasEntry) {
  233. stream.end();
  234. }
  235. return stream;
  236. };
  237. function prepareFile (fs, compiler, outname) {
  238. let path = fs.join(compiler.outputPath, outname);
  239. if (path.indexOf('?') !== -1) {
  240. path = path.split('?')[0];
  241. }
  242. const contents = fs.readFileSync(path);
  243. const file = new File({
  244. base: compiler.outputPath,
  245. path: nodePath.join(compiler.outputPath, outname),
  246. contents: contents
  247. });
  248. return file;
  249. }
  250. // Expose webpack if asked
  251. Object.defineProperty(module.exports, 'webpack', {
  252. get: function () {
  253. return require('webpack');
  254. }
  255. });