ExtensionAliasPlugin.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Ivan Kopeykin @vankop
  4. */
  5. "use strict";
  6. const forEachBail = require("./forEachBail");
  7. /** @typedef {import("./Resolver")} Resolver */
  8. /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
  9. /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
  10. /** @typedef {{ alias: string | string[], extension: string }} ExtensionAliasOption */
  11. module.exports = class ExtensionAliasPlugin {
  12. /**
  13. * @param {string | ResolveStepHook} source source
  14. * @param {ExtensionAliasOption} options options
  15. * @param {string | ResolveStepHook} target target
  16. */
  17. constructor(source, options, target) {
  18. this.source = source;
  19. this.options = options;
  20. this.target = target;
  21. }
  22. /**
  23. * @param {Resolver} resolver the resolver
  24. * @returns {void}
  25. */
  26. apply(resolver) {
  27. const target = resolver.ensureHook(this.target);
  28. const { extension, alias } = this.options;
  29. resolver
  30. .getHook(this.source)
  31. .tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => {
  32. // Two modes of operation:
  33. // - "request" mode: original request specifier still carries the
  34. // extension (e.g. user wrote `./foo.js`). We swap the extension
  35. // on `request.request` and re-resolve.
  36. // - "path" mode: the specifier has already been joined into an
  37. // absolute `request.path` (e.g. produced by the imports field).
  38. // We swap the extension on `request.path` and `request.relativePath`.
  39. const useRequest = request.request !== undefined;
  40. const source = useRequest
  41. ? /** @type {string} */ (request.request)
  42. : request.path;
  43. if (!source || !source.endsWith(extension)) return callback();
  44. const isAliasString = typeof alias === "string";
  45. // Hoist the base (everything before the old extension) out of the
  46. // per-alias `resolve` callback. For an array `alias`, the callback
  47. // runs once per candidate extension; the base does not change
  48. // between iterations, so there's no reason to recompute it.
  49. const sourceBase = source.slice(0, -extension.length);
  50. const relativePathBase =
  51. !useRequest &&
  52. request.relativePath &&
  53. request.relativePath.endsWith(extension)
  54. ? request.relativePath.slice(0, -extension.length)
  55. : null;
  56. /**
  57. * @param {string} alias extension alias
  58. * @param {(err?: null | Error, result?: null | ResolveRequest) => void} callback callback
  59. * @param {number=} index index
  60. * @returns {void}
  61. */
  62. const resolve = (alias, callback, index) => {
  63. const newValue = `${sourceBase}${alias}`;
  64. const nextRequest = useRequest
  65. ? {
  66. ...request,
  67. request: newValue,
  68. fullySpecified: true,
  69. }
  70. : {
  71. ...request,
  72. path: newValue,
  73. relativePath:
  74. relativePathBase !== null
  75. ? `${relativePathBase}${alias}`
  76. : request.relativePath,
  77. fullySpecified: true,
  78. };
  79. return resolver.doResolve(
  80. target,
  81. nextRequest,
  82. `aliased from extension alias with mapping '${extension}' to '${alias}'`,
  83. resolveContext,
  84. (err, result) => {
  85. // Throw error if we are on the last alias (for multiple aliases) and it failed, always throw if we are not an array or we have only one alias
  86. if (!isAliasString && index) {
  87. if (index !== this.options.alias.length) {
  88. if (resolveContext.log) {
  89. resolveContext.log(
  90. `Failed to alias from extension alias with mapping '${extension}' to '${alias}' for '${newValue}': ${err}`,
  91. );
  92. }
  93. return callback(null, result);
  94. }
  95. return callback(err, result);
  96. }
  97. callback(err, result);
  98. },
  99. );
  100. };
  101. /**
  102. * @param {(null | Error)=} err error
  103. * @param {(null | ResolveRequest)=} result result
  104. * @returns {void}
  105. */
  106. const stoppingCallback = (err, result) => {
  107. if (err) return callback(err);
  108. if (result) return callback(null, result);
  109. // Don't allow other aliasing or raw request
  110. return callback(null, null);
  111. };
  112. if (isAliasString) {
  113. resolve(alias, stoppingCallback);
  114. } else if (alias.length > 1) {
  115. forEachBail(alias, resolve, stoppingCallback);
  116. } else {
  117. resolve(alias[0], stoppingCallback);
  118. }
  119. });
  120. }
  121. };