From bd6d1266b50b43e2aac1e80457b7aa36e37e12c8 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 30 Jul 2026 14:16:49 +0800 Subject: [PATCH] chore: improve ui build scripts --- ui/eslint.config.mjs | 34 ++++++++-------- .../eslint/rules}/no-root-relative-url.mjs | 14 ++----- .../vite/plugins/preserve-files-plugin.ts | 40 +++++++++++++++++++ ui/src/pages/login/Login.tsx | 1 - ui/src/repository/_pocketbase.ts | 4 +- ui/src/utils/url.ts | 12 +++--- ui/tsconfig.node.json | 5 ++- ui/vite.config.ts | 38 ++++-------------- 8 files changed, 78 insertions(+), 70 deletions(-) rename ui/{eslint-rules => scripts/eslint/rules}/no-root-relative-url.mjs (66%) create mode 100644 ui/scripts/vite/plugins/preserve-files-plugin.ts diff --git a/ui/eslint.config.mjs b/ui/eslint.config.mjs index ca9573752..f0d32460b 100644 --- a/ui/eslint.config.mjs +++ b/ui/eslint.config.mjs @@ -7,13 +7,7 @@ import reactHooksPlugin from "eslint-plugin-react-hooks"; import reactRefreshPlugin from "eslint-plugin-react-refresh"; import typescriptPlugin from "typescript-eslint"; -import noRootRelativeUrlRule from "./eslint-rules/no-root-relative-url.mjs"; - -const certimatePlugin = { - rules: { - "no-root-relative-url": noRootRelativeUrlRule, - }, -}; +import noRootRelativeUrlRule from "./scripts/eslint/rules/no-root-relative-url.mjs"; /** * @type {import("eslint").Linter.Config[]} @@ -145,17 +139,6 @@ export default defineConfig( }, }, - // Certimate - { - name: "certimate", - plugins: { - certimate: certimatePlugin, - }, - rules: { - "certimate/no-root-relative-url": "error", - }, - }, - // TailwindCSS { name: "tailwindcss", @@ -174,5 +157,20 @@ export default defineConfig( entryPoint: "src/global.css", }, }, + }, + + // Certimate + { + name: "certimate", + plugins: { + certimate: { + rules: { + "no-root-relative-url": noRootRelativeUrlRule, + }, + }, + }, + rules: { + "certimate/no-root-relative-url": "error", + }, } ); diff --git a/ui/eslint-rules/no-root-relative-url.mjs b/ui/scripts/eslint/rules/no-root-relative-url.mjs similarity index 66% rename from ui/eslint-rules/no-root-relative-url.mjs rename to ui/scripts/eslint/rules/no-root-relative-url.mjs index 15c783112..74f4e36bf 100644 --- a/ui/eslint-rules/no-root-relative-url.mjs +++ b/ui/scripts/eslint/rules/no-root-relative-url.mjs @@ -1,4 +1,4 @@ -const TARGET_ATTRIBUTES = new Set(["href", "src"]); +const targetAttrs = ["href", "src"]; const getStaticPrefix = (node) => { if (!node) { @@ -21,27 +21,21 @@ const getStaticPrefix = (node) => { export default { meta: { type: "problem", - docs: { - description: "Disallow root-relative URLs in JSX href and src attributes", - }, - messages: { - useBasePath: "Root-relative {{attribute}} bypasses the application base path. Wrap the URL with withBasePath().", - }, schema: [], }, create(context) { return { JSXAttribute(node) { const attribute = node.name.type === "JSXIdentifier" ? node.name.name : ""; - if (!TARGET_ATTRIBUTES.has(attribute)) { + if (!targetAttrs.includes(attribute)) { return; } const value = node.value?.type === "JSXExpressionContainer" ? getStaticPrefix(node.value.expression) : getStaticPrefix(node.value); - if (value?.startsWith("/") && !value.startsWith("//")) { + if (!!value && value.startsWith("/") && !value.startsWith("//")) { context.report({ node: node.value ?? node, - messageId: "useBasePath", + message: "Root-relative {{attribute}} bypasses the application base path. Wrap the URL with `withBasePath()` utility function.", data: { attribute }, }); } diff --git a/ui/scripts/vite/plugins/preserve-files-plugin.ts b/ui/scripts/vite/plugins/preserve-files-plugin.ts new file mode 100644 index 000000000..01cc94f87 --- /dev/null +++ b/ui/scripts/vite/plugins/preserve-files-plugin.ts @@ -0,0 +1,40 @@ +import path from "node:path"; + +import fs from "fs-extra"; +import { type Plugin } from "vite"; + +interface Options { + files: string[]; +} + +export default function (options: Options): Plugin { + const tmpdir = path.resolve("./", "node_modules/.vite/certimate-temp"); + + return { + name: "preserve-files", + apply: "build", + buildStart() { + // 在构建开始时将要保留的文件或目录移动到临时位置 + options.files.forEach((file) => { + const src = path.resolve("./", file); + const dist = path.resolve(tmpdir, file); + if (fs.existsSync(src)) { + fs.moveSync(src, dist, { overwrite: true }); + } + }); + }, + closeBundle() { + // 在构建完成后将临时位置的文件或目录移回原来的位置 + options.files.forEach((file) => { + const src = path.resolve("./", file); + const dist = path.resolve(tmpdir, file); + if (fs.existsSync(dist)) { + fs.moveSync(dist, src, { overwrite: true }); + } + }); + + // 清理临时目录 + fs.emptyDirSync(tmpdir); + }, + }; +} diff --git a/ui/src/pages/login/Login.tsx b/ui/src/pages/login/Login.tsx index 80225927a..84f230826 100644 --- a/ui/src/pages/login/Login.tsx +++ b/ui/src/pages/login/Login.tsx @@ -11,7 +11,6 @@ import AppLocale from "@/components/AppLocale"; import AppTheme from "@/components/AppTheme"; import AppVersion from "@/components/AppVersion"; import { useAntdForm, useBrowserTheme } from "@/hooks"; - import { authWithPassword } from "@/repository/admin"; import { unwrapErrMsg } from "@/utils/error"; import { withBasePath } from "@/utils/url"; diff --git a/ui/src/repository/_pocketbase.ts b/ui/src/repository/_pocketbase.ts index 6b4a46f98..ee4d9cea8 100644 --- a/ui/src/repository/_pocketbase.ts +++ b/ui/src/repository/_pocketbase.ts @@ -1,11 +1,11 @@ import PocketBase from "pocketbase"; -import { APP_BASE_PATH } from "@/utils/url"; +import { getBasePath } from "@/utils/url"; let pb: PocketBase; export const getPocketBase = () => { if (pb) return pb; - pb = new PocketBase(APP_BASE_PATH); + pb = new PocketBase(getBasePath()); pb.afterSend = (res, data) => { if ((res.status === 401 || res.status === 403) && pb.authStore?.isValid) { pb.authStore.clear(); diff --git a/ui/src/utils/url.ts b/ui/src/utils/url.ts index ba8cbc7cd..3b8d7617c 100644 --- a/ui/src/utils/url.ts +++ b/ui/src/utils/url.ts @@ -1,15 +1,15 @@ -const appBaseUrl = new URL(import.meta.env.BASE_URL, document.baseURI); -const urlSchemePattern = /^[a-z][a-z\d+.-]*:/i; +const baseUrl = new URL(import.meta.env.BASE_URL, document.baseURI); -// PocketBase 会把自身的 /api 路径拼接到这里,因此基础路径必须以斜杠结尾。 -export const APP_BASE_PATH = appBaseUrl.pathname; +export const getBasePath = () => { + return baseUrl.pathname; +}; export const withBasePath = (path: string) => { // 完整 URL 和协议相对 URL 不属于应用内资源,保持调用方传入的地址不变。 - if (urlSchemePattern.test(path) || path.startsWith("//")) { + if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//")) { return path; } - const url = new URL(path.replace(/^\/+/, ""), appBaseUrl); + const url = new URL(path.replace(/^\/+/, ""), baseUrl); return `${url.pathname}${url.search}${url.hash}`; }; diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json index 387a89e79..b88ec43a9 100644 --- a/ui/tsconfig.node.json +++ b/ui/tsconfig.node.json @@ -10,6 +10,7 @@ "noEmit": true }, "include": [ - "vite.config.ts" + "vite.config.ts", + "scripts" ] -} +} \ No newline at end of file diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 6acf6cbf1..0bd1b70fa 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -4,34 +4,9 @@ import tailwindcssPlugin from "@tailwindcss/vite"; import legacyPlugin from "@vitejs/plugin-legacy"; import reactPlugin from "@vitejs/plugin-react"; import fs from "fs-extra"; -import { type Plugin, defineConfig } from "vite"; +import { defineConfig } from "vite"; -const preserveFilesPlugin = (filesToPreserve: string[]): Plugin => { - return { - name: "preserve-files", - apply: "build", - buildStart() { - // 在构建开始时将要保留的文件或目录移动到临时位置 - filesToPreserve.forEach((file) => { - const srcPath = path.resolve(__dirname, file); - const tempPath = path.resolve(__dirname, `node_modules/.tmp/build/${file}`); - if (fs.existsSync(srcPath)) { - fs.moveSync(srcPath, tempPath, { overwrite: true }); - } - }); - }, - closeBundle() { - // 在构建完成后将临时位置的文件或目录移回原来的位置 - filesToPreserve.forEach((file) => { - const srcPath = path.resolve(__dirname, file); - const tempPath = path.resolve(__dirname, `node_modules/.tmp/build/${file}`); - if (fs.existsSync(tempPath)) { - fs.moveSync(tempPath, srcPath, { overwrite: true }); - } - }); - }, - }; -}; +import preserveFilesPlugin from "./scripts/vite/plugins/preserve-files-plugin"; export default defineConfig(({ command }) => { let appVersion = undefined; @@ -45,14 +20,13 @@ export default defineConfig(({ command }) => { } console.info("[certimate] AppVersion is " + appVersion); } else { - throw new Error("AppVersion not found in '/internal/app/app.go'"); + throw new Error("`AppVersion` not found in '/internal/app/app.go'"); } } catch (err) { throw new Error("Could not read app version: " + (err as Error).message); } return { - // 使用相对基础路径,让同一份构建产物可以部署在根路径或任意反向代理子路径下。 base: "./", define: { __APP_VERSION__: JSON.stringify(appVersion), @@ -78,8 +52,10 @@ export default defineConfig(({ command }) => { renderLegacyChunks: false, renderModernChunks: true, }), - tailwindcssPlugin(), - preserveFilesPlugin(["dist/.gitkeep"]), + tailwindcssPlugin({}), + preserveFilesPlugin({ + files: ["dist/.gitkeep"], + }), ], resolve: { alias: {