You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

528 lines
14 KiB

4 years ago
  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 { cachedCleverMerge } = require("./util/cleverMerge");
  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] = cachedCleverMerge(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. if (matchResource === undefined) {
  312. loaders = results[0].concat(loaders, results[1], results[2]);
  313. } else {
  314. loaders = results[0].concat(results[1], loaders, results[2]);
  315. }
  316. process.nextTick(() => {
  317. const type = settings.type;
  318. const resolveOptions = settings.resolve;
  319. callback(null, {
  320. context: context,
  321. request: loaders
  322. .map(loaderToIdent)
  323. .concat([resource])
  324. .join("!"),
  325. dependencies: data.dependencies,
  326. userRequest,
  327. rawRequest: request,
  328. loaders,
  329. resource,
  330. matchResource,
  331. resourceResolveData,
  332. settings,
  333. type,
  334. parser: this.getParser(type, settings.parser),
  335. generator: this.getGenerator(type, settings.generator),
  336. resolveOptions
  337. });
  338. });
  339. }
  340. );
  341. }
  342. );
  343. });
  344. }
  345. create(data, callback) {
  346. const dependencies = data.dependencies;
  347. const cacheEntry = dependencyCache.get(dependencies[0]);
  348. if (cacheEntry) return callback(null, cacheEntry);
  349. const context = data.context || this.context;
  350. const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
  351. const request = dependencies[0].request;
  352. const contextInfo = data.contextInfo || {};
  353. this.hooks.beforeResolve.callAsync(
  354. {
  355. contextInfo,
  356. resolveOptions,
  357. context,
  358. request,
  359. dependencies
  360. },
  361. (err, result) => {
  362. if (err) return callback(err);
  363. // Ignored
  364. if (!result) return callback();
  365. const factory = this.hooks.factory.call(null);
  366. // Ignored
  367. if (!factory) return callback();
  368. factory(result, (err, module) => {
  369. if (err) return callback(err);
  370. if (module && this.cachePredicate(module)) {
  371. for (const d of dependencies) {
  372. dependencyCache.set(d, module);
  373. }
  374. }
  375. callback(null, module);
  376. });
  377. }
  378. );
  379. }
  380. resolveRequestArray(contextInfo, context, array, resolver, callback) {
  381. if (array.length === 0) return callback(null, []);
  382. asyncLib.map(
  383. array,
  384. (item, callback) => {
  385. resolver.resolve(
  386. contextInfo,
  387. context,
  388. item.loader,
  389. {},
  390. (err, result) => {
  391. if (
  392. err &&
  393. /^[^/]*$/.test(item.loader) &&
  394. !/-loader$/.test(item.loader)
  395. ) {
  396. return resolver.resolve(
  397. contextInfo,
  398. context,
  399. item.loader + "-loader",
  400. {},
  401. err2 => {
  402. if (!err2) {
  403. err.message =
  404. err.message +
  405. "\n" +
  406. "BREAKING CHANGE: It's no longer allowed to omit the '-loader' suffix when using loaders.\n" +
  407. ` You need to specify '${item.loader}-loader' instead of '${item.loader}',\n` +
  408. " see https://webpack.js.org/migrate/3/#automatic-loader-module-name-extension-removed";
  409. }
  410. callback(err);
  411. }
  412. );
  413. }
  414. if (err) return callback(err);
  415. const optionsOnly = item.options
  416. ? {
  417. options: item.options
  418. }
  419. : undefined;
  420. return callback(
  421. null,
  422. Object.assign({}, item, identToLoaderRequest(result), optionsOnly)
  423. );
  424. }
  425. );
  426. },
  427. callback
  428. );
  429. }
  430. getParser(type, parserOptions) {
  431. let ident = type;
  432. if (parserOptions) {
  433. if (parserOptions.ident) {
  434. ident = `${type}|${parserOptions.ident}`;
  435. } else {
  436. ident = JSON.stringify([type, parserOptions]);
  437. }
  438. }
  439. if (ident in this.parserCache) {
  440. return this.parserCache[ident];
  441. }
  442. return (this.parserCache[ident] = this.createParser(type, parserOptions));
  443. }
  444. createParser(type, parserOptions = {}) {
  445. const parser = this.hooks.createParser.for(type).call(parserOptions);
  446. if (!parser) {
  447. throw new Error(`No parser registered for ${type}`);
  448. }
  449. this.hooks.parser.for(type).call(parser, parserOptions);
  450. return parser;
  451. }
  452. getGenerator(type, generatorOptions) {
  453. let ident = type;
  454. if (generatorOptions) {
  455. if (generatorOptions.ident) {
  456. ident = `${type}|${generatorOptions.ident}`;
  457. } else {
  458. ident = JSON.stringify([type, generatorOptions]);
  459. }
  460. }
  461. if (ident in this.generatorCache) {
  462. return this.generatorCache[ident];
  463. }
  464. return (this.generatorCache[ident] = this.createGenerator(
  465. type,
  466. generatorOptions
  467. ));
  468. }
  469. createGenerator(type, generatorOptions = {}) {
  470. const generator = this.hooks.createGenerator
  471. .for(type)
  472. .call(generatorOptions);
  473. if (!generator) {
  474. throw new Error(`No generator registered for ${type}`);
  475. }
  476. this.hooks.generator.for(type).call(generator, generatorOptions);
  477. return generator;
  478. }
  479. getResolver(type, resolveOptions) {
  480. return this.resolverFactory.get(
  481. type,
  482. resolveOptions || EMPTY_RESOLVE_OPTIONS
  483. );
  484. }
  485. }
  486. module.exports = NormalModuleFactory;