NormalModuleFactory.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const path = require("path");
  7. const asyncLib = require("neo-async");
  8. const {
  9. Tapable,
  10. AsyncSeriesWaterfallHook,
  11. SyncWaterfallHook,
  12. SyncBailHook,
  13. SyncHook,
  14. HookMap
  15. } = require("tapable");
  16. const NormalModule = require("./NormalModule");
  17. const RawModule = require("./RawModule");
  18. const RuleSet = require("./RuleSet");
  19. const cachedMerge = require("./util/cachedMerge");
  20. const EMPTY_RESOLVE_OPTIONS = {};
  21. const MATCH_RESOURCE_REGEX = /^([^!]+)!=!/;
  22. const loaderToIdent = data => {
  23. if (!data.options) {
  24. return data.loader;
  25. }
  26. if (typeof data.options === "string") {
  27. return data.loader + "?" + data.options;
  28. }
  29. if (typeof data.options !== "object") {
  30. throw new Error("loader options must be string or object");
  31. }
  32. if (data.ident) {
  33. return data.loader + "??" + data.ident;
  34. }
  35. return data.loader + "?" + JSON.stringify(data.options);
  36. };
  37. const identToLoaderRequest = resultString => {
  38. const idx = resultString.indexOf("?");
  39. if (idx >= 0) {
  40. const loader = resultString.substr(0, idx);
  41. const options = resultString.substr(idx + 1);
  42. return {
  43. loader,
  44. options
  45. };
  46. } else {
  47. return {
  48. loader: resultString,
  49. options: undefined
  50. };
  51. }
  52. };
  53. const dependencyCache = new WeakMap();
  54. class NormalModuleFactory extends Tapable {
  55. constructor(context, resolverFactory, options) {
  56. super();
  57. this.hooks = {
  58. resolver: new SyncWaterfallHook(["resolver"]),
  59. factory: new SyncWaterfallHook(["factory"]),
  60. beforeResolve: new AsyncSeriesWaterfallHook(["data"]),
  61. afterResolve: new AsyncSeriesWaterfallHook(["data"]),
  62. createModule: new SyncBailHook(["data"]),
  63. module: new SyncWaterfallHook(["module", "data"]),
  64. createParser: new HookMap(() => new SyncBailHook(["parserOptions"])),
  65. parser: new HookMap(() => new SyncHook(["parser", "parserOptions"])),
  66. createGenerator: new HookMap(
  67. () => new SyncBailHook(["generatorOptions"])
  68. ),
  69. generator: new HookMap(
  70. () => new SyncHook(["generator", "generatorOptions"])
  71. )
  72. };
  73. this._pluginCompat.tap("NormalModuleFactory", options => {
  74. switch (options.name) {
  75. case "before-resolve":
  76. case "after-resolve":
  77. options.async = true;
  78. break;
  79. case "parser":
  80. this.hooks.parser
  81. .for("javascript/auto")
  82. .tap(options.fn.name || "unnamed compat plugin", options.fn);
  83. return true;
  84. }
  85. let match;
  86. match = /^parser (.+)$/.exec(options.name);
  87. if (match) {
  88. this.hooks.parser
  89. .for(match[1])
  90. .tap(
  91. options.fn.name || "unnamed compat plugin",
  92. options.fn.bind(this)
  93. );
  94. return true;
  95. }
  96. match = /^create-parser (.+)$/.exec(options.name);
  97. if (match) {
  98. this.hooks.createParser
  99. .for(match[1])
  100. .tap(
  101. options.fn.name || "unnamed compat plugin",
  102. options.fn.bind(this)
  103. );
  104. return true;
  105. }
  106. });
  107. this.resolverFactory = resolverFactory;
  108. this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
  109. this.cachePredicate =
  110. typeof options.unsafeCache === "function"
  111. ? options.unsafeCache
  112. : Boolean.bind(null, options.unsafeCache);
  113. this.context = context || "";
  114. this.parserCache = Object.create(null);
  115. this.generatorCache = Object.create(null);
  116. this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
  117. let resolver = this.hooks.resolver.call(null);
  118. // Ignored
  119. if (!resolver) return callback();
  120. resolver(result, (err, data) => {
  121. if (err) return callback(err);
  122. // Ignored
  123. if (!data) return callback();
  124. // direct module
  125. if (typeof data.source === "function") return callback(null, data);
  126. this.hooks.afterResolve.callAsync(data, (err, result) => {
  127. if (err) return callback(err);
  128. // Ignored
  129. if (!result) return callback();
  130. let createdModule = this.hooks.createModule.call(result);
  131. if (!createdModule) {
  132. if (!result.request) {
  133. return callback(new Error("Empty dependency (no request)"));
  134. }
  135. createdModule = new NormalModule(result);
  136. }
  137. createdModule = this.hooks.module.call(createdModule, result);
  138. return callback(null, createdModule);
  139. });
  140. });
  141. });
  142. this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
  143. const contextInfo = data.contextInfo;
  144. const context = data.context;
  145. const request = data.request;
  146. const loaderResolver = this.getResolver("loader");
  147. const normalResolver = this.getResolver("normal", data.resolveOptions);
  148. let matchResource = undefined;
  149. let requestWithoutMatchResource = request;
  150. const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request);
  151. if (matchResourceMatch) {
  152. matchResource = matchResourceMatch[1];
  153. if (/^\.\.?\//.test(matchResource)) {
  154. matchResource = path.join(context, matchResource);
  155. }
  156. requestWithoutMatchResource = request.substr(
  157. matchResourceMatch[0].length
  158. );
  159. }
  160. const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
  161. const noAutoLoaders =
  162. noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
  163. const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
  164. let elements = requestWithoutMatchResource
  165. .replace(/^-?!+/, "")
  166. .replace(/!!+/g, "!")
  167. .split("!");
  168. let resource = elements.pop();
  169. elements = elements.map(identToLoaderRequest);
  170. asyncLib.parallel(
  171. [
  172. callback =>
  173. this.resolveRequestArray(
  174. contextInfo,
  175. context,
  176. elements,
  177. loaderResolver,
  178. callback
  179. ),
  180. callback => {
  181. if (resource === "" || resource[0] === "?") {
  182. return callback(null, {
  183. resource
  184. });
  185. }
  186. normalResolver.resolve(
  187. contextInfo,
  188. context,
  189. resource,
  190. {},
  191. (err, resource, resourceResolveData) => {
  192. if (err) return callback(err);
  193. callback(null, {
  194. resourceResolveData,
  195. resource
  196. });
  197. }
  198. );
  199. }
  200. ],
  201. (err, results) => {
  202. if (err) return callback(err);
  203. let loaders = results[0];
  204. const resourceResolveData = results[1].resourceResolveData;
  205. resource = results[1].resource;
  206. // translate option idents
  207. try {
  208. for (const item of loaders) {
  209. if (typeof item.options === "string" && item.options[0] === "?") {
  210. const ident = item.options.substr(1);
  211. item.options = this.ruleSet.findOptionsByIdent(ident);
  212. item.ident = ident;
  213. }
  214. }
  215. } catch (e) {
  216. return callback(e);
  217. }
  218. if (resource === false) {
  219. // ignored
  220. return callback(
  221. null,
  222. new RawModule(
  223. "/* (ignored) */",
  224. `ignored ${context} ${request}`,
  225. `${request} (ignored)`
  226. )
  227. );
  228. }
  229. const userRequest =
  230. (matchResource !== undefined ? `${matchResource}!=!` : "") +
  231. loaders
  232. .map(loaderToIdent)
  233. .concat([resource])
  234. .join("!");
  235. let resourcePath =
  236. matchResource !== undefined ? matchResource : resource;
  237. let resourceQuery = "";
  238. const queryIndex = resourcePath.indexOf("?");
  239. if (queryIndex >= 0) {
  240. resourceQuery = resourcePath.substr(queryIndex);
  241. resourcePath = resourcePath.substr(0, queryIndex);
  242. }
  243. const result = this.ruleSet.exec({
  244. resource: resourcePath,
  245. realResource:
  246. matchResource !== undefined
  247. ? resource.replace(/\?.*/, "")
  248. : resourcePath,
  249. resourceQuery,
  250. issuer: contextInfo.issuer,
  251. compiler: contextInfo.compiler
  252. });
  253. const settings = {};
  254. const useLoadersPost = [];
  255. const useLoaders = [];
  256. const useLoadersPre = [];
  257. for (const r of result) {
  258. if (r.type === "use") {
  259. if (r.enforce === "post" && !noPrePostAutoLoaders) {
  260. useLoadersPost.push(r.value);
  261. } else if (
  262. r.enforce === "pre" &&
  263. !noPreAutoLoaders &&
  264. !noPrePostAutoLoaders
  265. ) {
  266. useLoadersPre.push(r.value);
  267. } else if (
  268. !r.enforce &&
  269. !noAutoLoaders &&
  270. !noPrePostAutoLoaders
  271. ) {
  272. useLoaders.push(r.value);
  273. }
  274. } else if (
  275. typeof r.value === "object" &&
  276. r.value !== null &&
  277. typeof settings[r.type] === "object" &&
  278. settings[r.type] !== null
  279. ) {
  280. settings[r.type] = cachedMerge(settings[r.type], r.value);
  281. } else {
  282. settings[r.type] = r.value;
  283. }
  284. }
  285. asyncLib.parallel(
  286. [
  287. this.resolveRequestArray.bind(
  288. this,
  289. contextInfo,
  290. this.context,
  291. useLoadersPost,
  292. loaderResolver
  293. ),
  294. this.resolveRequestArray.bind(
  295. this,
  296. contextInfo,
  297. this.context,
  298. useLoaders,
  299. loaderResolver
  300. ),
  301. this.resolveRequestArray.bind(
  302. this,
  303. contextInfo,
  304. this.context,
  305. useLoadersPre,
  306. loaderResolver
  307. )
  308. ],
  309. (err, results) => {
  310. if (err) return callback(err);
  311. loaders = results[0].concat(loaders, results[1], results[2]);
  312. process.nextTick(() => {
  313. const type = settings.type;
  314. const resolveOptions = settings.resolve;
  315. callback(null, {
  316. context: context,
  317. request: loaders
  318. .map(loaderToIdent)
  319. .concat([resource])
  320. .join("!"),
  321. dependencies: data.dependencies,
  322. userRequest,
  323. rawRequest: request,
  324. loaders,
  325. resource,
  326. matchResource,
  327. resourceResolveData,
  328. settings,
  329. type,
  330. parser: this.getParser(type, settings.parser),
  331. generator: this.getGenerator(type, settings.generator),
  332. resolveOptions
  333. });
  334. });
  335. }
  336. );
  337. }
  338. );
  339. });
  340. }
  341. create(data, callback) {
  342. const dependencies = data.dependencies;
  343. const cacheEntry = dependencyCache.get(dependencies[0]);
  344. if (cacheEntry) return callback(null, cacheEntry);
  345. const context = data.context || this.context;
  346. const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
  347. const request = dependencies[0].request;
  348. const contextInfo = data.contextInfo || {};
  349. this.hooks.beforeResolve.callAsync(
  350. {
  351. contextInfo,
  352. resolveOptions,
  353. context,
  354. request,
  355. dependencies
  356. },
  357. (err, result) => {
  358. if (err) return callback(err);
  359. // Ignored
  360. if (!result) return callback();
  361. const factory = this.hooks.factory.call(null);
  362. // Ignored
  363. if (!factory) return callback();
  364. factory(result, (err, module) => {
  365. if (err) return callback(err);
  366. if (module && this.cachePredicate(module)) {
  367. for (const d of dependencies) {
  368. dependencyCache.set(d, module);
  369. }
  370. }
  371. callback(null, module);
  372. });
  373. }
  374. );
  375. }
  376. resolveRequestArray(contextInfo, context, array, resolver, callback) {
  377. if (array.length === 0) return callback(null, []);
  378. asyncLib.map(
  379. array,
  380. (item, callback) => {
  381. resolver.resolve(
  382. contextInfo,
  383. context,
  384. item.loader,
  385. {},
  386. (err, result) => {
  387. if (
  388. err &&
  389. /^[^/]*$/.test(item.loader) &&
  390. !/-loader$/.test(item.loader)
  391. ) {
  392. return resolver.resolve(
  393. contextInfo,
  394. context,
  395. item.loader + "-loader",
  396. {},
  397. err2 => {
  398. if (!err2) {
  399. err.message =
  400. err.message +
  401. "\n" +
  402. "BREAKING CHANGE: It's no longer allowed to omit the '-loader' suffix when using loaders.\n" +
  403. ` You need to specify '${
  404. item.loader
  405. }-loader' instead of '${item.loader}',\n` +
  406. " see https://webpack.js.org/migrate/3/#automatic-loader-module-name-extension-removed";
  407. }
  408. callback(err);
  409. }
  410. );
  411. }
  412. if (err) return callback(err);
  413. const optionsOnly = item.options
  414. ? {
  415. options: item.options
  416. }
  417. : undefined;
  418. return callback(
  419. null,
  420. Object.assign({}, item, identToLoaderRequest(result), optionsOnly)
  421. );
  422. }
  423. );
  424. },
  425. callback
  426. );
  427. }
  428. getParser(type, parserOptions) {
  429. let ident = type;
  430. if (parserOptions) {
  431. if (parserOptions.ident) {
  432. ident = `${type}|${parserOptions.ident}`;
  433. } else {
  434. ident = JSON.stringify([type, parserOptions]);
  435. }
  436. }
  437. if (ident in this.parserCache) {
  438. return this.parserCache[ident];
  439. }
  440. return (this.parserCache[ident] = this.createParser(type, parserOptions));
  441. }
  442. createParser(type, parserOptions = {}) {
  443. const parser = this.hooks.createParser.for(type).call(parserOptions);
  444. if (!parser) {
  445. throw new Error(`No parser registered for ${type}`);
  446. }
  447. this.hooks.parser.for(type).call(parser, parserOptions);
  448. return parser;
  449. }
  450. getGenerator(type, generatorOptions) {
  451. let ident = type;
  452. if (generatorOptions) {
  453. if (generatorOptions.ident) {
  454. ident = `${type}|${generatorOptions.ident}`;
  455. } else {
  456. ident = JSON.stringify([type, generatorOptions]);
  457. }
  458. }
  459. if (ident in this.generatorCache) {
  460. return this.generatorCache[ident];
  461. }
  462. return (this.generatorCache[ident] = this.createGenerator(
  463. type,
  464. generatorOptions
  465. ));
  466. }
  467. createGenerator(type, generatorOptions = {}) {
  468. const generator = this.hooks.createGenerator
  469. .for(type)
  470. .call(generatorOptions);
  471. if (!generator) {
  472. throw new Error(`No generator registered for ${type}`);
  473. }
  474. this.hooks.generator.for(type).call(generator, generatorOptions);
  475. return generator;
  476. }
  477. getResolver(type, resolveOptions) {
  478. return this.resolverFactory.get(
  479. type,
  480. resolveOptions || EMPTY_RESOLVE_OPTIONS
  481. );
  482. }
  483. }
  484. module.exports = NormalModuleFactory;