diff --git a/src/services/data_dir.spec.ts b/src/services/data_dir.spec.ts index 37958b185..4c94cbe94 100644 --- a/src/services/data_dir.spec.ts +++ b/src/services/data_dir.spec.ts @@ -77,6 +77,11 @@ describe("data_dir.ts unit tests", async () => { ["w/ darwin it should return '~/Library/Application Support'", ["darwin", undefined], "/Users/mock/Library/Application Support", "/Users/mock"] ]; + beforeEach(() => { + // make sure OS does not set its own process.env.APPDATA, so that we can use our own supplied value + delete process.env.APPDATA; + }); + testCases.forEach((testCase) => { const [testDescription, fnValues, expected, osHomedirMockValue] = testCase; return it(testDescription, () => { diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts new file mode 100644 index 000000000..acebd94f7 --- /dev/null +++ b/src/services/import/mime.spec.ts @@ -0,0 +1,151 @@ +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 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" + ], + + [ + "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); + }); + }); +}); diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 10463c242..9930c9010 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -4,107 +4,109 @@ 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 = { - ".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) { - 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 && mime in CODE_MIME_TYPES) { - 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"; } } function normalizeMimeType(mime: string) { - mime = mime ? mime.toLowerCase() : ""; - const mappedMime = CODE_MIME_TYPES[mime]; + const mimeLc = mime.toLowerCase(); - if (mappedMime === true) { - return mime; - } else if (typeof mappedMime === "string") { - return mappedMime; - } - - return undefined; + //prettier-ignore + return CODE_MIME_TYPES.has(mimeLc) + ? mimeLc + : CODE_MIME_TYPES_OVERRIDE.get(mimeLc); } export default {