DescriptionFileUtils.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const forEachBail = require("./forEachBail");
  7. const { decodeText } = require("./util/fs");
  8. /** @typedef {import("./Resolver")} Resolver */
  9. /** @typedef {import("./Resolver").JsonObject} JsonObject */
  10. /** @typedef {import("./Resolver").JsonValue} JsonValue */
  11. /** @typedef {import("./Resolver").ResolveContext} ResolveContext */
  12. /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
  13. /**
  14. * @typedef {object} DescriptionFileInfo
  15. * @property {JsonObject=} content content
  16. * @property {string} path path
  17. * @property {string} directory directory
  18. */
  19. /**
  20. * @callback ErrorFirstCallback
  21. * @param {Error | null=} error
  22. * @param {DescriptionFileInfo=} result
  23. */
  24. /**
  25. * @typedef {object} Result
  26. * @property {string} path path to description file
  27. * @property {string} directory directory of description file
  28. * @property {JsonObject} content content of description file
  29. */
  30. const CHAR_SLASH = 47;
  31. const CHAR_BACKSLASH = 92;
  32. /**
  33. * Walk up one directory. Called once per package-root candidate and once per
  34. * `described-resolve` (to find the enclosing description file), so it's on
  35. * the resolver's hot path.
  36. *
  37. * Previous implementation called `lastIndexOf("/")` and `lastIndexOf("\\")`
  38. * separately and then picked the larger. For any non-trivial directory
  39. * string on POSIX, `lastIndexOf("\\")` scans the full string just to return
  40. * -1. A single reverse char-code scan does the same work in one pass.
  41. *
  42. * Any single-character directory is treated as a root — `directory.length
  43. * <= 1` collapses the `"/"`, `"\\"` and `""` branches into one compare.
  44. * Without the `"\\"` case, `cdUp("\\")` (reached from a UNC root or a DOS
  45. * device path like `\\?\…`) would return itself via `slice(0, i || 1)`
  46. * and trap `loadDescriptionFile` in an infinite loop. Once single-char
  47. * roots are filtered up front, the reverse scan always produces a
  48. * strictly shorter string.
  49. * @param {string} directory directory
  50. * @returns {string | null} parent directory or null
  51. */
  52. function cdUp(directory) {
  53. if (directory.length <= 1) return null;
  54. for (let i = directory.length - 1; i >= 0; i--) {
  55. const code = directory.charCodeAt(i);
  56. if (code === CHAR_SLASH || code === CHAR_BACKSLASH) {
  57. return directory.slice(0, i || 1);
  58. }
  59. }
  60. return null;
  61. }
  62. /**
  63. * @param {Resolver} resolver resolver
  64. * @param {string} directory directory
  65. * @param {string[]} filenames filenames
  66. * @param {DescriptionFileInfo | undefined} oldInfo oldInfo
  67. * @param {ResolveContext} resolveContext resolveContext
  68. * @param {ErrorFirstCallback} callback callback
  69. */
  70. function loadDescriptionFile(
  71. resolver,
  72. directory,
  73. filenames,
  74. oldInfo,
  75. resolveContext,
  76. callback,
  77. ) {
  78. // Hoist the per-filename iterator and the per-level done callback out
  79. // of `findDescriptionFile`. They both close over `directory`, which we
  80. // reassign as we walk up the tree, so the same closures keep working
  81. // across every level — the previous implementation re-allocated both
  82. // arrows on every recursion step, which adds up on deep walks (multiple
  83. // `DescriptionFilePlugin` taps per resolve, each climbing several
  84. // directories looking for `package.json`).
  85. /**
  86. * @param {string} filename filename
  87. * @param {(err?: null | Error, result?: null | Result) => void} iterCallback callback
  88. * @returns {void}
  89. */
  90. const iterFilename = (filename, iterCallback) => {
  91. const descriptionFilePath = resolver.join(directory, filename);
  92. /**
  93. * @param {(null | Error)=} err error
  94. * @param {JsonObject=} resolvedContent content
  95. * @returns {void}
  96. */
  97. function onJson(err, resolvedContent) {
  98. if (err) {
  99. if (resolveContext.log) {
  100. resolveContext.log(
  101. `${descriptionFilePath} (directory description file): ${err}`,
  102. );
  103. } else {
  104. err.message = `${descriptionFilePath} (directory description file): ${err}`;
  105. }
  106. return iterCallback(err);
  107. }
  108. iterCallback(null, {
  109. content: /** @type {JsonObject} */ (resolvedContent),
  110. directory,
  111. path: descriptionFilePath,
  112. });
  113. }
  114. if (resolver.fileSystem.readJson) {
  115. resolver.fileSystem.readJson(descriptionFilePath, (err, content) => {
  116. if (err) {
  117. if (
  118. typeof (/** @type {NodeJS.ErrnoException} */ (err).code) !==
  119. "undefined"
  120. ) {
  121. if (resolveContext.missingDependencies) {
  122. resolveContext.missingDependencies.add(descriptionFilePath);
  123. }
  124. return iterCallback();
  125. }
  126. if (resolveContext.fileDependencies) {
  127. resolveContext.fileDependencies.add(descriptionFilePath);
  128. }
  129. return onJson(err);
  130. }
  131. if (resolveContext.fileDependencies) {
  132. resolveContext.fileDependencies.add(descriptionFilePath);
  133. }
  134. onJson(null, content);
  135. });
  136. } else {
  137. resolver.fileSystem.readFile(descriptionFilePath, (err, content) => {
  138. if (err) {
  139. if (resolveContext.missingDependencies) {
  140. resolveContext.missingDependencies.add(descriptionFilePath);
  141. }
  142. return iterCallback();
  143. }
  144. if (resolveContext.fileDependencies) {
  145. resolveContext.fileDependencies.add(descriptionFilePath);
  146. }
  147. /** @type {JsonObject | undefined} */
  148. let json;
  149. if (content) {
  150. try {
  151. json = JSON.parse(decodeText(content));
  152. } catch (/** @type {unknown} */ err_) {
  153. return onJson(/** @type {Error} */ (err_));
  154. }
  155. } else {
  156. return onJson(new Error("No content in file"));
  157. }
  158. onJson(null, json);
  159. });
  160. }
  161. };
  162. // Forward-declared so the helpers below can reference each other
  163. // without falling foul of `no-use-before-define`.
  164. /** @type {() => void} */
  165. let findDescriptionFile;
  166. /**
  167. * @param {(null | Error)=} err error
  168. * @param {(null | Result)=} result result
  169. * @returns {void}
  170. */
  171. const onLevelDone = (err, result) => {
  172. if (err) return callback(err);
  173. if (result) return callback(null, result);
  174. const dir = cdUp(directory);
  175. if (!dir) {
  176. return callback();
  177. }
  178. directory = dir;
  179. return findDescriptionFile();
  180. };
  181. findDescriptionFile = () => {
  182. if (oldInfo && oldInfo.directory === directory) {
  183. // We already have info for this directory and can reuse it
  184. return callback(null, oldInfo);
  185. }
  186. forEachBail(filenames, iterFilename, onLevelDone);
  187. };
  188. findDescriptionFile();
  189. }
  190. /**
  191. * @param {JsonObject} content content
  192. * @param {string | string[]} field field
  193. * @returns {JsonValue | undefined} field data
  194. */
  195. function getField(content, field) {
  196. if (!content) return undefined;
  197. if (Array.isArray(field)) {
  198. /** @type {JsonValue} */
  199. let current = content;
  200. for (let j = 0; j < field.length; j++) {
  201. if (current === null || typeof current !== "object") {
  202. current = null;
  203. break;
  204. }
  205. current = /** @type {JsonValue} */ (
  206. /** @type {JsonObject} */
  207. (current)[field[j]]
  208. );
  209. }
  210. return current;
  211. }
  212. return content[field];
  213. }
  214. module.exports.cdUp = cdUp;
  215. module.exports.getField = getField;
  216. module.exports.loadDescriptionFile = loadDescriptionFile;