Skip to content

Commit 37d0a4c

Browse files
fix: migrate from mime-types to mime-db (#20812)
1 parent 9b45760 commit 37d0a4c

10 files changed

Lines changed: 333 additions & 13 deletions

File tree

.changeset/full-months-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Migrate from `mime-types` to `mime-db`.

cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,10 @@
127127
"jrburke",
128128
"jsfile",
129129
"jsons",
130+
"jshttp",
131+
"jsdoc",
130132
"junit",
131133
"Junya",
132-
"jsdoc",
133134
"jwalton",
134135
"kaios",
135136
"Kees",

lib/TemplatedPathPlugin.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
const { basename, extname } = require("path");
99
const util = require("util");
10-
const mime = require("mime-types");
1110
const Chunk = require("./Chunk");
1211
const Module = require("./Module");
1312
const { parseResource } = require("./util/identifier");
13+
const memoize = require("./util/memoize");
14+
15+
const getMimeTypes = memoize(() => require("./util/mimeTypes"));
1416

1517
/** @typedef {import("./ChunkGraph")} ChunkGraph */
1618
/** @typedef {import("./ChunkGraph").ModuleId} ModuleId */
@@ -163,7 +165,7 @@ const interpolate = (path, data, assetInfo) => {
163165
// check that filename is data uri
164166
const match = data.filename.match(/^data:([^;,]+)/);
165167
if (match) {
166-
const ext = mime.extension(match[1]);
168+
const ext = getMimeTypes().extension(match[1]);
167169
const emptyReplacer = replacer("", true);
168170
// "XXXX" used for `updateHash`, so we don't need it here
169171
const contentHash =

lib/asset/AssetGenerator.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const { makePathsRelative } = require("../util/identifier");
3030
const memoize = require("../util/memoize");
3131
const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
3232

33-
const getMimeTypes = memoize(() => require("mime-types"));
33+
const getMimeTypes = memoize(() => require("../util/mimeTypes"));
3434

3535
/** @typedef {import("webpack-sources").Source} Source */
3636
/** @typedef {import("../../declarations/WebpackOptions").AssetGeneratorDataUrlOptions} AssetGeneratorDataUrlOptions */
@@ -420,7 +420,7 @@ class AssetGenerator extends Generator {
420420
);
421421
}
422422

423-
/** @type {string | boolean | undefined} */
423+
/** @type {string | undefined} */
424424
let mimeType =
425425
/** @type {AssetGeneratorDataUrlOptions} */
426426
(this.dataUrlOptions).mimetype;

lib/util/mimeTypes.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
*/
4+
5+
"use strict";
6+
7+
const { extname } = require("path");
8+
const db = require("mime-db");
9+
10+
/**
11+
* RegExp to match type in RFC 6838
12+
*
13+
* type-name = restricted-name
14+
* subtype-name = restricted-name
15+
* restricted-name = restricted-name-first *126restricted-name-chars
16+
* restricted-name-first = ALPHA / DIGIT
17+
* restricted-name-chars = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "-" / "^" / "_"
18+
* restricted-name-chars =/ "." ; Characters before first dot always specify a facet name
19+
* restricted-name-chars =/ "+" ; Characters after last plus always specify a structured syntax suffix
20+
* ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
21+
* DIGIT = %x30-39 ; 0-9
22+
*/
23+
const TYPE_REGEXP =
24+
/^ *(([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126})) *(?:;.*)?$/;
25+
const extensions = Object.create(null);
26+
const types = Object.create(null);
27+
28+
// Score RFC facets (see https://tools.ietf.org/html/rfc6838#section-3)
29+
30+
/** @type {Record<string, number>} */
31+
const FACET_SCORES = {
32+
"prs.": 100,
33+
"x-": 200,
34+
"x.": 300,
35+
"vnd.": 400,
36+
default: 900
37+
};
38+
39+
/** @typedef {"nginx" | "apache" | "iana" | "default"} SourceScore */
40+
41+
// Score mime source (Logic originally from `jshttp/mime-types` module)
42+
/** @type {Record<SourceScore, number>} */
43+
const SOURCE_SCORES = {
44+
nginx: 10,
45+
apache: 20,
46+
iana: 40,
47+
default: 30
48+
};
49+
50+
/** @type {Record<string, number>} */
51+
const TYPE_SCORES = {
52+
// prefer application/xml over text/xml
53+
// prefer application/rtf over text/rtf
54+
application: 1,
55+
56+
// prefer font/woff over application/font-woff
57+
font: 2,
58+
59+
// prefer video/mp4 over audio/mp4 over application/mp4
60+
// See https://www.rfc-editor.org/rfc/rfc4337.html#section-2
61+
audio: 2,
62+
video: 3,
63+
64+
default: 0
65+
};
66+
67+
/**
68+
* @param {string} mimeType mime type
69+
* @param {SourceScore=} source source
70+
* @returns {number} min score
71+
*/
72+
function mimeScore(mimeType, source = "default") {
73+
if (mimeType === "application/octet-stream") {
74+
return 0;
75+
}
76+
77+
const [type, subtype] = mimeType.split("/");
78+
79+
const facet = subtype.replace(/(\.|x-).*/, "$1");
80+
81+
const facetScore = FACET_SCORES[facet] || FACET_SCORES.default;
82+
const sourceScore = SOURCE_SCORES[source] || SOURCE_SCORES.default;
83+
const typeScore = TYPE_SCORES[type] || TYPE_SCORES.default;
84+
85+
// All else being equal prefer shorter types
86+
const lengthScore = 1 - mimeType.length / 100;
87+
88+
return facetScore + sourceScore + typeScore + lengthScore;
89+
}
90+
91+
/**
92+
* @param {string} ext extension
93+
* @param {string} type0 the first type
94+
* @param {string} type1 the second type
95+
* @returns {string} preferred type
96+
*/
97+
const preferredType = (ext, type0, type1) => {
98+
const score0 = type0 ? mimeScore(type0, db[type0].source) : 0;
99+
const score1 = type1 ? mimeScore(type1, db[type1].source) : 0;
100+
101+
return score0 > score1 ? type0 : type1;
102+
};
103+
104+
/**
105+
* @param {Record<string, readonly string[]>} extensions extensions
106+
* @param {Record<string, string>} types types
107+
*/
108+
const populate = (extensions, types) => {
109+
for (const type of Object.keys(db)) {
110+
const mime = db[type];
111+
const foundExtensions = mime.extensions;
112+
113+
if (!foundExtensions || foundExtensions.length === 0) {
114+
continue;
115+
}
116+
117+
// mime -> extensions
118+
extensions[type] = foundExtensions;
119+
120+
// extension -> mime
121+
for (let i = 0; i < foundExtensions.length; i++) {
122+
const extension = foundExtensions[i];
123+
124+
types[extension] = preferredType(extension, types[extension], type);
125+
}
126+
}
127+
};
128+
129+
populate(extensions, types);
130+
131+
/**
132+
* Get the default extension for a MIME type.
133+
* @param {string} type type
134+
* @returns {undefined | string} resolve extension
135+
*/
136+
const extension = (type) => {
137+
if (!type) {
138+
return;
139+
}
140+
141+
const match = TYPE_REGEXP.exec(type);
142+
143+
if (!match) {
144+
return;
145+
}
146+
147+
const possibleExtensions = extensions[match[1].toLowerCase()];
148+
149+
if (!possibleExtensions || possibleExtensions.length === 0) {
150+
return;
151+
}
152+
153+
return possibleExtensions[0];
154+
};
155+
156+
/**
157+
* Lookup the MIME type for a file path/extension.
158+
* @param {string} path path
159+
* @returns {undefined | string} resolved MIME type
160+
*/
161+
const lookup = (path) => {
162+
if (!path) {
163+
return;
164+
}
165+
166+
// get the extension ("ext" or ".ext" or full path)
167+
const extension = extname(`x.${path}`).toLowerCase().slice(1);
168+
169+
if (!extension) {
170+
return;
171+
}
172+
173+
return types[extension];
174+
};
175+
176+
module.exports = { extension, lookup };

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
"graceful-fs": "^4.2.11",
105105
"json-parse-even-better-errors": "^2.3.1",
106106
"loader-runner": "^4.3.1",
107-
"mime-types": "^2.1.27",
107+
"mime-db": "^1.54.0",
108108
"neo-async": "^2.6.2",
109109
"schema-utils": "^4.3.3",
110110
"tapable": "^2.3.0",
@@ -121,7 +121,7 @@
121121
"@types/glob-to-regexp": "^0.4.4",
122122
"@types/graceful-fs": "^4.1.9",
123123
"@types/jest": "^30.0.0",
124-
"@types/mime-types": "^2.1.4",
124+
"@types/mime-db": "^1.43.6",
125125
"@types/neo-async": "^2.6.7",
126126
"@types/node": "^25.1.0",
127127
"@types/xxhashjs": "^0.2.4",

test/configCases/asset-modules/global-options/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use strict";
22

3-
const mimeTypes = require("mime-types");
43
const svgToMiniDataURI = require("mini-svg-data-uri");
4+
const mimeTypes = require("../../../../lib/util/mimeTypes");
55

66
/** @type {import("../../../../").Configuration} */
77
module.exports = {

test/configCases/asset-modules/query-and-custom-encoder/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use strict";
22

3-
const mimeTypes = require("mime-types");
43
const svgToMiniDataURI = require("mini-svg-data-uri");
4+
const mimeTypes = require("../../../../lib/util/mimeTypes");
55

66
/** @typedef {import("../../../../").GeneratorOptionsByModuleTypeKnown} GeneratorOptionsByModuleTypeKnown */
77

0 commit comments

Comments
 (0)