From 9a9fead9abb6f8ec6cec95acba076cbe62f831d8 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:38:23 +0800 Subject: [PATCH 1/2] perf: preprocess markdown transform at build time --- scripts/build-safari-15.js | 3 +- scripts/build-safari-16.4.js | 3 +- scripts/vite-plugin-marked-divest.js | 89 ++++++++++++++++++++++++++++ src/app/i18n.js | 1 - 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 scripts/vite-plugin-marked-divest.js diff --git a/scripts/build-safari-15.js b/scripts/build-safari-15.js index 7fe6c9c1..1466bf15 100644 --- a/scripts/build-safari-15.js +++ b/scripts/build-safari-15.js @@ -16,6 +16,7 @@ import { build } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; +import pluginMarkedDivest from "./vite-plugin-marked-divest.js"; import * as Utils from "./utils.js"; /** @type {import("vite").InlineConfig} */ @@ -36,7 +37,7 @@ const sharedConfig = { */ build({ ...Utils.baseConfig, - plugins: [svelte()], + plugins: [svelte(), pluginMarkedDivest()], build: { ...sharedConfig.build, outDir: `${Utils.APP_SHARED_RESOURCES}/dist/`, diff --git a/scripts/build-safari-16.4.js b/scripts/build-safari-16.4.js index 390b880a..866b844d 100644 --- a/scripts/build-safari-16.4.js +++ b/scripts/build-safari-16.4.js @@ -15,6 +15,7 @@ import { build } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; +import pluginMarkedDivest from "./vite-plugin-marked-divest.js"; import * as Utils from "./utils.js"; /** @type {import("vite").InlineConfig} */ @@ -35,7 +36,7 @@ const sharedConfig = { */ build({ ...Utils.baseConfig, - plugins: [svelte()], + plugins: [svelte(), pluginMarkedDivest()], build: { ...sharedConfig.build, outDir: `${Utils.APP_SHARED_RESOURCES}/dist/`, diff --git a/scripts/vite-plugin-marked-divest.js b/scripts/vite-plugin-marked-divest.js new file mode 100644 index 00000000..b7e35c60 --- /dev/null +++ b/scripts/vite-plugin-marked-divest.js @@ -0,0 +1,89 @@ +import { fileURLToPath } from "node:url"; +import { readFile } from "node:fs/promises"; +import { marked } from "marked"; + +/** + * Exclude files that do not need to be processed + * @param {string} id + * @returns {boolean} + */ +function isExclude(id) { + const res = [/\/src\/app\/_locales\/[A-Za-z0-9_]+\/messages.js/]; + for (const re of res) { + if (re.test(id)) return false; + } + return true; +} + +/** + * Preprocess `marked` calls and `.md?raw` transformations at build time + * to avoid including `marked` modules and runtime transformations + * @returns {import('vite').Plugin} + */ +function preprocessMarked() { + return { + name: "preprocess-marked", + resolveId: { + order: "pre", + handler(source, importer) { + if (isExclude(importer)) return; + if (source.endsWith(".md?raw")) { + const importerUrl = new URL(importer, import.meta.url); + const sourceUrl = new URL(source, importerUrl); + return fileURLToPath(sourceUrl) + "?marked"; + } + }, + }, + load: { + order: "pre", + async handler(id) { + if (!id.endsWith(".md?marked")) return; + const url = new URL(id, import.meta.url); + const text = await readFile(fileURLToPath(url), "utf8"); + const html = await marked.parse(text); + return `export default ${JSON.stringify(html)}`; + }, + }, + transform(code, id) { + if (isExclude(id)) return; + if (!code.includes("marked.parse")) return; + const node = this.parse(code); + if (node.sourceType !== "module") return; + for (const module of node.body) { + if (module.type !== "ImportDeclaration") continue; + if (!module.source.value.toString().endsWith(".md?raw")) continue; + const specifier = module.specifiers.find( + (specifier) => specifier.type === "ImportDefaultSpecifier", + ); + const varName = specifier.local.name; + code = code.replace(`await marked.parse(${varName})`, varName); + // console.debug(`optimizing 'await marked.parse(${varName})'`, id); // DEBUG + } + return { code }; + }, + }; +} + +/** + * Turn off `Marked` module side effects for better tree-shaking + * @returns {import('vite').Plugin} + */ +function disableMarkedSideEffects() { + return { + name: "disable-marked-side-effects", + transform(code, id) { + if (id.includes("/node_modules/marked")) { + return { code, moduleSideEffects: false }; + } + }, + }; +} + +/** + * @returns {import('vite').Plugin[]} + */ +function markedDivest() { + return [disableMarkedSideEffects(), preprocessMarked()]; +} + +export default markedDivest; diff --git a/src/app/i18n.js b/src/app/i18n.js index 8ed34019..e5a42ee5 100644 --- a/src/app/i18n.js +++ b/src/app/i18n.js @@ -14,7 +14,6 @@ function getLangFrom(messages, messageName, substitutions = undefined) { console.warn(`i18n - "${messageName}" not found`, messages); return ""; } - /** @type {string} message */ let text = messages[messageName].message; // handle substitutions if (typeof substitutions === "string") { From 831f67d24c723e30d0b8fddfbca629451f62196c Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:00:04 +0800 Subject: [PATCH 2/2] refactor: revise regular expression escaping --- scripts/vite-plugin-marked-divest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/vite-plugin-marked-divest.js b/scripts/vite-plugin-marked-divest.js index b7e35c60..27093567 100644 --- a/scripts/vite-plugin-marked-divest.js +++ b/scripts/vite-plugin-marked-divest.js @@ -8,7 +8,7 @@ import { marked } from "marked"; * @returns {boolean} */ function isExclude(id) { - const res = [/\/src\/app\/_locales\/[A-Za-z0-9_]+\/messages.js/]; + const res = [/\/src\/app\/_locales\/[A-Za-z0-9_]+\/messages\.js/]; for (const re of res) { if (re.test(id)) return false; }