From 1405e22f89eed332ae30ffe5cea1d676bac78a2a Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 19 Jan 2025 11:27:19 +0100 Subject: [PATCH 1/6] test(import/mime): add tests --- src/services/import/mime.spec.ts | 146 +++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/services/import/mime.spec.ts diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts new file mode 100644 index 000000000..bc2497ecc --- /dev/null +++ b/src/services/import/mime.spec.ts @@ -0,0 +1,146 @@ +import { describe, it, expect } from "vitest"; +import mimeService from "./mime.js"; + +type TestCase any, W> = [desc: string, fnParams: Parameters, expected: W]; + +describe("#getMime", () => { + // prettier-ignore + const testCases: TestCase[] = [ + [ + "Dockerfile should be handled correctly", + ["Dockerfile"], "text/x-dockerfile" + ], + + [ + "File extension that is defined in EXTENSION_TO_MIME", + ["test.py"], "text/x-python" + ], + + [ + "File extension that is not defined in EXTENSION_TO_MIME should use mimeTypes.lookup", + ["test.zip"], "application/zip" + ], + + [ + "unknown MIME type not recognized by mimeTypes.lookup", + ["test.fake"], false + ], + ]; + + testCases.forEach((testCase) => { + const [testDesc, fnParams, expected] = testCase; + it(`${testDesc}: '${fnParams} should return '${expected}'`, () => { + const actual = mimeService.getMime(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); + +describe("#getType", () => { + // prettier-ignore + const testCases: TestCase[] = [ + [ + "w/ no import options set and mime type empty – it should return 'file'", + [{}, ""], "file" + ], + + [ + "w/ no import options set and non-text or non-code mime type – it should return 'file'", + [{}, "application/zip"], "file" + ], + + [ + "w/ import options set and an image mime type – it should return 'image'", + [{}, "image/jpeg"], "image" + ], + + [ + "w/ image mime type and codeImportedAsCode: true – it should still return 'image'", + [{codeImportedAsCode: true}, "image/jpeg"], "image" + ], + + [ + "w/ image mime type and textImportedAsText: true – it should still return 'image'", + [{textImportedAsText: true}, "image/jpeg"], "image" + ], + + [ + "w/ codeImportedAsCode: true and a mime type that is in CODE_MIME_TYPES – it should return 'code'", + [{codeImportedAsCode: true}, "text/css"], "code" + ], + + [ + "w/ codeImportedAsCode: false and a mime type that is in CODE_MIME_TYPES – it should return 'file' not 'code'", + [{codeImportedAsCode: false}, "text/css"], "file" + ], + + [ + "w/ textImportedAsText: true and 'text/html' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/html"], "text" + ], + + [ + "w/ textImportedAsText: true and 'text/markdown' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/markdown"], "text" + ], + + [ + "w/ textImportedAsText: true and 'text/x-markdown' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/x-markdown"], "text" + ], + + [ + "w/ textImportedAsText: false and 'text/x-markdown' mime type – it should return 'file'", + [{textImportedAsText: false}, "text/x-markdown"], "file" + ], + + [ + "w/ textImportedAsText: false and 'text/html' mime type – it should return 'file'", + [{textImportedAsText: false}, "text/html"], "file" + ], + + ] + + testCases.forEach((testCase) => { + const [desc, fnParams, expected] = testCase; + it(desc, () => { + const actual = mimeService.getType(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); + +describe("#normalizeMimeType", () => { + // prettier-ignore + const testCases: TestCase[] = [ + + [ + "empty mime should return undefined", + [""], undefined + ], + [ + "a mime that's defined in CODE_MIME_TYPES should return the same mime", + ["text/x-python"], "text/x-python" + ], + [ + "a mime (with capitalization inconsistencies) that's defined in CODE_MIME_TYPES should return the same mime in lowercase", + ["text/X-pYthOn"], "text/x-python" + ], + [ + "a mime that's non defined in CODE_MIME_TYPES should return undefined", + ["application/zip"], undefined + ], + [ + "a mime that's defined in CODE_MIME_TYPES with a 'rewrite rule' should return the rewritten mime", + ["text/markdown"], "text/x-markdown" + ] + ]; + + testCases.forEach((testCase) => { + const [desc, fnParams, expected] = testCase; + it(desc, () => { + const actual = mimeService.normalizeMimeType(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); From 815929c3768545c57be9bca6c5fe7831cb1e4d41 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 19 Jan 2025 14:50:59 +0100 Subject: [PATCH 2/6] refactor(import/mime): split CODE_MIME_TYPES Record into two separate objects CODE_MIME_TYPES -> as a Set -> as we only care about the existance of those types CODE_MIME_TYPES_OVERRIDE -> as a Map with those keys and the "overwrite" values as associated value -> this way we don't have to unnecessarily store additional boolean values for everything *but* those hand ful of mime types -> also I've sorted the items alphabetically, while I was at it --- src/services/import/mime.ts | 88 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 10463c242..8fe97a5ea 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -4,45 +4,48 @@ import mimeTypes from "mime-types"; import path from "path"; import type { TaskData } from "../task_context_interface.js"; -const CODE_MIME_TYPES: Record = { - "text/plain": true, - "text/x-csrc": true, - "text/x-c++src": true, - "text/x-csharp": true, - "text/x-clojure": true, - "text/css": true, - "text/x-dockerfile": true, - "text/x-erlang": true, - "text/x-feature": true, - "text/x-go": true, - "text/x-groovy": true, - "text/x-haskell": true, - "text/html": true, - "message/http": true, - "text/x-java": true, - "application/javascript": "application/javascript;env=frontend", - "application/x-javascript": "application/javascript;env=frontend", - "application/json": true, - "text/x-kotlin": true, - "text/x-stex": true, - "text/x-lua": true, +const CODE_MIME_TYPES = new Set([ + "application/json", + "message/http", + "text/css", + "text/html", + "text/plain", + "text/x-clojure", + "text/x-csharp", + "text/x-c++src", + "text/x-csrc", + "text/x-dockerfile", + "text/x-erlang", + "text/x-feature", + "text/x-go", + "text/x-groovy", + "text/x-haskell", + "text/x-java", + "text/x-kotlin", + "text/x-lua", + "text/x-markdown", + "text/xml", + "text/x-objectivec", + "text/x-pascal", + "text/x-perl", + "text/x-php", + "text/x-python", + "text/x-ruby", + "text/x-rustsrc", + "text/x-scala", + "text/x-sh", + "text/x-sql", + "text/x-stex", + "text/x-swift", + "text/x-yaml" +]); + +const CODE_MIME_TYPES_OVERRIDE = new Map([ + ["application/javascript", "application/javascript;env=frontend"], + ["application/x-javascript", "application/javascript;env=frontend"], // possibly later migrate to text/markdown as primary MIME - "text/markdown": "text/x-markdown", - "text/x-markdown": true, - "text/x-objectivec": true, - "text/x-pascal": true, - "text/x-perl": true, - "text/x-php": true, - "text/x-python": true, - "text/x-ruby": true, - "text/x-rustsrc": true, - "text/x-scala": true, - "text/x-sh": true, - "text/x-sql": true, - "text/x-swift": true, - "text/xml": true, - "text/x-yaml": true -}; + ["text/markdown", "text/x-markdown"] +]); // extensions missing in mime-db const EXTENSION_TO_MIME: Record = { @@ -85,7 +88,7 @@ function getType(options: TaskData, mime: string) { if (options.textImportedAsText && (mime === "text/html" || ["text/markdown", "text/x-markdown"].includes(mime))) { return "text"; - } else if (options.codeImportedAsCode && mime in CODE_MIME_TYPES) { + } else if (options.codeImportedAsCode && CODE_MIME_TYPES.has(mime)) { return "code"; } else if (mime.startsWith("image/")) { return "image"; @@ -96,12 +99,11 @@ function getType(options: TaskData, mime: string) { function normalizeMimeType(mime: string) { mime = mime ? mime.toLowerCase() : ""; - const mappedMime = CODE_MIME_TYPES[mime]; - if (mappedMime === true) { + if (CODE_MIME_TYPES.has(mime)) { return mime; - } else if (typeof mappedMime === "string") { - return mappedMime; + } else if (CODE_MIME_TYPES_OVERRIDE.get(mime)) { + return CODE_MIME_TYPES_OVERRIDE.get(mime); } return undefined; From 91ae4b629e7084528c43a6d2f3738866e530c917 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:18:35 +0100 Subject: [PATCH 3/6] refactor(import/mime): simplify normalizeMimeType --- src/services/import/mime.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 8fe97a5ea..5476da78c 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -40,7 +40,7 @@ const CODE_MIME_TYPES = new Set([ "text/x-yaml" ]); -const CODE_MIME_TYPES_OVERRIDE = new Map([ +const CODE_MIME_TYPES_OVERRIDE = new Map([ ["application/javascript", "application/javascript;env=frontend"], ["application/x-javascript", "application/javascript;env=frontend"], // possibly later migrate to text/markdown as primary MIME @@ -98,15 +98,12 @@ function getType(options: TaskData, mime: string) { } function normalizeMimeType(mime: string) { - mime = mime ? mime.toLowerCase() : ""; + const mimeLc = mime.toLowerCase(); - if (CODE_MIME_TYPES.has(mime)) { - return mime; - } else if (CODE_MIME_TYPES_OVERRIDE.get(mime)) { - return CODE_MIME_TYPES_OVERRIDE.get(mime); - } - - return undefined; + //prettier-ignore + return CODE_MIME_TYPES.has(mimeLc) + ? mimeLc + : CODE_MIME_TYPES_OVERRIDE.get(mimeLc); } export default { From 6a0edb68de88b0be543e54bd97a5cd78c2cb7ef9 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:22:31 +0100 Subject: [PATCH 4/6] refactor(import/mime): simplify getType --- src/services/import/mime.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 5476da78c..a1b092556 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -84,16 +84,20 @@ function getMime(fileName: string) { } function getType(options: TaskData, mime: string) { - mime = mime ? mime.toLowerCase() : ""; + const mimeLc = mime?.toLowerCase(); - if (options.textImportedAsText && (mime === "text/html" || ["text/markdown", "text/x-markdown"].includes(mime))) { - return "text"; - } else if (options.codeImportedAsCode && CODE_MIME_TYPES.has(mime)) { - return "code"; - } else if (mime.startsWith("image/")) { - return "image"; - } else { - return "file"; + switch (true) { + case options.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown"].includes(mimeLc): + return "text"; + + case options.codeImportedAsCode && CODE_MIME_TYPES.has(mimeLc): + return "code"; + + case mime.startsWith("image/"): + return "image"; + + default: + return "file"; } } From 4e59f58ce683d2f6edd0e3df1d94aef1c5f8f4a9 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:34:22 +0100 Subject: [PATCH 5/6] refactor(import/mime): simplify getMime --- src/services/import/mime.ts | 51 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index a1b092556..9930c9010 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -48,39 +48,38 @@ const CODE_MIME_TYPES_OVERRIDE = new Map([ ]); // extensions missing in mime-db -const EXTENSION_TO_MIME: Record = { - ".c": "text/x-csrc", - ".cs": "text/x-csharp", - ".clj": "text/x-clojure", - ".erl": "text/x-erlang", - ".hrl": "text/x-erlang", - ".feature": "text/x-feature", - ".go": "text/x-go", - ".groovy": "text/x-groovy", - ".hs": "text/x-haskell", - ".lhs": "text/x-haskell", - ".http": "message/http", - ".kt": "text/x-kotlin", - ".m": "text/x-objectivec", - ".py": "text/x-python", - ".rb": "text/x-ruby", - ".scala": "text/x-scala", - ".swift": "text/x-swift" -}; +const EXTENSION_TO_MIME = new Map([ + [".c", "text/x-csrc"], + [".cs", "text/x-csharp"], + [".clj", "text/x-clojure"], + [".erl", "text/x-erlang"], + [".hrl", "text/x-erlang"], + [".feature", "text/x-feature"], + [".go", "text/x-go"], + [".groovy", "text/x-groovy"], + [".hs", "text/x-haskell"], + [".lhs", "text/x-haskell"], + [".http", "message/http"], + [".kt", "text/x-kotlin"], + [".m", "text/x-objectivec"], + [".py", "text/x-python"], + [".rb", "text/x-ruby"], + [".scala", "text/x-scala"], + [".swift", "text/x-swift"] +]); /** @returns false if MIME is not detected */ function getMime(fileName: string) { - if (fileName.toLowerCase() === "dockerfile") { + const fileNameLc = fileName?.toLowerCase(); + + if (fileNameLc === "dockerfile") { return "text/x-dockerfile"; } - const ext = path.extname(fileName).toLowerCase(); + const ext = path.extname(fileNameLc); + const mimeFromExt = EXTENSION_TO_MIME.get(ext); - if (ext in EXTENSION_TO_MIME) { - return EXTENSION_TO_MIME[ext]; - } - - return mimeTypes.lookup(fileName); + return mimeFromExt || mimeTypes.lookup(fileNameLc); } function getType(options: TaskData, mime: string) { From 4be675c4e126503b755cb4a59bf84f8c84ce9b97 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:34:52 +0100 Subject: [PATCH 6/6] test(import/mime): add additional test case for getMime --- src/services/import/mime.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts index bc2497ecc..acebd94f7 100644 --- a/src/services/import/mime.spec.ts +++ b/src/services/import/mime.spec.ts @@ -16,6 +16,11 @@ describe("#getMime", () => { ["test.py"], "text/x-python" ], + [ + "File extension with inconsisten capitalization that is defined in EXTENSION_TO_MIME", + ["test.gRoOvY"], "text/x-groovy" + ], + [ "File extension that is not defined in EXTENSION_TO_MIME should use mimeTypes.lookup", ["test.zip"], "application/zip"