JsonGenerator.js 8.0 KB

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