From a2f66463809c0030e0eee4d1445ee75231690356 Mon Sep 17 00:00:00 2001 From: Loris Sigrist Date: Tue, 3 Oct 2023 12:16:58 +0200 Subject: [PATCH] Standardize File Handling --- src/adapter/svelte/store.js | 6 +- src/formatHandlers/fileHandler.js | 76 ++++++++++++++++++ src/formatHandlers/json/index.js | 1 + src/formatHandlers/types.d.ts | 3 +- src/formatHandlers/yaml/index.js | 1 + src/index.js | 124 +++++++++--------------------- tsconfig.json | 5 +- 7 files changed, 124 insertions(+), 92 deletions(-) create mode 100644 src/formatHandlers/fileHandler.js diff --git a/src/adapter/svelte/store.js b/src/adapter/svelte/store.js index 8c30eec..9fa3d47 100644 --- a/src/adapter/svelte/store.js +++ b/src/adapter/svelte/store.js @@ -215,7 +215,7 @@ locale.subscribe((value) => { t.set(getMessage) }); if(import.meta.hot) { - import.meta.hot.on("t18s:createLocale", async (data)=>{ + import.meta.hot.on("t18s:createLocale", async (data) => { locales.update((locales) => [...locales, data.locale]); //Force-reload the module - Add a random query parameter to bust the cache @@ -227,7 +227,7 @@ if(import.meta.hot) { t.set(getMessage); //update the store }); - import.meta.hot.on("t18s:invalidateLocale", async (data)=>{ + import.meta.hot.on("t18s:invalidateLocale", async (data) => { //Force-reload the module - Add a random query parameter to bust the cache const newMessages = (await import(/* @vite-ignore */ "/@id/__x00__@t18s/messages/" + data.locale + "?" + Math.random())).default; console.info("[t18s] Reloading locale " + data.locale); @@ -236,7 +236,7 @@ if(import.meta.hot) { t.set(getMessage); //update the store }); - import.meta.hot.on("t18s:removeLocale", async (data)=>{ + import.meta.hot.on("t18s:removeLocale", async (data) => { console.info("[t18s] Removing locale " + data.locale); delete messages[data.locale]; diff --git a/src/formatHandlers/fileHandler.js b/src/formatHandlers/fileHandler.js new file mode 100644 index 0000000..03f89e1 --- /dev/null +++ b/src/formatHandlers/fileHandler.js @@ -0,0 +1,76 @@ +import { readFile } from "fs/promises"; +import { basename } from "path"; +import { LoadingException } from "./exception.js"; + +export class FileHandler { + /** @type {import("./types.js").FormatHandler[]} handlers */ + #handlers; + + /** @param {import("./types.js").FormatHandler[]} handlers */ + constructor(handlers) { + this.#handlers = handlers; + } + + /** + * @param {string} filePath Absolute path to the file that needs to be handled + * @param {string} locale The locale for which the file should be handled + * @returns {Promise} A dictionary + * + * @throws {LoadingException} If the file could not be handled + */ + async handle(filePath, locale) { + const handler = this.#getHandler(filePath); + if (!handler) throw new Error(`Could not find handler for ${filePath}`); + const textContent = await this.#readFileContent(filePath); + const dictionary = await handler.load(filePath, textContent, locale); + + return dictionary; + } + + /** + * Resolved which handler should be used for the given file + * @param {string} filePath + * @returns {import("./types.js").FormatHandler | null} + */ + #getHandler(filePath) { + const filename = basename(filePath); + const fileExtension = filename.split(".").at(-1); + if (typeof fileExtension !== "string") + throw new LoadingException( + "Could not determine file extension for ${filePath}", + ); + + const handler = this.#handlers.find((l) => + l.fileExtensions.includes(fileExtension), + ); + + return handler ?? null; + } + + /** + * Reads the raw text content of the given file + * @param {string} filePath + * @returns {Promise} + * + * @throws {LoadingException} If the file could not be read + */ + async #readFileContent(filePath) { + try { + const textContent = await readFile(filePath, "utf-8"); + return textContent; + } catch (e) { + if (!(e instanceof Error)) throw e; + throw new LoadingException(`Failed to read file ${filePath}`, { + cause: e, + }); + } + } + + /** + * Lists the file extensions for which a handler is available + * @returns {string[]} + */ + getSupportedFileExtensions() { + return this.#handlers.flatMap((h) => h.fileExtensions); + } +} diff --git a/src/formatHandlers/json/index.js b/src/formatHandlers/json/index.js index 22becf9..06bbeb5 100644 --- a/src/formatHandlers/json/index.js +++ b/src/formatHandlers/json/index.js @@ -9,6 +9,7 @@ export const JsonHandler = { const parsed = JSON.parse(content); return generateDictionaryFromTree(parsed, locale); } catch (e) { + if (!(e instanceof Error)) throw e; throw new LoadingException( `Could not parse JSON file ${filePath}: ${e.message}`, { cause: e }, diff --git a/src/formatHandlers/types.d.ts b/src/formatHandlers/types.d.ts index 2b51907..f79edf0 100644 --- a/src/formatHandlers/types.d.ts +++ b/src/formatHandlers/types.d.ts @@ -1,4 +1,5 @@ import { Dictionary } from "../types.js"; +import { LoadingException } from "./exception.js"; export type FormatHandler = { fileExtensions: string[]; @@ -8,7 +9,7 @@ export type FormatHandler = { * @param filePath - The path of this file - Only used in Error messages. * @param fileContent - The text content of this file. * @param locale - The locale this file is for. - * @throws {Error} - If the file content is invalid. + * @throws {LoadingException} - If the file content is invalid. * @returns A dictionary. */ load: ( diff --git a/src/formatHandlers/yaml/index.js b/src/formatHandlers/yaml/index.js index 1651275..b620e76 100644 --- a/src/formatHandlers/yaml/index.js +++ b/src/formatHandlers/yaml/index.js @@ -12,6 +12,7 @@ export const YamlHandler = { }); return generateDictionaryFromTree(parsed, locale); } catch (e) { + if (!(e instanceof Error)) throw e; throw new LoadingException( `Could not parse YAML file ${filePath}: ${e.message}`, { cause: e }, diff --git a/src/index.js b/src/index.js index 24bdea2..ca9cfe1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import { basename, resolve } from "path"; -import { readFile, readdir, writeFile } from "fs/promises"; +import { readdir, writeFile } from "fs/promises"; import { YamlHandler } from "./formatHandlers/yaml/index.js"; import { JsonHandler } from "./formatHandlers/json/index.js"; import { Logger } from "./utils/logger.js"; @@ -9,10 +9,9 @@ import { DEFAULT_CONFIG, } from "./constants.js"; import { SvelteStoreAdapter } from "./adapter/svelte/store.js"; +import { FileHandler } from "./formatHandlers/fileHandler.js"; import { LoadingException } from "./formatHandlers/exception.js"; -const HANDLERS = [YamlHandler, JsonHandler]; - /** * @typedef {{ * translationsDir: string, @@ -27,7 +26,8 @@ const HANDLERS = [YamlHandler, JsonHandler]; */ export function t18s(userConfig = {}) { const logger = new Logger(); - const Adapter = new SvelteStoreAdapter(); + const adapter = new SvelteStoreAdapter(); + const fileHandler = new FileHandler([YamlHandler, JsonHandler]); /** @type {import("./types.js").ResolvedPluginConfig} */ let config; @@ -35,75 +35,23 @@ export function t18s(userConfig = {}) { /** @type {import("./types.js").LocaleDictionaries} */ const localeDictionaries = new Map(); - /** - * Gets the correct handler for a file, using it's file extension. - * Logs error messages if no handler could be found. - * - * @param {string} filePath - * @returns {import("./formatHandlers/types.js").FormatHandler | null} - */ - function getHandler(filePath) { - const filename = basename(filePath); - const fileExtension = filename.split(".").at(-1); - - if (!fileExtension) { - logger.error(`Could not determine file extension for ${filePath}`); - return null; - } - - const handler = HANDLERS.find((l) => - l.fileExtensions.includes(fileExtension), - ); - - if (!handler) { - logger.warn( - `Could not find translation handler for .${fileExtension} files. Ignoring file ${filePath}`, - ); - return null; - } - - return handler; - } - /** * Register a new translation file. * @param {string} filePath Absolute path to the file that needs to be invalidated */ async function addTranslationFile(filePath) { - const handler = getHandler(filePath); - if (!handler) return; - - const filename = basename(filePath); - const fileExtension = filename.split(".").at(-1); - const locale = filename.split(".")[0]; + const locale = getLocale(filePath); - if (!fileExtension) { - logger.error(`Could not determine file extension for ${filePath}`); - return; - } - - //Attempt to read the file - let textContent = ""; try { - textContent = await readFile(filePath, "utf-8"); - } catch (e) { - logger.error(`Could not read file ${filePath}`); - return; - } - - /** @type {import("./types.js").Dictionary} */ - let dictionary; - try { - dictionary = await handler.load(filePath, textContent, locale); + const dictionary = await fileHandler.handle(filePath, locale); + localeDictionaries.set(locale, dictionary); } catch (e) { if (!(e instanceof LoadingException)) throw e; logger.error(e.message); } - localeDictionaries.set(locale, dictionary); - await regenerateDTS(); - Adapter.HMRAddLocale(locale); + adapter.HMRAddLocale(locale); } /** @@ -113,52 +61,39 @@ export function t18s(userConfig = {}) { * @param {string} filePath Absolute path to the file that needs to be invalidated */ async function invalidateTranslationFile(filePath) { - const handler = getHandler(filePath); - if (!handler) return; - - const filename = basename(filePath); - const fileExtension = filename.split(".").at(-1); - const locale = filename.split(".")[0]; - - if (!fileExtension) { - logger.error(`Could not determine file extension for ${filePath}`); - return; - } - const textContent = await readFile(filePath, "utf-8"); + const locale = getLocale(filePath); - /** @type {import("./types.js").Dictionary} */ - let dictionary; try { - dictionary = await handler.load(filePath, textContent, locale); + const dictionary = await fileHandler.handle(filePath, locale); + localeDictionaries.set(locale, dictionary); } catch (e) { if (!(e instanceof LoadingException)) throw e; logger.error(e.message); } - localeDictionaries.set(locale, dictionary); - await regenerateDTS(); - Adapter.HMRInvalidateLocale(locale); + adapter.HMRInvalidateLocale(locale); } /** * Remove a _translation_ file. * Assumes the file is in the `translationsDir` directory. - * @param {string} file Absolute path to the translation file that no longer exists + * @param {string} filePath Absolute path to the translation file that no longer exists * @returns void */ - async function removeTranslationFile(file) { - const filename = basename(file); + async function removeTranslationFile(filePath) { + const filename = basename(filePath); const locale = filename.split(".")[0]; + if (!locale) throw new Error("Could not determine locale for ${filePath}"); localeDictionaries.delete(locale); await regenerateDTS(); - Adapter.HMRRemoveLocale(locale); + adapter.HMRRemoveLocale(locale); } async function regenerateDTS() { - const dts = Adapter.getTypeDefinition(localeDictionaries); + const dts = adapter.getTypeDefinition(localeDictionaries); await writeFile(config.dtsPath, dts, { encoding: "utf-8" }); } @@ -186,6 +121,20 @@ export function t18s(userConfig = {}) { */ const isTranslationFile = (path) => path.startsWith(config.translationsDir); + /** + * Resolves the locale a given path belongs to. + * @param {string} path + * @returns {string} + * + * @throws {Error} If the path does not belong to any locale + */ + const getLocale = (path) => { + const filename = basename(path); + const locale = filename.split(".")[0]; + if (!locale) throw new Error("Could not determine locale for ${filePath}"); + return locale; + }; + return { name: "t18s", enforce: "pre", @@ -215,15 +164,16 @@ export function t18s(userConfig = {}) { }, load(id) { - id = id.split("?")[0]; //Remove query parameters + id = id.split("?")[0] ?? ""; //Remove query parameters if (!id.startsWith(RESOLVED_VIRTUAL_MODULE_PREFIX)) return; if (id === RESOLVED_VIRTUAL_MODULE_PREFIX) { - return Adapter.getMainCode(localeDictionaries); + return adapter.getMainCode(localeDictionaries); } const locale = id.split("/")[2]; - return Adapter.getDictionaryCode( + if (!locale) return; + return adapter.getDictionaryCode( localeDictionaries.get(locale) || new Map(), ); }, @@ -244,7 +194,7 @@ export function t18s(userConfig = {}) { await invalidateTranslationFile(path); }); - Adapter.useServer(server); + adapter.useServer(server); }, }; } diff --git a/tsconfig.json b/tsconfig.json index 1244706..72655a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,10 @@ "target": "ESNext", "noEmit": true, "moduleResolution": "NodeNext", - "module": "NodeNext" + "module": "NodeNext", + "noImplicitAny": true, + "noUncheckedIndexedAccess": true, + "strict": true }, "include": ["src"] }