theme: 切 A 类免插件接入(YschTheme 方案 §11),删 vendored vite 插件
build / build (push) Successful in 4m19s

- 接入点收敛为 index.html <head> 内一段同步 loader(env-config 引导之后、title/入口之前),
  document.write 保 parser-blocking;通道 init 已吸收 FOUC 门 + manifest 预取 + DCL 挂 <link>
  + href 去重交棒,主题逻辑升级全走 /static,不再需要重新构建本应用
- 域名四档:staticHost > baseHost > VITE_THEME_HOST(dev 跨源直连专用,需通道 CORS)> 同源相对。
  不复用 VITE_BASE_HOST:它与 SSO/API 共用、.env.dev 里本就非空,复用会让 dev 立刻跨源被 CORS 拦;
  build 产物不读 .env,占位符留字面量 → 置空 → 同源(产物零环境值烧入,与 2f423c2 口径一致)
- 删 plugins/vite-plugin-theme(-420 行 vendor)+ package.json/lock 的 file: 依赖,
  根治 CI 容器解析不到 file:../../YschTheme 那类 npm ci 秒失败
- vite.config:保留 dev /static 代理(联调便利,changeOrigin 顺带改写 Host,等价旧插件的
  headers.Host 补偿),注释端口纠错 5174 → 9527;theme.ts 同步到通道版本 20260929-1112-1b1724d
- 已验:npm run build 通过,dist/index.html 注入段存活且无 B 类痕迹,
  YschTheme npm run smoke 断言 3 张主题表按序挂载、FOUC 门先于首帧、manifest 只拉一次
This commit is contained in:
chenxing
2026-09-29 11:38:31 +08:00
parent 29c051350b
commit 05804f41a9
11 changed files with 37 additions and 443 deletions
+22
View File
@@ -29,6 +29,28 @@
} }
})(); })();
</script> </script>
<!-- 主题通道引导(A 类免插件接入,方案 §11):唯一接入点,不装依赖、不改 vite.config 也能吃主题。
origin 零写死,优先级四档:
__APP_CONFIG__.staticHost(专用通道覆盖,projman env-config 在线配)
> __APP_CONFIG__.baseHost(换域名改配置即生效)
> %VITE_THEME_HOST%(仅 dev 注入:主题通道专用域,需通道已开 CORS)
> 同源相对 "/static/…"(生产常态:通道与应用同域;本地 dev 由 vite /static 代理兜到通道)
为何不直接复用 .env.dev 的 VITE_BASE_HOST:那会让 dev 跨源直连线上域,而通道 Caddy 至今没有
Access-Control-Allow-Origin(方案 §10.4 现网无该头)→ manifest 是 fetch 跨源,会被拦成降级。
运维加上那行头之后,在 .env.dev 补 VITE_THEME_HOST=http://base.yunhaixingtong.com 即切直连,零改码。
document.write 保证 parser-blocking:init 的档位挂载与 FOUC 门先于首帧(同旧插件 initBootstrap 机制,
且 vite build 不解析内联脚本里的字符串,规避 "/static/…" 被当本仓资源解析报错;build 产物不读
.env.*,占位符原样留字面量 → 判首字符 % 置空,产物零环境值烧入)。
挂载/FOUC/CSS 排序全部由通道 init 脚本承担,逻辑升级全走 /static 通道,无需重新构建本应用 -->
<script>
(function () {
var c = window.__APP_CONFIG__ || {};
var themeHost = "%VITE_THEME_HOST%";
if (themeHost.charAt(0) === "%") themeHost = "";
var host = String(c.staticHost || c.baseHost || themeHost).replace(/\/+$/, "");
document.write('<script src="' + host + '/static/js/yh-theme-init.js"><\/script>');
})();
</script>
<title><%- title %></title> <title><%- title %></title>
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" /> <link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
<script src="./libs/jquery.min.js"></script> <script src="./libs/jquery.min.js"></script>
-14
View File
@@ -33,7 +33,6 @@
"@unocss/preset-rem-to-px": "^66.6.0", "@unocss/preset-rem-to-px": "^66.6.0",
"@unocss/preset-uno": "^66.6.0", "@unocss/preset-uno": "^66.6.0",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@yunhai/vite-plugin-theme": "file:./plugins/vite-plugin-theme",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"unocss": "^66.6.0", "unocss": "^66.6.0",
@@ -3002,10 +3001,6 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/@yunhai/vite-plugin-theme": {
"resolved": "plugins/vite-plugin-theme",
"link": true
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.18.0", "version": "8.18.0",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz",
@@ -12056,15 +12051,6 @@
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD" "license": "0BSD"
},
"plugins/vite-plugin-theme": {
"name": "@yunhai/vite-plugin-theme",
"version": "0.1.0",
"dev": true,
"license": "UNLICENSED",
"peerDependencies": {
"vite": ">=4.0.0"
}
} }
} }
} }
-1
View File
@@ -40,7 +40,6 @@
"@unocss/preset-rem-to-px": "^66.6.0", "@unocss/preset-rem-to-px": "^66.6.0",
"@unocss/preset-uno": "^66.6.0", "@unocss/preset-uno": "^66.6.0",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@yunhai/vite-plugin-theme": "file:./plugins/vite-plugin-theme",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"unocss": "^66.6.0", "unocss": "^66.6.0",
-166
View File
@@ -1,166 +0,0 @@
'use strict';
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
DEFAULT_PROXY_TARGET: () => DEFAULT_PROXY_TARGET,
FALLBACK_STYLE: () => FALLBACK_STYLE,
MANIFEST: () => MANIFEST,
RESOLVED_VIRTUAL_ID: () => RESOLVED_VIRTUAL_ID,
VIRTUAL_ID: () => VIRTUAL_ID,
default: () => src_default,
initBootstrap: () => initBootstrap,
isValidStaticBase: () => isValidStaticBase,
loaderSource: () => loaderSource,
yhTheme: () => yhTheme
});
module.exports = __toCommonJS(src_exports);
var DEFAULT_PROXY_TARGET = "http://base.yunhaixingtong.com";
var VIRTUAL_ID = "virtual:yh-theme-loader";
var RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_ID}`;
var MANIFEST = "manifest.json";
var FALLBACK_STYLE = `html:not(.yh-theme-ready) body { opacity: 0; }
html.yh-theme-ready body { opacity: 1; transition: opacity .15s; }`;
function isValidStaticBase(v) {
return v === "" || /^\//.test(v) || /^https?:\/\//.test(v);
}
function loaderSource(staticBase) {
return `// BASE\uFF1A\u6839\u76F8\u5BF9\uFF08\u4E09\u7AEF\u540C\u6E90\uFF09\u3002\u7981\u6B62\u7528 BASE_URL \u4E4B\u7C7B\u7684\u6784\u5EFA\u671F\u76F8\u5BF9\u57FA\u51C6\u2014\u2014
// Notice \u7684 vite base \u662F './'\uFF0C\u4F1A\u89E3\u6790\u6210 /notice/front/static/... \u5BFC\u81F4 404
const BASE = ${JSON.stringify(staticBase)} || (window.__APP_CONFIG__ && window.__APP_CONFIG__.staticHost != null ? window.__APP_CONFIG__.staticHost : '')
const markReady = () => document.documentElement.classList.add('yh-theme-ready')
const markFailed = (f) => {
console.warn(\`[yh-theme] \${f} \u52A0\u8F7D\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u7EC4\u4EF6\u5E93\u9ED8\u8BA4\u89C2\u611F\`)
document.documentElement.classList.add('yh-theme-failed', 'yh-theme-ready')
}
// \u4F18\u5148\u590D\u7528 init \u811A\u672C\u5728 <head> \u91CC\u5DF2\u542F\u52A8\u7684 manifest \u9884\u53D6\uFF08\u542B\u5E26\u7248\u672C preload\uFF09
;(window.__YH_THEME_MANIFEST__ ?? fetch(\`\${BASE}/static/${MANIFEST}\`).then(r => r.json()))
.then((m) => {
let loaded = 0
m.files.forEach((f) => {
const l = document.createElement('link')
l.rel = 'stylesheet'
l.href = \`\${BASE}/static/\${f}?v=\${m.version}\` // \u4E0E init preload \u7684 URL \u4E00\u5B57\u4E0D\u5DEE
l.onload = () => { if (++loaded === m.files.length) markReady() }
l.onerror = () => markFailed(f)
document.head.appendChild(l)
})
setTimeout(markReady, 500) // \u515C\u5E95\uFF1A500ms \u672A load \u5B8C\u4E5F\u653E\u884C
})
.catch(() => markFailed('manifest'))
`;
}
function initBootstrap(staticBase) {
const url = `${staticBase.replace(/\/+$/, "")}/static/js/yh-theme-init.js`;
const setBase = staticBase ? `window.__YH_THEME_STATIC_BASE__=${JSON.stringify(staticBase)};` : "";
return `(function(){${setBase}document.write('<script src="${url}"><\\/script>');})();`;
}
function yhTheme(options = {}) {
const target = options.target ?? "pc";
const staticBase = options.staticBase ?? "";
const proxyTarget = options.proxyTarget ?? DEFAULT_PROXY_TARGET;
if (target !== "pc") {
throw new Error(`[yh-theme] target='${target}' \u7B2C\u4E00\u6CE2\u672A\u5B9E\u73B0\uFF08app \u6CE2\u6B21\u518D\u8865 manifest.app.json \u4E0E app \u4EA7\u7269\uFF09`);
}
if (!isValidStaticBase(staticBase)) {
throw new Error(
`[yh-theme] staticBase \u53EA\u63A5\u53D7 '' / '/xxx' \u6839\u76F8\u5BF9\u524D\u7F00 / 'https?://\u2026'\uFF0C\u6536\u5230 '${staticBase}'\u3002\u4E0D\u8981\u628A vite base \u4F20\u8FDB\u6765\uFF08Notice \u7684 base \u662F './'\uFF0C\u4F1A\u89E3\u6790\u6210 /notice/front/static/... 404\uFF09`
);
}
const entryRe = options.entry ?? /(^|\/)src\/main\.(ts|js)$/;
const proxyFromEnv = () => {
var _a;
const env = (_a = globalThis.process) == null ? void 0 : _a.env;
const v = env == null ? void 0 : env.YH_THEME_PROXY_TARGET;
return v && v.trim() ? v.trim() : void 0;
};
return {
name: "@yunhai/vite-plugin-theme",
/**
* proxy 在 config 钩子里合并:Vite 的 proxy 中间件在 configureServer 之前就已建立,
* 到 configureServer 里再改 server.config.server.proxy 不会生效(B4 的语义在这里落地)。
*/
config(userConfig) {
var _a;
const url = proxyFromEnv() ?? proxyTarget;
const existing = ((_a = userConfig.server) == null ? void 0 : _a.proxy) ?? {};
if (existing["/static"]) return {};
const patch = {
proxy: {
...existing,
"/static": { target: url, changeOrigin: true }
}
};
if (!proxyFromEnv() && url !== DEFAULT_PROXY_TARGET) {
patch.proxy["/static"] = {
target: url,
changeOrigin: true,
headers: { Host: DEFAULT_PROXY_TARGET.replace(/^https?:\/\//, "") }
};
}
return { server: patch };
},
resolveId(id) {
return id === VIRTUAL_ID ? RESOLVED_VIRTUAL_ID : null;
},
load(id) {
return id === RESOLVED_VIRTUAL_ID ? loaderSource(staticBase) : null;
},
transform(code, id) {
const cleanId = id.split("?")[0].replace(/\\/g, "/");
if (!entryRe.test(cleanId)) return null;
if (code.includes(VIRTUAL_ID)) return null;
return { code: `${code}
import "${VIRTUAL_ID}";
`, map: null };
},
transformIndexHtml: {
order: "post",
handler() {
return [
{ tag: "style", children: FALLBACK_STYLE, injectTo: "head" },
{ tag: "script", children: initBootstrap(staticBase), injectTo: "head" }
];
}
},
configureServer(server) {
var _a;
const proxy = ((_a = server.config.server) == null ? void 0 : _a.proxy) ?? {};
const target0 = proxy["/static"];
server.config.logger.info(
`[yh-theme] dev /static \u2192 ${(target0 == null ? void 0 : target0.target) ?? "(\u672A\u6302\uFF1A\u5E94\u7528\u4FA7\u5DF2\u81EA\u5E26\u540C\u540D proxy\uFF1F)"}${proxyFromEnv() ? `\uFF08YH_THEME_PROXY_TARGET=${proxyFromEnv()}\uFF09` : ""}`
);
}
};
}
var src_default = yhTheme;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
DEFAULT_PROXY_TARGET,
FALLBACK_STYLE,
MANIFEST,
RESOLVED_VIRTUAL_ID,
VIRTUAL_ID,
initBootstrap,
isValidStaticBase,
loaderSource,
yhTheme
});
-44
View File
@@ -1,44 +0,0 @@
import { Plugin } from 'vite';
/**
* @yunhai/vite-plugin-theme — 云海管理端远程主题接入插件。
*
* 接入面收敛成一行:plugins: [vue(), yhTheme()]。插件负责四件事:
* 1. transformIndexHtml:<head> 内注入 yh-theme-init.js(在 env-config bootstrap 之后)+ 首帧兜底 inline style;
* 刻意不静态注入 preload——构建期 URL 没有 ?v=,与 loader 运行时版本化 URL 永不匹配(缓存键含 query)。
* 2. virtual:yh-theme-loader:入口末位运行时 append <link>,保证远程 CSS 排在 viewuiplus.css 之后(dev/build 同构)。
* 3. transform:自动在入口末位 import 该虚拟模块,应用侧不用手改 main.ts。
* 4. /static dev proxy:本地联调不必装内网 DNS;环境变量 YH_THEME_PROXY_TARGET 可指向模拟通道。
*/
interface YhThemeOptions {
/** 产物档:pc 为第一波实现,app 占位 */
target?: "pc" | "app";
/** 静态通道前缀:'' 同源根相对(默认)| '/xxx' | 'http(s)://…'。禁止传 vite base(如 './') */
staticBase?: string;
/** dev /static proxy 目标;环境变量 YH_THEME_PROXY_TARGET 优先 */
proxyTarget?: string;
/** 入口匹配(相对路径正则),默认 src/main.(ts|js) */
entry?: RegExp;
}
declare const DEFAULT_PROXY_TARGET = "http://base.yunhaixingtong.com";
declare const VIRTUAL_ID = "virtual:yh-theme-loader";
declare const RESOLVED_VIRTUAL_ID = "\0virtual:yh-theme-loader";
declare const MANIFEST = "manifest.json";
declare const FALLBACK_STYLE = "html:not(.yh-theme-ready) body { opacity: 0; }\nhtml.yh-theme-ready body { opacity: 1; transition: opacity .15s; }";
/** staticBase 只接受根相对绝对前缀或完整 URL:vite base(Notice 是 './')会让 /static 解析成 /notice/front/static/… */
declare function isValidStaticBase(v: string): boolean;
/**
* loader 运行时实现 —— 基准为《第一波实施清单》§B3 代码块,语义一字不改。
* 复用 init 已启动的 manifest 预取;URL 与 init 的带版本 preload 一字不差;500ms 兜底;失败挂双类。
*/
declare function loaderSource(staticBase: string): string;
/**
* init 用 document.write 挂:直接写 <script src="/static/…"> 会让 vite:build-html 把这条绝对路径
* 当本仓资源解析(构建报 "Failed to resolve"),而远程通道的文件根本不在应用构建图里。
* document.write 在 <head> 内是 parser-blocking 的,恰好保住 init 必须先于首帧执行的语义。
*/
declare function initBootstrap(staticBase: string): string;
declare function yhTheme(options?: YhThemeOptions): Plugin;
export { DEFAULT_PROXY_TARGET, FALLBACK_STYLE, MANIFEST, RESOLVED_VIRTUAL_ID, VIRTUAL_ID, type YhThemeOptions, yhTheme as default, initBootstrap, isValidStaticBase, loaderSource, yhTheme };
-44
View File
@@ -1,44 +0,0 @@
import { Plugin } from 'vite';
/**
* @yunhai/vite-plugin-theme — 云海管理端远程主题接入插件。
*
* 接入面收敛成一行:plugins: [vue(), yhTheme()]。插件负责四件事:
* 1. transformIndexHtml:<head> 内注入 yh-theme-init.js(在 env-config bootstrap 之后)+ 首帧兜底 inline style;
* 刻意不静态注入 preload——构建期 URL 没有 ?v=,与 loader 运行时版本化 URL 永不匹配(缓存键含 query)。
* 2. virtual:yh-theme-loader:入口末位运行时 append <link>,保证远程 CSS 排在 viewuiplus.css 之后(dev/build 同构)。
* 3. transform:自动在入口末位 import 该虚拟模块,应用侧不用手改 main.ts。
* 4. /static dev proxy:本地联调不必装内网 DNS;环境变量 YH_THEME_PROXY_TARGET 可指向模拟通道。
*/
interface YhThemeOptions {
/** 产物档:pc 为第一波实现,app 占位 */
target?: "pc" | "app";
/** 静态通道前缀:'' 同源根相对(默认)| '/xxx' | 'http(s)://…'。禁止传 vite base(如 './') */
staticBase?: string;
/** dev /static proxy 目标;环境变量 YH_THEME_PROXY_TARGET 优先 */
proxyTarget?: string;
/** 入口匹配(相对路径正则),默认 src/main.(ts|js) */
entry?: RegExp;
}
declare const DEFAULT_PROXY_TARGET = "http://base.yunhaixingtong.com";
declare const VIRTUAL_ID = "virtual:yh-theme-loader";
declare const RESOLVED_VIRTUAL_ID = "\0virtual:yh-theme-loader";
declare const MANIFEST = "manifest.json";
declare const FALLBACK_STYLE = "html:not(.yh-theme-ready) body { opacity: 0; }\nhtml.yh-theme-ready body { opacity: 1; transition: opacity .15s; }";
/** staticBase 只接受根相对绝对前缀或完整 URL:vite base(Notice 是 './')会让 /static 解析成 /notice/front/static/… */
declare function isValidStaticBase(v: string): boolean;
/**
* loader 运行时实现 —— 基准为《第一波实施清单》§B3 代码块,语义一字不改。
* 复用 init 已启动的 manifest 预取;URL 与 init 的带版本 preload 一字不差;500ms 兜底;失败挂双类。
*/
declare function loaderSource(staticBase: string): string;
/**
* init 用 document.write 挂:直接写 <script src="/static/…"> 会让 vite:build-html 把这条绝对路径
* 当本仓资源解析(构建报 "Failed to resolve"),而远程通道的文件根本不在应用构建图里。
* document.write 在 <head> 内是 parser-blocking 的,恰好保住 init 必须先于首帧执行的语义。
*/
declare function initBootstrap(staticBase: string): string;
declare function yhTheme(options?: YhThemeOptions): Plugin;
export { DEFAULT_PROXY_TARGET, FALLBACK_STYLE, MANIFEST, RESOLVED_VIRTUAL_ID, VIRTUAL_ID, type YhThemeOptions, yhTheme as default, initBootstrap, isValidStaticBase, loaderSource, yhTheme };
-132
View File
@@ -1,132 +0,0 @@
// src/index.ts
var DEFAULT_PROXY_TARGET = "http://base.yunhaixingtong.com";
var VIRTUAL_ID = "virtual:yh-theme-loader";
var RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_ID}`;
var MANIFEST = "manifest.json";
var FALLBACK_STYLE = `html:not(.yh-theme-ready) body { opacity: 0; }
html.yh-theme-ready body { opacity: 1; transition: opacity .15s; }`;
function isValidStaticBase(v) {
return v === "" || /^\//.test(v) || /^https?:\/\//.test(v);
}
function loaderSource(staticBase) {
return `// BASE\uFF1A\u6839\u76F8\u5BF9\uFF08\u4E09\u7AEF\u540C\u6E90\uFF09\u3002\u7981\u6B62\u7528 BASE_URL \u4E4B\u7C7B\u7684\u6784\u5EFA\u671F\u76F8\u5BF9\u57FA\u51C6\u2014\u2014
// Notice \u7684 vite base \u662F './'\uFF0C\u4F1A\u89E3\u6790\u6210 /notice/front/static/... \u5BFC\u81F4 404
const BASE = ${JSON.stringify(staticBase)} || (window.__APP_CONFIG__ && window.__APP_CONFIG__.staticHost != null ? window.__APP_CONFIG__.staticHost : '')
const markReady = () => document.documentElement.classList.add('yh-theme-ready')
const markFailed = (f) => {
console.warn(\`[yh-theme] \${f} \u52A0\u8F7D\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u7EC4\u4EF6\u5E93\u9ED8\u8BA4\u89C2\u611F\`)
document.documentElement.classList.add('yh-theme-failed', 'yh-theme-ready')
}
// \u4F18\u5148\u590D\u7528 init \u811A\u672C\u5728 <head> \u91CC\u5DF2\u542F\u52A8\u7684 manifest \u9884\u53D6\uFF08\u542B\u5E26\u7248\u672C preload\uFF09
;(window.__YH_THEME_MANIFEST__ ?? fetch(\`\${BASE}/static/${MANIFEST}\`).then(r => r.json()))
.then((m) => {
let loaded = 0
m.files.forEach((f) => {
const l = document.createElement('link')
l.rel = 'stylesheet'
l.href = \`\${BASE}/static/\${f}?v=\${m.version}\` // \u4E0E init preload \u7684 URL \u4E00\u5B57\u4E0D\u5DEE
l.onload = () => { if (++loaded === m.files.length) markReady() }
l.onerror = () => markFailed(f)
document.head.appendChild(l)
})
setTimeout(markReady, 500) // \u515C\u5E95\uFF1A500ms \u672A load \u5B8C\u4E5F\u653E\u884C
})
.catch(() => markFailed('manifest'))
`;
}
function initBootstrap(staticBase) {
const url = `${staticBase.replace(/\/+$/, "")}/static/js/yh-theme-init.js`;
const setBase = staticBase ? `window.__YH_THEME_STATIC_BASE__=${JSON.stringify(staticBase)};` : "";
return `(function(){${setBase}document.write('<script src="${url}"><\\/script>');})();`;
}
function yhTheme(options = {}) {
const target = options.target ?? "pc";
const staticBase = options.staticBase ?? "";
const proxyTarget = options.proxyTarget ?? DEFAULT_PROXY_TARGET;
if (target !== "pc") {
throw new Error(`[yh-theme] target='${target}' \u7B2C\u4E00\u6CE2\u672A\u5B9E\u73B0\uFF08app \u6CE2\u6B21\u518D\u8865 manifest.app.json \u4E0E app \u4EA7\u7269\uFF09`);
}
if (!isValidStaticBase(staticBase)) {
throw new Error(
`[yh-theme] staticBase \u53EA\u63A5\u53D7 '' / '/xxx' \u6839\u76F8\u5BF9\u524D\u7F00 / 'https?://\u2026'\uFF0C\u6536\u5230 '${staticBase}'\u3002\u4E0D\u8981\u628A vite base \u4F20\u8FDB\u6765\uFF08Notice \u7684 base \u662F './'\uFF0C\u4F1A\u89E3\u6790\u6210 /notice/front/static/... 404\uFF09`
);
}
const entryRe = options.entry ?? /(^|\/)src\/main\.(ts|js)$/;
const proxyFromEnv = () => {
var _a;
const env = (_a = globalThis.process) == null ? void 0 : _a.env;
const v = env == null ? void 0 : env.YH_THEME_PROXY_TARGET;
return v && v.trim() ? v.trim() : void 0;
};
return {
name: "@yunhai/vite-plugin-theme",
/**
* proxy 在 config 钩子里合并:Vite 的 proxy 中间件在 configureServer 之前就已建立,
* 到 configureServer 里再改 server.config.server.proxy 不会生效(B4 的语义在这里落地)。
*/
config(userConfig) {
var _a;
const url = proxyFromEnv() ?? proxyTarget;
const existing = ((_a = userConfig.server) == null ? void 0 : _a.proxy) ?? {};
if (existing["/static"]) return {};
const patch = {
proxy: {
...existing,
"/static": { target: url, changeOrigin: true }
}
};
if (!proxyFromEnv() && url !== DEFAULT_PROXY_TARGET) {
patch.proxy["/static"] = {
target: url,
changeOrigin: true,
headers: { Host: DEFAULT_PROXY_TARGET.replace(/^https?:\/\//, "") }
};
}
return { server: patch };
},
resolveId(id) {
return id === VIRTUAL_ID ? RESOLVED_VIRTUAL_ID : null;
},
load(id) {
return id === RESOLVED_VIRTUAL_ID ? loaderSource(staticBase) : null;
},
transform(code, id) {
const cleanId = id.split("?")[0].replace(/\\/g, "/");
if (!entryRe.test(cleanId)) return null;
if (code.includes(VIRTUAL_ID)) return null;
return { code: `${code}
import "${VIRTUAL_ID}";
`, map: null };
},
transformIndexHtml: {
order: "post",
handler() {
return [
{ tag: "style", children: FALLBACK_STYLE, injectTo: "head" },
{ tag: "script", children: initBootstrap(staticBase), injectTo: "head" }
];
}
},
configureServer(server) {
var _a;
const proxy = ((_a = server.config.server) == null ? void 0 : _a.proxy) ?? {};
const target0 = proxy["/static"];
server.config.logger.info(
`[yh-theme] dev /static \u2192 ${(target0 == null ? void 0 : target0.target) ?? "(\u672A\u6302\uFF1A\u5E94\u7528\u4FA7\u5DF2\u81EA\u5E26\u540C\u540D proxy\uFF1F)"}${proxyFromEnv() ? `\uFF08YH_THEME_PROXY_TARGET=${proxyFromEnv()}\uFF09` : ""}`
);
}
};
}
var src_default = yhTheme;
export {
DEFAULT_PROXY_TARGET,
FALLBACK_STYLE,
MANIFEST,
RESOLVED_VIRTUAL_ID,
VIRTUAL_ID,
src_default as default,
initBootstrap,
isValidStaticBase,
loaderSource,
yhTheme
};
-34
View File
@@ -1,34 +0,0 @@
{
"name": "@yunhai/vite-plugin-theme",
"version": "0.1.0",
"description": "云海管理端远程主题接入插件:注入 yh-theme-init / 首帧兜底样式 / virtual:yh-theme-loader / dev /static proxy",
"_vendored": "临时 vendored 自 YschTheme/packages/vite-plugin 的构建产物(lib/,原 dist 改名以规避本仓 .gitignore 的 dist 规则)。已剥离 devDependencies/scripts(消费端无需重建)。Nexus 建好 npm 仓后改回按版本号消费并删除本目录。",
"type": "module",
"main": "./lib/index.cjs",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"exports": {
".": {
"import": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"require": {
"types": "./lib/index.d.cts",
"default": "./lib/index.cjs"
}
}
},
"files": [
"lib"
],
"keywords": [
"vite-plugin",
"yunhai",
"theme"
],
"license": "UNLICENSED",
"peerDependencies": {
"vite": ">=4.0.0"
}
}
+2 -2
View File
@@ -13,8 +13,8 @@ import mittBus from "@/utils/mitt";
import "view-ui-plus/dist/styles/viewuiplus.css"; import "view-ui-plus/dist/styles/viewuiplus.css";
import "@/assets/styles/main.scss"; import "@/assets/styles/main.scss";
// 布局/主题/图标(base.layout.css + iview.override.css + icons.css)改由中央静态通道下发: // 布局/主题/图标(base.layout.css + iview.override.css + icons.css)由中央静态通道下发:
// yhTheme() 在入口末位注入 virtual:yh-theme-loader,运行时按 manifest 版本挂 <link> // index.html 的 yh-theme-init.js 在 DOMContentLoaded 后按 manifest 版本追加 <link>,天然排在本文件组件库样式之后
//导入自定义指令 //导入自定义指令
import directive from "@/utils/directive/index"; import directive from "@/utils/directive/index";
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20260924-1142-e34cbc4),勿手改。 * 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20260929-1112-1b1724d),勿手改。
* 分发方式:YschTheme `npm run sync` 拷进各端 src/utils/theme.ts(与 ThemeSwitcher.vue 配套,MD5 对齐报告)。 * 分发方式:YschTheme `npm run sync` 拷进各端 src/utils/theme.ts(与 ThemeSwitcher.vue 配套,MD5 对齐报告)。
* 运行时档位类名由 /static/js/yh-theme-init.js 在 <head> 内先行挂好,本模块只负责读写与切换。 * 运行时档位类名由 /static/js/yh-theme-init.js 在 <head> 内先行挂好,本模块只负责读写与切换。
*/ */
+12 -5
View File
@@ -11,8 +11,6 @@ import presetUno from "@unocss/preset-uno";
import presetRemToPx from "@unocss/preset-rem-to-px"; import presetRemToPx from "@unocss/preset-rem-to-px";
import { yhTheme } from "@yunhai/vite-plugin-theme";
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => { export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
// 构建产物直出 dist/(env-config 模式,与 Cms/UserBase 同风格):CI zip 打包取 dist 根, // 构建产物直出 dist/(env-config 模式,与 Cms/UserBase 同风格):CI zip 打包取 dist 根,
// 不再有 dist/base 中间层(此前 outDir=dist/base 时 zip 打 dist 整层,zip 根只有 base/ // 不再有 dist/base 中间层(此前 outDir=dist/base 时 zip 打 dist 整层,zip 根只有 base/
@@ -91,9 +89,9 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
}, },
}, },
}), }),
// 主题插件放末位:transformIndexHtml 用 order:'post',必须晚于 createHtmlPlugin 才能把 // 主题接入已免插件化(A 类,YschTheme《管理端主题远程化与公共静态通道方案》§11):
// init script + 兜底 style 追加到最终 html;dev 的 /static 代理也由它自动配 // 唯一接入点 = index.html 一段同步 loader,挂载/FOUC/CSS 排序全由通道 init 承担;
yhTheme(), // 下面的 /static 代理只是 dev 联调便利(prod 同源用不到),删除不影响线上主题
], ],
resolve: { resolve: {
alias: { alias: {
@@ -110,6 +108,15 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
target: "http://localhost:8101", target: "http://localhost:8101",
changeOrigin: true, changeOrigin: true,
}, },
// 主题通道本地联调:dev 下 loader 走同源相对(见 index.html 注释的优先级四档),
// manifest/CSS 经此代理到中央通道;changeOrigin 会把 Host 改写成目标域,Caddy 按 Host 匹配站点,
// 无需再手工补 Host 头(这是旧 vite 插件里那段 headers.Host 的等价替代)。
// 联调本地通道源:YschTheme 侧 npm run serve:static(默认 9527),再用
// YH_THEME_PROXY_TARGET=http://localhost:9527 覆盖本 target
"/static": {
target: process.env.YH_THEME_PROXY_TARGET || "http://base.yunhaixingtong.com",
changeOrigin: true,
},
}, },
}, },
// css预处理器 // css预处理器