JsonGenerator.js 7.2 KB

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