@tixxin/nuxt-theme-engine 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/default-contracts.d.mts +68 -0
- package/dist/default-contracts.mjs +11 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +14 -7
- package/dist/runtime/components/ThemeComponent.vue +87 -17
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
pnpm add @tixxin/nuxt-theme-engine
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
默认博客契约已经内置在 `@tixxin/nuxt-theme-engine` 发布包内,消费者**不需要额外安装** `@tixxin/theme-contracts`。
|
|
48
|
+
|
|
47
49
|
```ts
|
|
48
50
|
// nuxt.config.ts
|
|
49
51
|
export default defineNuxtConfig({
|
|
@@ -74,11 +76,15 @@ export default defineNuxtConfig({
|
|
|
74
76
|
</template>
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
`ThemeComponent` 在主题切换时会优先保留当前已显示的组件树,等目标主题组件实际加载完成后再原子切换,以减少异步主题组件带来的空白闪屏。
|
|
80
|
+
|
|
77
81
|
## 自定义契约
|
|
78
82
|
|
|
79
83
|
引擎本身不强绑定某一套业务契约。
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
模块默认会使用内置的博客示例契约,等价导入路径为 `@tixxin/nuxt-theme-engine/default-contracts`。
|
|
86
|
+
|
|
87
|
+
仓库里的 `@tixxin/theme-contracts` 现在只是**开发期/示例来源**。如果你的项目是电商、文档站、社区或其他业务,可以直接换成自己的契约入口。
|
|
82
88
|
|
|
83
89
|
### 使用本地契约文件
|
|
84
90
|
|
|
@@ -145,7 +151,7 @@ pnpm dev
|
|
|
145
151
|
pnpm dev # 启动 playground
|
|
146
152
|
pnpm dev:prepare # 生成 .nuxt 类型声明
|
|
147
153
|
pnpm typecheck # 类型检查模块与 playground
|
|
148
|
-
pnpm build #
|
|
154
|
+
pnpm build # 构建主题引擎模块与内置默认契约
|
|
149
155
|
```
|
|
150
156
|
|
|
151
157
|
仓库结构:
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface ThemePostSummary {
|
|
2
|
+
id: string | number;
|
|
3
|
+
title: string;
|
|
4
|
+
excerpt?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
likesCount?: number;
|
|
7
|
+
commentsCount?: number;
|
|
8
|
+
readingTime?: string;
|
|
9
|
+
}
|
|
10
|
+
interface ThemePostDetail extends ThemePostSummary {
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
interface HomeLayoutProps {
|
|
14
|
+
title?: string;
|
|
15
|
+
subtitle?: string;
|
|
16
|
+
}
|
|
17
|
+
interface PostListProps {
|
|
18
|
+
posts: ThemePostSummary[];
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
}
|
|
21
|
+
interface PostDetailProps {
|
|
22
|
+
post: ThemePostDetail;
|
|
23
|
+
}
|
|
24
|
+
interface NavItem {
|
|
25
|
+
label: string;
|
|
26
|
+
icon?: string;
|
|
27
|
+
path: string;
|
|
28
|
+
}
|
|
29
|
+
interface SidebarNavProps {
|
|
30
|
+
items: NavItem[];
|
|
31
|
+
currentPath?: string;
|
|
32
|
+
}
|
|
33
|
+
interface SiteStatsData {
|
|
34
|
+
runningDays: number;
|
|
35
|
+
postCount: number;
|
|
36
|
+
viewCount: string;
|
|
37
|
+
commentCount: number;
|
|
38
|
+
tagCount: number;
|
|
39
|
+
}
|
|
40
|
+
interface SiteStatsProps {
|
|
41
|
+
stats: SiteStatsData;
|
|
42
|
+
}
|
|
43
|
+
interface SubscribeCardProps {
|
|
44
|
+
description?: string;
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
}
|
|
47
|
+
interface BlogFooterProps {
|
|
48
|
+
copyright?: string;
|
|
49
|
+
poweredBy?: string;
|
|
50
|
+
links?: {
|
|
51
|
+
label: string;
|
|
52
|
+
url: string;
|
|
53
|
+
}[];
|
|
54
|
+
}
|
|
55
|
+
interface ThemeComponentContracts {
|
|
56
|
+
HomeLayout: HomeLayoutProps;
|
|
57
|
+
PostList: PostListProps;
|
|
58
|
+
PostDetail: PostDetailProps;
|
|
59
|
+
SidebarNav: SidebarNavProps;
|
|
60
|
+
SiteStats: SiteStatsProps;
|
|
61
|
+
SubscribeCard: SubscribeCardProps;
|
|
62
|
+
BlogFooter: BlogFooterProps;
|
|
63
|
+
}
|
|
64
|
+
declare const themeContractNames: readonly ["HomeLayout", "PostList", "PostDetail", "SidebarNav", "SiteStats", "SubscribeCard", "BlogFooter"];
|
|
65
|
+
type ThemeContractName = typeof themeContractNames[number];
|
|
66
|
+
|
|
67
|
+
export { themeContractNames };
|
|
68
|
+
export type { BlogFooterProps, HomeLayoutProps, NavItem, PostDetailProps, PostListProps, SidebarNavProps, SiteStatsData, SiteStatsProps, SubscribeCardProps, ThemeComponentContracts, ThemeContractName, ThemePostDetail, ThemePostSummary };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -470,6 +470,7 @@ async function checkCssVars(rootDir, themes, requiredCssVars) {
|
|
|
470
470
|
});
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
const DEFAULT_CONTRACTS_IMPORT_ID$1 = "@tixxin/nuxt-theme-engine/default-contracts";
|
|
473
474
|
function parseContractNames(source) {
|
|
474
475
|
const match = source.match(/themeContractNames\s*=\s*\[([\s\S]*?)\]\s*as const/);
|
|
475
476
|
if (!match) {
|
|
@@ -481,9 +482,10 @@ function parseContractNames(source) {
|
|
|
481
482
|
async function generateThemeComponentDts(contractsSourcePath, contractsImportId) {
|
|
482
483
|
const source = await promises.readFile(contractsSourcePath, "utf8");
|
|
483
484
|
const names = parseContractNames(source);
|
|
485
|
+
const importSource = contractsImportId || DEFAULT_CONTRACTS_IMPORT_ID$1;
|
|
484
486
|
const union = names.length > 0 ? names.map((name) => `'${name}'`).join(" | ") : "never";
|
|
485
487
|
return `import type { DefineComponent } from 'vue'
|
|
486
|
-
import type { ThemeComponentContracts } from ${JSON.stringify(
|
|
488
|
+
import type { ThemeComponentContracts } from ${JSON.stringify(importSource)}
|
|
487
489
|
|
|
488
490
|
declare module '#build/theme-engine.contracts.mjs' {
|
|
489
491
|
export const themeEngineContracts: {
|
|
@@ -691,14 +693,15 @@ async function loadThemeDefinitions(themesDir) {
|
|
|
691
693
|
return resolvedThemes.sort((left, right) => left.name.localeCompare(right.name));
|
|
692
694
|
}
|
|
693
695
|
|
|
696
|
+
const DEFAULT_CONTRACTS_IMPORT_ID = "@tixxin/nuxt-theme-engine/default-contracts";
|
|
694
697
|
const DEFAULTS = {
|
|
695
698
|
themesDir: "themes",
|
|
696
699
|
defaultTheme: "base",
|
|
697
700
|
cookieKey: "theme-pref",
|
|
698
701
|
lazyLoadThemes: false,
|
|
699
702
|
requiredCssVars: [],
|
|
700
|
-
contractsEntry:
|
|
701
|
-
contractsImportId:
|
|
703
|
+
contractsEntry: DEFAULT_CONTRACTS_IMPORT_ID,
|
|
704
|
+
contractsImportId: DEFAULT_CONTRACTS_IMPORT_ID
|
|
702
705
|
};
|
|
703
706
|
const require$1 = createRequire(import.meta.url);
|
|
704
707
|
async function pathExists(path) {
|
|
@@ -712,10 +715,12 @@ async function pathExists(path) {
|
|
|
712
715
|
function isLocalSpecifier(specifier) {
|
|
713
716
|
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("~") || /^[A-Za-z]:[\\/]/.test(specifier);
|
|
714
717
|
}
|
|
715
|
-
async function resolveContractsEntry(entry, importId, rootDir, moduleRoot, aliases) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
718
|
+
async function resolveContractsEntry(entry, importId, rootDir, moduleRoot, defaultContractsEntry, aliases) {
|
|
719
|
+
if (entry === DEFAULT_CONTRACTS_IMPORT_ID || importId === DEFAULT_CONTRACTS_IMPORT_ID) {
|
|
720
|
+
return defaultContractsEntry;
|
|
721
|
+
}
|
|
722
|
+
if (entry === "packages/theme-contracts/src/index.ts" && await pathExists(resolve(rootDir, entry))) {
|
|
723
|
+
return resolve(rootDir, entry);
|
|
719
724
|
}
|
|
720
725
|
const aliasTarget = aliases[entry] ?? aliases[importId];
|
|
721
726
|
if (typeof aliasTarget === "string") {
|
|
@@ -800,6 +805,7 @@ const module$1 = defineNuxtModule({
|
|
|
800
805
|
async setup(moduleOptions, nuxt) {
|
|
801
806
|
const resolver = createResolver(import.meta.url);
|
|
802
807
|
const moduleRoot = resolver.resolve("..");
|
|
808
|
+
const defaultContractsEntry = resolver.resolve("./default-contracts");
|
|
803
809
|
const options = {
|
|
804
810
|
...DEFAULTS,
|
|
805
811
|
...moduleOptions
|
|
@@ -811,6 +817,7 @@ const module$1 = defineNuxtModule({
|
|
|
811
817
|
contractsImportId,
|
|
812
818
|
nuxt.options.rootDir,
|
|
813
819
|
moduleRoot,
|
|
820
|
+
defaultContractsEntry,
|
|
814
821
|
nuxt.options.alias
|
|
815
822
|
);
|
|
816
823
|
const themes = await loadThemeDefinitions(themesDir);
|
|
@@ -1,8 +1,54 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { computed, defineAsyncComponent, defineComponent, h, useAttrs, useSlots } from "vue";
|
|
2
|
+
import { computed, defineAsyncComponent, defineComponent, h, onServerPrefetch, shallowRef, useAttrs, useSlots, watch } from "vue";
|
|
3
3
|
import { themeComponentLoaders, themeComponentRegistry } from "#build/theme-engine.registry.mjs";
|
|
4
4
|
import { useThemeEngine } from "../composables/useThemeEngine";
|
|
5
5
|
const asyncComponentCache = /* @__PURE__ */ new Map();
|
|
6
|
+
const loadedComponentCache = /* @__PURE__ */ new Map();
|
|
7
|
+
const loadingTaskCache = /* @__PURE__ */ new Map();
|
|
8
|
+
function resolveThemeComponent(themeName, componentName, firstThemeName) {
|
|
9
|
+
const entry = themeComponentRegistry[themeName]?.[componentName] ?? themeComponentRegistry[firstThemeName]?.[componentName];
|
|
10
|
+
const loader = themeComponentLoaders[themeName]?.[componentName] ?? themeComponentLoaders[firstThemeName]?.[componentName];
|
|
11
|
+
if (!entry || !loader) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
cacheKey: `${entry.sourceTheme}:${componentName}`,
|
|
16
|
+
loader
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function loadThemeComponent(cacheKey, loader) {
|
|
20
|
+
const cachedComponent = loadedComponentCache.get(cacheKey);
|
|
21
|
+
if (cachedComponent) {
|
|
22
|
+
return cachedComponent;
|
|
23
|
+
}
|
|
24
|
+
const loadingTask = loadingTaskCache.get(cacheKey);
|
|
25
|
+
if (loadingTask) {
|
|
26
|
+
return loadingTask;
|
|
27
|
+
}
|
|
28
|
+
const task = loader().then((module) => {
|
|
29
|
+
const component = module.default ?? null;
|
|
30
|
+
if (component) {
|
|
31
|
+
loadedComponentCache.set(cacheKey, component);
|
|
32
|
+
}
|
|
33
|
+
return component;
|
|
34
|
+
}).finally(() => {
|
|
35
|
+
loadingTaskCache.delete(cacheKey);
|
|
36
|
+
});
|
|
37
|
+
loadingTaskCache.set(cacheKey, task);
|
|
38
|
+
return task;
|
|
39
|
+
}
|
|
40
|
+
function getAsyncComponent(cacheKey, loader) {
|
|
41
|
+
if (!asyncComponentCache.has(cacheKey)) {
|
|
42
|
+
asyncComponentCache.set(cacheKey, defineAsyncComponent(async () => {
|
|
43
|
+
const component = await loadThemeComponent(cacheKey, loader);
|
|
44
|
+
if (!component) {
|
|
45
|
+
throw new Error(`Theme component "${cacheKey}" failed to load.`);
|
|
46
|
+
}
|
|
47
|
+
return component;
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
return asyncComponentCache.get(cacheKey) ?? null;
|
|
51
|
+
}
|
|
6
52
|
export default defineComponent({
|
|
7
53
|
name: "ThemeComponent",
|
|
8
54
|
inheritAttrs: false,
|
|
@@ -17,29 +63,53 @@ export default defineComponent({
|
|
|
17
63
|
const slots = useSlots();
|
|
18
64
|
const { currentTheme } = useThemeEngine();
|
|
19
65
|
const firstThemeName = Object.keys(themeComponentRegistry)[0] ?? "";
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
66
|
+
const displayedComponent = shallowRef(null);
|
|
67
|
+
const displayedCacheKey = shallowRef(null);
|
|
68
|
+
const targetComponent = computed(() => resolveThemeComponent(currentTheme.value, props.name, firstThemeName));
|
|
69
|
+
let activeRequestId = 0;
|
|
70
|
+
async function syncDisplayedComponent() {
|
|
71
|
+
const requestId = ++activeRequestId;
|
|
72
|
+
const resolvedTarget = targetComponent.value;
|
|
73
|
+
if (!resolvedTarget) {
|
|
74
|
+
displayedComponent.value = null;
|
|
75
|
+
displayedCacheKey.value = null;
|
|
76
|
+
return;
|
|
27
77
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return null;
|
|
78
|
+
if (displayedCacheKey.value === resolvedTarget.cacheKey && displayedComponent.value) {
|
|
79
|
+
return;
|
|
31
80
|
}
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
81
|
+
const cachedComponent = loadedComponentCache.get(resolvedTarget.cacheKey);
|
|
82
|
+
if (cachedComponent) {
|
|
83
|
+
displayedComponent.value = cachedComponent;
|
|
84
|
+
displayedCacheKey.value = resolvedTarget.cacheKey;
|
|
85
|
+
return;
|
|
35
86
|
}
|
|
36
|
-
|
|
87
|
+
const hasDisplayedComponent = Boolean(displayedComponent.value);
|
|
88
|
+
if (!hasDisplayedComponent) {
|
|
89
|
+
displayedComponent.value = getAsyncComponent(resolvedTarget.cacheKey, resolvedTarget.loader);
|
|
90
|
+
displayedCacheKey.value = resolvedTarget.cacheKey;
|
|
91
|
+
}
|
|
92
|
+
const loadedComponent = await loadThemeComponent(resolvedTarget.cacheKey, resolvedTarget.loader);
|
|
93
|
+
if (!loadedComponent || requestId !== activeRequestId) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
displayedComponent.value = loadedComponent;
|
|
97
|
+
displayedCacheKey.value = resolvedTarget.cacheKey;
|
|
98
|
+
}
|
|
99
|
+
watch([
|
|
100
|
+
() => currentTheme.value,
|
|
101
|
+
() => props.name
|
|
102
|
+
], () => {
|
|
103
|
+
void syncDisplayedComponent();
|
|
104
|
+
}, {
|
|
105
|
+
immediate: true
|
|
37
106
|
});
|
|
107
|
+
onServerPrefetch(syncDisplayedComponent);
|
|
38
108
|
return () => {
|
|
39
|
-
if (!
|
|
109
|
+
if (!displayedComponent.value) {
|
|
40
110
|
return null;
|
|
41
111
|
}
|
|
42
|
-
return h(
|
|
112
|
+
return h(displayedComponent.value, attrs, slots);
|
|
43
113
|
};
|
|
44
114
|
}
|
|
45
115
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tixxin/nuxt-theme-engine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A Nuxt 4 theme engine with layered themes, typed contracts, and runtime theme switching.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"types": "./dist/types.d.mts",
|
|
12
12
|
"import": "./dist/module.mjs"
|
|
13
13
|
},
|
|
14
|
+
"./default-contracts": {
|
|
15
|
+
"types": "./dist/default-contracts.d.mts",
|
|
16
|
+
"import": "./dist/default-contracts.mjs"
|
|
17
|
+
},
|
|
14
18
|
"./runtime/*": "./dist/runtime/*"
|
|
15
19
|
},
|
|
16
20
|
"files": [
|
|
@@ -19,7 +23,7 @@
|
|
|
19
23
|
"scripts": {
|
|
20
24
|
"dev": "nuxi dev playground",
|
|
21
25
|
"dev:prepare": "nuxi prepare playground",
|
|
22
|
-
"build": "
|
|
26
|
+
"build": "nuxt-module-build build",
|
|
23
27
|
"prepack": "pnpm build",
|
|
24
28
|
"lint": "eslint .",
|
|
25
29
|
"typecheck": "pnpm dev:prepare && tsc --noEmit -p tsconfig.json && vue-tsc --noEmit -p playground/.nuxt/tsconfig.json"
|
|
@@ -28,7 +32,6 @@
|
|
|
28
32
|
"nuxt": "^4.0.0"
|
|
29
33
|
},
|
|
30
34
|
"dependencies": {
|
|
31
|
-
"@tixxin/theme-contracts": "workspace:*",
|
|
32
35
|
"defu": "^6.1.6",
|
|
33
36
|
"fast-glob": "^3.3.3",
|
|
34
37
|
"pathe": "^2.0.3",
|