JsonGenerator.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { RawSource } = require("webpack-sources");
  7. const ConcatenationScope = require("../ConcatenationScope");
  8. const { UsageState } = require("../ExportsInfo");
  9. const Generator = require("../Generator");
  10. const { JAVASCRIPT_TYPES } = require("../ModuleSourceTypeConstants");
  11. const RuntimeGlobals = require("../RuntimeGlobals");
  12. /** @import { Source } from "webpack-sources" */
  13. /** @import { JsonGeneratorOptions } from "../../declarations/WebpackOptions" */
  14. /** @import ExportsInfo from "../ExportsInfo" */
  15. /** @import { GenerateContext, UpdateHashContext } from "../Generator" */
  16. /** @import Hash from "../util/Hash" */
  17. /**
  18. * @import {
  19. * ConcatenationBailoutReasonContext,
  20. * SourceType,
  21. * SourceTypes
  22. * } from "../Module"
  23. */
  24. /** @import NormalModule from "../NormalModule" */
  25. /** @import { RuntimeSpec } from "../util/runtime" */
  26. /** @import { JsonArray, JsonObject, JsonValue } from "../util/fs" */
  27. /**
  28. * Returns stringified data.
  29. * @param {JsonValue} data Raw JSON data
  30. * @returns {undefined | string} stringified data
  31. */
  32. const stringifySafe = (data) => {
  33. const stringified = JSON.stringify(data);
  34. if (!stringified) {
  35. return; // Invalid JSON
  36. }
  37. return stringified.replace(/\u2028|\u2029/g, (str) =>
  38. str === "\u2029" ? "\\u2029" : "\\u2028"
  39. ); // invalid in JavaScript but valid JSON
  40. };
  41. /**
  42. * Collapsing an array to an object of used indices is smaller, but the value
  43. * stops being an array, so it is only safe while nothing but indices and
  44. * `length` is read.
  45. * @param {ExportsInfo} exportsInfo exports info of the array
  46. * @param {RuntimeSpec} runtime the runtime
  47. * @returns {boolean} true when the array may collapse into an object
  48. */
  49. const canCollapseArrToObject = (exportsInfo, runtime) => {
  50. for (const exportInfo of exportsInfo.ownedExports) {
  51. if (exportInfo.getUsed(runtime) === UsageState.Unused) continue;
  52. const name = exportInfo.name;
  53. // non-index names resolve on the prototype, where array and object differ
  54. if (name !== "length" && `${Number(name)}` !== name) return false;
  55. }
  56. return true;
  57. };
  58. /**
  59. * Creates an object for exports info.
  60. * @param {JsonObject | JsonArray} data Raw JSON data (always an object or array)
  61. * @param {ExportsInfo} exportsInfo exports info
  62. * @param {RuntimeSpec} runtime the runtime
  63. * @returns {JsonObject | JsonArray} reduced data
  64. */
  65. const createObjectForExportsInfo = (data, exportsInfo, runtime) => {
  66. if (exportsInfo.otherExportsInfo.getUsed(runtime) !== UsageState.Unused) {
  67. return data;
  68. }
  69. const isArray = Array.isArray(data);
  70. /** @type {JsonObject | JsonArray} */
  71. const reducedData = isArray ? [] : {};
  72. for (const key of Object.keys(data)) {
  73. const exportInfo = exportsInfo.getReadOnlyExportInfo(key);
  74. const used = exportInfo.getUsed(runtime);
  75. if (used === UsageState.Unused) continue;
  76. // The real type is `JsonObject | JsonArray`, but typescript doesn't work `Object.keys(['string', 'other-string', 'etc'])` properly
  77. const newData = /** @type {JsonObject} */ (data)[key];
  78. const value =
  79. used === UsageState.OnlyPropertiesUsed &&
  80. exportInfo.exportsInfo &&
  81. typeof newData === "object" &&
  82. newData
  83. ? createObjectForExportsInfo(newData, exportInfo.exportsInfo, runtime)
  84. : newData;
  85. const name = /** @type {string} */ (exportInfo.getUsedName(key, runtime));
  86. /** @type {JsonObject} */
  87. (reducedData)[name] = value;
  88. }
  89. if (isArray) {
  90. const arrayLengthWhenUsed =
  91. exportsInfo.getReadOnlyExportInfo("length").getUsed(runtime) !==
  92. UsageState.Unused
  93. ? data.length
  94. : undefined;
  95. let sizeObjectMinusArray = 0;
  96. const reducedDataLength =
  97. /** @type {JsonArray} */
  98. (reducedData).length;
  99. for (let i = 0; i < reducedDataLength; i++) {
  100. if (/** @type {JsonArray} */ (reducedData)[i] === undefined) {
  101. sizeObjectMinusArray -= 2;
  102. } else {
  103. sizeObjectMinusArray += `${i}`.length + 3;
  104. }
  105. }
  106. if (arrayLengthWhenUsed !== undefined) {
  107. sizeObjectMinusArray +=
  108. `${arrayLengthWhenUsed}`.length +
  109. 8 -
  110. (arrayLengthWhenUsed - reducedDataLength) * 2;
  111. }
  112. if (
  113. sizeObjectMinusArray < 0 &&
  114. canCollapseArrToObject(exportsInfo, runtime)
  115. ) {
  116. return Object.assign(
  117. arrayLengthWhenUsed === undefined
  118. ? {}
  119. : { length: arrayLengthWhenUsed },
  120. reducedData
  121. );
  122. }
  123. /** @type {number} */
  124. const generatedLength =
  125. arrayLengthWhenUsed !== undefined
  126. ? Math.max(arrayLengthWhenUsed, reducedDataLength)
  127. : reducedDataLength;
  128. for (let i = 0; i < generatedLength; i++) {
  129. if (/** @type {JsonArray} */ (reducedData)[i] === undefined) {
  130. /** @type {JsonArray} */
  131. (reducedData)[i] = 0;
  132. }
  133. }
  134. }
  135. return reducedData;
  136. };
  137. class JsonGenerator extends Generator {
  138. /**
  139. * Creates an instance of JsonGenerator.
  140. * @param {JsonGeneratorOptions} options options
  141. */
  142. constructor(options) {
  143. super();
  144. /** @type {JsonGeneratorOptions} */
  145. this.options = options;
  146. }
  147. /**
  148. * Returns the source types available for this module.
  149. * @param {NormalModule} module fresh module
  150. * @returns {SourceTypes} available types (do not mutate)
  151. */
  152. getTypes(module) {
  153. return JAVASCRIPT_TYPES;
  154. }
  155. /**
  156. * Returns the estimated size for the requested source type.
  157. * @param {NormalModule} module the module
  158. * @param {SourceType=} type source type
  159. * @returns {number} estimate size of the module
  160. */
  161. getSize(module, type) {
  162. /** @type {JsonValue | undefined} */
  163. const data =
  164. module.buildInfo &&
  165. module.buildInfo.jsonData &&
  166. module.buildInfo.jsonData.get();
  167. if (!data) return 0;
  168. return /** @type {string} */ (stringifySafe(data)).length + 10;
  169. }
  170. /**
  171. * Returns the reason this module cannot be concatenated, when one exists.
  172. * @param {NormalModule} module module for which the bailout reason should be determined
  173. * @param {ConcatenationBailoutReasonContext} context context
  174. * @returns {string | undefined} reason why this module can't be concatenated, undefined when it can be concatenated
  175. */
  176. getConcatenationBailoutReason(module, context) {
  177. return undefined;
  178. }
  179. /**
  180. * Generates generated code for this runtime module.
  181. * @param {NormalModule} module module for which the code should be generated
  182. * @param {GenerateContext} generateContext context for generate
  183. * @returns {Source | null} generated code
  184. */
  185. generate(
  186. module,
  187. {
  188. moduleGraph,
  189. runtimeTemplate,
  190. runtimeRequirements,
  191. runtime,
  192. concatenationScope
  193. }
  194. ) {
  195. /** @type {JsonValue | undefined} */
  196. const data =
  197. module.buildInfo &&
  198. module.buildInfo.jsonData &&
  199. module.buildInfo.jsonData.get();
  200. if (data === undefined) {
  201. return new RawSource(
  202. runtimeTemplate.missingModuleStatement({
  203. request: module.rawRequest
  204. })
  205. );
  206. }
  207. const exportsInfo = moduleGraph.getExportsInfo(module);
  208. /** @type {JsonValue} */
  209. const finalJson =
  210. typeof data === "object" &&
  211. data &&
  212. exportsInfo.otherExportsInfo.getUsed(runtime) === UsageState.Unused
  213. ? createObjectForExportsInfo(data, exportsInfo, runtime)
  214. : data;
  215. // Use JSON because JSON.parse() is much faster than JavaScript evaluation
  216. const jsonStr = /** @type {string} */ (stringifySafe(finalJson));
  217. const jsonExpr =
  218. this.options.JSONParse &&
  219. jsonStr.length > 20 &&
  220. typeof finalJson === "object"
  221. ? `/*#__PURE__*/JSON.parse('${jsonStr.replace(/[\\']/g, "\\$&")}')`
  222. : jsonStr.replace(/"__proto__":/g, '["__proto__"]:');
  223. /** @type {string} */
  224. let content;
  225. if (concatenationScope) {
  226. content = `${runtimeTemplate.renderConst()} ${
  227. ConcatenationScope.NAMESPACE_OBJECT_EXPORT
  228. } = ${jsonExpr};`;
  229. concatenationScope.registerNamespaceExport(
  230. ConcatenationScope.NAMESPACE_OBJECT_EXPORT
  231. );
  232. } else {
  233. runtimeRequirements.add(RuntimeGlobals.module);
  234. content = `${module.moduleArgument}.exports = ${jsonExpr};`;
  235. }
  236. return new RawSource(content);
  237. }
  238. /**
  239. * Generates fallback output for the provided error condition.
  240. * @param {Error} error the error
  241. * @param {NormalModule} module module for which the code should be generated
  242. * @param {GenerateContext} generateContext context for generate
  243. * @returns {Source | null} generated code
  244. */
  245. generateError(error, module, generateContext) {
  246. return new RawSource(Generator.throwBuildErrorCode(error));
  247. }
  248. /**
  249. * Updates the hash with the data contributed by this instance.
  250. * @param {Hash} hash hash that will be modified
  251. * @param {UpdateHashContext} updateHashContext context for updating hash
  252. */
  253. updateHash(hash, updateHashContext) {
  254. if (this.options.JSONParse) {
  255. hash.update("json-parse");
  256. }
  257. }
  258. }
  259. module.exports = JsonGenerator;