mirror of https://github.com/webpack/webpack.git
feat: implement `module.generator.json.JSONParse`
For large `.json` modules, webpack will generate `JSON.parse` by default for better performance. But there are some circumstances that `JSON.parse` is not a good choice (e.g.: when doing AOT compilation). Thus, a new generator option `module.generator.json.JSONParse` is added to disable generating `JSON.parse` for `.json` module. The default value is kept as `true` and can be opt-out by custom rules. fix: #19319
This commit is contained in:
parent
cb25853b58
commit
8ab85e29bb
|
@ -3346,6 +3346,15 @@ export interface JavascriptParserOptions {
|
|||
wrappedContextRegExp?: RegExp;
|
||||
[k: string]: any;
|
||||
}
|
||||
/**
|
||||
* Generator options for json modules.
|
||||
*/
|
||||
export interface JsonGeneratorOptions {
|
||||
/**
|
||||
* Use `JSON.parse` when the JSON string is longer than 20 characters.
|
||||
*/
|
||||
JSONParse?: boolean;
|
||||
}
|
||||
/**
|
||||
* Options for the default backend.
|
||||
*/
|
||||
|
@ -3882,6 +3891,10 @@ export interface GeneratorOptionsByModuleTypeKnown {
|
|||
* No generator options are supported for this module type.
|
||||
*/
|
||||
"javascript/esm"?: EmptyGeneratorOptions;
|
||||
/**
|
||||
* Generator options for json modules.
|
||||
*/
|
||||
json?: JsonGeneratorOptions;
|
||||
}
|
||||
/**
|
||||
* Specify options for each generator.
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* This file was automatically generated.
|
||||
* DO NOT MODIFY BY HAND.
|
||||
* Run `yarn special-lint-fix` to update
|
||||
*/
|
||||
|
||||
export interface JsonModulesPluginGeneratorOptions {
|
||||
/**
|
||||
* Use `JSON.parse` when the JSON string is longer than 20 characters.
|
||||
*/
|
||||
JSONParse?: boolean;
|
||||
}
|
|
@ -47,6 +47,7 @@ const {
|
|||
/** @typedef {import("../../declarations/WebpackOptions").GeneratorOptionsByModuleTypeKnown} GeneratorOptionsByModuleTypeKnown */
|
||||
/** @typedef {import("../../declarations/WebpackOptions").InfrastructureLogging} InfrastructureLogging */
|
||||
/** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
|
||||
/** @typedef {import("../../declarations/WebpackOptions").JsonGeneratorOptions} JsonGeneratorOptions */
|
||||
/** @typedef {import("../../declarations/WebpackOptions").Library} Library */
|
||||
/** @typedef {import("../../declarations/WebpackOptions").LibraryName} LibraryName */
|
||||
/** @typedef {import("../../declarations/WebpackOptions").LibraryOptions} LibraryOptions */
|
||||
|
@ -581,6 +582,14 @@ const applyJavascriptParserOptionsDefaults = (
|
|||
if (futureDefaults) D(parserOptions, "exportsPresence", "error");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {JsonGeneratorOptions} generatorOptions generator options
|
||||
* @returns {void}
|
||||
*/
|
||||
const applyJsonGeneratorOptionsDefaults = generatorOptions => {
|
||||
D(generatorOptions, "JSONParse", true);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {CssGeneratorOptions} generatorOptions generator options
|
||||
* @param {object} options options
|
||||
|
@ -682,6 +691,12 @@ const applyModuleDefaults = (
|
|||
}
|
||||
);
|
||||
|
||||
F(module.generator, "json", () => ({}));
|
||||
applyJsonGeneratorOptionsDefaults(
|
||||
/** @type {NonNullable<GeneratorOptionsByModuleTypeKnown["json"]>} */
|
||||
(module.generator.json)
|
||||
);
|
||||
|
||||
if (css) {
|
||||
F(module.parser, CSS_MODULE_TYPE, () => ({}));
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ const { JS_TYPES } = require("../ModuleSourceTypesConstants");
|
|||
const RuntimeGlobals = require("../RuntimeGlobals");
|
||||
|
||||
/** @typedef {import("webpack-sources").Source} Source */
|
||||
/** @typedef {import("../../declarations/WebpackOptions").JsonGeneratorOptions} JsonGeneratorOptions */
|
||||
/** @typedef {import("../ExportsInfo")} ExportsInfo */
|
||||
/** @typedef {import("../Generator").GenerateContext} GenerateContext */
|
||||
/** @typedef {import("../Module").ConcatenationBailoutReasonContext} ConcatenationBailoutReasonContext */
|
||||
|
@ -106,6 +107,14 @@ const createObjectForExportsInfo = (data, exportsInfo, runtime) => {
|
|||
};
|
||||
|
||||
class JsonGenerator extends Generator {
|
||||
/**
|
||||
* @param {JsonGeneratorOptions} options options
|
||||
*/
|
||||
constructor(options) {
|
||||
super();
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NormalModule} module fresh module
|
||||
* @returns {SourceTypes} available types (do not mutate)
|
||||
|
@ -176,7 +185,9 @@ class JsonGenerator extends Generator {
|
|||
// Use JSON because JSON.parse() is much faster than JavaScript evaluation
|
||||
const jsonStr = /** @type {string} */ (stringifySafe(finalJson));
|
||||
const jsonExpr =
|
||||
jsonStr.length > 20 && typeof finalJson === "object"
|
||||
this.options.JSONParse &&
|
||||
jsonStr.length > 20 &&
|
||||
typeof finalJson === "object"
|
||||
? `/*#__PURE__*/JSON.parse('${jsonStr.replace(/[\\']/g, "\\$&")}')`
|
||||
: jsonStr.replace(/"__proto__":/g, '["__proto__"]:');
|
||||
/** @type {string} */
|
||||
|
|
|
@ -22,6 +22,15 @@ const validate = createSchemaValidation(
|
|||
}
|
||||
);
|
||||
|
||||
const validateGenerator = createSchemaValidation(
|
||||
require("../../schemas/plugins/JsonModulesPluginGenerator.check.js"),
|
||||
() => require("../../schemas/plugins/JsonModulesPluginGenerator.json"),
|
||||
{
|
||||
name: "Json Modules Plugin",
|
||||
baseDataPath: "generator"
|
||||
}
|
||||
);
|
||||
|
||||
const PLUGIN_NAME = "JsonModulesPlugin";
|
||||
|
||||
/**
|
||||
|
@ -46,7 +55,10 @@ class JsonModulesPlugin {
|
|||
});
|
||||
normalModuleFactory.hooks.createGenerator
|
||||
.for(JSON_MODULE_TYPE)
|
||||
.tap(PLUGIN_NAME, () => new JsonGenerator());
|
||||
.tap(PLUGIN_NAME, generatorOptions => {
|
||||
validateGenerator(generatorOptions);
|
||||
return new JsonGenerator(generatorOptions);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -187,5 +187,6 @@
|
|||
"node node_modules/prettier/bin/prettier.cjs --cache --write --ignore-unknown",
|
||||
"cspell --cache --no-must-find-files"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1571,6 +1571,9 @@
|
|||
},
|
||||
"javascript/esm": {
|
||||
"$ref": "#/definitions/EmptyGeneratorOptions"
|
||||
},
|
||||
"json": {
|
||||
"$ref": "#/definitions/JsonGeneratorOptions"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2009,6 +2012,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"JsonGeneratorOptions": {
|
||||
"description": "Generator options for json modules.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"JSONParse": {
|
||||
"description": "Use `JSON.parse` when the JSON string is longer than 20 characters.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Layer": {
|
||||
"description": "Specifies the layer in which modules of this entrypoint are placed.",
|
||||
"anyOf": [
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* This file was automatically generated.
|
||||
* DO NOT MODIFY BY HAND.
|
||||
* Run `yarn special-lint-fix` to update
|
||||
*/
|
||||
declare const check: (options: import("../../declarations/plugins/JsonModulesPluginGenerator").JsonModulesPluginGeneratorOptions) => boolean;
|
||||
export = check;
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* This file was automatically generated.
|
||||
* DO NOT MODIFY BY HAND.
|
||||
* Run `yarn special-lint-fix` to update
|
||||
*/
|
||||
"use strict";function r(e,{instancePath:t="",parentData:a,parentDataProperty:o,rootData:s=e}={}){if(!e||"object"!=typeof e||Array.isArray(e))return r.errors=[{params:{type:"object"}}],!1;{const t=0;for(const t in e)if("JSONParse"!==t)return r.errors=[{params:{additionalProperty:t}}],!1;if(0===t&&void 0!==e.JSONParse&&"boolean"!=typeof e.JSONParse)return r.errors=[{params:{type:"boolean"}}],!1}return r.errors=null,!0}module.exports=r,module.exports.default=r;
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"title": "JsonModulesPluginGeneratorOptions",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"JSONParse": {
|
||||
"description": "Use `JSON.parse` when the JSON string is longer than 20 characters.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -225,7 +225,11 @@ describe("snapshots", () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
"generator": Object {},
|
||||
"generator": Object {
|
||||
"json": Object {
|
||||
"JSONParse": true,
|
||||
},
|
||||
},
|
||||
"noParse": undefined,
|
||||
"parser": Object {
|
||||
"asset": Object {
|
||||
|
@ -2290,7 +2294,7 @@ describe("snapshots", () => {
|
|||
+ },
|
||||
+ "resolve": Object {
|
||||
+ "fullySpecified": true,
|
||||
+ },
|
||||
@@ ... @@
|
||||
+ },
|
||||
+ ],
|
||||
+ "test": /\\.wasm$/i,
|
||||
|
@ -2299,7 +2303,7 @@ describe("snapshots", () => {
|
|||
+ Object {
|
||||
+ "mimetype": "application/wasm",
|
||||
+ "rules": Array [
|
||||
+ Object {
|
||||
@@ ... @@
|
||||
+ "descriptionData": Object {
|
||||
+ "type": "module",
|
||||
+ },
|
||||
|
@ -2331,12 +2335,11 @@ describe("snapshots", () => {
|
|||
+ "resolve": Object {
|
||||
+ "fullySpecified": true,
|
||||
+ "preferRelative": true,
|
||||
@@ ... @@
|
||||
+ },
|
||||
+ "type": "css",
|
||||
+ },
|
||||
+ Object {
|
||||
@@ ... @@
|
||||
- "generator": Object {},
|
||||
+ "generator": Object {
|
||||
+ "css": Object {
|
||||
+ "esModule": true,
|
||||
+ "exportsOnly": false,
|
||||
|
@ -2353,14 +2356,12 @@ describe("snapshots", () => {
|
|||
+ "exportsConvention": "as-is",
|
||||
+ "localIdentName": "[uniqueName]-[id]-[local]",
|
||||
+ },
|
||||
+ },
|
||||
@@ ... @@
|
||||
+ },
|
||||
@@ ... @@
|
||||
+ "css": Object {
|
||||
+ "import": true,
|
||||
+ "namedExports": true,
|
||||
+ "url": true,
|
||||
+ },
|
||||
@@ ... @@
|
||||
+ "exportsPresence": "error",
|
||||
@@ ... @@
|
||||
|
|
|
@ -1672,6 +1672,19 @@ Object {
|
|||
"multiple": false,
|
||||
"simpleType": "string",
|
||||
},
|
||||
"module-generator-json-json-parse": Object {
|
||||
"configs": Array [
|
||||
Object {
|
||||
"description": "Use \`JSON.parse\` when the JSON string is longer than 20 characters.",
|
||||
"multiple": false,
|
||||
"path": "module.generator.json.JSONParse",
|
||||
"type": "boolean",
|
||||
},
|
||||
],
|
||||
"description": "Use \`JSON.parse\` when the JSON string is longer than 20 characters.",
|
||||
"multiple": false,
|
||||
"simpleType": "boolean",
|
||||
},
|
||||
"module-no-parse": Object {
|
||||
"configs": Array [
|
||||
Object {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{"this is a large JSON object": "that should be converted to JSON.parse by default"}
|
|
@ -0,0 +1,5 @@
|
|||
[
|
||||
{
|
||||
"this is a large JSON object": "that should be converted to JSON.parse by default"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
it("should avoid JSON.parse", () => {
|
||||
const JSONParse = jest.spyOn(JSON, 'parse');
|
||||
JSONParse.mockClear();
|
||||
|
||||
const data = require('./data.json');
|
||||
const data2 = require('data:application/json,{"this is a large JSON object": "that should be converted to JSON.parse by default"}');
|
||||
const data3 = require('./data1.json');
|
||||
|
||||
expect(data).toMatchObject({["this is a large JSON object"]: "that should be converted to JSON.parse by default"});
|
||||
expect(data2).toMatchObject({["this is a large JSON object"]: "that should be converted to JSON.parse by default"});
|
||||
expect(data3).toMatchObject([{"this is a large JSON object": "that should be converted to JSON.parse by default"}]);
|
||||
|
||||
expect(JSONParse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should JSON.parse when resourceQuery is JSONParse=true", () => {
|
||||
const JSONParse = jest.spyOn(JSON, 'parse');
|
||||
JSONParse.mockClear();
|
||||
|
||||
const data = require('./data.json?JSONParse=true');
|
||||
|
||||
expect(data).toMatchObject({["this is a large JSON object"]: "that should be converted to JSON.parse by default"});
|
||||
expect(JSONParse).toHaveBeenCalledTimes(1);
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/** @type {import("../../../../").Configuration} */
|
||||
module.exports = {
|
||||
devtool: false,
|
||||
mode: "development",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.json$/,
|
||||
resourceQuery: /JSONParse=true/,
|
||||
type: "json",
|
||||
generator: { JSONParse: true }
|
||||
}
|
||||
],
|
||||
generator: { json: { JSONParse: false } }
|
||||
}
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
{"123this is a large JSON object": "that should be converted to JSON.parse by default"}
|
|
@ -0,0 +1,5 @@
|
|||
[
|
||||
{
|
||||
"this is a large JSON object": "that should be converted to JSON.parse by default"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
it("should use JSON.parse", () => {
|
||||
const JSONParse = jest.spyOn(JSON, 'parse');
|
||||
JSONParse.mockClear();
|
||||
|
||||
const data = require('./data.json');
|
||||
const data2 = require('data:application/json,{"this is a large JSON object": "that should be converted to JSON.parse by default"}');
|
||||
const data3 = require('./data1.json');
|
||||
|
||||
expect(data).toMatchObject({["123this is a large JSON object"]: "that should be converted to JSON.parse by default"});
|
||||
expect(data2).toMatchObject({["this is a large JSON object"]: "that should be converted to JSON.parse by default"});
|
||||
expect(data3).toMatchObject([{"this is a large JSON object": "that should be converted to JSON.parse by default"}]);
|
||||
|
||||
expect(JSONParse).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("should not call JSON.parse when resourceQuery is JSONParse=false", () => {
|
||||
const JSONParse = jest.spyOn(JSON, 'parse');
|
||||
JSONParse.mockClear();
|
||||
|
||||
const data = require('./data.json?JSONParse=false');
|
||||
|
||||
expect(data).toMatchObject({["123this is a large JSON object"]: "that should be converted to JSON.parse by default"});
|
||||
expect(JSONParse).not.toHaveBeenCalled();
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/** @type {import("../../../../").Configuration} */
|
||||
module.exports = {
|
||||
devtool: false,
|
||||
mode: "development",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.json$/,
|
||||
resourceQuery: /JSONParse=false/,
|
||||
type: "json",
|
||||
generator: { JSONParse: false }
|
||||
}
|
||||
],
|
||||
generator: { json: { JSONParse: true } }
|
||||
}
|
||||
};
|
|
@ -5350,6 +5350,11 @@ declare interface GeneratorOptionsByModuleTypeKnown {
|
|||
* No generator options are supported for this module type.
|
||||
*/
|
||||
"javascript/esm"?: EmptyGeneratorOptions;
|
||||
|
||||
/**
|
||||
* Generator options for json modules.
|
||||
*/
|
||||
json?: JsonGeneratorOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7419,6 +7424,16 @@ declare interface JavascriptParserOptions {
|
|||
*/
|
||||
wrappedContextRegExp?: RegExp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator options for json modules.
|
||||
*/
|
||||
declare interface JsonGeneratorOptions {
|
||||
/**
|
||||
* Use `JSON.parse` when the JSON string is longer than 20 characters.
|
||||
*/
|
||||
JSONParse?: boolean;
|
||||
}
|
||||
type JsonObjectFs = { [index: string]: JsonValueFs } & {
|
||||
[index: string]:
|
||||
| undefined
|
||||
|
|
Loading…
Reference in New Issue