Skip to content

Commit

Permalink
fead: use LZW to compress css head meta in the production mode
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Mar 6, 2024
2 parents 0217dc7 + 45c136e commit 6842c9a
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 8 deletions.
12 changes: 12 additions & 0 deletions declarations/WebpackOptions.d.ts
Expand Up @@ -477,6 +477,10 @@ export type CssChunkFilename = FilenameTemplate;
* Specifies the filename template of output css files on disk. You must **not** specify an absolute path here, but the path may contain folders separated by '/'! The specified path is joined with the value of the 'output.path' option to determine the location on disk.
*/
export type CssFilename = FilenameTemplate;
/**
* Compress the data in the head tag of CSS files.
*/
export type CssHeadDataCompression = boolean;
/**
* Similar to `output.devtoolModuleFilenameTemplate`, but used in the case of duplicate module identifiers.
*/
Expand Down Expand Up @@ -2081,6 +2085,10 @@ export interface Output {
* Specifies the filename template of output css files on disk. You must **not** specify an absolute path here, but the path may contain folders separated by '/'! The specified path is joined with the value of the 'output.path' option to determine the location on disk.
*/
cssFilename?: CssFilename;
/**
* Compress the data in the head tag of CSS files.
*/
cssHeadDataCompression?: CssHeadDataCompression;
/**
* Similar to `output.devtoolModuleFilenameTemplate`, but used in the case of duplicate module identifiers.
*/
Expand Down Expand Up @@ -3401,6 +3409,10 @@ export interface OutputNormalized {
* Specifies the filename template of output css files on disk. You must **not** specify an absolute path here, but the path may contain folders separated by '/'! The specified path is joined with the value of the 'output.path' option to determine the location on disk.
*/
cssFilename?: CssFilename;
/**
* Compress the data in the head tag of CSS files.
*/
cssHeadDataCompression?: CssHeadDataCompression;
/**
* Similar to `output.devtoolModuleFilenameTemplate`, but used in the case of duplicate module identifiers.
*/
Expand Down
1 change: 1 addition & 0 deletions lib/config/defaults.js
Expand Up @@ -923,6 +923,7 @@ const applyOutputDefaults = (
}
return "[id].css";
});
D(output, "cssHeadDataCompression", !development);
D(output, "assetModuleFilename", "[hash][ext][query]");
D(output, "webassemblyModuleFilename", "[hash].module.wasm");
D(output, "compareBeforeEmit", true);
Expand Down
1 change: 1 addition & 0 deletions lib/config/normalization.js
Expand Up @@ -320,6 +320,7 @@ const getNormalizedWebpackOptions = config => {
chunkLoadTimeout: output.chunkLoadTimeout,
cssFilename: output.cssFilename,
cssChunkFilename: output.cssChunkFilename,
cssHeadDataCompression: output.cssHeadDataCompression,
clean: output.clean,
compareBeforeEmit: output.compareBeforeEmit,
crossOriginLoading: output.crossOriginLoading,
Expand Down
25 changes: 22 additions & 3 deletions lib/css/CssLoadingRuntimeModule.js
Expand Up @@ -66,7 +66,8 @@ class CssLoadingRuntimeModule extends RuntimeModule {
outputOptions: {
crossOriginLoading,
uniqueName,
chunkLoadTimeout: loadTimeout
chunkLoadTimeout: loadTimeout,
cssHeadDataCompression: withCompression
}
} = /** @type {Compilation} */ (compilation);
const fn = RuntimeGlobals.ensureChunkHandlers;
Expand Down Expand Up @@ -183,7 +184,7 @@ class CssLoadingRuntimeModule extends RuntimeModule {
[
`var data, token = "", token2 = "", exports = {}, ${
withHmr ? "moduleIds = [], " : ""
}name = ${name}, i = 0, cc = 1;`,
}name = ${name}, i, cc = 1;`,
"try {",
Template.indent([
"if(!link) link = loadStylesheet(chunkId);",
Expand All @@ -205,7 +206,25 @@ class CssLoadingRuntimeModule extends RuntimeModule {
]),
"}",
"if(!data) return [];",
"for(; cc; i++) {",
withCompression
? Template.asString([
// LZW decode
`var map = {}, char = data[0], oldPhrase = char, decoded = char, code = 256, maxCode = "\uffff".charCodeAt(0), phrase;`,
"for (i = 1; i < data.length; i++) {",
Template.indent([
"cc = data[i].charCodeAt(0);",
"if (cc < 256) phrase = data[i]; else phrase = map[cc] ? map[cc] : (oldPhrase + char);",
"decoded += phrase;",
"char = phrase.charAt(0);",
"map[code] = oldPhrase + char;",
"if (++code > maxCode) { code = 256; map = {}; }",
"oldPhrase = phrase;"
]),
"}",
"data = decoded;"
])
: "// css head data compression is disabled",
"for(i = 0; cc; i++) {",
Template.indent([
"cc = data.charCodeAt(i);",
`if(cc == ${cc(":")}) { token2 = token; token = ""; }`,
Expand Down
36 changes: 35 additions & 1 deletion lib/css/CssModulesPlugin.js
Expand Up @@ -128,6 +128,35 @@ const escapeCss = (str, omitOptionalUnderscore) => {
: escaped;
};

/**
* @param {string} str string
* @returns {string} encoded string
*/
const LZWEncode = str => {
/** @type {Map<string, string>} */
const map = new Map();
let encoded = "";
let phrase = str[0];
let code = 256;
let maxCode = "\uffff".charCodeAt(0);
for (let i = 1; i < str.length; i++) {
const c = str[i];
if (map.has(phrase + c)) {
phrase += c;
} else {
encoded += phrase.length > 1 ? map.get(phrase) : phrase;
map.set(phrase + c, String.fromCharCode(code));
phrase = c;
if (++code > maxCode) {
code = 256;
map.clear();
}
}
}
encoded += phrase.length > 1 ? map.get(phrase) : phrase;
return encoded;
};

const plugin = "CssModulesPlugin";

class CssModulesPlugin {
Expand Down Expand Up @@ -332,6 +361,8 @@ class CssModulesPlugin {
chunkGraph,
codeGenerationResults,
uniqueName: compilation.outputOptions.uniqueName,
cssHeadDataCompression:
compilation.outputOptions.cssHeadDataCompression,
modules
}),
filenameTemplate: CssModulesPlugin.getChunkFilenameTemplate(
Expand Down Expand Up @@ -543,6 +574,7 @@ class CssModulesPlugin {
/**
* @param {Object} options options
* @param {string | undefined} options.uniqueName unique name
* @param {boolean | undefined} options.cssHeadDataCompression compress css head data
* @param {Chunk} options.chunk chunk
* @param {ChunkGraph} options.chunkGraph chunk graph
* @param {CodeGenerationResults} options.codeGenerationResults code generation results
Expand All @@ -551,6 +583,7 @@ class CssModulesPlugin {
*/
renderChunk({
uniqueName,
cssHeadDataCompression,
chunk,
chunkGraph,
codeGenerationResults,
Expand Down Expand Up @@ -637,11 +670,12 @@ class CssModulesPlugin {
throw e;
}
}
const metaDataStr = metaData.join(",");
source.add(
`head{--webpack-${escapeCss(
(uniqueName ? uniqueName + "-" : "") + chunk.id,
true
)}:${metaData.join(",")};}`
)}:${cssHeadDataCompression ? LZWEncode(metaDataStr) : metaDataStr};}`
);
return source;
}
Expand Down
2 changes: 1 addition & 1 deletion schemas/WebpackOptions.check.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions schemas/WebpackOptions.json
Expand Up @@ -466,6 +466,10 @@
}
}
},
"CssHeadDataCompression": {
"description": "Compress the data in the head tag of CSS files.",
"type": "boolean"
},
"CssModuleGeneratorOptions": {
"description": "Generator options for css/module modules.",
"type": "object",
Expand Down Expand Up @@ -3291,6 +3295,9 @@
"cssFilename": {
"$ref": "#/definitions/CssFilename"
},
"cssHeadDataCompression": {
"$ref": "#/definitions/CssHeadDataCompression"
},
"devtoolFallbackModuleFilenameTemplate": {
"$ref": "#/definitions/DevtoolFallbackModuleFilenameTemplate"
},
Expand Down Expand Up @@ -3497,6 +3504,9 @@
"cssFilename": {
"$ref": "#/definitions/CssFilename"
},
"cssHeadDataCompression": {
"$ref": "#/definitions/CssHeadDataCompression"
},
"devtoolFallbackModuleFilenameTemplate": {
"$ref": "#/definitions/DevtoolFallbackModuleFilenameTemplate"
},
Expand Down
7 changes: 7 additions & 0 deletions test/Defaults.unittest.js
Expand Up @@ -329,6 +329,7 @@ describe("snapshots", () => {
"crossOriginLoading": false,
"cssChunkFilename": "[name].css",
"cssFilename": "[name].css",
"cssHeadDataCompression": true,
"devtoolFallbackModuleFilenameTemplate": undefined,
"devtoolModuleFilenameTemplate": undefined,
"devtoolNamespace": "webpack",
Expand Down Expand Up @@ -843,6 +844,9 @@ describe("snapshots", () => {
- "minRemainingSize": undefined,
+ "minRemainingSize": 0,
@@ ... @@
- "cssHeadDataCompression": true,
+ "cssHeadDataCompression": false,
@@ ... @@
- "pathinfo": false,
+ "pathinfo": true,
@@ ... @@
Expand Down Expand Up @@ -1867,6 +1871,9 @@ describe("snapshots", () => {
- "minRemainingSize": undefined,
+ "minRemainingSize": 0,
@@ ... @@
- "cssHeadDataCompression": true,
+ "cssHeadDataCompression": false,
@@ ... @@
- "pathinfo": false,
+ "pathinfo": true,
@@ ... @@
Expand Down
2 changes: 1 addition & 1 deletion test/Validation.test.js
Expand Up @@ -510,7 +510,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration.output has an unknown property 'ecmaVersion'. These properties are valid:
object { amdContainer?, assetModuleFilename?, asyncChunks?, auxiliaryComment?, charset?, chunkFilename?, chunkFormat?, chunkLoadTimeout?, chunkLoading?, chunkLoadingGlobal?, clean?, compareBeforeEmit?, crossOriginLoading?, cssChunkFilename?, cssFilename?, devtoolFallbackModuleFilenameTemplate?, devtoolModuleFilenameTemplate?, devtoolNamespace?, enabledChunkLoadingTypes?, enabledLibraryTypes?, enabledWasmLoadingTypes?, environment?, filename?, globalObject?, hashDigest?, hashDigestLength?, hashFunction?, hashSalt?, hotUpdateChunkFilename?, hotUpdateGlobal?, hotUpdateMainFilename?, ignoreBrowserWarnings?, iife?, importFunctionName?, importMetaName?, library?, libraryExport?, libraryTarget?, module?, path?, pathinfo?, publicPath?, scriptType?, sourceMapFilename?, sourcePrefix?, strictModuleErrorHandling?, strictModuleExceptionHandling?, trustedTypes?, umdNamedDefine?, uniqueName?, wasmLoading?, webassemblyModuleFilename?, workerChunkLoading?, workerPublicPath?, workerWasmLoading? }
object { amdContainer?, assetModuleFilename?, asyncChunks?, auxiliaryComment?, charset?, chunkFilename?, chunkFormat?, chunkLoadTimeout?, chunkLoading?, chunkLoadingGlobal?, clean?, compareBeforeEmit?, crossOriginLoading?, cssChunkFilename?, cssFilename?, cssHeadDataCompression?, devtoolFallbackModuleFilenameTemplate?, devtoolModuleFilenameTemplate?, devtoolNamespace?, enabledChunkLoadingTypes?, enabledLibraryTypes?, enabledWasmLoadingTypes?, environment?, filename?, globalObject?, hashDigest?, hashDigestLength?, hashFunction?, hashSalt?, hotUpdateChunkFilename?, hotUpdateGlobal?, hotUpdateMainFilename?, ignoreBrowserWarnings?, iife?, importFunctionName?, importMetaName?, library?, libraryExport?, libraryTarget?, module?, path?, pathinfo?, publicPath?, scriptType?, sourceMapFilename?, sourcePrefix?, strictModuleErrorHandling?, strictModuleExceptionHandling?, trustedTypes?, umdNamedDefine?, uniqueName?, wasmLoading?, webassemblyModuleFilename?, workerChunkLoading?, workerPublicPath?, workerWasmLoading? }
-> Options affecting the output of the compilation. \`output\` options tell webpack how to write the compiled files to disk.
Did you mean output.environment (output.ecmaVersion was a temporary configuration option during webpack 5 beta)?"
`)
Expand Down
13 changes: 13 additions & 0 deletions test/__snapshots__/Cli.basictest.js.snap
Expand Up @@ -6038,6 +6038,19 @@ Object {
"multiple": false,
"simpleType": "string",
},
"output-css-head-data-compression": Object {
"configs": Array [
Object {
"description": "Compress the data in the head tag of CSS files.",
"multiple": false,
"path": "output.cssHeadDataCompression",
"type": "boolean",
},
],
"description": "Compress the data in the head tag of CSS files.",
"multiple": false,
"simpleType": "boolean",
},
"output-devtool-fallback-module-filename-template": Object {
"configs": Array [
Object {
Expand Down
14 changes: 13 additions & 1 deletion test/__snapshots__/ConfigCacheTestCases.longtest.js.snap
Expand Up @@ -2549,7 +2549,7 @@ exports[`ConfigCacheTestCases css css-modules exported tests should allow to cre
--my-app-194-c5: 10px;
}
head{--webpack-my-app-226:zg:my-app-235-zg/Hi:my-app-235-Hi/OB:my-app-235-OB/VE:my-app-235-VE/O2:my-app-235-O2/Vj:my-app-235-Vj/OH:my-app-235-OH/H5:my-app-235-H5/aq:my-app-235-aq/VN:my-app-235-VN/VM:my-app-235-VM/AO:my-app-235-AO/Hq:my-app-235-Hq/O4:my-app-235-O4/Hb:my-app-235-Hb/OP:my-app-235-OP/Hw:my-app-235-Hw/nb:my-app-235-nb/\\\\$Q:my-app-235-\\\\$Q/bD:my-app-235-bD/\\\\$t:my-app-235-\\\\$t/qi:--my-app-235-qi/xB:--my-app-235-xB/\\\\$6:--my-app-235-\\\\$6/gJ:--my-app-235-gJ/x:my-app-235-x/lY:my-app-235-lY/f:my-app-235-f/uz:--my-app-235-uz/aK:my-app-235-aK/a7:my-app-235-a7/uf:my-app-235-uf/sW:my-app-235-sW/TZ:my-app-235-TZ/aY:my-app-235-aY/II:my-app-235-II/ij:my-app-235-ij/zG:my-app-235-zG/Dk:my-app-235-Dk/XE:my-app-235-XE/I0:--my-app-235-I0/wt:my-app-235-wt/nc:my-app-235-nc/iZ:my-app-235-iZ/ZP:--my-app-235-ZP/M6:my-app-235-M6/rX:--my-app-235-rX/dW:my-app-235-dW/cD:my-app-235-cD/V0:my-app-235-V0/Ci:my-app-235-Ci/bK:my-app-235-bK/Y1:my-app-235-Y1/Y:--my-app-235-Y/t6:--my-app-235-t6/KR:--my-app-235-KR/Fk:my-app-235-Fk/_235,k:my-app-666-k/_666,_816,RJ:--my-app-194-RJ/ZL:my-app-194-ZL/c5:--my-app-194-c5/_194;}"
head{--webpack-my-app-226:zg:my-app-235-Ā/HiĂĄĆĈĊČĐ/OBĒąćĉċ-Ě/VEĜĔğČĤę2ĦĞĖġ2ģjĭĕĠVjęHĴĨġHď5ĻįH5/aqŁĠņģNňĩNģMō-VM/AOŒŗďŇăĝĵėqę4ŒO4ďbŒHbęPŤPďwũw/nŨŝħįŵ/\\\\$QŒżQ/bDŒƃŻ$tſƈ/qđ--ŷĮĠƍ/xěƏƑş-ƖƇ6:ƘēƒČż6/gJƟƐơƚƧƕŒx/lYŒƲ/fŒf/uzƩƙļƻŅKŒaKŅ7ǃ7ƺƷƾįuƹsWŒǐ/TZŒǕŅƳnjʼnY/IIŒǟ/iijǛČǤ/zGŒǪ/DkŒǯ/XĥǦ-ǴǞ0ƽƫļI0/wƉǶȁŴcŒncǣǖǶiZ/ZŭƠŞļȐ/MƞǶȗ/rXǻȓįȜ/dǑǶȣ/cƄǶȨģǺǶVǿCđǶȱƂǂǶbDžY1ŒȺ/ƳȒŸĠǝtƞɀƢ-Ʉ/KRȞɁČɋ/FǰǶɒ/_Ė,ɓǼ6ɜ-kɖɜ6,_81ɢRƨɆĈ194-ɨȏLĻɬɮZLȧŀɪ-ɴ-cń_ɴ;}"
`;
exports[`ConfigCacheTestCases css css-modules exported tests should allow to create css modules: dev 1`] = `
Expand Down Expand Up @@ -2986,6 +2986,18 @@ Object {
}
`;
exports[`ConfigCacheTestCases css large-css-head-data-compression exported tests should allow to create css modules: dev 1`] = `
Object {
"placeholder": "my-app-_large_tailwind_module_css-placeholder-gray-700",
}
`;
exports[`ConfigCacheTestCases css large-css-head-data-compression exported tests should allow to create css modules: prod 1`] = `
Object {
"placeholder": "-658-Oh6j",
}
`;
exports[`ConfigCacheTestCases css local-ident-name exported tests should have correct local ident for css export locals 1`] = `
Object {
"btn--info_is-disabled_1": "-_style_module_css-btn--info_is-disabled_1",
Expand Down
14 changes: 13 additions & 1 deletion test/__snapshots__/ConfigTestCases.basictest.js.snap
Expand Up @@ -2549,7 +2549,7 @@ exports[`ConfigTestCases css css-modules exported tests should allow to create c
--my-app-194-c5: 10px;
}
head{--webpack-my-app-226:zg:my-app-235-zg/Hi:my-app-235-Hi/OB:my-app-235-OB/VE:my-app-235-VE/O2:my-app-235-O2/Vj:my-app-235-Vj/OH:my-app-235-OH/H5:my-app-235-H5/aq:my-app-235-aq/VN:my-app-235-VN/VM:my-app-235-VM/AO:my-app-235-AO/Hq:my-app-235-Hq/O4:my-app-235-O4/Hb:my-app-235-Hb/OP:my-app-235-OP/Hw:my-app-235-Hw/nb:my-app-235-nb/\\\\$Q:my-app-235-\\\\$Q/bD:my-app-235-bD/\\\\$t:my-app-235-\\\\$t/qi:--my-app-235-qi/xB:--my-app-235-xB/\\\\$6:--my-app-235-\\\\$6/gJ:--my-app-235-gJ/x:my-app-235-x/lY:my-app-235-lY/f:my-app-235-f/uz:--my-app-235-uz/aK:my-app-235-aK/a7:my-app-235-a7/uf:my-app-235-uf/sW:my-app-235-sW/TZ:my-app-235-TZ/aY:my-app-235-aY/II:my-app-235-II/ij:my-app-235-ij/zG:my-app-235-zG/Dk:my-app-235-Dk/XE:my-app-235-XE/I0:--my-app-235-I0/wt:my-app-235-wt/nc:my-app-235-nc/iZ:my-app-235-iZ/ZP:--my-app-235-ZP/M6:my-app-235-M6/rX:--my-app-235-rX/dW:my-app-235-dW/cD:my-app-235-cD/V0:my-app-235-V0/Ci:my-app-235-Ci/bK:my-app-235-bK/Y1:my-app-235-Y1/Y:--my-app-235-Y/t6:--my-app-235-t6/KR:--my-app-235-KR/Fk:my-app-235-Fk/_235,k:my-app-666-k/_666,_816,RJ:--my-app-194-RJ/ZL:my-app-194-ZL/c5:--my-app-194-c5/_194;}"
head{--webpack-my-app-226:zg:my-app-235-Ā/HiĂĄĆĈĊČĐ/OBĒąćĉċ-Ě/VEĜĔğČĤę2ĦĞĖġ2ģjĭĕĠVjęHĴĨġHď5ĻįH5/aqŁĠņģNňĩNģMō-VM/AOŒŗďŇăĝĵėqę4ŒO4ďbŒHbęPŤPďwũw/nŨŝħįŵ/\\\\$QŒżQ/bDŒƃŻ$tſƈ/qđ--ŷĮĠƍ/xěƏƑş-ƖƇ6:ƘēƒČż6/gJƟƐơƚƧƕŒx/lYŒƲ/fŒf/uzƩƙļƻŅKŒaKŅ7ǃ7ƺƷƾįuƹsWŒǐ/TZŒǕŅƳnjʼnY/IIŒǟ/iijǛČǤ/zGŒǪ/DkŒǯ/XĥǦ-ǴǞ0ƽƫļI0/wƉǶȁŴcŒncǣǖǶiZ/ZŭƠŞļȐ/MƞǶȗ/rXǻȓįȜ/dǑǶȣ/cƄǶȨģǺǶVǿCđǶȱƂǂǶbDžY1ŒȺ/ƳȒŸĠǝtƞɀƢ-Ʉ/KRȞɁČɋ/FǰǶɒ/_Ė,ɓǼ6ɜ-kɖɜ6,_81ɢRƨɆĈ194-ɨȏLĻɬɮZLȧŀɪ-ɴ-cń_ɴ;}"
`;
exports[`ConfigTestCases css css-modules exported tests should allow to create css modules: dev 1`] = `
Expand Down Expand Up @@ -2986,6 +2986,18 @@ Object {
}
`;
exports[`ConfigTestCases css large-css-head-data-compression exported tests should allow to create css modules: dev 1`] = `
Object {
"placeholder": "my-app-_large_tailwind_module_css-placeholder-gray-700",
}
`;
exports[`ConfigTestCases css large-css-head-data-compression exported tests should allow to create css modules: prod 1`] = `
Object {
"placeholder": "-658-Oh6j",
}
`;
exports[`ConfigTestCases css local-ident-name exported tests should have correct local ident for css export locals 1`] = `
Object {
"btn--info_is-disabled_1": "-_style_module_css-btn--info_is-disabled_1",
Expand Down
19 changes: 19 additions & 0 deletions test/configCases/css/large-css-head-data-compression/index.js
@@ -0,0 +1,19 @@
const prod = process.env.NODE_ENV === "production";

it("should allow to create css modules", done => {
prod
? __non_webpack_require__("./530.bundle1.js")
: __non_webpack_require__("./large_use-style_js.bundle0.js");
import("../large/use-style.js").then(({ default: x }) => {
try {
expect(x).toMatchSnapshot(prod ? "prod" : "dev");
} catch (e) {
return done(e);
}
done();
}, done);
});

it("should allow to process tailwind as global css", done => {
import("../large/tailwind.min.css").then(() => done(), done);
});
@@ -0,0 +1,25 @@
/** @type {import("../../../../").Configuration[]} */
module.exports = [
{
target: "web",
mode: "development",
output: {
uniqueName: "my-app",
cssHeadDataCompression: true
},
experiments: {
css: true
}
},
{
target: "web",
mode: "production",
output: {
cssHeadDataCompression: false
},
performance: false,
experiments: {
css: true
}
}
];
10 changes: 10 additions & 0 deletions types.d.ts
Expand Up @@ -9415,6 +9415,11 @@ declare interface Output {
| string
| ((pathData: PathData, assetInfo?: AssetInfo) => string);

/**
* Compress the data in the head tag of CSS files.
*/
cssHeadDataCompression?: boolean;

/**
* Similar to `output.devtoolModuleFilenameTemplate`, but used in the case of duplicate module identifiers.
*/
Expand Down Expand Up @@ -9734,6 +9739,11 @@ declare interface OutputNormalized {
| string
| ((pathData: PathData, assetInfo?: AssetInfo) => string);

/**
* Compress the data in the head tag of CSS files.
*/
cssHeadDataCompression?: boolean;

/**
* Similar to `output.devtoolModuleFilenameTemplate`, but used in the case of duplicate module identifiers.
*/
Expand Down

0 comments on commit 6842c9a

Please sign in to comment.