@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 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
- 仓库里的 `@tixxin/theme-contracts` 只是**默认博客示例契约**。如果你的项目是电商、文档站、社区或其他业务,可以直接换成自己的契约入口。
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 };
@@ -0,0 +1,11 @@
1
+ const themeContractNames = [
2
+ "HomeLayout",
3
+ "PostList",
4
+ "PostDetail",
5
+ "SidebarNav",
6
+ "SiteStats",
7
+ "SubscribeCard",
8
+ "BlogFooter"
9
+ ];
10
+
11
+ export { themeContractNames };
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "0.0.1",
7
+ "version": "0.0.3",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
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(contractsImportId)}
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: "@tixxin/theme-contracts",
701
- contractsImportId: "@tixxin/theme-contracts"
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
- const internalDefaultEntry = resolve(moduleRoot, "packages/theme-contracts/src/index.ts");
717
- if ((entry === "@tixxin/theme-contracts" || entry === "packages/theme-contracts/src/index.ts") && await pathExists(internalDefaultEntry)) {
718
- return internalDefaultEntry;
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 entry = computed(() => {
21
- return themeComponentRegistry[currentTheme.value]?.[props.name] ?? themeComponentRegistry[firstThemeName]?.[props.name];
22
- });
23
- const resolvedComponent = computed(() => {
24
- const resolvedEntry = entry.value;
25
- if (!resolvedEntry) {
26
- return null;
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
- const loader = themeComponentLoaders[currentTheme.value]?.[props.name] ?? themeComponentLoaders[firstThemeName]?.[props.name];
29
- if (!loader) {
30
- return null;
78
+ if (displayedCacheKey.value === resolvedTarget.cacheKey && displayedComponent.value) {
79
+ return;
31
80
  }
32
- const cacheKey = `${resolvedEntry.sourceTheme}:${props.name}`;
33
- if (!asyncComponentCache.has(cacheKey)) {
34
- asyncComponentCache.set(cacheKey, defineAsyncComponent(loader));
81
+ const cachedComponent = loadedComponentCache.get(resolvedTarget.cacheKey);
82
+ if (cachedComponent) {
83
+ displayedComponent.value = cachedComponent;
84
+ displayedCacheKey.value = resolvedTarget.cacheKey;
85
+ return;
35
86
  }
36
- return asyncComponentCache.get(cacheKey) ?? null;
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 (!resolvedComponent.value) {
109
+ if (!displayedComponent.value) {
40
110
  return null;
41
111
  }
42
- return h(resolvedComponent.value, attrs, slots);
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.1",
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": "pnpm --filter @tixxin/theme-contracts build && nuxt-module-build 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",