chore: improve ui build scripts

This commit is contained in:
Fu Diwei
2026-07-30 14:16:49 +08:00
parent 7c36e6f98f
commit bd6d1266b5
8 changed files with 78 additions and 70 deletions

View File

@@ -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",
},
}
);

View File

@@ -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 },
});
}

View File

@@ -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);
},
};
}

View File

@@ -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";

View File

@@ -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();

View File

@@ -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}`;
};

View File

@@ -10,6 +10,7 @@
"noEmit": true
},
"include": [
"vite.config.ts"
"vite.config.ts",
"scripts"
]
}
}

View File

@@ -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: {