Skip to content

Commit cf61dd0

Browse files
test: added
1 parent 5e0a193 commit cf61dd0

5 files changed

Lines changed: 219 additions & 29 deletions

File tree

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/util/mimeTypes.js

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,85 @@ const db = require("mime-db");
2121
* DIGIT = %x30-39 ; 0-9
2222
*/
2323
const TYPE_REGEXP =
24-
/^ *([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}) *$/;
24+
/^ *(([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126})) *(?:;.*)?$/;
2525
const extensions = Object.create(null);
2626
const types = Object.create(null);
2727

28-
// source preference (least -> most)
29-
const PREFERENCE = ["nginx", "apache", undefined, "iana"];
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+
};
30103

31104
/**
32105
* @param {Record<string, readonly string[]>} extensions extensions
@@ -35,35 +108,20 @@ const PREFERENCE = ["nginx", "apache", undefined, "iana"];
35108
const populate = (extensions, types) => {
36109
for (const type of Object.keys(db)) {
37110
const mime = db[type];
38-
const exts = mime.extensions;
111+
const foundExtensions = mime.extensions;
39112

40-
if (!exts || !exts.length) {
113+
if (!foundExtensions || foundExtensions.length === 0) {
41114
continue;
42115
}
43116

44117
// mime -> extensions
45-
extensions[type] = exts;
118+
extensions[type] = foundExtensions;
46119

47120
// extension -> mime
48-
for (let i = 0; i < exts.length; i++) {
49-
const extension = exts[i];
50-
51-
if (types[extension]) {
52-
const from = PREFERENCE.indexOf(db[types[extension]].source);
53-
const to = PREFERENCE.indexOf(mime.source);
54-
55-
if (
56-
types[extension] !== "application/octet-stream" &&
57-
(from > to ||
58-
(from === to && types[extension].slice(0, 12) === "application/"))
59-
) {
60-
// skip the remapping
61-
continue;
62-
}
63-
}
64-
65-
// set the extension -> mime
66-
types[extension] = type;
121+
for (let i = 0; i < foundExtensions.length; i++) {
122+
const extension = foundExtensions[i];
123+
124+
types[extension] = preferredType(extension, types[extension], type);
67125
}
68126
}
69127
};
@@ -86,7 +144,7 @@ const extension = (type) => {
86144
return;
87145
}
88146

89-
const possibleExtensions = extensions[match[0].toLowerCase()];
147+
const possibleExtensions = extensions[match[1].toLowerCase()];
90148

91149
if (!possibleExtensions || possibleExtensions.length === 0) {
92150
return;

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

test/mimeTypes.unittest.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"use strict";
2+
3+
const mimeTypes = require("../lib/util/mimeTypes");
4+
5+
describe("mimeTypes", () => {
6+
describe("extension", () => {
7+
it("should return extension for mime type", () => {
8+
expect(mimeTypes.extension("text/html")).toBe("html");
9+
expect(mimeTypes.extension(" text/html")).toBe("html");
10+
expect(mimeTypes.extension("text/html ")).toBe("html");
11+
});
12+
13+
it("should return undefined for unknown type", () => {
14+
expect(mimeTypes.extension("application/x-bogus")).toBeUndefined();
15+
});
16+
17+
it("should return undefined for non-type string", () => {
18+
expect(mimeTypes.extension("bogus")).toBeUndefined();
19+
});
20+
21+
it("should return undefined for non-strings", () => {
22+
expect(mimeTypes.extension(null)).toBeUndefined();
23+
expect(mimeTypes.extension(undefined)).toBeUndefined();
24+
expect(mimeTypes.extension(42)).toBeUndefined();
25+
expect(mimeTypes.extension({})).toBeUndefined();
26+
expect(mimeTypes.extension("")).toBeUndefined();
27+
});
28+
29+
it("should return extension for mime type with parameters", () => {
30+
expect(mimeTypes.extension("text/html;charset=UTF-8")).toBe("html");
31+
expect(mimeTypes.extension("text/HTML; charset=UTF-8")).toBe("html");
32+
expect(mimeTypes.extension("text/html; charset=UTF-8")).toBe("html");
33+
expect(mimeTypes.extension("text/html; charset=UTF-8 ")).toBe("html");
34+
expect(mimeTypes.extension("text/html ; charset=UTF-8")).toBe("html");
35+
});
36+
});
37+
38+
describe("lookup", () => {
39+
it('should return mime type for ".html"', () => {
40+
expect(mimeTypes.lookup(".html")).toBe("text/html");
41+
});
42+
43+
it('should return mime type for ".js"', () => {
44+
expect(mimeTypes.lookup(".js")).toBe("text/javascript");
45+
});
46+
47+
it('should return mime type for ".json"', () => {
48+
expect(mimeTypes.lookup(".json")).toBe("application/json");
49+
});
50+
51+
it('should return mime type for ".rtf"', () => {
52+
expect(mimeTypes.lookup(".rtf")).toBe("application/rtf");
53+
});
54+
55+
it('should return mime type for ".txt"', () => {
56+
expect(mimeTypes.lookup(".txt")).toBe("text/plain");
57+
});
58+
59+
it('should return mime type for ".xml"', () => {
60+
expect(mimeTypes.lookup(".xml")).toBe("application/xml");
61+
});
62+
63+
it('should return mime type for ".mp4"', () => {
64+
expect(mimeTypes.lookup(".mp4")).toBe("video/mp4");
65+
});
66+
67+
it("should work without the leading dot", () => {
68+
expect(mimeTypes.lookup("html")).toBe("text/html");
69+
expect(mimeTypes.lookup("xml")).toBe("application/xml");
70+
});
71+
72+
it("should be case insensitive", () => {
73+
expect(mimeTypes.lookup("HTML")).toBe("text/html");
74+
expect(mimeTypes.lookup(".Xml")).toBe("application/xml");
75+
});
76+
77+
it("should return undefined for unknown extension", () => {
78+
expect(mimeTypes.lookup(".bogus")).toBeUndefined();
79+
expect(mimeTypes.lookup("bogus")).toBeUndefined();
80+
});
81+
82+
it("should return undefined for non-strings", () => {
83+
expect(mimeTypes.lookup(null)).toBeUndefined();
84+
expect(mimeTypes.lookup(undefined)).toBeUndefined();
85+
expect(mimeTypes.lookup(42)).toBeUndefined();
86+
expect(mimeTypes.lookup({})).toBeUndefined();
87+
expect(mimeTypes.lookup("")).toBeUndefined();
88+
});
89+
90+
it("should return mime type for file name", () => {
91+
expect(mimeTypes.lookup("page.html")).toBe("text/html");
92+
});
93+
94+
it("should return mime type for relative path", () => {
95+
expect(mimeTypes.lookup("path/to/page.html")).toBe("text/html");
96+
expect(mimeTypes.lookup("path\\to\\page.html")).toBe("text/html");
97+
});
98+
99+
it("should return mime type for absolute path", () => {
100+
expect(mimeTypes.lookup("/path/to/page.html")).toBe("text/html");
101+
expect(mimeTypes.lookup("C:\\path\\to\\page.html")).toBe("text/html");
102+
});
103+
104+
it("should be case insensitive for path", () => {
105+
expect(mimeTypes.lookup("/path/to/PAGE.HTML")).toBe("text/html");
106+
expect(mimeTypes.lookup("C:\\path\\to\\PAGE.HTML")).toBe("text/html");
107+
});
108+
109+
it("should return undefined for unknown extension for path", () => {
110+
expect(mimeTypes.lookup("/path/to/file.bogus")).toBeUndefined();
111+
});
112+
113+
it("should return undefined for path without extension", () => {
114+
expect(mimeTypes.lookup("/path/to/json")).toBeUndefined();
115+
});
116+
117+
it("should return undefined when extension-less", () => {
118+
expect(mimeTypes.lookup("/path/to/.json")).toBeUndefined();
119+
});
120+
121+
it("should return mime type when there is extension", () => {
122+
expect(mimeTypes.lookup("/path/to/.config.json")).toBe(
123+
"application/json"
124+
);
125+
});
126+
127+
it("should return mime type when there is extension, but no path", () => {
128+
expect(mimeTypes.lookup(".config.json")).toBe("application/json");
129+
});
130+
});
131+
});

0 commit comments

Comments
 (0)